From 2c7c42ee93a61f39562590f6a75eb2dd8b220fb8 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Mon, 31 Oct 2022 13:37:56 -0700 Subject: Move image/svg handling into `iced_graphics` The `TextureStore` trait is implemented by the atlas, and can also be implemented in the glow renderer or in a software renderer. The API here may be improved in the future, but API stability is presumably not a huge issue since these types will only be used by renderer backends. --- graphics/src/image.rs | 34 +++++++ graphics/src/image/raster.rs | 236 +++++++++++++++++++++++++++++++++++++++++++ graphics/src/image/vector.rs | 183 +++++++++++++++++++++++++++++++++ graphics/src/lib.rs | 1 + 4 files changed, 454 insertions(+) create mode 100644 graphics/src/image.rs create mode 100644 graphics/src/image/raster.rs create mode 100644 graphics/src/image/vector.rs (limited to 'graphics/src') diff --git a/graphics/src/image.rs b/graphics/src/image.rs new file mode 100644 index 00000000..f1153882 --- /dev/null +++ b/graphics/src/image.rs @@ -0,0 +1,34 @@ +//! Image loading and caching + +use std::fmt::Debug; + +#[cfg(feature = "image_rs")] +pub mod raster; + +#[cfg(feature = "svg")] +pub mod vector; + +/// Entry in the texture store +pub trait TextureStoreEntry: Debug { + /// Width and height of the entry + fn size(&self) -> (u32, u32); +} + +/// Stores cached image data for use in rendering +pub trait TextureStore { + /// Entry in the texture store + type Entry: TextureStoreEntry; + /// State passed to upload/remove + type State<'a>; + + /// Upload image data + fn upload( + &mut self, + width: u32, + height: u32, + data: &[u8], + state: &mut Self::State<'_>, + ) -> Option; + /// Remome image from store + fn remove(&mut self, entry: &Self::Entry, state: &mut Self::State<'_>); +} diff --git a/graphics/src/image/raster.rs b/graphics/src/image/raster.rs new file mode 100644 index 00000000..f9672dd5 --- /dev/null +++ b/graphics/src/image/raster.rs @@ -0,0 +1,236 @@ +//! Raster image loading and caching + +use iced_native::image; +use std::collections::{HashMap, HashSet}; + +use bitflags::bitflags; + +use super::{TextureStore, TextureStoreEntry}; + +/// Entry in cache corresponding to an image handle +#[derive(Debug)] +pub enum Memory { + /// Image data on host + Host(::image_rs::ImageBuffer<::image_rs::Rgba, Vec>), + /// Texture store entry + Device(T::Entry), + /// Image not found + NotFound, + /// Invalid image data + Invalid, +} + +impl Memory { + /// Width and height of image + pub fn dimensions(&self) -> (u32, u32) { + match self { + Memory::Host(image) => image.dimensions(), + Memory::Device(entry) => entry.size(), + Memory::NotFound => (1, 1), + Memory::Invalid => (1, 1), + } + } +} + +/// Caches image raster data +#[derive(Debug)] +pub struct Cache { + map: HashMap>, + hits: HashSet, +} + +impl Cache { + /// Load image + pub fn load(&mut self, handle: &image::Handle) -> &mut Memory { + if self.contains(handle) { + return self.get(handle).unwrap(); + } + + let memory = match handle.data() { + image::Data::Path(path) => { + if let Ok(image) = image_rs::open(path) { + let operation = std::fs::File::open(path) + .ok() + .map(std::io::BufReader::new) + .and_then(|mut reader| { + Operation::from_exif(&mut reader).ok() + }) + .unwrap_or_else(Operation::empty); + + Memory::Host(operation.perform(image.to_rgba8())) + } else { + Memory::NotFound + } + } + image::Data::Bytes(bytes) => { + if let Ok(image) = image_rs::load_from_memory(bytes) { + let operation = + Operation::from_exif(&mut std::io::Cursor::new(bytes)) + .ok() + .unwrap_or_else(Operation::empty); + + Memory::Host(operation.perform(image.to_rgba8())) + } else { + Memory::Invalid + } + } + image::Data::Pixels { + width, + height, + pixels, + } => { + if let Some(image) = image_rs::ImageBuffer::from_vec( + *width, + *height, + pixels.to_vec(), + ) { + Memory::Host(image) + } else { + Memory::Invalid + } + } + }; + + self.insert(handle, memory); + self.get(handle).unwrap() + } + + /// Load image and upload raster data + pub fn upload( + &mut self, + handle: &image::Handle, + state: &mut T::State<'_>, + store: &mut T, + ) -> Option<&T::Entry> { + let memory = self.load(handle); + + if let Memory::Host(image) = memory { + let (width, height) = image.dimensions(); + + let entry = store.upload(width, height, image, state)?; + + *memory = Memory::Device(entry); + } + + if let Memory::Device(allocation) = memory { + Some(allocation) + } else { + None + } + } + + /// Trim cache misses from cache + pub fn trim(&mut self, store: &mut T, state: &mut T::State<'_>) { + let hits = &self.hits; + + self.map.retain(|k, memory| { + let retain = hits.contains(k); + + if !retain { + if let Memory::Device(entry) = memory { + store.remove(entry, state); + } + } + + retain + }); + + self.hits.clear(); + } + + fn get(&mut self, handle: &image::Handle) -> Option<&mut Memory> { + let _ = self.hits.insert(handle.id()); + + self.map.get_mut(&handle.id()) + } + + fn insert(&mut self, handle: &image::Handle, memory: Memory) { + let _ = self.map.insert(handle.id(), memory); + } + + fn contains(&self, handle: &image::Handle) -> bool { + self.map.contains_key(&handle.id()) + } +} + +impl Default for Cache { + fn default() -> Self { + Self { + map: HashMap::new(), + hits: HashSet::new(), + } + } +} + +bitflags! { + struct Operation: u8 { + const FLIP_HORIZONTALLY = 0b001; + const ROTATE_180 = 0b010; + const FLIP_DIAGONALLY = 0b100; + } +} + +impl Operation { + // Meaning of the returned value is described e.g. at: + // https://magnushoff.com/articles/jpeg-orientation/ + fn from_exif(reader: &mut R) -> Result + where + R: std::io::BufRead + std::io::Seek, + { + let exif = exif::Reader::new().read_from_container(reader)?; + + Ok(exif + .get_field(exif::Tag::Orientation, exif::In::PRIMARY) + .and_then(|field| field.value.get_uint(0)) + .and_then(|value| u8::try_from(value).ok()) + .and_then(|value| Self::from_bits(value.saturating_sub(1))) + .unwrap_or_else(Self::empty)) + } + + fn perform

