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
+642 -225

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
`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,
and prepared glyphs live in a per-`Ui` generational arena; a text widget holds
one compact handle into it, while the arena and its shaping state are shared by
one `Rc<RefCell<_>>`. Registering another family invalidates the live arena
entries and dirties their active owner 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.
and prepared glyphs live in a per-`Ui` `Resources<TextRsc>` generational arena;
a text widget's `TextHandle` wraps the UI-local `RscHandle` into it. The same
generic arena owns texture and managed-paint lifetimes. Its `StrongRscId` and
`WeakRscId` are sendable IDs rather than data pointers; clone and drop events
cross one arena-owned standard channel, and reference counts remain in the
arena instead of allocating one atomic counter per resource. Registering
another family invalidates the live text entries and dirties their active owner
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
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 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<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);
}
}
}
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<PaintLease>,
strong: Option<StrongRscId<PaintRsc>>,
}
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<PaintRsc>,
entries: Vec<LinearRgba>,
free: Vec<u32>,
dirty: Dirty,
send: Sender<u32>,
recv: Receiver<u32>,
}
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
+62 -82
View File
@@ -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<RenderedText>,
@@ -619,7 +616,7 @@ pub struct TextResource {
owner: Option<WidgetId>,
}
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<TextResource>,
send: Sender<SlotId>,
recv: Receiver<SlotId>,
entries: Rc<RefCell<Resources<TextRsc>>>,
}
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<RefCell<Self>>, 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<RefCell<Self>>, id: StrongRscId<TextRsc>) -> 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<RefCell<Self>>, id: WeakRscId<TextRsc>) -> Option<TextHandle> {
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<WidgetId> {
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<f32>,
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<TextRsc>,
resources: Rc<RefCell<TextResources>>,
send: Sender<SlotId>,
}
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> {
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<String>) -> 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<R>(&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<SpanStyle>) {
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<R>(&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<RenderedText> {
self.resources.borrow().entry(self.id).rendered.clone()
self.rsc.get().rendered.clone()
}
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 {
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<PaintId>> {
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());
}
}
+85 -67
View File
@@ -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<TextureRsc>,
}
/// 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<u32>,
resources: Rc<RefCell<Resources<TextureRsc>>>,
images: Vec<Option<DynamicImage>>,
kinds: Vec<TextureKind>,
/// 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<SharedTextureKey, TextureHandle>,
/// 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<Update>,
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<DynamicImage>) -> 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<TextureRsc>) -> 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<TextureRsc>) -> Option<TextureHandle> {
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<Item = TextureUpdate<'_>> {
@@ -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<TextureRsc> {
self.rsc.strong()
}
pub fn weak(&self) -> WeakRscId<TextureRsc> {
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();
+1 -2
View File
@@ -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;
+1 -1
View File
@@ -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();
}
}
+2
View File
@@ -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::*;
+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);
}
}
+36 -5
View File
@@ -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<T> {
data: Vec<(u32, Option<T>)>,
free: Vec<u32>,
len: usize,
}
impl<T> SlotVec<T> {
@@ -25,11 +30,12 @@ impl<T> SlotVec<T> {
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<T> SlotVec<T> {
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<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];
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<T> SlotVec<T> {
}
pub fn len(&self) -> usize {
self.data.len() - self.free.len()
self.len
}
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> {
self.data.iter_mut().filter_map(|(_, value)| value.as_mut())
}
pub fn capacity(&self) -> usize {
self.data.len()
}
}
impl<T> Default for SlotVec<T> {
+4 -4
View File
@@ -117,8 +117,8 @@ impl TextView {
self.text.text().to_string()
}
pub fn update_attrs<R>(&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<R>(&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()
}
}