diff --git a/core/src/image.rs b/core/src/image.rs index f985636a..4b72004e 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -4,8 +4,10 @@ pub use bytes::Bytes; use crate::{Radians, Rectangle, Size}; use rustc_hash::FxHasher; + use std::hash::{Hash, Hasher}; use std::path::{Path, PathBuf}; +use std::sync::{Arc, Weak}; /// A raster image that can be drawn. #[derive(Debug, Clone, PartialEq)] @@ -227,6 +229,56 @@ pub enum FilterMethod { Nearest, } +/// A memory allocation of a [`Handle`], often in GPU memory. +/// +/// Renderers tend to decode and upload image data concurrently to +/// avoid blocking the user interface. This means that when you use a +/// [`Handle`] in a widget, there may be a slight frame delay until it +/// is finally visible. If you are animating images, this can cause +/// undesirable flicker. +/// +/// When you obtain an [`Allocation`] explicitly, you get the guarantee +/// that using a [`Handle`] will draw the corresponding [`Image`] +/// immediately in the next frame. +/// +/// This guarantee is valid as long as you hold an [`Allocation`]. +/// Only when you drop all its clones, the renderer may choose to free +/// the memory of the [`Handle`]. Be careful! +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Allocation(Arc); + +/// Some memory taken by an [`Allocation`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Memory(Handle); + +impl Allocation { + /// Returns a weak reference to the [`Memory`] of the [`Allocation`]. + pub fn downgrade(&self) -> Weak { + Arc::downgrade(&self.0) + } + + /// Upgrades a [`Weak`] memory reference to an [`Allocation`]. + pub fn upgrade(weak: &Weak) -> Option { + Weak::upgrade(weak).map(Allocation) + } + + /// Returns the [`Handle`] of this [`Allocation`]. + pub fn handle(&self) -> &Handle { + &self.0.0 + } +} + +/// Creates a new [`Allocation`] for the given handle. +/// +/// This should only be used internally by renderer implementations. +/// +/// # Safety +/// Must only be created once the [`Handle`] is allocated in memory. +#[allow(unsafe_code)] +pub unsafe fn allocate(handle: &Handle) -> Allocation { + Allocation(Arc::new(Memory(handle.clone()))) +} + /// A [`Renderer`] that can render raster graphics. /// /// [renderer]: crate::renderer diff --git a/core/src/renderer.rs b/core/src/renderer.rs index 84d48304..6e24f661 100644 --- a/core/src/renderer.rs +++ b/core/src/renderer.rs @@ -2,6 +2,7 @@ #[cfg(debug_assertions)] mod null; +use crate::image; use crate::{ Background, Border, Color, Font, Pixels, Rectangle, Shadow, Size, Transformation, Vector, @@ -62,6 +63,13 @@ pub trait Renderer { /// Resets the [`Renderer`] to start drawing in the `new_bounds` from scratch. fn reset(&mut self, new_bounds: Rectangle); + + /// Creates an [`image::Allocation`] for the given [`image::Handle`] and calls the given callback with it. + fn allocate_image( + &mut self, + handle: &image::Handle, + callback: impl FnOnce(image::Allocation) + Send + 'static, + ); } /// A polygon with four sides. diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index ff5baa3b..8b3fbb0c 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -24,6 +24,15 @@ impl Renderer for () { _background: impl Into, ) { } + + fn allocate_image( + &mut self, + handle: &image::Handle, + callback: impl FnOnce(image::Allocation) + Send + 'static, + ) { + #[allow(unsafe_code)] + callback(unsafe { image::allocate(handle) }); + } } impl text::Renderer for () { diff --git a/examples/gallery/src/main.rs b/examples/gallery/src/main.rs index 03768604..87a8e811 100644 --- a/examples/gallery/src/main.rs +++ b/examples/gallery/src/main.rs @@ -46,8 +46,9 @@ enum Message { ImagesListed(Result, Error>), ImagePoppedIn(Id), ImagePoppedOut(Id), - ImageDownloaded(Result), + ImageDownloaded(Result), ThumbnailDownloaded(Id, Result), + ThumbnailAllocated(Id, image::Allocation), ThumbnailHovered(Id, bool), BlurhashDecoded(Id, civitai::Blurhash), Open(Id), @@ -110,14 +111,16 @@ impl Gallery { let _ = self.visible.insert(id); if self.downloaded.contains(&id) { - if let Some(Preview::Ready { thumbnail, .. }) = + let Some(Preview::Ready { thumbnail, .. }) = self.previews.get_mut(&id) - { - thumbnail.fade_in = - Animation::new(false).slow().go(true, now); - } + else { + return Task::none(); + }; - return Task::none(); + return image::allocate(image::Handle::from_bytes( + thumbnail.bytes.clone(), + )) + .map(Message::ThumbnailAllocated.with(id)); } let _ = self.downloaded.insert(id); @@ -134,22 +137,56 @@ impl Gallery { Message::ImagePoppedOut(id) => { let _ = self.visible.remove(&id); + if let Some(Preview::Ready { + thumbnail, + blurhash, + }) = self.previews.get_mut(&id) + { + thumbnail.reset(); + + if let Some(blurhash) = blurhash { + blurhash.reset(); + } + } + Task::none() } - Message::ImageDownloaded(Ok(bytes)) => { - self.viewer.show(bytes, self.now); + Message::ImageDownloaded(Ok(allocation)) => { + self.viewer.show(allocation, self.now); Task::none() } Message::ThumbnailDownloaded(id, Ok(bytes)) => { - let thumbnail = if let Some(preview) = self.previews.remove(&id) - { - preview.load(bytes, self.now) + let preview = if let Some(preview) = self.previews.remove(&id) { + preview.load(bytes.clone()) } else { - Preview::ready(bytes, self.now) + Preview::ready(bytes.clone()) }; - let _ = self.previews.insert(id, thumbnail); + let _ = self.previews.insert(id, preview); + + image::allocate(image::Handle::from_bytes(bytes)) + .map(Message::ThumbnailAllocated.with(id)) + } + Message::ThumbnailAllocated(id, allocation) => { + if !self.visible.contains(&id) { + return Task::none(); + } + + let Some(Preview::Ready { + thumbnail, + blurhash, + .. + }) = self.previews.get_mut(&id) + else { + return Task::none(); + }; + + if let Some(blurhash) = blurhash { + blurhash.show(now); + } + + thumbnail.show(allocation, now); Task::none() } @@ -181,10 +218,12 @@ impl Gallery { self.viewer.open(self.now); - Task::perform( - image.download(Size::Original), - Message::ImageDownloaded, - ) + Task::future(image.download(Size::Original)) + .and_then(|bytes| { + image::allocate(image::Handle::from_bytes(bytes)) + .map(Ok) + }) + .map(Message::ImageDownloaded) } Message::Close => { self.viewer.close(self.now); @@ -238,9 +277,11 @@ fn card<'a>( ) -> Element<'a, Message> { let image = if let Some(preview) = preview { let thumbnail: Element<'_, _> = - if let Preview::Ready { thumbnail, .. } = &preview { + if let Preview::Ready { thumbnail, .. } = &preview + && let Some(allocation) = &thumbnail.allocation + { float( - image(&thumbnail.handle) + image(allocation.handle()) .width(Fill) .content_fit(ContentFit::Cover) .opacity(thumbnail.fade_in.interpolate(0.0, 1.0, now)), @@ -320,8 +361,21 @@ struct Blurhash { fade_in: Animation, } +impl Blurhash { + pub fn show(&mut self, now: Instant) { + self.fade_in.go_mut(true, now); + } + + pub fn reset(&mut self) { + self.fade_in = Animation::new(false) + .easing(animation::Easing::EaseIn) + .slow(); + } +} + struct Thumbnail { - handle: image::Handle, + bytes: Bytes, + allocation: Option, fade_in: Animation, zoom: Animation, } @@ -346,21 +400,21 @@ impl Preview { } } - fn ready(bytes: Bytes, now: Instant) -> Self { + fn ready(bytes: Bytes) -> Self { Self::Ready { blurhash: None, - thumbnail: Thumbnail::new(bytes, now), + thumbnail: Thumbnail::new(bytes), } } - fn load(self, bytes: Bytes, now: Instant) -> Self { + fn load(self, bytes: Bytes) -> Self { let Self::Loading { blurhash } = self else { return self; }; Self::Ready { blurhash: Some(blurhash), - thumbnail: Thumbnail::new(bytes, now), + thumbnail: Thumbnail::new(bytes), } } @@ -387,26 +441,45 @@ impl Preview { blurhash: Some(blurhash), thumbnail, .. - } if thumbnail.fade_in.is_animating(now) => Some(blurhash), + } if !thumbnail.fade_in.value() + || thumbnail.fade_in.is_animating(now) => + { + Some(blurhash) + } Self::Ready { .. } => None, } } } impl Thumbnail { - pub fn new(bytes: Bytes, now: Instant) -> Self { + pub fn new(bytes: Bytes) -> Self { Self { - handle: image::Handle::from_bytes(bytes), - fade_in: Animation::new(false).slow().go(true, now), + bytes, + allocation: None, + fade_in: Animation::new(false) + .easing(animation::Easing::EaseIn) + .slow(), zoom: Animation::new(false) .quick() .easing(animation::Easing::EaseInOut), } } + + pub fn reset(&mut self) { + self.allocation = None; + self.fade_in = Animation::new(false) + .easing(animation::Easing::EaseIn) + .slow(); + } + + pub fn show(&mut self, allocation: image::Allocation, now: Instant) { + self.allocation = Some(allocation); + self.fade_in.go_mut(true, now); + } } struct Viewer { - image: Option, + image: Option, background_fade_in: Animation, image_fade_in: Animation, } @@ -429,8 +502,8 @@ impl Viewer { self.background_fade_in.go_mut(true, now); } - fn show(&mut self, bytes: Bytes, now: Instant) { - self.image = Some(image::Handle::from_bytes(bytes)); + fn show(&mut self, allocation: image::Allocation, now: Instant) { + self.image = Some(allocation); self.background_fade_in.go_mut(true, now); self.image_fade_in.go_mut(true, now); } @@ -452,8 +525,8 @@ impl Viewer { return None; } - let image = self.image.as_ref().map(|handle| { - image(handle) + let image = self.image.as_ref().map(|allocation| { + image(allocation.handle()) .width(Fill) .height(Fill) .opacity(self.image_fade_in.interpolate(0.0, 1.0, now)) diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index d488e6ea..9f2d3c09 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -69,6 +69,14 @@ where fn end_transformation(&mut self) { delegate!(self, renderer, renderer.end_transformation()); } + + fn allocate_image( + &mut self, + handle: &image::Handle, + callback: impl FnOnce(image::Allocation) + Send + 'static, + ) { + delegate!(self, renderer, renderer.allocate_image(handle, callback)); + } } impl core::text::Renderer for Renderer diff --git a/runtime/src/image.rs b/runtime/src/image.rs new file mode 100644 index 00000000..d35f7006 --- /dev/null +++ b/runtime/src/image.rs @@ -0,0 +1,24 @@ +//! Allocate images explicitly to control presentation. +use crate::core::image::Handle; +use crate::futures::futures::channel::oneshot; +use crate::task::{self, Task}; + +pub use crate::core::image::Allocation; + +/// An image action. +#[derive(Debug)] +pub enum Action { + /// Allocates the given [`Handle`]. + Allocate(Handle, oneshot::Sender), +} + +/// Allocates an image [`Handle`]. +/// +/// When you obtain an [`Allocation`] explicitly, you get the guarantee +/// that using a [`Handle`] will draw the corresponding image immediately +/// in the next frame. +pub fn allocate(handle: Handle) -> Task { + task::oneshot(|sender| { + crate::Action::Image(Action::Allocate(handle, sender)) + }) +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 691d6d73..dd85139f 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -11,6 +11,7 @@ #![cfg_attr(docsrs, feature(doc_cfg))] pub mod clipboard; pub mod font; +pub mod image; pub mod keyboard; pub mod system; pub mod task; @@ -55,6 +56,9 @@ pub enum Action { /// Run a system action. System(system::Action), + /// An image action. + Image(image::Action), + /// Recreate all user interfaces and redraw all windows. Reload, @@ -81,6 +85,7 @@ impl Action { Action::Clipboard(action) => Err(Action::Clipboard(action)), Action::Window(action) => Err(Action::Window(action)), Action::System(action) => Err(Action::System(action)), + Action::Image(action) => Err(Action::Image(action)), Action::Reload => Err(Action::Reload), Action::Exit => Err(Action::Exit), } @@ -105,6 +110,7 @@ where } Action::Window(_) => write!(f, "Action::Window"), Action::System(action) => write!(f, "Action::System({action:?})"), + Action::Image(_) => write!(f, "Action::Image"), Action::Reload => write!(f, "Action::Reload"), Action::Exit => write!(f, "Action::Exit"), } diff --git a/runtime/src/task.rs b/runtime/src/task.rs index 0ed78b54..97f64b61 100644 --- a/runtime/src/task.rs +++ b/runtime/src/task.rs @@ -379,14 +379,16 @@ impl Task> { /// The success value is provided to the closure to create the subsequent [`Task`]. pub fn and_then( self, - f: impl Fn(T) -> Task + MaybeSend + 'static, - ) -> Task + f: impl Fn(T) -> Task> + MaybeSend + 'static, + ) -> Task> where T: MaybeSend + 'static, E: MaybeSend + 'static, A: MaybeSend + 'static, { - self.then(move |option| option.map_or_else(|_| Task::none(), &f)) + self.then(move |result| { + result.map_or_else(|error| Task::done(Err(error)), &f) + }) } } diff --git a/src/lib.rs b/src/lib.rs index 129fc682..88d26ee1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -625,6 +625,13 @@ pub mod widget { pub use iced_runtime::widget::*; pub use iced_widget::*; + #[cfg(feature = "image")] + pub mod image { + //! Images display raster graphics in different formats (PNG, JPG, etc.). + pub use iced_runtime::image::{Allocation, allocate}; + pub use iced_widget::image::*; + } + // We hide the re-exported modules by `iced_widget` mod core {} mod graphics {} diff --git a/test/src/emulator.rs b/test/src/emulator.rs index 0bd653d7..cc013a45 100644 --- a/test/src/emulator.rs +++ b/test/src/emulator.rs @@ -257,6 +257,10 @@ impl Emulator

