From 1be6cc22486f194c89fbe3c86abb325e1008d53f Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Sat, 12 Sep 2026 17:13:49 -0400 Subject: [PATCH] iris: share resource handle bookkeeping --- core/src/primitive/color.rs | 82 +++---- core/src/primitive/text.rs | 144 +++++------- core/src/primitive/texture.rs | 152 +++++++------ core/src/render/atlas.rs | 3 +- core/src/ui/mod.rs | 2 +- core/src/util/mod.rs | 2 + core/src/util/resources.rs | 414 ++++++++++++++++++++++++++++++++++ core/src/util/slot.rs | 41 +++- src/widget/text/mod.rs | 8 +- 9 files changed, 631 insertions(+), 217 deletions(-) create mode 100644 core/src/util/resources.rs diff --git a/core/src/primitive/color.rs b/core/src/primitive/color.rs index ed39e7c..cb0746b 100644 --- a/core/src/primitive/color.rs +++ b/core/src/primitive/color.rs @@ -1,10 +1,5 @@ -use crate::util::{Dirty, RefCounter}; -use std::{ - cell::RefCell, - fmt, - rc::Rc, - sync::mpsc::{Receiver, Sender, channel}, -}; +use crate::util::{Dirty, Resources, StrongRscId}; +use std::{cell::RefCell, fmt, rc::Rc}; /// Encoded, straight-alpha sRGB at an input boundary. /// @@ -167,30 +162,7 @@ mod private { impl Sealed for super::PaintId {} } -#[derive(Debug)] -struct PaintLease { - slot: u32, - counter: RefCounter, - send: Sender, -} - -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); - } - } -} +struct PaintRsc; /// A stable reference to one entry in a UI's paint table. /// @@ -200,7 +172,7 @@ impl Drop for PaintLease { #[derive(Clone, Debug)] pub struct PaintId { slot: u32, - lease: Option, + strong: Option>, } impl PaintId { @@ -221,7 +193,7 @@ impl PaintId { pub const NONE: Self = Self::builtin(14); const fn builtin(slot: u32) -> Self { - Self { slot, lease: None } + Self { slot, strong: None } } pub(crate) fn slot(&self) -> u32 { @@ -229,7 +201,7 @@ impl PaintId { } 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. pub struct Paints { + resources: Resources, entries: Vec, - free: Vec, dirty: Dirty, - send: Sender, - recv: Receiver, } impl Paints { 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 { + resources, entries: BUILTIN_PAINTS.map(Srgba8::to_linear).to_vec(), - free: Vec::new(), dirty: Dirty::new_all(), - send, - recv, } } @@ -371,23 +343,19 @@ impl Paints { fn add_linear(&mut self, value: LinearRgba) -> PaintId { 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.dirty.mark(slot as usize); - slot } else { - let slot = self.entries.len() as u32; self.entries.push(value); self.dirty.mark(slot as usize); - slot - }; + } PaintId { slot, - lease: Some(PaintLease { - slot, - counter: RefCounter::new(), - send: self.send.clone(), - }), + strong: Some(strong), } } @@ -411,11 +379,13 @@ impl Paints { } pub fn free_released(&mut self) { - for slot in self.recv.try_iter() { - self.entries[slot as usize] = LinearRgba::NONE; - self.dirty.mark(slot as usize); - self.free.push(slot); - } + let entries = &mut self.entries; + let dirty = &mut self.dirty; + self.resources.apply(|id, _| { + 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 diff --git a/core/src/primitive/text.rs b/core/src/primitive/text.rs index 9ceca49..ceeab5c 100644 --- a/core/src/primitive/text.rs +++ b/core/src/primitive/text.rs @@ -1,6 +1,6 @@ use crate::{ Align, GlyphAtlas, GlyphKey, PaintId, PlacedGlyph, RegionAlign, Textures, WidgetId, - util::{SlotId, SlotVec, Vec2}, + util::{Resources, RscHandle, StrongRscId, Vec2, WeakRscId}, }; use parley::{ Alignment, AlignmentOptions, FontContext, FontFamily, FontStyle, FontWeight, GenericFamily, @@ -8,15 +8,12 @@ use parley::{ fontique::{Blob, FontInfoOverride}, }; use std::{ - cell::{Ref, RefCell}, + cell::{Ref, RefCell, RefMut}, collections::HashMap, fmt, ops::{Deref, DerefMut, Range}, rc::Rc, - sync::{ - Arc, - mpsc::{Receiver, Sender, channel}, - }, + sync::Arc, }; use swash::{ FontRef, @@ -610,7 +607,7 @@ pub struct RenderedText { pub generation: u64, } -pub struct TextResource { +pub struct TextRsc { buffer: TextBuffer, attrs: TextAttrs, rendered: Option, @@ -619,7 +616,7 @@ pub struct TextResource { owner: Option, } -impl TextResource { +impl TextRsc { fn new(buffer: TextBuffer, attrs: TextAttrs) -> Self { Self { buffer, @@ -641,57 +638,54 @@ impl TextResource { /// compact [`TextHandle`] into this arena rather than owning Parley layouts. pub struct TextResources { data: TextData, - entries: SlotVec, - send: Sender, - recv: Receiver, + entries: Rc>>, } impl TextResources { pub fn new() -> Self { - let (send, recv) = channel(); Self { data: TextData::default(), - entries: SlotVec::new(), - send, - recv, + entries: Rc::new(RefCell::new(Resources::new())), } } pub fn add(resources: Rc>, buffer: TextBuffer, attrs: TextAttrs) -> TextHandle { - let (id, send) = { + let entries = { let mut resources = resources.borrow_mut(); resources.free_released(); - let id = resources.entries.add(TextResource::new(buffer, attrs)); - (id, resources.send.clone()) + resources.entries.clone() }; TextHandle { - id, + rsc: RscHandle::add(entries, TextRsc::new(buffer, attrs)), resources, - send, } } - fn entry(&self, id: SlotId) -> &TextResource { - self.entries - .get(id) - .expect("text resource handle points at a released slot") + pub fn handle(resources: Rc>, id: StrongRscId) -> TextHandle { + let entries = resources.borrow().entries.clone(); + TextHandle { + rsc: RscHandle::new(id, entries), + resources, + } } - fn entry_mut(&mut self, id: SlotId) -> &mut TextResource { - self.entries - .get_mut(id) - .expect("text resource handle points at a released slot") + pub fn upgrade(resources: Rc>, id: WeakRscId) -> Option { + let entries = { + let mut resources = resources.borrow_mut(); + resources.free_released(); + resources.entries.clone() + }; + let id = entries.borrow_mut().upgrade(id)?; + Some(Self::handle(resources, id)) } pub fn free_released(&mut self) { - for id in self.recv.try_iter() { - self.entries.free(id); - } + self.entries.borrow_mut().apply(|_, _| {}); } pub fn invalidate_all(&mut self) -> Vec { let mut owners = Vec::new(); - for resource in self.entries.values_mut() { + for resource in self.entries.borrow_mut().values_mut() { resource.invalidate(); if let Some(owner) = resource.owner && !owners.contains(&owner) @@ -702,27 +696,22 @@ impl TextResources { owners } - fn shape(&mut self, id: SlotId) { + fn shape(&mut self, resource: &mut TextRsc) { 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 .buffer - .shape(data, &resource.attrs, resource.width, density); + .shape(&mut self.data, &resource.attrs, resource.width, density); } fn render( &mut self, - id: SlotId, + resource: &mut TextRsc, width: Option, owner: WidgetId, textures: &mut Textures, density: f32, ) -> (RenderedText, bool) { let atlas_generation = self.data.atlas.generation(); - let resource = self.entry_mut(id); resource.owner = Some(owner); if resource.width == width && resource.density == density @@ -733,11 +722,7 @@ impl TextResources { } resource.width = width; resource.density = density; - let Self { data, entries, .. } = self; - let resource = entries - .get_mut(id) - .expect("text resource handle points at a released slot"); - let rendered = data.render( + let rendered = self.data.render( &mut resource.buffer, &resource.attrs, width, @@ -773,28 +758,30 @@ impl DerefMut for TextResources { /// shared once per UI; constructing a handle does not allocate a resource of /// its own. pub struct TextHandle { - id: SlotId, + rsc: RscHandle, resources: Rc>, - send: Sender, } impl TextHandle { + pub fn strong(&self) -> StrongRscId { + self.rsc.strong() + } + + pub fn weak(&self) -> WeakRscId { + self.rsc.weak() + } + pub fn text(&self) -> Ref<'_, str> { - Ref::map(self.resources.borrow(), |resources| { - resources.entry(self.id).buffer.text() - }) + Ref::map(self.rsc.get(), |resource| resource.buffer.text()) } pub fn attrs(&self) -> Ref<'_, TextAttrs> { - Ref::map(self.resources.borrow(), |resources| { - &resources.entry(self.id).attrs - }) + Ref::map(self.rsc.get(), |resource| &resource.attrs) } pub fn set_text(&mut self, text: impl Into) -> bool { let text = text.into(); - let mut resources = self.resources.borrow_mut(); - let resource = resources.entry_mut(self.id); + let mut resource = self.rsc.get_mut(); if resource.buffer.text() == text { return false; } @@ -804,46 +791,45 @@ impl TextHandle { } pub fn edit_text(&mut self, edit: impl FnOnce(&mut String) -> R) -> R { - let mut resources = self.resources.borrow_mut(); - let resource = resources.entry_mut(self.id); + let mut resource = self.rsc.get_mut(); resource.rendered = None; edit(resource.buffer.edit()) } pub fn set_spans(&mut self, spans: Vec) { - let mut resources = self.resources.borrow_mut(); - let resource = resources.entry_mut(self.id); + let mut resource = self.rsc.get_mut(); resource.buffer.set_spans(spans); resource.rendered = None; } - pub fn update_attrs(&mut self, update: impl FnOnce(&mut TextAttrs) -> R) -> R { - let mut resources = self.resources.borrow_mut(); - let resource = resources.entry_mut(self.id); + pub fn attrs_mut(&mut self) -> RefMut<'_, TextAttrs> { + let mut resource = self.rsc.get_mut(); resource.invalidate(); - update(&mut resource.attrs) + RefMut::map(resource, |resource| &mut resource.attrs) } pub fn rendered(&self) -> Option { - self.resources.borrow().entry(self.id).rendered.clone() + self.rsc.get().rendered.clone() } pub fn width(&self) -> Option { - self.resources.borrow().entry(self.id).width + self.rsc.get().width } pub fn with_layout(&self, f: impl FnOnce(&Layout, &str) -> R) -> R { let mut resources = self.resources.borrow_mut(); - resources.shape(self.id); - let resource = resources.entry(self.id); + let mut resource = self.rsc.get_mut_shared(); + resources.shape(&mut resource); f(resource.buffer.layout(), resource.buffer.text()) } pub fn layout(&self) -> Ref<'_, Layout> { - self.resources.borrow_mut().shape(self.id); - Ref::map(self.resources.borrow(), |resources| { - resources.entry(self.id).buffer.layout() - }) + { + let mut resources = self.resources.borrow_mut(); + let mut resource = self.rsc.get_mut_shared(); + resources.shape(&mut resource); + } + Ref::map(self.rsc.get(), |resource| resource.buffer.layout()) } pub fn render( @@ -853,15 +839,9 @@ impl TextHandle { textures: &mut Textures, density: f32, ) -> (RenderedText, bool) { - self.resources - .borrow_mut() - .render(self.id, width, owner, textures, density) - } -} - -impl Drop for TextHandle { - fn drop(&mut self) { - let _ = self.send.send(self.id); + let mut resources = self.resources.borrow_mut(); + let mut resource = self.rsc.get_mut_shared(); + resources.render(&mut resource, width, owner, textures, density) } } @@ -886,11 +866,11 @@ mod tests { TextBuffer::new("temporary"), TextAttrs::default(), ); - assert_eq!(resources.borrow().entries.len(), 1); + assert_eq!(resources.borrow().entries.borrow().len(), 1); drop(text); resources.borrow_mut().free_released(); - assert!(resources.borrow().entries.is_empty()); + assert!(resources.borrow().entries.borrow().is_empty()); } } diff --git a/core/src/primitive/texture.rs b/core/src/primitive/texture.rs index 4424616..d94e0e1 100644 --- a/core/src/primitive/texture.rs +++ b/core/src/primitive/texture.rs @@ -1,10 +1,6 @@ -use crate::util::{RefCounter, Vec2}; +use crate::util::{Resources, RscHandle, StrongRscId, Vec2, WeakRscId}; use image::{DynamicImage, GenericImageView}; -use std::{ - collections::HashMap, - ops::Index, - sync::mpsc::{Receiver, Sender, channel}, -}; +use std::{cell::RefCell, collections::HashMap, ops::Index, rc::Rc}; /// 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 @@ -28,19 +24,20 @@ pub struct SharedTextureKey { pub id: u64, } -#[derive(Debug, Clone)] -pub struct TextureHandle { - slot: u32, +pub struct TextureRsc { kind: TextureKind, size: Vec2, - counter: RefCounter, - send: Sender<(TextureKind, u32)>, +} + +#[derive(Debug, Clone)] +pub struct TextureHandle { + rsc: RscHandle, } /// a texture manager for a ui /// note that this is heavily oriented towards wgpu's renderer so the primitives don't need mapped pub struct Textures { - free: Vec, + resources: Rc>>, images: Vec>, kinds: Vec, /// 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 /// drawing it and its slot is never recycled underneath one. shared: HashMap, - /// Next layer to hand out to an atlas page. Pages are never freed (no - /// atlas eviction), so this only grows and `free` never holds one. + /// Next layer to hand out to an atlas page. Page layers and resource slots + /// are never reused, even if an explicit atlas clear drops their handles. next_page_layer: u32, updates: Vec, - send: Sender<(TextureKind, u32)>, - recv: Receiver<(TextureKind, u32)>, } pub enum TextureUpdate<'a> { @@ -86,16 +81,13 @@ enum Update { impl Textures { pub fn new() -> Self { - let (send, recv) = channel(); Self { - free: Vec::new(), + resources: Rc::new(RefCell::new(Resources::new())), images: Vec::new(), kinds: Vec::new(), shared: HashMap::new(), next_page_layer: 0, updates: Vec::new(), - send, - recv, } } @@ -103,14 +95,7 @@ impl Textures { let image = image.into(); let size = image.dimensions().into(); let kind = TextureKind::Image; - let slot = self.push(kind, image); - TextureHandle { - slot, - kind, - size, - counter: RefCounter::new(), - send: self.send.clone(), - } + self.push(kind, size, image, true) } pub fn add_page(&mut self, image: impl Into) -> TextureHandle { @@ -119,28 +104,49 @@ impl Textures { let layer = self.next_page_layer; self.next_page_layer += 1; let kind = TextureKind::Page { layer }; - let slot = self.push(kind, image); + self.push(kind, size, image, false) + } + + pub fn handle(&self, id: StrongRscId) -> TextureHandle { TextureHandle { - slot, - kind, - size, - counter: RefCounter::new(), - send: self.send.clone(), + rsc: RscHandle::new(id, self.resources.clone()), } } - fn push(&mut self, kind: TextureKind, image: DynamicImage) -> u32 { - if let Some(i) = self.free.pop() { + pub fn upgrade(&mut self, id: WeakRscId) -> Option { + 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.kinds[i as usize] = kind; self.updates.push(Update::Set(kind, i)); - i } else { - let i = self.images.len() as u32; self.images.push(Some(image)); self.kinds.push(kind); 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 { - self.images[handle.slot as usize] + self.images[handle.rsc.id().slot() as usize] .as_mut() .expect("texture was freed while still held") } 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 @@ -185,24 +192,20 @@ impl Textures { /// ones after them still land where they were. pub fn reupload(&mut self) { self.updates.clear(); - self.updates - .extend((0..self.images.len() as u32).map(|i| Update::Push(self.kinds[i as usize], i))); + self.updates.extend( + (0..self.resources.borrow().capacity() as u32) + .map(|i| Update::Push(self.kinds[i as usize], i)), + ); } pub fn free(&mut self) { - for (kind, idx) in self.recv.try_iter() { - self.images[idx as usize] = None; - self.updates.push(Update::Free(idx)); - // A page's slot is never reclaimed: `GlyphAtlas` never drops the - // handles it holds, and there is no eviction path for a hole in - // the middle of the array's layers. If that ever changes, this - // 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); - } - } + let updates = &mut self.updates; + let images = &mut self.images; + self.resources.borrow_mut().apply(|id, _| { + let idx = id.slot(); + images[idx as usize] = None; + updates.push(Update::Free(idx)); + }); } pub fn updates(&mut self) -> impl Iterator> { @@ -226,7 +229,7 @@ impl Textures { impl TextureHandle { pub fn size(&self) -> Vec2 { - self.size + self.rsc.get().size } /// 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), /// not a recoverable condition, so it panics rather than drawing garbage. pub fn image_index(&self) -> u32 { - match self.kind { - TextureKind::Image => self.slot, + match self.rsc.get().kind { + TextureKind::Image => self.rsc.id().slot(), TextureKind::Page { .. } => panic!("image_index() called on an atlas page handle"), } } pub fn layer(&self) -> u32 { - match self.kind { + match self.rsc.get().kind { TextureKind::Page { layer } => layer, TextureKind::Image => panic!("layer() called on a standalone image handle"), } } -} -impl Drop for TextureHandle { - fn drop(&mut self) { - if self.counter.drop() { - let _ = self.send.send((self.kind, self.slot)); - } + pub fn strong(&self) -> StrongRscId { + self.rsc.strong() + } + + pub fn weak(&self) -> WeakRscId { + self.rsc.weak() } } @@ -261,7 +264,9 @@ impl Index<&TextureHandle> for Textures { type Output = DynamicImage; 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] fn reupload_replays_every_slot_in_order_including_the_empty_ones() { let mut textures = Textures::new(); diff --git a/core/src/render/atlas.rs b/core/src/render/atlas.rs index e121967..470947b 100644 --- a/core/src/render/atlas.rs +++ b/core/src/render/atlas.rs @@ -81,14 +81,13 @@ impl GlyphAtlas { let rgba = img.as_mut_rgba8().expect("atlas page is rgba8"); write_glyph(rgba, image, x, y); - let handle = page.handle.clone(); let rect = PatchRect { x, y, width: w, height: h, }; - textures.patch(&handle, rect); + textures.patch(&page.handle, rect); let page = &self.pages[page_idx]; let scale = 1.0 / PAGE as f32; diff --git a/core/src/ui/mod.rs b/core/src/ui/mod.rs index 997e96d..f008893 100644 --- a/core/src/ui/mod.rs +++ b/core/src/ui/mod.rs @@ -182,8 +182,8 @@ pub trait UiRsc { while let Some(id) = self.widgets_mut().free_next() { self.on_remove(id); } + self.ui_mut().text.borrow_mut().free_released(); self.ui_mut().textures.free(); self.ui_mut().paints.free_released(); - self.ui_mut().text.borrow_mut().free_released(); } } diff --git a/core/src/util/mod.rs b/core/src/util/mod.rs index d462326..9b381c1 100644 --- a/core/src/util/mod.rs +++ b/core/src/util/mod.rs @@ -5,6 +5,7 @@ mod dirty; mod id; mod math; mod refcount; +mod resources; mod slot; mod trust; mod typemap; @@ -17,6 +18,7 @@ pub use dirty::*; pub use id::*; pub use math::*; pub use refcount::*; +pub use resources::*; pub use slot::*; pub use trust::*; pub use typemap::*; diff --git a/core/src/util/resources.rs b/core/src/util/resources.rs new file mode 100644 index 0000000..f17773a --- /dev/null +++ b/core/src/util/resources.rs @@ -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 { + value: T, + strong: Option, + recycle: bool, +} + +/// Generational storage and deferred strong-reference accounting for one kind +/// of UI resource. +pub struct Resources { + entries: SlotVec>, + send: Sender, + recv: Receiver, +} + +impl Resources { + pub fn new() -> Self { + let (send, recv) = channel(); + Self { + entries: SlotVec::new(), + send, + recv, + } + } + + pub fn add(&mut self, value: T) -> StrongRscId { + self.add_with(value, true) + } + + pub fn add_unrecycled(&mut self, value: T) -> StrongRscId { + self.add_with(value, false) + } + + fn add_with(&mut self, value: T, recycle: bool) -> StrongRscId { + 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 { + 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) -> Option> { + 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) -> Option<&T> { + Some(&self.entries.get(id.rsc_id())?.value) + } + + pub fn get_mut(&mut self, id: impl RscId) -> Option<&mut T> { + Some(&mut self.entries.get_mut(id.rsc_id())?.value) + } + + pub fn values_mut(&mut self) -> impl Iterator { + 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 Default for Resources { + fn default() -> Self { + Self::new() + } +} + +pub trait RscId { + 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 { + id: SlotId, + send: Sender, + ty: PhantomData T>, +} + +impl StrongRscId { + fn new(id: SlotId, send: Sender) -> Self { + Self { + id, + send, + ty: PhantomData, + } + } + + pub fn weak(&self) -> WeakRscId { + WeakRscId::new(self.id) + } + + pub fn id(&self) -> SlotId { + self.id + } + + pub(crate) fn slot(&self) -> u32 { + self.id.slot() + } +} + +impl Clone for StrongRscId { + fn clone(&self) -> Self { + let _ = self.send.send(Event::Clone(self.id)); + Self::new(self.id, self.send.clone()) + } +} + +impl Drop for StrongRscId { + fn drop(&mut self) { + let _ = self.send.send(Event::Drop(self.id)); + } +} + +impl RscId for StrongRscId { + fn rsc_id(&self) -> SlotId { + self.id + } +} + +impl RscId for &StrongRscId { + fn rsc_id(&self) -> SlotId { + self.id + } +} + +impl fmt::Debug for StrongRscId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.id.fmt(f) + } +} + +impl PartialEq for StrongRscId { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for StrongRscId {} + +impl Hash for StrongRscId { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +/// A sendable, copyable ID that does not keep its [`Resources`] entry alive. +pub struct WeakRscId { + id: SlotId, + ty: PhantomData T>, +} + +impl WeakRscId { + 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 Clone for WeakRscId { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for WeakRscId {} + +impl RscId for WeakRscId { + fn rsc_id(&self) -> SlotId { + self.id + } +} + +impl RscId for &WeakRscId { + fn rsc_id(&self) -> SlotId { + self.id + } +} + +impl fmt::Debug for WeakRscId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.id.fmt(f) + } +} + +impl PartialEq for WeakRscId { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for WeakRscId {} + +impl Hash for WeakRscId { + fn hash(&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 { + id: StrongRscId, + resources: Rc>>, +} + +impl RscHandle { + pub fn new(id: StrongRscId, resources: Rc>>) -> Self { + Self { id, resources } + } + + pub fn add(resources: Rc>>, 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 { + self.id.clone() + } + + pub fn weak(&self) -> WeakRscId { + self.id.weak() + } + + pub fn id(&self) -> SlotId { + self.id.id() + } +} + +impl Clone for RscHandle { + fn clone(&self) -> Self { + Self::new(self.id.clone(), self.resources.clone()) + } +} + +impl fmt::Debug for RscHandle { + 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() {} + assert_send::>(); + assert_send::>(); + + 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); + } +} diff --git a/core/src/util/slot.rs b/core/src/util/slot.rs index c6768d7..3e18b42 100644 --- a/core/src/util/slot.rs +++ b/core/src/util/slot.rs @@ -5,6 +5,10 @@ pub struct SlotId { } impl SlotId { + pub(crate) fn slot(self) -> u32 { + self.idx + } + /// A stable, collision-free `u64` encoding of this id -- for a caller /// (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 @@ -18,6 +22,7 @@ impl SlotId { pub struct SlotVec { data: Vec<(u32, Option)>, free: Vec, + len: usize, } impl SlotVec { @@ -25,11 +30,12 @@ impl SlotVec { Self { data: Default::default(), free: Default::default(), + len: 0, } } 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]; *data = Some(x); SlotId { idx, genr: *genr } @@ -38,14 +44,35 @@ impl SlotVec { let genr = 0; self.data.push((genr, Some(x))); SlotId { idx, genr } - } + }; + self.len += 1; + id } pub fn free(&mut self, id: SlotId) { + let _ = self.remove(id); + } + + pub fn remove(&mut self, id: SlotId) -> Option { + self.remove_inner(id, true) + } + + pub fn remove_unrecycled(&mut self, id: SlotId) -> Option { + self.remove_inner(id, false) + } + + fn remove_inner(&mut self, id: SlotId, recycle: bool) -> Option { let (genr, data) = &mut self.data[id.idx as usize]; + if *genr != id.genr { + return None; + } *genr += 1; - *data = None; - self.free.push(id.idx); + let value = data.take()?; + self.len -= 1; + if recycle { + self.free.push(id.idx); + } + Some(value) } pub fn get(&self, id: SlotId) -> Option<&T> { @@ -65,7 +92,7 @@ impl SlotVec { } pub fn len(&self) -> usize { - self.data.len() - self.free.len() + self.len } pub fn is_empty(&self) -> bool { @@ -75,6 +102,10 @@ impl SlotVec { pub fn values_mut(&mut self) -> impl Iterator { self.data.iter_mut().filter_map(|(_, value)| value.as_mut()) } + + pub fn capacity(&self) -> usize { + self.data.len() + } } impl Default for SlotVec { diff --git a/src/widget/text/mod.rs b/src/widget/text/mod.rs index 831a7ac..aedb8c9 100644 --- a/src/widget/text/mod.rs +++ b/src/widget/text/mod.rs @@ -117,8 +117,8 @@ impl TextView { self.text.text().to_string() } - pub fn update_attrs(&mut self, update: impl FnOnce(&mut TextAttrs) -> R) -> R { - self.text.update_attrs(update) + pub fn attrs_mut(&mut self) -> std::cell::RefMut<'_, TextAttrs> { + self.text.attrs_mut() } } @@ -147,8 +147,8 @@ impl Text { self.view.content() } - pub fn update_attrs(&mut self, update: impl FnOnce(&mut TextAttrs) -> R) -> R { - self.view.update_attrs(update) + pub fn attrs_mut(&mut self) -> std::cell::RefMut<'_, TextAttrs> { + self.view.attrs_mut() } }