( + self, + image: image_rs::ImageBuffer>, + ) -> image_rs::ImageBuffer> + where + P: image_rs::Pixel + 'static, + { + use image_rs::imageops; + + let mut image = if self.contains(Self::FLIP_DIAGONALLY) { + flip_diagonally(image) + } else { + image + }; + + if self.contains(Self::ROTATE_180) { + imageops::rotate180_in_place(&mut image); + } + + if self.contains(Self::FLIP_HORIZONTALLY) { + imageops::flip_horizontal_in_place(&mut image); + } + + image + } +} + +fn flip_diagonally( + image: I, +) -> image_rs::ImageBuffer::Subpixel>> +where + I: image_rs::GenericImage, + I::Pixel: 'static, +{ + let (width, height) = image.dimensions(); + let mut out = image_rs::ImageBuffer::new(height, width); + + for x in 0..width { + for y in 0..height { + let p = image.get_pixel(x, y); + + out.put_pixel(y, x, p); + } + } + + out +} diff --git a/graphics/src/image/vector.rs b/graphics/src/image/vector.rs new file mode 100644 index 00000000..0062e2ce --- /dev/null +++ b/graphics/src/image/vector.rs @@ -0,0 +1,183 @@ +//! Vector image loading and caching + +use iced_native::svg; + +use std::collections::{HashMap, HashSet}; +use std::fs; + +use super::TextureStore; + +/// Entry in cache corresponding to an svg handle +pub enum Svg { + /// Parsed svg + Loaded(usvg::Tree), + /// Svg not found or failed to parse + NotFound, +} + +impl Svg { + /// Viewport width and height + pub fn viewport_dimensions(&self) -> (u32, u32) { + match self { + Svg::Loaded(tree) => { + let size = tree.svg_node().size; + + (size.width() as u32, size.height() as u32) + } + Svg::NotFound => (1, 1), + } + } +} + +/// Caches svg vector and raster data +#[derive(Debug)] +pub struct Cache { + svgs: HashMap, + rasterized: HashMap<(u64, u32, u32), T::Entry>, + svg_hits: HashSet, + rasterized_hits: HashSet<(u64, u32, u32)>, +} + +impl Cache { + /// Load svg + pub fn load(&mut self, handle: &svg::Handle) -> &Svg { + if self.svgs.contains_key(&handle.id()) { + return self.svgs.get(&handle.id()).unwrap(); + } + + let svg = match handle.data() { + svg::Data::Path(path) => { + let tree = fs::read_to_string(path).ok().and_then(|contents| { + usvg::Tree::from_str( + &contents, + &usvg::Options::default().to_ref(), + ) + .ok() + }); + + tree.map(Svg::Loaded).unwrap_or(Svg::NotFound) + } + svg::Data::Bytes(bytes) => { + match usvg::Tree::from_data( + bytes, + &usvg::Options::default().to_ref(), + ) { + Ok(tree) => Svg::Loaded(tree), + Err(_) => Svg::NotFound, + } + } + }; + + let _ = self.svgs.insert(handle.id(), svg); + self.svgs.get(&handle.id()).unwrap() + } + + /// Load svg and upload raster data + pub fn upload( + &mut self, + handle: &svg::Handle, + [width, height]: [f32; 2], + scale: f32, + state: &mut T::State<'_>, + texture_store: &mut T, + ) -> Option<&T::Entry> { + let id = handle.id(); + + let (width, height) = ( + (scale * width).ceil() as u32, + (scale * height).ceil() as u32, + ); + + // TODO: Optimize! + // We currently rerasterize the SVG when its size changes. This is slow + // as heck. A GPU rasterizer like `pathfinder` may perform better. + // It would be cool to be able to smooth resize the `svg` example. + if self.rasterized.contains_key(&(id, width, height)) { + let _ = self.svg_hits.insert(id); + let _ = self.rasterized_hits.insert((id, width, height)); + + return self.rasterized.get(&(id, width, height)); + } + + match self.load(handle) { + Svg::Loaded(tree) => { + if width == 0 || height == 0 { + return None; + } + + // TODO: Optimize! + // We currently rerasterize the SVG when its size changes. This is slow + // as heck. A GPU rasterizer like `pathfinder` may perform better. + // It would be cool to be able to smooth resize the `svg` example. + let mut img = tiny_skia::Pixmap::new(width, height)?; + + resvg::render( + tree, + if width > height { + usvg::FitTo::Width(width) + } else { + usvg::FitTo::Height(height) + }, + img.as_mut(), + )?; + + let mut rgba = img.take(); + rgba.chunks_exact_mut(4).for_each(|rgba| rgba.swap(0, 2)); + + let allocation = texture_store.upload( + width, + height, + bytemuck::cast_slice(rgba.as_slice()), + state, + )?; + log::debug!("allocating {} {}x{}", id, width, height); + + let _ = self.svg_hits.insert(id); + let _ = self.rasterized_hits.insert((id, width, height)); + let _ = self.rasterized.insert((id, width, height), allocation); + + self.rasterized.get(&(id, width, height)) + } + Svg::NotFound => None, + } + } + + /// Load svg and upload raster data + pub fn trim(&mut self, texture_store: &mut T, state: &mut T::State<'_>) { + let svg_hits = &self.svg_hits; + let rasterized_hits = &self.rasterized_hits; + + self.svgs.retain(|k, _| svg_hits.contains(k)); + self.rasterized.retain(|k, entry| { + let retain = rasterized_hits.contains(k); + + if !retain { + texture_store.remove(entry, state); + } + + retain + }); + self.svg_hits.clear(); + self.rasterized_hits.clear(); + } +} + +impl Default for Cache { + fn default() -> Self { + Self { + svgs: HashMap::new(), + rasterized: HashMap::new(), + svg_hits: HashSet::new(), + rasterized_hits: HashSet::new(), + } + } +} + +impl std::fmt::Debug for Svg { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Svg::Loaded(_) => write!(f, "Svg::Loaded"), + Svg::NotFound => write!(f, "Svg::NotFound"), + } + } +} diff --git a/graphics/src/lib.rs b/graphics/src/lib.rs index ec28ee58..d39dd90c 100644 --- a/graphics/src/lib.rs +++ b/graphics/src/lib.rs @@ -30,6 +30,7 @@ mod viewport; pub mod backend; pub mod font; pub mod gradient; +pub mod image; pub mod layer; pub mod overlay; pub mod renderer; -- cgit From 8ce8d374b1e8d1d394a42a5ee2bca8af790f0b71 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 5 Nov 2022 03:13:04 +0100 Subject: Refactor some `image` traits a bit - Use `Size` were applicable. - Rename `TextureStore` to `image::Storage`. - Rename `TextureStoreEntry` to `image::storage::Entry`. - Wire up `viewport_dimensions` to `iced_glow` for `Svg`. --- graphics/src/backend.rs | 4 ++-- graphics/src/image.rs | 30 +++--------------------------- graphics/src/image/raster.rs | 32 +++++++++++++++++++------------- graphics/src/image/storage.rs | 31 +++++++++++++++++++++++++++++++ graphics/src/image/vector.rs | 16 ++++++++-------- graphics/src/renderer.rs | 4 ++-- 6 files changed, 65 insertions(+), 52 deletions(-) create mode 100644 graphics/src/image/storage.rs (limited to 'graphics/src') diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs index 7e0af2cc..2f8e9fc3 100644 --- a/graphics/src/backend.rs +++ b/graphics/src/backend.rs @@ -66,11 +66,11 @@ pub trait Text { /// A graphics backend that supports image rendering. pub trait Image { /// Returns the dimensions of the provided image. - fn dimensions(&self, handle: &image::Handle) -> (u32, u32); + fn dimensions(&self, handle: &image::Handle) -> Size; } /// A graphics backend that supports SVG rendering. pub trait Svg { /// Returns the viewport dimensions of the provided SVG. - fn viewport_dimensions(&self, handle: &svg::Handle) -> (u32, u32); + fn viewport_dimensions(&self, handle: &svg::Handle) -> Size; } diff --git a/graphics/src/image.rs b/graphics/src/image.rs index f1153882..04f4ff9d 100644 --- a/graphics/src/image.rs +++ b/graphics/src/image.rs @@ -1,34 +1,10 @@ -//! Image loading and caching - -use std::fmt::Debug; - +//! Render images. #[cfg(feature = "image_rs")] pub mod raster; #[cfg(feature = "svg")] pub mod vector; -/// Entry in the texture store -pub trait TextureStoreEntry: Debug { - /// Width and height of the entry - fn size(&self) -> (u32, u32); -} - -/// Stores cached image data for use in rendering -pub trait TextureStore { - /// Entry in the texture store - type Entry: TextureStoreEntry; - /// State passed to upload/remove - type State<'a>; +pub mod storage; - /// Upload image data - fn upload( - &mut self, - width: u32, - height: u32, - data: &[u8], - state: &mut Self::State<'_>, - ) -> Option; - /// Remome image from store - fn remove(&mut self, entry: &Self::Entry, state: &mut Self::State<'_>); -} +pub use storage::Storage; diff --git a/graphics/src/image/raster.rs b/graphics/src/image/raster.rs index f9672dd5..8ed9615d 100644 --- a/graphics/src/image/raster.rs +++ b/graphics/src/image/raster.rs @@ -1,15 +1,15 @@ -//! Raster image loading and caching +//! Raster image loading and caching. +use crate::image::Storage; +use crate::Size; use iced_native::image; -use std::collections::{HashMap, HashSet}; use bitflags::bitflags; - -use super::{TextureStore, TextureStoreEntry}; +use std::collections::{HashMap, HashSet}; /// Entry in cache corresponding to an image handle #[derive(Debug)] -pub enum Memory { +pub enum Memory { /// Image data on host Host(::image_rs::ImageBuffer<::image_rs::Rgba, Vec>), /// Texture store entry @@ -20,26 +20,32 @@ pub enum Memory { Invalid, } -impl Memory { +impl Memory { /// Width and height of image - pub fn dimensions(&self) -> (u32, u32) { + pub fn dimensions(&self) -> Size { + use crate::image::storage::Entry; + match self { - Memory::Host(image) => image.dimensions(), + Memory::Host(image) => { + let (width, height) = image.dimensions(); + + Size::new(width, height) + } Memory::Device(entry) => entry.size(), - Memory::NotFound => (1, 1), - Memory::Invalid => (1, 1), + Memory::NotFound => Size::new(1, 1), + Memory::Invalid => Size::new(1, 1), } } } /// Caches image raster data #[derive(Debug)] -pub struct Cache { +pub struct Cache { map: HashMap>, hits: HashSet, } -impl Cache { +impl Cache { /// Load image pub fn load(&mut self, handle: &image::Handle) -> &mut Memory { if self.contains(handle) { @@ -153,7 +159,7 @@ impl Cache { } } -impl Default for Cache { +impl Default for Cache { fn default() -> Self { Self { map: HashMap::new(), diff --git a/graphics/src/image/storage.rs b/graphics/src/image/storage.rs new file mode 100644 index 00000000..2098c7b2 --- /dev/null +++ b/graphics/src/image/storage.rs @@ -0,0 +1,31 @@ +//! Store images. +use crate::Size; + +use std::fmt::Debug; + +/// Stores cached image data for use in rendering +pub trait Storage { + /// The type of an [`Entry`] in the [`Storage`]. + type Entry: Entry; + + /// State provided to upload or remove a [`Self::Entry`]. + type State<'a>; + + /// Upload the image data of a [`Self::Entry`]. + fn upload( + &mut self, + width: u32, + height: u32, + data: &[u8], + state: &mut Self::State<'_>, + ) -> Option; + + /// Romve a [`Self::Entry`] from the [`Storage`]. + fn remove(&mut self, entry: &Self::Entry, state: &mut Self::State<'_>); +} + +/// An entry in some [`Storage`], +pub trait Entry: Debug { + /// The [`Size`] of the [`Entry`]. + fn size(&self) -> Size; +} diff --git a/graphics/src/image/vector.rs b/graphics/src/image/vector.rs index 0062e2ce..818fdd20 100644 --- a/graphics/src/image/vector.rs +++ b/graphics/src/image/vector.rs @@ -1,12 +1,12 @@ //! Vector image loading and caching +use crate::image::Storage; use iced_native::svg; +use iced_native::Size; use std::collections::{HashMap, HashSet}; use std::fs; -use super::TextureStore; - /// Entry in cache corresponding to an svg handle pub enum Svg { /// Parsed svg @@ -17,28 +17,28 @@ pub enum Svg { impl Svg { /// Viewport width and height - pub fn viewport_dimensions(&self) -> (u32, u32) { + pub fn viewport_dimensions(&self) -> Size { match self { Svg::Loaded(tree) => { let size = tree.svg_node().size; - (size.width() as u32, size.height() as u32) + Size::new(size.width() as u32, size.height() as u32) } - Svg::NotFound => (1, 1), + Svg::NotFound => Size::new(1, 1), } } } /// Caches svg vector and raster data #[derive(Debug)] -pub struct Cache { +pub struct Cache { svgs: HashMap, rasterized: HashMap<(u64, u32, u32), T::Entry>, svg_hits: HashSet, rasterized_hits: HashSet<(u64, u32, u32)>, } -impl Cache { +impl Cache { /// Load svg pub fn load(&mut self, handle: &svg::Handle) -> &Svg { if self.svgs.contains_key(&handle.id()) { @@ -162,7 +162,7 @@ impl Cache { } } -impl Default for Cache { +impl Default for Cache { fn default() -> Self { Self { svgs: HashMap::new(), diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index cdbc4f40..036b398c 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -183,7 +183,7 @@ where { type Handle = image::Handle; - fn dimensions(&self, handle: &image::Handle) -> (u32, u32) { + fn dimensions(&self, handle: &image::Handle) -> Size { self.backend().dimensions(handle) } @@ -196,7 +196,7 @@ impl svg::Renderer for Renderer where B: Backend + backend::Svg, { - fn dimensions(&self, handle: &svg::Handle) -> (u32, u32) { + fn dimensions(&self, handle: &svg::Handle) -> Size { self.backend().viewport_dimensions(handle) } -- cgit From 438f97a6d00ad0312e7c84b4c1529968bdfba849 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 5 Nov 2022 03:18:13 +0100 Subject: Use RGBA texture for `image` and `svg` pipelines --- graphics/src/image/raster.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'graphics/src') diff --git a/graphics/src/image/raster.rs b/graphics/src/image/raster.rs index 8ed9615d..bad017a9 100644 --- a/graphics/src/image/raster.rs +++ b/graphics/src/image/raster.rs @@ -80,7 +80,7 @@ impl Cache { Memory::Invalid } } - image::Data::Pixels { + image::Data::Rgba { width, height, pixels, -- cgit From 7a24b4ba69e19459646648634c96d6426eaed462 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Sat, 5 Nov 2022 03:39:59 +0100 Subject: Replace `texture_store` and `store` with `storage` --- graphics/src/image/raster.rs | 10 +++++----- graphics/src/image/vector.rs | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) (limited to 'graphics/src') diff --git a/graphics/src/image/raster.rs b/graphics/src/image/raster.rs index bad017a9..da46c30f 100644 --- a/graphics/src/image/raster.rs +++ b/graphics/src/image/raster.rs @@ -12,7 +12,7 @@ use std::collections::{HashMap, HashSet}; pub enum Memory { /// Image data on host Host(::image_rs::ImageBuffer<::image_rs::Rgba, Vec>), - /// Texture store entry + /// Storage entry Device(T::Entry), /// Image not found NotFound, @@ -106,14 +106,14 @@ impl Cache { &mut self, handle: &image::Handle, state: &mut T::State<'_>, - store: &mut T, + storage: &mut T, ) -> Option<&T::Entry> { let memory = self.load(handle); if let Memory::Host(image) = memory { let (width, height) = image.dimensions(); - let entry = store.upload(width, height, image, state)?; + let entry = storage.upload(width, height, image, state)?; *memory = Memory::Device(entry); } @@ -126,7 +126,7 @@ impl Cache { } /// Trim cache misses from cache - pub fn trim(&mut self, store: &mut T, state: &mut T::State<'_>) { + pub fn trim(&mut self, storage: &mut T, state: &mut T::State<'_>) { let hits = &self.hits; self.map.retain(|k, memory| { @@ -134,7 +134,7 @@ impl Cache { if !retain { if let Memory::Device(entry) = memory { - store.remove(entry, state); + storage.remove(entry, state); } } diff --git a/graphics/src/image/vector.rs b/graphics/src/image/vector.rs index 818fdd20..dc271c9e 100644 --- a/graphics/src/image/vector.rs +++ b/graphics/src/image/vector.rs @@ -79,7 +79,7 @@ impl Cache { [width, height]: [f32; 2], scale: f32, state: &mut T::State<'_>, - texture_store: &mut T, + storage: &mut T, ) -> Option<&T::Entry> { let id = handle.id(); @@ -124,7 +124,7 @@ impl Cache { let mut rgba = img.take(); rgba.chunks_exact_mut(4).for_each(|rgba| rgba.swap(0, 2)); - let allocation = texture_store.upload( + let allocation = storage.upload( width, height, bytemuck::cast_slice(rgba.as_slice()), @@ -143,7 +143,7 @@ impl Cache { } /// Load svg and upload raster data - pub fn trim(&mut self, texture_store: &mut T, state: &mut T::State<'_>) { + pub fn trim(&mut self, storage: &mut T, state: &mut T::State<'_>) { let svg_hits = &self.svg_hits; let rasterized_hits = &self.rasterized_hits; @@ -152,7 +152,7 @@ impl Cache { let retain = rasterized_hits.contains(k); if !retain { - texture_store.remove(entry, state); + storage.remove(entry, state); } retain -- cgit