iris: share resource handle bookkeeping

This commit is contained in:
iris committed 2026-09-12 17:13:49 -04:00
1 parent a8093b002b
commit c97df72015
10 files changed
+640 -223

No files matched your search

+11 -8
View File
@@ -818,14 +818,17 @@ event types or manually request redraws.
them by the same string used by text widgets. The public boundaries accept them by the same string used by text widgets. The public boundaries accept
`AsRef<str>`, so an application can use bare strings or put its own semantic `AsRef<str>`, so an application can use bare strings or put its own semantic
enum in front of them without Iris hardcoding the roles. Text buffers, layouts, enum in front of them without Iris hardcoding the roles. Text buffers, layouts,
and prepared glyphs live in a per-`Ui` generational arena; a text widget holds and prepared glyphs live in a per-`Ui` `Resources<TextRsc>` generational arena;
one compact handle into it, while the arena and its shaping state are shared by a text widget's `TextHandle` wraps the UI-local `RscHandle` into it. The same
one `Rc<RefCell<_>>`. Registering another family invalidates the live arena generic arena owns texture and managed-paint lifetimes. Its `StrongRscId` and
entries and dirties their active owner widgets, so registration is also valid `WeakRscId` are sendable IDs rather than data pointers; clone and drop events
after the first shape. ai-app owns the `ai-app-icons` family, its Nerd Fonts cross one arena-owned standard channel, and reference counts remain in the
subset, its codepoints, its license and the script that rebuilds it; the CSS arena instead of allocating one atomic counter per resource. Registering
generic names `sans-serif` and `monospace` continue to resolve through the another family invalidates the live text entries and dirties their active owner
platform. widgets, so registration is also valid after the first shape. ai-app owns the
`ai-app-icons` family, its Nerd Fonts subset, its codepoints, its license and
the script that rebuilds it; the CSS generic names `sans-serif` and `monospace`
continue to resolve through the platform.
`cargo-iris` is an installable Cargo subcommand, rather than a script callers `cargo-iris` is an installable Cargo subcommand, rather than a script callers
must find inside an Iris checkout. `cargo iris apk` builds the Rust `cdylib` must find inside an Iris checkout. `cargo iris apk` builds the Rust `cdylib`
+26 -56
View File
@@ -1,10 +1,5 @@
use crate::util::{Dirty, RefCounter}; use crate::util::{Dirty, Resources, StrongRscId};
use std::{ use std::{cell::RefCell, fmt, rc::Rc};
cell::RefCell,
fmt,
rc::Rc,
sync::mpsc::{Receiver, Sender, channel},
};
/// Encoded, straight-alpha sRGB at an input boundary. /// Encoded, straight-alpha sRGB at an input boundary.
/// ///
@@ -167,30 +162,7 @@ mod private {
impl Sealed for super::PaintId {} impl Sealed for super::PaintId {}
} }
#[derive(Debug)] struct PaintRsc;
struct PaintLease {
slot: u32,
counter: RefCounter,
send: Sender<u32>,
}
impl Clone for PaintLease {
fn clone(&self) -> Self {
Self {
slot: self.slot,
counter: self.counter.clone(),
send: self.send.clone(),
}
}
}
impl Drop for PaintLease {
fn drop(&mut self) {
if self.counter.drop() {
let _ = self.send.send(self.slot);
}
}
}
/// A stable reference to one entry in a UI's paint table. /// A stable reference to one entry in a UI's paint table.
/// ///
@@ -200,7 +172,7 @@ impl Drop for PaintLease {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct PaintId { pub struct PaintId {
slot: u32, slot: u32,
lease: Option<PaintLease>, strong: Option<StrongRscId<PaintRsc>>,
} }
impl PaintId { impl PaintId {
@@ -221,7 +193,7 @@ impl PaintId {
pub const NONE: Self = Self::builtin(14); pub const NONE: Self = Self::builtin(14);
const fn builtin(slot: u32) -> Self { const fn builtin(slot: u32) -> Self {
Self { slot, lease: None } Self { slot, strong: None }
} }
pub(crate) fn slot(&self) -> u32 { pub(crate) fn slot(&self) -> u32 {
@@ -229,7 +201,7 @@ impl PaintId {
} }
fn is_managed(&self) -> bool { fn is_managed(&self) -> bool {
self.lease.is_some() self.strong.is_some()
} }
} }
@@ -346,22 +318,22 @@ const BUILTIN_PAINTS: [Srgba8; 15] = [
/// CPU-side paint table and the dirty set for its GPU mirror. /// CPU-side paint table and the dirty set for its GPU mirror.
pub struct Paints { pub struct Paints {
resources: Resources<PaintRsc>,
entries: Vec<LinearRgba>, entries: Vec<LinearRgba>,
free: Vec<u32>,
dirty: Dirty, dirty: Dirty,
send: Sender<u32>,
recv: Receiver<u32>,
} }
impl Paints { impl Paints {
pub fn new() -> Self { pub fn new() -> Self {
let (send, recv) = channel(); let mut resources = Resources::new();
for (slot, _) in BUILTIN_PAINTS.iter().enumerate() {
let id = resources.add_static(PaintRsc);
assert_eq!(id.slot(), slot as u32);
}
Self { Self {
resources,
entries: BUILTIN_PAINTS.map(Srgba8::to_linear).to_vec(), entries: BUILTIN_PAINTS.map(Srgba8::to_linear).to_vec(),
free: Vec::new(),
dirty: Dirty::new_all(), dirty: Dirty::new_all(),
send,
recv,
} }
} }
@@ -371,23 +343,19 @@ impl Paints {
fn add_linear(&mut self, value: LinearRgba) -> PaintId { fn add_linear(&mut self, value: LinearRgba) -> PaintId {
self.free_released(); self.free_released();
let slot = if let Some(slot) = self.free.pop() { let old_capacity = self.resources.capacity();
let strong = self.resources.add(PaintRsc);
let slot = strong.slot();
if (slot as usize) < old_capacity {
self.entries[slot as usize] = value; self.entries[slot as usize] = value;
self.dirty.mark(slot as usize); self.dirty.mark(slot as usize);
slot
} else { } else {
let slot = self.entries.len() as u32;
self.entries.push(value); self.entries.push(value);
self.dirty.mark(slot as usize); self.dirty.mark(slot as usize);
slot }
};
PaintId { PaintId {
slot, slot,
lease: Some(PaintLease { strong: Some(strong),
slot,
counter: RefCounter::new(),
send: self.send.clone(),
}),
} }
} }
@@ -411,11 +379,13 @@ impl Paints {
} }
pub fn free_released(&mut self) { pub fn free_released(&mut self) {
for slot in self.recv.try_iter() { let entries = &mut self.entries;
self.entries[slot as usize] = LinearRgba::NONE; let dirty = &mut self.dirty;
self.dirty.mark(slot as usize); self.resources.apply(|id, _| {
self.free.push(slot); let slot = id.slot();
} entries[slot as usize] = LinearRgba::NONE;
dirty.mark(slot as usize);
});
} }
/// A new GPU device has no copy of this table even when the CPU-side UI /// A new GPU device has no copy of this table even when the CPU-side UI
+62 -82
View File
@@ -1,6 +1,6 @@
use crate::{ use crate::{
Align, GlyphAtlas, GlyphKey, PaintId, PlacedGlyph, RegionAlign, Textures, WidgetId, Align, GlyphAtlas, GlyphKey, PaintId, PlacedGlyph, RegionAlign, Textures, WidgetId,
util::{SlotId, SlotVec, Vec2}, util::{Resources, RscHandle, StrongRscId, Vec2, WeakRscId},
}; };
use parley::{ use parley::{
Alignment, AlignmentOptions, FontContext, FontFamily, FontStyle, FontWeight, GenericFamily, Alignment, AlignmentOptions, FontContext, FontFamily, FontStyle, FontWeight, GenericFamily,
@@ -8,15 +8,12 @@ use parley::{
fontique::{Blob, FontInfoOverride}, fontique::{Blob, FontInfoOverride},
}; };
use std::{ use std::{
cell::{Ref, RefCell}, cell::{Ref, RefCell, RefMut},
collections::HashMap, collections::HashMap,
fmt, fmt,
ops::{Deref, DerefMut, Range}, ops::{Deref, DerefMut, Range},
rc::Rc, rc::Rc,
sync::{ sync::Arc,
Arc,
mpsc::{Receiver, Sender, channel},
},
}; };
use swash::{ use swash::{
FontRef, FontRef,
@@ -610,7 +607,7 @@ pub struct RenderedText {
pub generation: u64, pub generation: u64,
} }
pub struct TextResource { pub struct TextRsc {
buffer: TextBuffer, buffer: TextBuffer,
attrs: TextAttrs, attrs: TextAttrs,
rendered: Option<RenderedText>, rendered: Option<RenderedText>,
@@ -619,7 +616,7 @@ pub struct TextResource {
owner: Option<WidgetId>, owner: Option<WidgetId>,
} }
impl TextResource { impl TextRsc {
fn new(buffer: TextBuffer, attrs: TextAttrs) -> Self { fn new(buffer: TextBuffer, attrs: TextAttrs) -> Self {
Self { Self {
buffer, buffer,
@@ -641,57 +638,54 @@ impl TextResource {
/// compact [`TextHandle`] into this arena rather than owning Parley layouts. /// compact [`TextHandle`] into this arena rather than owning Parley layouts.
pub struct TextResources { pub struct TextResources {
data: TextData, data: TextData,
entries: SlotVec<TextResource>, entries: Rc<RefCell<Resources<TextRsc>>>,
send: Sender<SlotId>,
recv: Receiver<SlotId>,
} }
impl TextResources { impl TextResources {
pub fn new() -> Self { pub fn new() -> Self {
let (send, recv) = channel();
Self { Self {
data: TextData::default(), data: TextData::default(),
entries: SlotVec::new(), entries: Rc::new(RefCell::new(Resources::new())),
send,
recv,
} }
} }
pub fn add(resources: Rc<RefCell<Self>>, buffer: TextBuffer, attrs: TextAttrs) -> TextHandle { pub fn add(resources: Rc<RefCell<Self>>, buffer: TextBuffer, attrs: TextAttrs) -> TextHandle {
let (id, send) = { let entries = {
let mut resources = resources.borrow_mut(); let mut resources = resources.borrow_mut();
resources.free_released(); resources.free_released();
let id = resources.entries.add(TextResource::new(buffer, attrs)); resources.entries.clone()
(id, resources.send.clone())
}; };
TextHandle { TextHandle {
id, rsc: RscHandle::add(entries, TextRsc::new(buffer, attrs)),
resources, resources,
send,
} }
} }
fn entry(&self, id: SlotId) -> &TextResource { pub fn handle(resources: Rc<RefCell<Self>>, id: StrongRscId<TextRsc>) -> TextHandle {
self.entries let entries = resources.borrow().entries.clone();
.get(id) TextHandle {
.expect("text resource handle points at a released slot") rsc: RscHandle::new(id, entries),
resources,
}
} }
fn entry_mut(&mut self, id: SlotId) -> &mut TextResource { pub fn upgrade(resources: Rc<RefCell<Self>>, id: WeakRscId<TextRsc>) -> Option<TextHandle> {
self.entries let entries = {
.get_mut(id) let mut resources = resources.borrow_mut();
.expect("text resource handle points at a released slot") resources.free_released();
resources.entries.clone()
};
let id = entries.borrow_mut().upgrade(id)?;
Some(Self::handle(resources, id))
} }
pub fn free_released(&mut self) { pub fn free_released(&mut self) {
for id in self.recv.try_iter() { self.entries.borrow_mut().apply(|_, _| {});
self.entries.free(id);
}
} }
pub fn invalidate_all(&mut self) -> Vec<WidgetId> { pub fn invalidate_all(&mut self) -> Vec<WidgetId> {
let mut owners = Vec::new(); let mut owners = Vec::new();
for resource in self.entries.values_mut() { for resource in self.entries.borrow_mut().values_mut() {
resource.invalidate(); resource.invalidate();
if let Some(owner) = resource.owner if let Some(owner) = resource.owner
&& !owners.contains(&owner) && !owners.contains(&owner)
@@ -702,27 +696,22 @@ impl TextResources {
owners owners
} }
fn shape(&mut self, id: SlotId) { fn shape(&mut self, resource: &mut TextRsc) {
let density = self.data.density; let density = self.data.density;
let Self { data, entries, .. } = self;
let resource = entries
.get_mut(id)
.expect("text resource handle points at a released slot");
resource resource
.buffer .buffer
.shape(data, &resource.attrs, resource.width, density); .shape(&mut self.data, &resource.attrs, resource.width, density);
} }
fn render( fn render(
&mut self, &mut self,
id: SlotId, resource: &mut TextRsc,
width: Option<f32>, width: Option<f32>,
owner: WidgetId, owner: WidgetId,
textures: &mut Textures, textures: &mut Textures,
density: f32, density: f32,
) -> (RenderedText, bool) { ) -> (RenderedText, bool) {
let atlas_generation = self.data.atlas.generation(); let atlas_generation = self.data.atlas.generation();
let resource = self.entry_mut(id);
resource.owner = Some(owner); resource.owner = Some(owner);
if resource.width == width if resource.width == width
&& resource.density == density && resource.density == density
@@ -733,11 +722,7 @@ impl TextResources {
} }
resource.width = width; resource.width = width;
resource.density = density; resource.density = density;
let Self { data, entries, .. } = self; let rendered = self.data.render(
let resource = entries
.get_mut(id)
.expect("text resource handle points at a released slot");
let rendered = data.render(
&mut resource.buffer, &mut resource.buffer,
&resource.attrs, &resource.attrs,
width, width,
@@ -773,28 +758,30 @@ impl DerefMut for TextResources {
/// shared once per UI; constructing a handle does not allocate a resource of /// shared once per UI; constructing a handle does not allocate a resource of
/// its own. /// its own.
pub struct TextHandle { pub struct TextHandle {
id: SlotId, rsc: RscHandle<TextRsc>,
resources: Rc<RefCell<TextResources>>, resources: Rc<RefCell<TextResources>>,
send: Sender<SlotId>,
} }
impl TextHandle { impl TextHandle {
pub fn strong(&self) -> StrongRscId<TextRsc> {
self.rsc.strong()
}
pub fn weak(&self) -> WeakRscId<TextRsc> {
self.rsc.weak()
}
pub fn text(&self) -> Ref<'_, str> { pub fn text(&self) -> Ref<'_, str> {
Ref::map(self.resources.borrow(), |resources| { Ref::map(self.rsc.get(), |resource| resource.buffer.text())
resources.entry(self.id).buffer.text()
})
} }
pub fn attrs(&self) -> Ref<'_, TextAttrs> { pub fn attrs(&self) -> Ref<'_, TextAttrs> {
Ref::map(self.resources.borrow(), |resources| { Ref::map(self.rsc.get(), |resource| &resource.attrs)
&resources.entry(self.id).attrs
})
} }
pub fn set_text(&mut self, text: impl Into<String>) -> bool { pub fn set_text(&mut self, text: impl Into<String>) -> bool {
let text = text.into(); let text = text.into();
let mut resources = self.resources.borrow_mut(); let mut resource = self.rsc.get_mut();
let resource = resources.entry_mut(self.id);
if resource.buffer.text() == text { if resource.buffer.text() == text {
return false; return false;
} }
@@ -804,46 +791,45 @@ impl TextHandle {
} }
pub fn edit_text<R>(&mut self, edit: impl FnOnce(&mut String) -> R) -> R { pub fn edit_text<R>(&mut self, edit: impl FnOnce(&mut String) -> R) -> R {
let mut resources = self.resources.borrow_mut(); let mut resource = self.rsc.get_mut();
let resource = resources.entry_mut(self.id);
resource.rendered = None; resource.rendered = None;
edit(resource.buffer.edit()) edit(resource.buffer.edit())
} }
pub fn set_spans(&mut self, spans: Vec<SpanStyle>) { pub fn set_spans(&mut self, spans: Vec<SpanStyle>) {
let mut resources = self.resources.borrow_mut(); let mut resource = self.rsc.get_mut();
let resource = resources.entry_mut(self.id);
resource.buffer.set_spans(spans); resource.buffer.set_spans(spans);
resource.rendered = None; resource.rendered = None;
} }
pub fn update_attrs<R>(&mut self, update: impl FnOnce(&mut TextAttrs) -> R) -> R { pub fn attrs_mut(&mut self) -> RefMut<'_, TextAttrs> {
let mut resources = self.resources.borrow_mut(); let mut resource = self.rsc.get_mut();
let resource = resources.entry_mut(self.id);
resource.invalidate(); resource.invalidate();
update(&mut resource.attrs) RefMut::map(resource, |resource| &mut resource.attrs)
} }
pub fn rendered(&self) -> Option<RenderedText> { pub fn rendered(&self) -> Option<RenderedText> {
self.resources.borrow().entry(self.id).rendered.clone() self.rsc.get().rendered.clone()
} }
pub fn width(&self) -> Option<f32> { pub fn width(&self) -> Option<f32> {
self.resources.borrow().entry(self.id).width self.rsc.get().width
} }
pub fn with_layout<R>(&self, f: impl FnOnce(&Layout<PaintId>, &str) -> R) -> R { pub fn with_layout<R>(&self, f: impl FnOnce(&Layout<PaintId>, &str) -> R) -> R {
let mut resources = self.resources.borrow_mut(); let mut resources = self.resources.borrow_mut();
resources.shape(self.id); let mut resource = self.rsc.get_mut_shared();
let resource = resources.entry(self.id); resources.shape(&mut resource);
f(resource.buffer.layout(), resource.buffer.text()) f(resource.buffer.layout(), resource.buffer.text())
} }
pub fn layout(&self) -> Ref<'_, Layout<PaintId>> { pub fn layout(&self) -> Ref<'_, Layout<PaintId>> {
self.resources.borrow_mut().shape(self.id); {
Ref::map(self.resources.borrow(), |resources| { let mut resources = self.resources.borrow_mut();
resources.entry(self.id).buffer.layout() let mut resource = self.rsc.get_mut_shared();
}) resources.shape(&mut resource);
}
Ref::map(self.rsc.get(), |resource| resource.buffer.layout())
} }
pub fn render( pub fn render(
@@ -853,15 +839,9 @@ impl TextHandle {
textures: &mut Textures, textures: &mut Textures,
density: f32, density: f32,
) -> (RenderedText, bool) { ) -> (RenderedText, bool) {
self.resources let mut resources = self.resources.borrow_mut();
.borrow_mut() let mut resource = self.rsc.get_mut_shared();
.render(self.id, width, owner, textures, density) resources.render(&mut resource, width, owner, textures, density)
}
}
impl Drop for TextHandle {
fn drop(&mut self) {
let _ = self.send.send(self.id);
} }
} }
@@ -886,11 +866,11 @@ mod tests {
TextBuffer::new("temporary"), TextBuffer::new("temporary"),
TextAttrs::default(), TextAttrs::default(),
); );
assert_eq!(resources.borrow().entries.len(), 1); assert_eq!(resources.borrow().entries.borrow().len(), 1);
drop(text); drop(text);
resources.borrow_mut().free_released(); resources.borrow_mut().free_released();
assert!(resources.borrow().entries.is_empty()); assert!(resources.borrow().entries.borrow().is_empty());
} }
} }
+84 -66
View File
@@ -1,10 +1,6 @@
use crate::util::{RefCounter, Vec2}; use crate::util::{Resources, RscHandle, StrongRscId, Vec2, WeakRscId};
use image::{DynamicImage, GenericImageView}; use image::{DynamicImage, GenericImageView};
use std::{ use std::{cell::RefCell, collections::HashMap, ops::Index, rc::Rc};
collections::HashMap,
ops::Index,
sync::mpsc::{Receiver, Sender, channel},
};
/// Which of the two things a texture slot holds. See TEXTURES.md's /// Which of the two things a texture slot holds. See TEXTURES.md's
/// "Recommended shape" for why these are drawn so differently: a page is a /// "Recommended shape" for why these are drawn so differently: a page is a
@@ -28,19 +24,20 @@ pub struct SharedTextureKey {
pub id: u64, pub id: u64,
} }
#[derive(Debug, Clone)] pub struct TextureRsc {
pub struct TextureHandle {
slot: u32,
kind: TextureKind, kind: TextureKind,
size: Vec2, size: Vec2,
counter: RefCounter, }
send: Sender<(TextureKind, u32)>,
#[derive(Debug, Clone)]
pub struct TextureHandle {
rsc: RscHandle<TextureRsc>,
} }
/// a texture manager for a ui /// a texture manager for a ui
/// note that this is heavily oriented towards wgpu's renderer so the primitives don't need mapped /// note that this is heavily oriented towards wgpu's renderer so the primitives don't need mapped
pub struct Textures { pub struct Textures {
free: Vec<u32>, resources: Rc<RefCell<Resources<TextureRsc>>>,
images: Vec<Option<DynamicImage>>, images: Vec<Option<DynamicImage>>,
kinds: Vec<TextureKind>, kinds: Vec<TextureKind>,
/// Textures built from a description rather than from a file, one per /// Textures built from a description rather than from a file, one per
@@ -48,12 +45,10 @@ pub struct Textures {
/// reference of its own, so a shared texture outlives every widget /// reference of its own, so a shared texture outlives every widget
/// drawing it and its slot is never recycled underneath one. /// drawing it and its slot is never recycled underneath one.
shared: HashMap<SharedTextureKey, TextureHandle>, shared: HashMap<SharedTextureKey, TextureHandle>,
/// Next layer to hand out to an atlas page. Pages are never freed (no /// Next layer to hand out to an atlas page. Page layers and resource slots
/// atlas eviction), so this only grows and `free` never holds one. /// are never reused, even if an explicit atlas clear drops their handles.
next_page_layer: u32, next_page_layer: u32,
updates: Vec<Update>, updates: Vec<Update>,
send: Sender<(TextureKind, u32)>,
recv: Receiver<(TextureKind, u32)>,
} }
pub enum TextureUpdate<'a> { pub enum TextureUpdate<'a> {
@@ -86,16 +81,13 @@ enum Update {
impl Textures { impl Textures {
pub fn new() -> Self { pub fn new() -> Self {
let (send, recv) = channel();
Self { Self {
free: Vec::new(), resources: Rc::new(RefCell::new(Resources::new())),
images: Vec::new(), images: Vec::new(),
kinds: Vec::new(), kinds: Vec::new(),
shared: HashMap::new(), shared: HashMap::new(),
next_page_layer: 0, next_page_layer: 0,
updates: Vec::new(), updates: Vec::new(),
send,
recv,
} }
} }
@@ -103,14 +95,7 @@ impl Textures {
let image = image.into(); let image = image.into();
let size = image.dimensions().into(); let size = image.dimensions().into();
let kind = TextureKind::Image; let kind = TextureKind::Image;
let slot = self.push(kind, image); self.push(kind, size, image, true)
TextureHandle {
slot,
kind,
size,
counter: RefCounter::new(),
send: self.send.clone(),
}
} }
pub fn add_page(&mut self, image: impl Into<DynamicImage>) -> TextureHandle { pub fn add_page(&mut self, image: impl Into<DynamicImage>) -> TextureHandle {
@@ -119,28 +104,49 @@ impl Textures {
let layer = self.next_page_layer; let layer = self.next_page_layer;
self.next_page_layer += 1; self.next_page_layer += 1;
let kind = TextureKind::Page { layer }; let kind = TextureKind::Page { layer };
let slot = self.push(kind, image); self.push(kind, size, image, false)
}
pub fn handle(&self, id: StrongRscId<TextureRsc>) -> TextureHandle {
TextureHandle { TextureHandle {
slot, rsc: RscHandle::new(id, self.resources.clone()),
kind,
size,
counter: RefCounter::new(),
send: self.send.clone(),
} }
} }
fn push(&mut self, kind: TextureKind, image: DynamicImage) -> u32 { pub fn upgrade(&mut self, id: WeakRscId<TextureRsc>) -> Option<TextureHandle> {
if let Some(i) = self.free.pop() { self.free();
let id = self.resources.borrow_mut().upgrade(id)?;
Some(self.handle(id))
}
fn push(
&mut self,
kind: TextureKind,
size: Vec2,
image: DynamicImage,
recycle: bool,
) -> TextureHandle {
self.free();
let old_capacity = self.resources.borrow().capacity();
let id = if recycle {
self.resources.borrow_mut().add(TextureRsc { kind, size })
} else {
self.resources
.borrow_mut()
.add_unrecycled(TextureRsc { kind, size })
};
let i = id.slot();
if (i as usize) < old_capacity {
self.images[i as usize] = Some(image); self.images[i as usize] = Some(image);
self.kinds[i as usize] = kind; self.kinds[i as usize] = kind;
self.updates.push(Update::Set(kind, i)); self.updates.push(Update::Set(kind, i));
i
} else { } else {
let i = self.images.len() as u32;
self.images.push(Some(image)); self.images.push(Some(image));
self.kinds.push(kind); self.kinds.push(kind);
self.updates.push(Update::Push(kind, i)); self.updates.push(Update::Push(kind, i));
i }
TextureHandle {
rsc: RscHandle::new(id, self.resources.clone()),
} }
} }
@@ -161,13 +167,14 @@ impl Textures {
} }
pub fn image_mut(&mut self, handle: &TextureHandle) -> &mut DynamicImage { pub fn image_mut(&mut self, handle: &TextureHandle) -> &mut DynamicImage {
self.images[handle.slot as usize] self.images[handle.rsc.id().slot() as usize]
.as_mut() .as_mut()
.expect("texture was freed while still held") .expect("texture was freed while still held")
} }
pub fn patch(&mut self, handle: &TextureHandle, rect: PatchRect) { pub fn patch(&mut self, handle: &TextureHandle, rect: PatchRect) {
self.updates.push(Update::Patch(handle.slot, rect)); self.updates
.push(Update::Patch(handle.rsc.id().slot(), rect));
} }
/// A new device starts with no textures, and the renderer-side mirror /// A new device starts with no textures, and the renderer-side mirror
@@ -185,24 +192,20 @@ impl Textures {
/// ones after them still land where they were. /// ones after them still land where they were.
pub fn reupload(&mut self) { pub fn reupload(&mut self) {
self.updates.clear(); self.updates.clear();
self.updates self.updates.extend(
.extend((0..self.images.len() as u32).map(|i| Update::Push(self.kinds[i as usize], i))); (0..self.resources.borrow().capacity() as u32)
.map(|i| Update::Push(self.kinds[i as usize], i)),
);
} }
pub fn free(&mut self) { pub fn free(&mut self) {
for (kind, idx) in self.recv.try_iter() { let updates = &mut self.updates;
self.images[idx as usize] = None; let images = &mut self.images;
self.updates.push(Update::Free(idx)); self.resources.borrow_mut().apply(|id, _| {
// A page's slot is never reclaimed: `GlyphAtlas` never drops the let idx = id.slot();
// handles it holds, and there is no eviction path for a hole in images[idx as usize] = None;
// the middle of the array's layers. If that ever changes, this updates.push(Update::Free(idx));
// is where a freed page's layer would need to go on a free list });
// of its own, separate from `free`, which only ever holds
// ordinary image slots today.
if kind == TextureKind::Image {
self.free.push(idx);
}
}
} }
pub fn updates(&mut self) -> impl Iterator<Item = TextureUpdate<'_>> { pub fn updates(&mut self) -> impl Iterator<Item = TextureUpdate<'_>> {
@@ -226,7 +229,7 @@ impl Textures {
impl TextureHandle { impl TextureHandle {
pub fn size(&self) -> Vec2 { pub fn size(&self) -> Vec2 {
self.size self.rsc.get().size
} }
/// The bind-group index this handle draws with. Only valid for a /// The bind-group index this handle draws with. Only valid for a
@@ -235,25 +238,25 @@ impl TextureHandle {
/// a caller bug (the wrong kind of handle reached the wrong draw path), /// a caller bug (the wrong kind of handle reached the wrong draw path),
/// not a recoverable condition, so it panics rather than drawing garbage. /// not a recoverable condition, so it panics rather than drawing garbage.
pub fn image_index(&self) -> u32 { pub fn image_index(&self) -> u32 {
match self.kind { match self.rsc.get().kind {
TextureKind::Image => self.slot, TextureKind::Image => self.rsc.id().slot(),
TextureKind::Page { .. } => panic!("image_index() called on an atlas page handle"), TextureKind::Page { .. } => panic!("image_index() called on an atlas page handle"),
} }
} }
pub fn layer(&self) -> u32 { pub fn layer(&self) -> u32 {
match self.kind { match self.rsc.get().kind {
TextureKind::Page { layer } => layer, TextureKind::Page { layer } => layer,
TextureKind::Image => panic!("layer() called on a standalone image handle"), TextureKind::Image => panic!("layer() called on a standalone image handle"),
} }
} }
pub fn strong(&self) -> StrongRscId<TextureRsc> {
self.rsc.strong()
} }
impl Drop for TextureHandle { pub fn weak(&self) -> WeakRscId<TextureRsc> {
fn drop(&mut self) { self.rsc.weak()
if self.counter.drop() {
let _ = self.send.send((self.kind, self.slot));
}
} }
} }
@@ -261,7 +264,9 @@ impl Index<&TextureHandle> for Textures {
type Output = DynamicImage; type Output = DynamicImage;
fn index(&self, index: &TextureHandle) -> &Self::Output { fn index(&self, index: &TextureHandle) -> &Self::Output {
self.images[index.slot as usize].as_ref().unwrap() self.images[index.rsc.id().slot() as usize]
.as_ref()
.unwrap()
} }
} }
@@ -315,6 +320,19 @@ mod tests {
); );
} }
#[test]
fn a_released_atlas_page_slot_is_not_reused_by_an_image() {
let mut textures = Textures::new();
let page = textures.add_page(image(4));
let page_slot = page.rsc.id().slot();
drop(page);
textures.free();
let plain = textures.add(image(4));
assert_ne!(plain.image_index(), page_slot);
}
#[test] #[test]
fn reupload_replays_every_slot_in_order_including_the_empty_ones() { fn reupload_replays_every_slot_in_order_including_the_empty_ones() {
let mut textures = Textures::new(); let mut textures = Textures::new();
+1 -2
View File
@@ -81,14 +81,13 @@ impl GlyphAtlas {
let rgba = img.as_mut_rgba8().expect("atlas page is rgba8"); let rgba = img.as_mut_rgba8().expect("atlas page is rgba8");
write_glyph(rgba, image, x, y); write_glyph(rgba, image, x, y);
let handle = page.handle.clone();
let rect = PatchRect { let rect = PatchRect {
x, x,
y, y,
width: w, width: w,
height: h, height: h,
}; };
textures.patch(&handle, rect); textures.patch(&page.handle, rect);
let page = &self.pages[page_idx]; let page = &self.pages[page_idx];
let scale = 1.0 / PAGE as f32; let scale = 1.0 / PAGE as f32;
+1 -1
View File
@@ -182,8 +182,8 @@ pub trait UiRsc {
while let Some(id) = self.widgets_mut().free_next() { while let Some(id) = self.widgets_mut().free_next() {
self.on_remove(id); self.on_remove(id);
} }
self.ui_mut().text.borrow_mut().free_released();
self.ui_mut().textures.free(); self.ui_mut().textures.free();
self.ui_mut().paints.free_released(); self.ui_mut().paints.free_released();
self.ui_mut().text.borrow_mut().free_released();
} }
} }
+2
View File
@@ -5,6 +5,7 @@ mod dirty;
mod id; mod id;
mod math; mod math;
mod refcount; mod refcount;
mod resources;
mod slot; mod slot;
mod trust; mod trust;
mod typemap; mod typemap;
@@ -17,6 +18,7 @@ pub use dirty::*;
pub use id::*; pub use id::*;
pub use math::*; pub use math::*;
pub use refcount::*; pub use refcount::*;
pub use resources::*;
pub use slot::*; pub use slot::*;
pub use trust::*; pub use trust::*;
pub use typemap::*; pub use typemap::*;
+414
View File
@@ -0,0 +1,414 @@
use super::{SlotId, SlotVec};
use std::{
cell::{Ref, RefCell, RefMut},
fmt,
hash::{Hash, Hasher},
marker::PhantomData,
rc::Rc,
sync::mpsc::{Receiver, Sender, channel},
};
enum Event {
Clone(SlotId),
Drop(SlotId),
}
struct Entry<T> {
value: T,
strong: Option<u32>,
recycle: bool,
}
/// Generational storage and deferred strong-reference accounting for one kind
/// of UI resource.
pub struct Resources<T> {
entries: SlotVec<Entry<T>>,
send: Sender<Event>,
recv: Receiver<Event>,
}
impl<T> Resources<T> {
pub fn new() -> Self {
let (send, recv) = channel();
Self {
entries: SlotVec::new(),
send,
recv,
}
}
pub fn add(&mut self, value: T) -> StrongRscId<T> {
self.add_with(value, true)
}
pub fn add_unrecycled(&mut self, value: T) -> StrongRscId<T> {
self.add_with(value, false)
}
fn add_with(&mut self, value: T, recycle: bool) -> StrongRscId<T> {
let id = self.entries.add(Entry {
value,
strong: Some(1),
recycle,
});
StrongRscId::new(id, self.send.clone())
}
/// Add an entry whose lifetime is the lifetime of the arena itself.
pub fn add_static(&mut self, value: T) -> WeakRscId<T> {
WeakRscId::new(self.entries.add(Entry {
value,
strong: None,
recycle: false,
}))
}
pub fn apply(&mut self, mut dropped: impl FnMut(SlotId, T)) {
for event in self.recv.try_iter() {
match event {
Event::Clone(id) => {
let entry = self
.entries
.get_mut(id)
.expect("cloned resource id points at a released slot");
let strong = entry
.strong
.as_mut()
.expect("a static resource cannot have a strong id");
*strong = strong.checked_add(1).expect("resource reference overflow");
}
Event::Drop(id) => {
let remove = {
let entry = self
.entries
.get_mut(id)
.expect("dropped resource id points at a released slot");
let strong = entry
.strong
.as_mut()
.expect("a static resource cannot have a strong id");
*strong = strong.checked_sub(1).expect("resource reference underflow");
*strong == 0
};
if remove {
let recycle = self.entries.get(id).unwrap().recycle;
let entry = if recycle {
self.entries.remove(id)
} else {
self.entries.remove_unrecycled(id)
}
.unwrap();
dropped(id, entry.value);
}
}
}
}
}
/// The owning manager must call [`Self::apply`] first so a queued final
/// drop cannot be mistaken for a still-live entry.
pub fn upgrade(&mut self, id: WeakRscId<T>) -> Option<StrongRscId<T>> {
let entry = self.entries.get_mut(id.id)?;
let strong = entry.strong.as_mut()?;
*strong = strong.checked_add(1).expect("resource reference overflow");
Some(StrongRscId::new(id.id, self.send.clone()))
}
pub fn get(&self, id: impl RscId<T>) -> Option<&T> {
Some(&self.entries.get(id.rsc_id())?.value)
}
pub fn get_mut(&mut self, id: impl RscId<T>) -> Option<&mut T> {
Some(&mut self.entries.get_mut(id.rsc_id())?.value)
}
pub fn values_mut(&mut self) -> impl Iterator<Item = &mut T> {
self.entries.values_mut().map(|entry| &mut entry.value)
}
pub fn capacity(&self) -> usize {
self.entries.capacity()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
impl<T> Default for Resources<T> {
fn default() -> Self {
Self::new()
}
}
pub trait RscId<T> {
fn rsc_id(&self) -> SlotId;
}
/// A sendable owning ID for one entry in [`Resources`]. It keeps the entry
/// alive but does not provide access to its value.
pub struct StrongRscId<T> {
id: SlotId,
send: Sender<Event>,
ty: PhantomData<fn() -> T>,
}
impl<T> StrongRscId<T> {
fn new(id: SlotId, send: Sender<Event>) -> Self {
Self {
id,
send,
ty: PhantomData,
}
}
pub fn weak(&self) -> WeakRscId<T> {
WeakRscId::new(self.id)
}
pub fn id(&self) -> SlotId {
self.id
}
pub(crate) fn slot(&self) -> u32 {
self.id.slot()
}
}
impl<T> Clone for StrongRscId<T> {
fn clone(&self) -> Self {
let _ = self.send.send(Event::Clone(self.id));
Self::new(self.id, self.send.clone())
}
}
impl<T> Drop for StrongRscId<T> {
fn drop(&mut self) {
let _ = self.send.send(Event::Drop(self.id));
}
}
impl<T> RscId<T> for StrongRscId<T> {
fn rsc_id(&self) -> SlotId {
self.id
}
}
impl<T> RscId<T> for &StrongRscId<T> {
fn rsc_id(&self) -> SlotId {
self.id
}
}
impl<T> fmt::Debug for StrongRscId<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.id.fmt(f)
}
}
impl<T> PartialEq for StrongRscId<T> {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl<T> Eq for StrongRscId<T> {}
impl<T> Hash for StrongRscId<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
/// A sendable, copyable ID that does not keep its [`Resources`] entry alive.
pub struct WeakRscId<T> {
id: SlotId,
ty: PhantomData<fn() -> T>,
}
impl<T> WeakRscId<T> {
fn new(id: SlotId) -> Self {
Self {
id,
ty: PhantomData,
}
}
pub fn id(self) -> SlotId {
self.id
}
pub(crate) fn slot(self) -> u32 {
self.id.slot()
}
}
impl<T> Clone for WeakRscId<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for WeakRscId<T> {}
impl<T> RscId<T> for WeakRscId<T> {
fn rsc_id(&self) -> SlotId {
self.id
}
}
impl<T> RscId<T> for &WeakRscId<T> {
fn rsc_id(&self) -> SlotId {
self.id
}
}
impl<T> fmt::Debug for WeakRscId<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.id.fmt(f)
}
}
impl<T> PartialEq for WeakRscId<T> {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl<T> Eq for WeakRscId<T> {}
impl<T> Hash for WeakRscId<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
/// Convenient UI-thread access to a resource through an owning ID. The `Rc`
/// deliberately makes this local; tasks carry strong or weak IDs instead.
pub struct RscHandle<T> {
id: StrongRscId<T>,
resources: Rc<RefCell<Resources<T>>>,
}
impl<T> RscHandle<T> {
pub fn new(id: StrongRscId<T>, resources: Rc<RefCell<Resources<T>>>) -> Self {
Self { id, resources }
}
pub fn add(resources: Rc<RefCell<Resources<T>>>, value: T) -> Self {
let id = resources.borrow_mut().add(value);
Self::new(id, resources)
}
pub fn get(&self) -> Ref<'_, T> {
Ref::map(self.resources.borrow(), |resources| {
resources
.get(&self.id)
.expect("resource handle points at a released slot")
})
}
pub fn get_mut(&mut self) -> RefMut<'_, T> {
RefMut::map(self.resources.borrow_mut(), |resources| {
resources
.get_mut(&self.id)
.expect("resource handle points at a released slot")
})
}
pub(crate) fn get_mut_shared(&self) -> RefMut<'_, T> {
RefMut::map(self.resources.borrow_mut(), |resources| {
resources
.get_mut(&self.id)
.expect("resource handle points at a released slot")
})
}
pub fn strong(&self) -> StrongRscId<T> {
self.id.clone()
}
pub fn weak(&self) -> WeakRscId<T> {
self.id.weak()
}
pub fn id(&self) -> SlotId {
self.id.id()
}
}
impl<T> Clone for RscHandle<T> {
fn clone(&self) -> Self {
Self::new(self.id.clone(), self.resources.clone())
}
}
impl<T> fmt::Debug for RscHandle<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.id.fmt(f)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_slot_lives_until_every_strong_id_is_dropped() {
let mut resources = Resources::new();
let first = resources.add("value");
let weak = first.weak();
let second = first.clone();
drop(first);
resources.apply(|_, _| {});
assert_eq!(resources.get(weak), Some(&"value"));
drop(second);
resources.apply(|_, _| {});
assert_eq!(resources.get(weak), None);
}
#[test]
fn a_weak_id_can_be_upgraded_while_the_resource_is_alive() {
let mut resources = Resources::new();
let first = resources.add("value");
let weak = first.weak();
let second = resources.upgrade(weak).unwrap();
drop(first);
resources.apply(|_, _| {});
assert_eq!(resources.get(&second), Some(&"value"));
}
#[test]
fn strong_and_weak_ids_can_cross_threads() {
fn assert_send<T: Send>() {}
assert_send::<StrongRscId<String>>();
assert_send::<WeakRscId<String>>();
let mut resources = Resources::new();
let first = resources.add("value");
let second = first.clone();
std::thread::spawn(move || drop(second)).join().unwrap();
drop(first);
resources.apply(|_, _| {});
assert!(resources.is_empty());
}
#[test]
fn an_unrecycled_slot_stays_a_hole() {
let mut resources = Resources::new();
let first = resources.add_unrecycled("first");
let first_slot = first.slot();
drop(first);
resources.apply(|_, _| {});
let second = resources.add("second");
assert_ne!(second.slot(), first_slot);
assert_eq!(resources.len(), 1);
}
}
+35 -4
View File
@@ -5,6 +5,10 @@ pub struct SlotId {
} }
impl SlotId { impl SlotId {
pub(crate) fn slot(self) -> u32 {
self.idx
}
/// A stable, collision-free `u64` encoding of this id -- for a caller /// A stable, collision-free `u64` encoding of this id -- for a caller
/// (accesskit's `NodeId`, today) that wants a flat integer key rather /// (accesskit's `NodeId`, today) that wants a flat integer key rather
/// than the two `u32`s. `idx` is offset by one so no real id ever /// than the two `u32`s. `idx` is offset by one so no real id ever
@@ -18,6 +22,7 @@ impl SlotId {
pub struct SlotVec<T> { pub struct SlotVec<T> {
data: Vec<(u32, Option<T>)>, data: Vec<(u32, Option<T>)>,
free: Vec<u32>, free: Vec<u32>,
len: usize,
} }
impl<T> SlotVec<T> { impl<T> SlotVec<T> {
@@ -25,11 +30,12 @@ impl<T> SlotVec<T> {
Self { Self {
data: Default::default(), data: Default::default(),
free: Default::default(), free: Default::default(),
len: 0,
} }
} }
pub fn add(&mut self, x: T) -> SlotId { pub fn add(&mut self, x: T) -> SlotId {
if let Some(idx) = self.free.pop() { let id = if let Some(idx) = self.free.pop() {
let (genr, data) = &mut self.data[idx as usize]; let (genr, data) = &mut self.data[idx as usize];
*data = Some(x); *data = Some(x);
SlotId { idx, genr: *genr } SlotId { idx, genr: *genr }
@@ -38,15 +44,36 @@ impl<T> SlotVec<T> {
let genr = 0; let genr = 0;
self.data.push((genr, Some(x))); self.data.push((genr, Some(x)));
SlotId { idx, genr } SlotId { idx, genr }
} };
self.len += 1;
id
} }
pub fn free(&mut self, id: SlotId) { pub fn free(&mut self, id: SlotId) {
let _ = self.remove(id);
}
pub fn remove(&mut self, id: SlotId) -> Option<T> {
self.remove_inner(id, true)
}
pub fn remove_unrecycled(&mut self, id: SlotId) -> Option<T> {
self.remove_inner(id, false)
}
fn remove_inner(&mut self, id: SlotId, recycle: bool) -> Option<T> {
let (genr, data) = &mut self.data[id.idx as usize]; let (genr, data) = &mut self.data[id.idx as usize];
if *genr != id.genr {
return None;
}
*genr += 1; *genr += 1;
*data = None; let value = data.take()?;
self.len -= 1;
if recycle {
self.free.push(id.idx); self.free.push(id.idx);
} }
Some(value)
}
pub fn get(&self, id: SlotId) -> Option<&T> { pub fn get(&self, id: SlotId) -> Option<&T> {
let slot = &self.data[id.idx as usize]; let slot = &self.data[id.idx as usize];
@@ -65,7 +92,7 @@ impl<T> SlotVec<T> {
} }
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.data.len() - self.free.len() self.len
} }
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
@@ -75,6 +102,10 @@ impl<T> SlotVec<T> {
pub fn values_mut(&mut self) -> impl Iterator<Item = &mut T> { pub fn values_mut(&mut self) -> impl Iterator<Item = &mut T> {
self.data.iter_mut().filter_map(|(_, value)| value.as_mut()) self.data.iter_mut().filter_map(|(_, value)| value.as_mut())
} }
pub fn capacity(&self) -> usize {
self.data.len()
}
} }
impl<T> Default for SlotVec<T> { impl<T> Default for SlotVec<T> {
+4 -4
View File
@@ -117,8 +117,8 @@ impl TextView {
self.text.text().to_string() self.text.text().to_string()
} }
pub fn update_attrs<R>(&mut self, update: impl FnOnce(&mut TextAttrs) -> R) -> R { pub fn attrs_mut(&mut self) -> std::cell::RefMut<'_, TextAttrs> {
self.text.update_attrs(update) self.text.attrs_mut()
} }
} }
@@ -147,8 +147,8 @@ impl Text {
self.view.content() self.view.content()
} }
pub fn update_attrs<R>(&mut self, update: impl FnOnce(&mut TextAttrs) -> R) -> R { pub fn attrs_mut(&mut self) -> std::cell::RefMut<'_, TextAttrs> {
self.view.update_attrs(update) self.view.attrs_mut()
} }
} }