{ // TODO dbg!(action); } + iced_runtime::Action::Image(action) => { + // TODO + dbg!(action); + } runtime::Action::Exit => { // TODO } diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index 085468dc..fe24078b 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -228,6 +228,16 @@ impl core::Renderer for Renderer { fn reset(&mut self, new_bounds: Rectangle) { self.layers.reset(new_bounds); } + + fn allocate_image( + &mut self, + handle: &core::image::Handle, + callback: impl FnOnce(core::image::Allocation) + Send + 'static, + ) { + // TODO: Concurrency + #[allow(unsafe_code)] + callback(unsafe { core::image::allocate(handle) }); + } } impl core::text::Renderer for Renderer { diff --git a/wgpu/src/image/cache.rs b/wgpu/src/image/cache.rs index cc0e7d69..f3253e63 100644 --- a/wgpu/src/image/cache.rs +++ b/wgpu/src/image/cache.rs @@ -3,12 +3,11 @@ use crate::graphics::Shell; use crate::image::atlas::{self, Atlas}; #[cfg(feature = "image")] -use std::collections::BTreeSet; +use std::collections::HashMap; #[cfg(feature = "image")] use std::sync::mpsc; -#[derive(Debug)] pub struct Cache { atlas: Atlas, #[cfg(feature = "image")] @@ -46,7 +45,7 @@ impl Cache { #[cfg(feature = "image")] raster: Raster { cache: crate::image::raster::Cache::default(), - pending: BTreeSet::new(), + pending: HashMap::new(), jobs: jobs.clone(), }, #[cfg(feature = "svg")] @@ -60,6 +59,44 @@ impl Cache { } } + #[cfg(feature = "image")] + pub fn allocate_image( + &mut self, + handle: &core::image::Handle, + callback: impl FnOnce(core::image::Allocation) + Send + 'static, + ) { + use crate::image::raster::Memory; + + let callback = Box::new(callback); + + if let Some(callbacks) = self.raster.pending.get_mut(&handle.id()) { + callbacks.push(callback); + return; + } + + if let Some(Memory::Device { allocation, .. }) = + self.raster.cache.get_mut(handle) + { + if let Some(allocation) = allocation + .as_ref() + .and_then(core::image::Allocation::upgrade) + { + callback(allocation); + return; + } + + #[allow(unsafe_code)] + let new = unsafe { core::image::allocate(handle) }; + *allocation = Some(new.downgrade()); + callback(new); + + return; + } + + let _ = self.raster.pending.insert(handle.id(), vec![callback]); + let _ = self.raster.jobs.send(Job::Load(handle.clone())); + } + #[cfg(feature = "image")] pub fn measure_image(&mut self, handle: &core::image::Handle) -> Size { self.receive(); @@ -69,6 +106,7 @@ impl Cache { &mut self.raster.pending, &mut self.raster.jobs, handle, + None, ) { return memory.dimensions(); } @@ -99,9 +137,13 @@ impl Cache { &mut self.raster.pending, &mut self.raster.jobs, handle, + None, )?; - if let Memory::Device { entry, bind_group } = memory { + if let Memory::Device { + entry, bind_group, .. + } = memory + { return Some(( entry, bind_group.as_ref().unwrap_or(self.atlas.bind_group()), @@ -126,6 +168,7 @@ impl Cache { *memory = Memory::Device { entry, bind_group: None, + allocation: None, }; if let Memory::Device { entry, .. } = memory { @@ -133,7 +176,7 @@ impl Cache { } } - if !self.raster.pending.contains(&handle.id()) { + if !self.raster.pending.contains_key(&handle.id()) { let _ = self.jobs.send(Job::Upload { handle: handle.clone(), rgba: image.clone().into_raw(), @@ -141,7 +184,7 @@ impl Cache { height: image.height(), }); - let _ = self.raster.pending.insert(handle.id()); + let _ = self.raster.pending.insert(handle.id(), Vec::new()); } None @@ -194,15 +237,32 @@ impl Cache { entry, bind_group, } => { + let callbacks = self.raster.pending.remove(&handle.id()); + + let allocation = if let Some(callbacks) = callbacks { + #[allow(unsafe_code)] + let allocation = + unsafe { core::image::allocate(&handle) }; + + let reference = allocation.downgrade(); + + for callback in callbacks { + callback(allocation.clone()); + } + + Some(reference) + } else { + None + }; + self.raster.cache.insert( &handle, Memory::Device { entry, bind_group: Some(bind_group), + allocation, }, ); - - let _ = self.raster.pending.remove(&handle.id()); } Work::Error { handle, error } => { self.raster.cache.insert(&handle, Memory::error(error)); @@ -225,19 +285,22 @@ impl Drop for Cache { } #[cfg(feature = "image")] -#[derive(Debug)] struct Raster { cache: crate::image::raster::Cache, - pending: BTreeSet, + pending: HashMap>, jobs: mpsc::SyncSender, } +#[cfg(feature = "image")] +type Callback = Box; + #[cfg(feature = "image")] fn load_image<'a>( cache: &'a mut crate::image::raster::Cache, - pending: &mut BTreeSet, + pending: &mut HashMap>, jobs: &mut mpsc::SyncSender, handle: &core::image::Handle, + callback: Option, ) -> Option<&'a mut crate::image::raster::Memory> { use crate::image::raster::Memory; @@ -248,9 +311,9 @@ fn load_image<'a>( } else if let core::image::Handle::Rgba { .. } = handle { // Load RGBA handles synchronously, since it's very cheap cache.insert(handle, Memory::load(handle)); - } else if !pending.contains(&handle.id()) { + } else if !pending.contains_key(&handle.id()) { let _ = jobs.send(Job::Load(handle.clone())); - let _ = pending.insert(handle.id()); + let _ = pending.insert(handle.id(), Vec::from_iter(callback)); } } diff --git a/wgpu/src/image/raster.rs b/wgpu/src/image/raster.rs index 57662433..5d54196e 100644 --- a/wgpu/src/image/raster.rs +++ b/wgpu/src/image/raster.rs @@ -5,6 +5,7 @@ use crate::graphics::image::image_rs; use crate::image::atlas::{self, Atlas}; use rustc_hash::{FxHashMap, FxHashSet}; +use std::sync::Weak; type Image = image_rs::ImageBuffer, image::Bytes>; @@ -17,6 +18,7 @@ pub enum Memory { Device { entry: atlas::Entry, bind_group: Option, + allocation: Option>, }, /// Image not found NotFound, @@ -100,7 +102,16 @@ impl Cache { self.map.retain(|k, memory| { let retain = hits.contains(k); - if !retain && let Memory::Device { entry, bind_group } = memory { + if !retain + && let Memory::Device { + entry, + bind_group, + allocation: memory, + } = memory + && memory + .as_ref() + .is_none_or(|memory| memory.strong_count() == 0) + { if let Some(bind_group) = bind_group.take() { on_drop(bind_group); } else { diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 93733fc5..6991564f 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -696,6 +696,17 @@ impl core::Renderer for Renderer { fn reset(&mut self, new_bounds: Rectangle) { self.layers.reset(new_bounds); } + + fn allocate_image( + &mut self, + _handle: &core::image::Handle, + _callback: impl FnOnce(core::image::Allocation) + Send + 'static, + ) { + #[cfg(feature = "image")] + self.image_cache + .get_mut() + .allocate_image(_handle, _callback); + } } impl core::text::Renderer for Renderer { diff --git a/winit/src/lib.rs b/winit/src/lib.rs index cd163f16..764fc1c3 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -50,6 +50,7 @@ use crate::futures::futures::{Future, StreamExt}; use crate::futures::subscription; use crate::futures::{Executor, Runtime}; use crate::graphics::{Compositor, Shell, compositor}; +use crate::runtime::image; use crate::runtime::system; use crate::runtime::user_interface::{self, UserInterface}; use crate::runtime::{Action, Task}; @@ -1730,6 +1731,21 @@ fn run_action<'a, P, C>( } } } + Action::Image(action) => match action { + image::Action::Allocate(handle, sender) => { + use core::Renderer as _; + + // TODO: Shared image cache in compositor + if let Some((_id, window)) = window_manager.iter_mut().next() { + window.renderer.allocate_image( + &handle, + move |allocation| { + let _ = sender.send(allocation); + }, + ); + } + } + }, Action::LoadFont { bytes, channel } => { if let Some(compositor) = compositor { // TODO: Error handling (?)