use std::hash::{Hash, Hasher};
use std::path::PathBuf;
use std::sync::mpsc;
use std::sync::{Arc, Condvar, Mutex, MutexGuard};
use crate::app_state::with_app_state;
use crate::reactive::owner::with_root_owner;
use rustc_hash::FxHashMap;
use crate::image_metadata::SvgTree;
use crate::reactive::{OwnerId, RwSignal, WriteSignal, create_signal, dispose_owner, with_owner};
use crate::widgets::image::ImageSource;
#[derive(Clone)]
pub struct DecodedImage(Arc<Shared>);
struct Shared {
key: DecodeKey,
write: WriteSignal<DecodeState>,
svg: Option<SvgTree>,
slots: Mutex<Slots>,
}
pub(crate) type Extent = Option<(u32, u32)>;
pub(crate) const INLINE_RASTER_BYTES: usize = 128 * 128 * 4;
fn inline((width, height): (u32, u32)) -> bool {
(width as usize) * (height as usize) * 4 <= INLINE_RASTER_BYTES
}
#[derive(Default)]
struct Slots {
by_extent: FxHashMap<Extent, Slot>,
asked: u64,
abandoned: bool,
}
impl Slots {
fn ask(&mut self, extent: Extent) {
self.asked += 1;
self.by_extent.insert(extent, Slot::Pending(self.asked));
}
fn take(&mut self, extent: Extent) -> Option<Pixels> {
let Some(&Slot::Filled(asked, _)) = self.by_extent.get(&extent) else {
return None;
};
self.by_extent
.retain(|_, slot| !matches!(slot, Slot::Filled(earlier, _) if *earlier < asked));
match self.by_extent.insert(extent, Slot::Uploaded) {
Some(Slot::Filled(_, pixels)) => Some(pixels),
_ => None,
}
}
}
enum Slot {
Pending(u64),
Filled(u64, Pixels),
Uploaded,
}
#[derive(Debug)]
pub(crate) struct Pixels {
pub(crate) width: u32,
pub(crate) height: u32,
pub(crate) rgba: Vec<u8>,
}
impl DecodedImage {
fn slots(&self) -> MutexGuard<'_, Slots> {
self.0
.slots
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub(crate) fn take(&self) -> Option<Pixels> {
self.slots().take(None)
}
pub(crate) fn take_or_rasterize(&self, extent: Extent) -> Option<Pixels> {
let size = extent?;
if inline(size) {
let Some(pixels) = rasterize(self.0.svg.as_ref()?, size) else {
log::warn!("Failed to rasterize {} at {size:?}", describe(self.key()));
self.report(ImageEvent::Failed);
return None;
};
self.slots().by_extent.insert(extent, Slot::Uploaded);
self.report(ImageEvent::Drawn);
return Some(pixels);
}
{
let mut slots = self.slots();
if let Some(pixels) = slots.take(extent) {
drop(slots);
self.report(ImageEvent::Drawn);
return Some(pixels);
}
if matches!(slots.by_extent.get(&extent), Some(Slot::Pending(_))) {
return None;
}
slots.ask(extent);
}
if !send_job(self, extent) {
self.report(ImageEvent::Failed);
}
None
}
pub(crate) fn drew_nothing(&self) {
self.report(ImageEvent::Waiting);
}
pub(crate) fn upload_failed(&self) {
self.slots().by_extent.remove(&None);
self.report(ImageEvent::Failed);
}
fn report(&self, event: fn(DecodeKey) -> ImageEvent) {
let event = event(self.key().clone());
with_app_state(|app| app.image_events.push(event));
}
pub(crate) fn key(&self) -> &DecodeKey {
&self.0.key
}
pub(crate) fn svg_size(&self) -> Option<(f32, f32)> {
self.0.svg.as_ref().map(crate::image_metadata::svg_size)
}
#[cfg(feature = "testing")]
pub(crate) fn byte_size(&self) -> usize {
self.slots()
.by_extent
.values()
.map(|slot| match slot {
Slot::Filled(_, pixels) => pixels.rgba.len(),
_ => 0,
})
.sum()
}
fn fill(&self, extent: Extent, pixels: Pixels) {
let mut slots = self.slots();
if slots.abandoned {
return;
}
if let Some(slot) = slots.by_extent.get_mut(&extent)
&& let Slot::Pending(asked) = *slot
{
*slot = Slot::Filled(asked, pixels);
}
}
fn uploaded(&self) -> bool {
self.slots()
.by_extent
.values()
.any(|slot| matches!(slot, Slot::Uploaded))
}
}
impl std::fmt::Debug for DecodedImage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("DecodedImage").field(&self.0.key).finish()
}
}
impl PartialEq for DecodedImage {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
}
impl Eq for DecodedImage {}
impl Hash for DecodedImage {
fn hash<H: Hasher>(&self, state: &mut H) {
Arc::as_ptr(&self.0).hash(state);
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum DecodeState {
Pending,
Ready,
Failed,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) enum DecodeKey {
Path(PathBuf),
Bytes(SampledBytes),
SvgPath(PathBuf),
SvgBytes(SampledBytes),
}
#[derive(Clone, Debug)]
pub(crate) struct SampledBytes(pub(crate) Arc<[u8]>);
impl std::ops::Deref for SampledBytes {
type Target = [u8];
fn deref(&self) -> &[u8] {
&self.0
}
}
impl PartialEq for SampledBytes {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0) || self.0[..] == other.0[..]
}
}
impl Eq for SampledBytes {}
impl Hash for SampledBytes {
fn hash<H: Hasher>(&self, state: &mut H) {
hash_sampled(&self.0, state);
}
}
impl DecodeKey {
pub(crate) fn of(source: &ImageSource) -> Option<Self> {
match source {
ImageSource::Path(path) => Some(Self::Path(path.clone())),
ImageSource::Bytes(bytes) => Some(Self::Bytes(SampledBytes(bytes.clone()))),
ImageSource::SvgPath(path) => Some(Self::SvgPath(path.clone())),
ImageSource::SvgBytes(bytes) => Some(Self::SvgBytes(SampledBytes(bytes.clone()))),
ImageSource::Rgba { .. } => None,
}
}
}
const HASH_SAMPLE_SIZE: usize = 256;
fn hash_sampled(bytes: &[u8], hasher: &mut impl Hasher) {
bytes.len().hash(hasher);
if bytes.len() < 1024 {
bytes.hash(hasher);
return;
}
let sample = HASH_SAMPLE_SIZE;
bytes[..sample].hash(hasher);
let mid = bytes.len() / 2 - sample / 2;
bytes[mid..mid + sample].hash(hasher);
bytes[bytes.len() - sample..].hash(hasher);
}
#[derive(Clone)]
pub(crate) struct DecodeEntry {
state: RwSignal<DecodeState>,
pixels: DecodedImage,
scope: OwnerId,
size: Option<(f32, f32)>,
users: u32,
}
fn entry(key: DecodeKey, source: &ImageSource, users: u32) -> DecodeEntry {
let found = with_app_state(|app| {
app.decoded_images.borrow_mut().get_mut(&key).map(|entry| {
entry.users += users;
entry.clone()
})
});
if let Some(found) = found {
return found;
}
let (svg, size) = if source.is_svg() {
let svg = crate::image_metadata::parse_svg(source);
let size = svg.as_ref().map(crate::image_metadata::svg_size);
(svg, size)
} else {
(None, crate::image_metadata::get_intrinsic_size(source))
};
let quality = crate::renderer::constants::SVG_QUALITY_MULTIPLIER;
let initial = if let (Some(_), Some((width, height))) = (&svg, size) {
let guess = (
(width * quality).ceil() as u32,
(height * quality).ceil() as u32,
);
if inline(guess) {
DecodeState::Ready
} else {
DecodeState::Pending
}
} else if size.is_some() {
DecodeState::Pending
} else {
log::warn!("Failed to read the image header of {}", describe(&key));
DecodeState::Failed
};
let (state, scope) = with_root_owner(|| with_owner(|| create_signal(initial)));
let entry = DecodeEntry {
state,
pixels: DecodedImage(Arc::new(Shared {
key: key.clone(),
write: state.writer(),
svg,
slots: Mutex::default(),
})),
scope,
size,
users,
};
if size.is_some() && !source.is_svg() && !decode_later(&entry.pixels, None) {
state.set(DecodeState::Failed);
}
with_app_state(|app| {
app.decoded_images.borrow_mut().insert(key, entry.clone());
});
entry
}
fn describe(key: &DecodeKey) -> String {
match key {
DecodeKey::Path(path) | DecodeKey::SvgPath(path) => path.display().to_string(),
DecodeKey::Bytes(bytes) | DecodeKey::SvgBytes(bytes) => {
format!("an in-memory image of {} bytes", bytes.len())
}
}
}
pub(crate) fn state(source: &ImageSource) -> Option<(DecodeState, DecodedImage)> {
let key = DecodeKey::of(source)?;
let entry = entry(key, source, 0);
Some((entry.state.get(), entry.pixels))
}
pub(crate) fn paints(state: DecodeState, pixels: &DecodedImage) -> bool {
match state {
DecodeState::Ready => true,
DecodeState::Pending => pixels.0.svg.is_some(),
DecodeState::Failed => false,
}
}
pub(crate) fn is_ready(source: &ImageSource) -> bool {
!matches!(
state(source),
Some((DecodeState::Pending | DecodeState::Failed, _))
)
}
pub(crate) fn acquire(source: &ImageSource) -> Option<DecodeHandle> {
let entry = entry(DecodeKey::of(source)?, source, 1);
Some(DecodeHandle {
state: entry.state,
pixels: entry.pixels,
size: entry.size,
})
}
pub(crate) struct DecodeHandle {
state: RwSignal<DecodeState>,
pixels: DecodedImage,
size: Option<(f32, f32)>,
}
impl DecodeHandle {
pub(crate) fn state(&self) -> (DecodeState, DecodedImage) {
(self.state.get(), self.pixels.clone())
}
pub(crate) fn size(&self) -> Option<(f32, f32)> {
self.size
}
}
impl Drop for DecodeHandle {
fn drop(&mut self) {
let key = self.pixels.key();
with_app_state(|app| {
let last = app
.decoded_images
.borrow_mut()
.get_mut(key)
.is_some_and(|entry| {
entry.users = entry.users.saturating_sub(1);
entry.users == 0
});
if last {
app.image_events.push(ImageEvent::Released(key.clone()));
}
});
}
}
pub(crate) enum ImageEvent {
Missing(DecodeKey),
Evicted(DecodeKey, Extent),
Released(DecodeKey),
Failed(DecodeKey),
Drawn(DecodeKey),
Waiting(DecodeKey),
}
pub(crate) fn texture_missing(source: &ImageSource) {
if let Some(key) = DecodeKey::of(source) {
with_app_state(|app| app.image_events.push(ImageEvent::Missing(key)));
}
}
pub(crate) fn texture_evicted(key: DecodeKey, extent: Extent) {
with_app_state(|app| app.image_events.push(ImageEvent::Evicted(key, extent)));
}
pub(crate) fn settle_image_events() {
for event in with_app_state(|app| app.image_events.drain()) {
let key = match &event {
ImageEvent::Missing(key)
| ImageEvent::Evicted(key, _)
| ImageEvent::Released(key)
| ImageEvent::Failed(key)
| ImageEvent::Drawn(key)
| ImageEvent::Waiting(key) => key,
};
let Some(entry) = with_app_state(|app| app.decoded_images.borrow().get(key).cloned())
else {
continue;
};
match event {
ImageEvent::Missing(_) => {
let gone = !matches!(
entry.pixels.slots().by_extent.get(&None),
Some(Slot::Filled(..))
);
if gone && entry.state.get_untracked() == DecodeState::Ready {
entry.state.set(DecodeState::Pending);
if !decode_later(&entry.pixels, None) {
entry.state.set(DecodeState::Failed);
}
}
}
ImageEvent::Evicted(key, extent) => {
{
let mut slots = entry.pixels.slots();
if matches!(slots.by_extent.get(&extent), Some(Slot::Uploaded)) {
slots.by_extent.remove(&extent);
}
}
remove_if_unheld(&key, entry);
}
ImageEvent::Released(key) => remove_if_unheld(&key, entry),
ImageEvent::Failed(_) => entry.state.set(DecodeState::Failed),
ImageEvent::Drawn(_) => entry.state.set(DecodeState::Ready),
ImageEvent::Waiting(_) => {
if entry.state.get_untracked() == DecodeState::Ready {
entry.state.set(DecodeState::Pending);
}
}
}
}
}
fn remove_if_unheld(key: &DecodeKey, entry: DecodeEntry) {
if entry.users > 0 || entry.pixels.uploaded() {
return;
}
with_app_state(|app| app.decoded_images.borrow_mut().remove(key));
entry.pixels.slots().abandoned = true;
entry.state.set_always(DecodeState::Pending);
dispose_owner(entry.scope);
}
struct DecodeJob {
image: DecodedImage,
extent: Extent,
}
pub(crate) struct Decoder {
jobs: mpsc::Sender<DecodeJob>,
progress: Arc<Progress>,
}
#[derive(Default)]
struct Progress {
state: Mutex<ProgressState>,
changed: Condvar,
}
#[derive(Default)]
struct ProgressState {
in_flight: usize,
started: u64,
held: bool,
}
impl Progress {
fn lock(&self) -> std::sync::MutexGuard<'_, ProgressState> {
self.state
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
}
impl Decoder {
fn spawn() -> Option<Self> {
let (jobs, received) = mpsc::channel::<DecodeJob>();
let progress = Arc::new(Progress::default());
let worker = progress.clone();
std::thread::Builder::new()
.name("guido-image-decode".into())
.spawn(move || {
for job in received {
drop(worker.changed.wait_while(worker.lock(), |state| state.held));
let state = match decode(&job.image, job.extent) {
Some(pixels) => {
job.image.fill(job.extent, pixels);
DecodeState::Ready
}
None => DecodeState::Failed,
};
job.image.0.write.set_always(state);
worker.lock().in_flight -= 1;
worker.changed.notify_all();
}
})
.map_err(|e| log::warn!("failed to spawn the image decode thread: {e}"))
.ok()?;
Some(Self { jobs, progress })
}
}
fn with_decoder<R>(f: impl FnOnce(&Decoder) -> R) -> Option<R> {
with_app_state(|app| {
let mut decoder = app.image_decoder.borrow_mut();
if decoder.is_none() {
*decoder = Decoder::spawn();
}
decoder.as_ref().map(f)
})
}
fn decode_later(image: &DecodedImage, extent: Extent) -> bool {
image.slots().ask(extent);
send_job(image, extent)
}
fn send_job(image: &DecodedImage, extent: Extent) -> bool {
let job = DecodeJob {
image: image.clone(),
extent,
};
with_decoder(|decoder| {
{
let mut progress = decoder.progress.lock();
progress.in_flight += 1;
progress.started += 1;
}
decoder.jobs.send(job).is_ok()
}) == Some(true)
}
fn decode(image: &DecodedImage, extent: Extent) -> Option<Pixels> {
let key = &image.0.key;
let decoded = match key {
DecodeKey::Path(path) => image::ImageReader::open(path)
.and_then(image::ImageReader::with_guessed_format)
.map_err(image::ImageError::IoError)
.and_then(image::ImageReader::decode),
DecodeKey::Bytes(bytes) => image::load_from_memory(bytes),
DecodeKey::SvgPath(_) | DecodeKey::SvgBytes(_) => {
return rasterize(image.0.svg.as_ref()?, extent?);
}
};
match decoded {
Ok(image) => {
let has_alpha = image.color().has_alpha();
let mut rgba = image.into_rgba8();
if has_alpha {
premultiply(&mut rgba);
}
let (width, height) = rgba.dimensions();
if width == 0 || height == 0 {
log::warn!("{} decodes to an empty image", describe(key));
return None;
}
Some(Pixels {
width,
height,
rgba: rgba.into_raw(),
})
}
Err(e) => {
log::warn!("Failed to decode {}: {e}", describe(key));
None
}
}
}
pub(crate) fn premultiply(rgba: &mut [u8]) {
for texel in rgba.as_chunks_mut::<4>().0 {
let alpha = u16::from(texel[3]);
if alpha != 255 {
for channel in &mut texel[..3] {
let product = u16::from(*channel) * alpha + 128;
*channel = ((product + (product >> 8)) >> 8) as u8;
}
}
}
}
pub(crate) fn premultiplied(rgba: &[u8]) -> std::borrow::Cow<'_, [u8]> {
if rgba.as_chunks::<4>().0.iter().all(|texel| texel[3] == 255) {
return std::borrow::Cow::Borrowed(rgba);
}
let mut copy = rgba.to_vec();
premultiply(&mut copy);
std::borrow::Cow::Owned(copy)
}
#[cfg(feature = "svg")]
fn rasterize(tree: &SvgTree, (width, height): (u32, u32)) -> Option<Pixels> {
let mut pixmap = resvg::tiny_skia::Pixmap::new(width, height)?;
let size = tree.size();
let transform = resvg::tiny_skia::Transform::from_scale(
width as f32 / size.width(),
height as f32 / size.height(),
);
resvg::render(tree, transform, &mut pixmap.as_mut());
Some(Pixels {
width,
height,
rgba: pixmap.take(),
})
}
#[cfg(not(feature = "svg"))]
fn rasterize(tree: &SvgTree, _extent: (u32, u32)) -> Option<Pixels> {
match *tree {}
}
#[cfg(feature = "testing")]
pub struct DecodeHold(Arc<Progress>);
#[cfg(feature = "testing")]
impl DecodeHold {
pub fn release(self) {}
}
#[cfg(feature = "testing")]
impl Drop for DecodeHold {
fn drop(&mut self) {
self.0.lock().held = false;
self.0.changed.notify_all();
}
}
#[cfg(feature = "testing")]
pub(crate) fn hold() -> DecodeHold {
let progress = with_decoder(|decoder| decoder.progress.clone())
.expect("the image decode thread could not be spawned");
progress.lock().held = true;
DecodeHold(progress)
}
#[cfg(feature = "testing")]
pub(crate) fn wait() {
let Some(progress) = with_app_state(|app| {
app.image_decoder
.borrow()
.as_ref()
.map(|d| d.progress.clone())
}) else {
return;
};
let state = progress.lock();
assert!(
state.in_flight == 0 || !state.held,
"waiting for image decodes that are held: release the hold first"
);
let (state, timeout) = progress
.changed
.wait_timeout_while(state, std::time::Duration::from_secs(30), |state| {
state.in_flight > 0
})
.unwrap_or_else(|poisoned| poisoned.into_inner());
assert!(
!timeout.timed_out(),
"{} image decodes still running after 30 s",
state.in_flight
);
}
#[cfg(feature = "testing")]
pub(crate) fn held_bytes() -> usize {
with_app_state(|app| {
app.decoded_images
.borrow()
.values()
.map(|entry| entry.pixels.byte_size())
.sum()
})
}
#[cfg(any(test, feature = "testing"))]
pub(crate) fn started() -> u64 {
with_app_state(|app| {
app.image_decoder
.borrow()
.as_ref()
.map_or(0, |d| d.progress.lock().started)
})
}
#[cfg(test)]
mod tests {
use super::*;
fn entries() -> usize {
with_app_state(|app| app.decoded_images.borrow().len())
}
#[test]
fn a_decode_holder_borrows_the_entrys_source_identity() {
for source in [
ImageSource::Bytes(Arc::from(&b"not a raster"[..])),
ImageSource::SvgBytes(Arc::from(&b"not an SVG"[..])),
] {
let bytes = match &source {
ImageSource::Bytes(bytes) | ImageSource::SvgBytes(bytes) => bytes,
_ => unreachable!(),
};
let before = entries();
let first = acquire(&source).expect("an encoded source has an entry");
let source_refs = Arc::strong_count(bytes);
let second = acquire(&source).expect("the same entry again");
assert_eq!(
Arc::strong_count(bytes),
source_refs,
"another holder must share the entry's source identity"
);
assert_eq!(entries(), before + 1);
assert_eq!(first.state(), second.state(), "one shared decode");
assert_eq!(first.state().0, DecodeState::Failed);
assert_eq!(first.size(), None);
assert_eq!(second.size(), None);
drop(first);
settle_image_events();
assert_eq!(entries(), before + 1, "the second holder keeps it");
drop(second);
settle_image_events();
assert_eq!(entries(), before, "the last holder releases it");
assert_eq!(Arc::strong_count(bytes), 1, "no source left in the cache");
assert_eq!(started(), 0, "invalid headers start no worker");
}
}
#[test]
fn an_entry_goes_with_the_last_image_that_holds_it() {
let source = ImageSource::Path("not-there/decode-entry.png".into());
let before = entries();
let first = acquire(&source).expect("a raster source has an entry");
let second = acquire(&source).expect("and the same one again");
assert_eq!(entries(), before + 1, "one entry for one source");
assert_eq!(
state(&source).map(|(state, _)| state),
Some(DecodeState::Failed)
);
assert_eq!(first.size(), None, "the header could not be read");
drop(first);
settle_image_events();
assert_eq!(entries(), before + 1, "still held by the second");
drop(second);
settle_image_events();
assert_eq!(entries(), before, "gone with the last");
assert_eq!(started(), 0, "and nothing was handed to a worker");
}
#[test]
fn raw_pixels_need_no_decode() {
let rgba = ImageSource::Rgba {
width: 1,
height: 1,
pixels: vec![0; 4].into(),
};
assert!(acquire(&rgba).is_none());
assert!(is_ready(&rgba));
}
#[test]
fn an_upload_failure_is_not_sent_back_to_the_decoder() {
let source = ImageSource::Path("not-there/upload-failure.png".into());
let before = entries();
let held = acquire(&source).unwrap();
let entry =
with_app_state(|app| app.decoded_images.borrow().get(held.pixels.key()).cloned())
.unwrap();
entry.state.set(DecodeState::Ready);
entry.pixels.slots().by_extent.insert(
None,
Slot::Filled(
1,
Pixels {
width: 1,
height: 1,
rgba: vec![255; 4],
},
),
);
assert!(entry.pixels.take().is_some());
assert!(entry.pixels.uploaded());
entry.pixels.upload_failed();
texture_missing(&source);
settle_image_events();
assert_eq!(entry.state.get_untracked(), DecodeState::Failed);
assert_eq!(started(), 0);
drop(held);
settle_image_events();
assert_eq!(
entries(),
before,
"a failed upload has no texture holding its entry"
);
}
}