Introduce explicit image::allocate API

This commit is contained in:
Héctor Ramón Jiménez 2025-10-25 00:07:13 +02:00
parent 6fa54f7f6b
commit 23039e758e
No known key found for this signature in database
GPG key ID: 7CC46565708259A7
15 changed files with 355 additions and 51 deletions

View file

@ -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<Memory>);
/// 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<Memory> {
Arc::downgrade(&self.0)
}
/// Upgrades a [`Weak`] memory reference to an [`Allocation`].
pub fn upgrade(weak: &Weak<Memory>) -> Option<Allocation> {
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

View file

@ -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.

View file

@ -24,6 +24,15 @@ impl Renderer for () {
_background: impl Into<Background>,
) {
}
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 () {

View file

@ -46,8 +46,9 @@ enum Message {
ImagesListed(Result<Vec<Image>, Error>),
ImagePoppedIn(Id),
ImagePoppedOut(Id),
ImageDownloaded(Result<Bytes, Error>),
ImageDownloaded(Result<image::Allocation, Error>),
ThumbnailDownloaded(Id, Result<Bytes, Error>),
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<bool>,
}
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<image::Allocation>,
fade_in: Animation<bool>,
zoom: Animation<bool>,
}
@ -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::Handle>,
image: Option<image::Allocation>,
background_fade_in: Animation<bool>,
image_fade_in: Animation<bool>,
}
@ -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))

View file

@ -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<A, B> core::text::Renderer for Renderer<A, B>

24
runtime/src/image.rs Normal file
View file

@ -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<Allocation>),
}
/// 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<Allocation> {
task::oneshot(|sender| {
crate::Action::Image(Action::Allocate(handle, sender))
})
}

View file

@ -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<T> {
/// 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<T> Action<T> {
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"),
}

View file

@ -379,14 +379,16 @@ impl<T, E> Task<Result<T, E>> {
/// The success value is provided to the closure to create the subsequent [`Task`].
pub fn and_then<A>(
self,
f: impl Fn(T) -> Task<A> + MaybeSend + 'static,
) -> Task<A>
f: impl Fn(T) -> Task<Result<A, E>> + MaybeSend + 'static,
) -> Task<Result<A, E>>
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)
})
}
}

View file

@ -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 {}

View file

@ -257,6 +257,10 @@ impl<P: Program + 'static> Emulator<P> {
// TODO
dbg!(action);
}
iced_runtime::Action::Image(action) => {
// TODO
dbg!(action);
}
runtime::Action::Exit => {
// TODO
}

View file

@ -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 {

View file

@ -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<u32> {
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<core::image::Id>,
pending: HashMap<core::image::Id, Vec<Callback>>,
jobs: mpsc::SyncSender<Job>,
}
#[cfg(feature = "image")]
type Callback = Box<dyn FnOnce(core::image::Allocation) + Send>;
#[cfg(feature = "image")]
fn load_image<'a>(
cache: &'a mut crate::image::raster::Cache,
pending: &mut BTreeSet<core::image::Id>,
pending: &mut HashMap<core::image::Id, Vec<Callback>>,
jobs: &mut mpsc::SyncSender<Job>,
handle: &core::image::Handle,
callback: Option<Callback>,
) -> 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));
}
}

View file

@ -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_rs::Rgba<u8>, image::Bytes>;
@ -17,6 +18,7 @@ pub enum Memory {
Device {
entry: atlas::Entry,
bind_group: Option<wgpu::BindGroup>,
allocation: Option<Weak<image::Memory>>,
},
/// 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 {

View file

@ -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 {

View file

@ -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 (?)