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 crate::reactive::{OwnerId, RwSignal, WriteSignal, create_signal, dispose_owner, with_owner};
use crate::widgets::image::ImageSource;
#[derive(Clone, Debug, Default)]
pub struct DecodedImage(Arc<Mutex<Slot>>);
#[derive(Debug, Default)]
enum Slot {
#[default]
Waiting,
Filled(Pixels),
Uploaded,
Abandoned,
}
#[derive(Debug)]
pub(crate) struct Pixels {
pub(crate) width: u32,
pub(crate) height: u32,
pub(crate) rgba: Vec<u8>,
}
impl DecodedImage {
fn slot(&self) -> MutexGuard<'_, Slot> {
self.0
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub(crate) fn take(&self) -> Option<Pixels> {
let mut slot = self.slot();
match std::mem::take(&mut *slot) {
Slot::Filled(pixels) => {
*slot = Slot::Uploaded;
Some(pixels)
}
other => {
*slot = other;
None
}
}
}
#[cfg(feature = "testing")]
pub(crate) fn byte_size(&self) -> usize {
match &*self.slot() {
Slot::Filled(pixels) => pixels.rgba.len(),
_ => 0,
}
}
fn fill(&self, pixels: Pixels) {
let mut slot = self.slot();
if !matches!(*slot, Slot::Abandoned) {
*slot = Slot::Filled(pixels);
}
}
}
impl PartialEq for DecodedImage {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum DecodeState {
Pending,
Ready,
Failed,
}
#[derive(Clone, Debug)]
pub(crate) enum DecodeKey {
Path(PathBuf),
Bytes(Arc<[u8]>),
}
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(bytes.clone())),
ImageSource::Rgba { .. } | ImageSource::SvgPath(_) | ImageSource::SvgBytes(_) => None,
}
}
}
impl PartialEq for DecodeKey {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Path(a), Self::Path(b)) => a == b,
(Self::Bytes(a), Self::Bytes(b)) => Arc::ptr_eq(a, b) || a[..] == b[..],
_ => false,
}
}
}
impl Eq for DecodeKey {}
impl Hash for DecodeKey {
fn hash<H: Hasher>(&self, state: &mut H) {
match self {
Self::Path(path) => {
0u8.hash(state);
path.hash(state);
}
Self::Bytes(bytes) => {
1u8.hash(state);
hash_sampled(bytes, state);
}
}
}
}
const HASH_SAMPLE_SIZE: usize = 256;
pub(crate) 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<(u32, u32)>,
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 size = crate::image_metadata::get_intrinsic_size(source);
let initial = 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::default(),
scope,
size,
users,
};
if size.is_some() {
decode_later(key.clone(), &entry);
}
with_app_state(|app| {
app.decoded_images.borrow_mut().insert(key, entry.clone());
});
entry
}
fn describe(key: &DecodeKey) -> String {
match key {
DecodeKey::Path(path) => path.display().to_string(),
DecodeKey::Bytes(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 is_ready(source: &ImageSource) -> bool {
!matches!(
state(source),
Some((DecodeState::Pending | DecodeState::Failed, _))
)
}
pub(crate) fn acquire(source: &ImageSource) -> Option<DecodeHandle> {
let key = DecodeKey::of(source)?;
let entry = entry(key.clone(), source, 1);
Some(DecodeHandle {
key,
state: entry.state,
pixels: entry.pixels,
size: entry.size,
})
}
pub(crate) struct DecodeHandle {
key: DecodeKey,
state: RwSignal<DecodeState>,
pixels: DecodedImage,
size: Option<(u32, u32)>,
}
impl DecodeHandle {
pub(crate) fn state(&self) -> (DecodeState, DecodedImage) {
(self.state.get(), self.pixels.clone())
}
pub(crate) fn size(&self) -> Option<(u32, u32)> {
self.size
}
}
impl Drop for DecodeHandle {
fn drop(&mut self) {
with_app_state(|app| {
let last = app
.decoded_images
.borrow_mut()
.get_mut(&self.key)
.is_some_and(|entry| {
entry.users = entry.users.saturating_sub(1);
entry.users == 0
});
if last {
app.image_events
.push(ImageEvent::Released(self.key.clone()));
}
});
}
}
pub(crate) enum ImageEvent {
Missing(DecodeKey),
Evicted(DecodeKey),
Released(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) {
with_app_state(|app| app.image_events.push(ImageEvent::Evicted(key)));
}
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) => key,
};
let Some(entry) = with_app_state(|app| app.decoded_images.borrow().get(key).cloned())
else {
continue;
};
match event {
ImageEvent::Missing(key) => {
let gone = !matches!(*entry.pixels.slot(), Slot::Filled(_));
if gone && entry.state.get_untracked() == DecodeState::Ready {
entry.state.set(DecodeState::Pending);
decode_later(key, &entry);
}
}
ImageEvent::Evicted(key) => {
{
let mut slot = entry.pixels.slot();
if matches!(*slot, Slot::Uploaded) {
*slot = Slot::Waiting;
}
}
remove_if_unheld(&key, entry);
}
ImageEvent::Released(key) => remove_if_unheld(&key, entry),
}
}
}
fn remove_if_unheld(key: &DecodeKey, entry: DecodeEntry) {
if entry.users > 0 || matches!(*entry.pixels.slot(), Slot::Uploaded) {
return;
}
with_app_state(|app| app.decoded_images.borrow_mut().remove(key));
*entry.pixels.slot() = Slot::Abandoned;
entry.state.set_always(DecodeState::Pending);
dispose_owner(entry.scope);
}
struct DecodeJob {
key: DecodeKey,
pixels: DecodedImage,
write: WriteSignal<DecodeState>,
}
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.key) {
Some(pixels) => {
job.pixels.fill(pixels);
DecodeState::Ready
}
None => DecodeState::Failed,
};
job.write.set(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(key: DecodeKey, entry: &DecodeEntry) {
let write = entry.state.writer();
let pixels = entry.pixels.clone();
let sent = with_decoder(|decoder| {
{
let mut progress = decoder.progress.lock();
progress.in_flight += 1;
progress.started += 1;
}
decoder.jobs.send(DecodeJob { key, pixels, write }).is_ok()
});
if sent != Some(true) {
write.set(DecodeState::Failed);
}
}
fn decode(key: &DecodeKey) -> Option<Pixels> {
let decoded = match key {
DecodeKey::Path(path) => image::open(path),
DecodeKey::Bytes(bytes) => image::load_from_memory(bytes),
};
match decoded {
Ok(image) => {
let rgba = image.into_rgba8();
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
}
}
}
#[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 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_and_svg_need_no_decode() {
let rgba = ImageSource::Rgba {
width: 1,
height: 1,
pixels: vec![0; 4].into(),
};
let svg = ImageSource::SvgBytes(b"<svg/>".to_vec().into());
for source in [rgba, svg] {
assert!(acquire(&source).is_none());
assert!(is_ready(&source));
}
}
}