kui_core/resources.rs
1//! Long-lived, host-registered resources: fonts, images, sounds and
2//! fragment shaders, behind typed handles.
3//!
4//! A handle ([`FontId`], [`ImageId`], [`SoundId`], [`FragmentId`]) is a
5//! slotmap key with generational use-after-free protection. It converts
6//! to and from `u64` (`to_ffi` / `from_ffi`) so it crosses a scripting
7//! boundary as a plain integer with the generation check intact. The
8//! registry is the session's, so a resource registered through one window
9//! draws and plays in every window of the session.
10//!
11//! Registering through a [`Core`](crate::Core):
12//!
13//! ```rust,no_run
14//! use kui_core::{Core, NodeSpec, Size, TextStyle};
15//!
16//! let mut core = Core::new();
17//! // A font: raw TTF/OTF bytes; `None` when the data holds no usable face.
18//! let font = core.add_font_data(std::fs::read("Inter.ttf").unwrap()).unwrap();
19//! // An image: RGBA, `width * height * 4` bytes.
20//! let logo = core.resources.add_image(2, 2, vec![255; 16]);
21//! // A sound: encoded file bytes the runner's audio backend decodes.
22//! let ding = core.add_sound(std::fs::read("ding.ogg").unwrap());
23//!
24//! let mut ui = core.frame(Size::new(400.0, 300.0), 1.0);
25//! ui.text("Hello", TextStyle::new(14.0).font(font));
26//! ui.image(logo, NodeSpec::row().size(64.0, 64.0));
27//! ui.finish();
28//! core.play(ding, Default::default());
29//! ```
30//!
31//! Removing a resource (`remove_image`, `remove_font`, `remove_sound`,
32//! `remove_fragment` on `Core`) makes its handle a miss: the image draws
33//! nothing, the font shapes as sans, the sound is silent.
34//!
35//! **A handle is unique to the process, not to its session.** Every
36//! `Session` has its own registry, but the keys come from one process-wide
37//! mint per kind, which also records the session that owns each. So an
38//! `ImageId` from one session, looked up in another, is a detectable miss
39//! rather than an alias for that session's first image: it behaves as a
40//! removed handle does, plus a `foreign-resource` warning the next
41//! `Core::take_warnings` reports. The mint is touched on registration,
42//! removal and the miss path only; a live lookup never locks it.
43
44use std::cell::RefCell;
45use std::sync::atomic::{AtomicU64, Ordering};
46use std::sync::{Arc, LazyLock, Mutex, MutexGuard};
47
48use slotmap::{Key as _, SlotMap, SparseSecondaryMap, new_key_type};
49
50use crate::spec::FontFamily;
51
52new_key_type! {
53 pub struct ImageId;
54 pub struct PainterId;
55 /// A registered font (`Core::add_font_data` / `add_system_font`), used
56 /// through `TextStyle::font`.
57 pub struct FontId;
58 /// A registered sound (`Core::add_sound`): encoded file bytes the
59 /// driver's audio backend decodes. Played through `Core::play`, an
60 /// `audio` node, or `NodeSpec::click_sound` / `hover_sound`.
61 pub struct SoundId;
62 /// A registered WGSL fragment function (`Core::add_fragment`), drawn
63 /// by a `fragment` node.
64 pub struct FragmentId;
65}
66
67impl FragmentId {
68 /// The handle as a plain integer for C/Lua/JS (generation check intact).
69 pub fn to_ffi(self) -> u64 {
70 self.data().as_ffi()
71 }
72
73 pub fn from_ffi(raw: u64) -> Self {
74 Self::from(slotmap::KeyData::from_ffi(raw))
75 }
76}
77
78impl SoundId {
79 /// The handle as a plain integer for C/Lua/JS (generation check intact).
80 pub fn to_ffi(self) -> u64 {
81 self.data().as_ffi()
82 }
83
84 pub fn from_ffi(raw: u64) -> Self {
85 Self::from(slotmap::KeyData::from_ffi(raw))
86 }
87}
88
89impl FontId {
90 /// The handle as a plain integer for C/Lua/JS (generation check intact).
91 pub fn to_ffi(self) -> u64 {
92 self.data().as_ffi()
93 }
94
95 pub fn from_ffi(raw: u64) -> Self {
96 Self::from(slotmap::KeyData::from_ffi(raw))
97 }
98}
99
100impl ImageId {
101 /// The handle as a plain integer for C/Lua (generation check intact).
102 pub fn to_ffi(self) -> u64 {
103 self.data().as_ffi()
104 }
105
106 pub fn from_ffi(raw: u64) -> Self {
107 Self::from(slotmap::KeyData::from_ffi(raw))
108 }
109}
110
111/// Which `Session` a registry — and so every handle it minted — belongs
112/// to. Process-wide unique, from a counter; never serialized, so the
113/// number means nothing across runs and is only ever compared or printed.
114#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
115pub struct SessionId(pub u64);
116
117impl SessionId {
118 /// The next unused id. Starts at 1 so a zeroed struct is never a
119 /// session.
120 pub(crate) fn next() -> Self {
121 static NEXT: AtomicU64 = AtomicU64::new(1);
122 Self(NEXT.fetch_add(1, Ordering::Relaxed))
123 }
124}
125
126/// The kind of resource a handle names, for the warning that reports a
127/// foreign one.
128#[derive(Clone, Copy, Debug, PartialEq, Eq)]
129pub enum ResourceKind {
130 Image,
131 Font,
132 Sound,
133 Fragment,
134}
135
136impl ResourceKind {
137 pub fn name(self) -> &'static str {
138 match self {
139 Self::Image => "image",
140 Self::Font => "font",
141 Self::Sound => "sound",
142 Self::Fragment => "fragment",
143 }
144 }
145
146 /// What a handle of this kind does when it does not resolve.
147 fn fallback(self) -> &'static str {
148 match self {
149 Self::Image => "draws nothing",
150 Self::Font => "shapes as sans-serif",
151 Self::Sound => "plays nothing",
152 Self::Fragment => "draws nothing",
153 }
154 }
155}
156
157/// A handle from another session that this registry was asked to resolve:
158/// what `diag::foreign_resource` turns into a warning.
159#[derive(Clone, Copy, Debug, PartialEq, Eq)]
160pub struct Foreign {
161 pub kind: ResourceKind,
162 /// The handle as `to_ffi` shows it — what a host would have logged.
163 pub raw: u64,
164 /// The session the handle is live in.
165 pub owner: SessionId,
166 /// The session that was asked.
167 pub here: SessionId,
168}
169
170impl Foreign {
171 /// The one-line explanation: which handle, whose it is, what it did
172 /// instead, and the fix.
173 pub fn message(&self) -> String {
174 let Foreign {
175 kind,
176 raw,
177 owner,
178 here,
179 } = *self;
180 let what = kind.name();
181 let did = kind.fallback();
182 format!(
183 "{what} handle {raw:#x} belongs to session #{} and was used in session #{}: a \
184 handle is only valid in the session that registered it, so this one is treated \
185 as removed and {did} — register the {what} through a core of this session, or \
186 build both cores against one `Session` (`Core::new_in`)",
187 owner.0, here.0
188 )
189 }
190}
191
192/// Foreign hits a registry keeps before it stops recording: a `Core`
193/// drains them on every `take_warnings`, and the diagnostics' own dedup
194/// means one line per handle, so a handful is plenty.
195const MAX_FOREIGN: usize = 64;
196
197/// The process's one allocator of handles, per kind, with the session each
198/// live handle belongs to. A session's registry never mints a key itself:
199/// it takes one from here and stores its entry under it, which is what
200/// keeps two sessions from ever holding the same bits for different
201/// things.
202#[derive(Default)]
203struct Mint {
204 images: SlotMap<ImageId, SessionId>,
205 fonts: SlotMap<FontId, SessionId>,
206 sounds: SlotMap<SoundId, SessionId>,
207 fragments: SlotMap<FragmentId, SessionId>,
208}
209
210static MINT: LazyLock<Mutex<Mint>> = LazyLock::new(Mutex::default);
211
212/// The mint, past a poisoned lock: the maps hold plain ids and session
213/// numbers, so a panic mid-insert on another thread leaves nothing to
214/// distrust.
215fn mint() -> MutexGuard<'static, Mint> {
216 MINT.lock().unwrap_or_else(|e| e.into_inner())
217}
218
219/// An RGBA image registered by the host (rendering lands in a later pass).
220pub struct ImageEntry {
221 pub width: u32,
222 pub height: u32,
223 /// Shared rather than owned so a frame's display list can hand a
224 /// backend the pixels of a texture-backed image without copying them
225 /// (`DisplayList::texture_pixels`): an update replaces the `Arc`, and
226 /// a backend mid-upload keeps the old one alive until it is done.
227 pub rgba: std::sync::Arc<Vec<u8>>,
228 /// Moves on every [`Resources::update_image`]; a backend re-uploads a
229 /// texture-backed image when the revision it uploaded is behind.
230 pub rev: u32,
231 /// Where the pixels live on the GPU.
232 pub backing: ImageBacking,
233 /// The buffer the last [`Resources::update_image_with`] replaced, kept
234 /// for the next one to write into once no display list holds it.
235 /// An update between frames finds `rgba` still shared
236 /// with the last frame's `texture_pixels`, so without it every update
237 /// was a fresh `w × h × 4` allocation and the previous one freed —
238 /// 590 µs of page faults and 170 µs of release at 1080p on Windows,
239 /// three times the copy itself. Two buffers per streamed image, and a
240 /// stream stops allocating from its fourth update. Kept only for an
241 /// image updated before, at the same size, and dropped by
242 /// [`Resources::release_spares`] once [`SPARE_FRAMES`] frames pass
243 /// with no update: an image updated once, or a stream that stopped,
244 /// holds one buffer again.
245 pub(crate) spare: Option<std::sync::Arc<Vec<u8>>>,
246 /// [`Resources::frames`] when `spare` was last set.
247 spare_at: u64,
248}
249
250/// How many frames an image's spare buffer outlives its last update.
251/// Long enough for a stream slower than the display — a
252/// 30 fps video beside a 120 Hz animation updates every fourth frame —
253/// and short enough that one that stopped gives its buffer back.
254pub const SPARE_FRAMES: u64 = 30;
255
256/// Where a registered image's pixels are kept for drawing.
257/// The core
258/// decides on the two facts that matter — whether the image fits an atlas
259/// page, and whether its pixels were ever replaced — and the app never
260/// chooses.
261#[derive(Clone, Copy, Debug, PartialEq, Eq)]
262pub enum ImageBacking {
263 /// Blitted into the glyph atlas at first draw and drawn as
264 /// [`crate::display::QuadKind::Image`]: icons, thumbnails, anything
265 /// that fits and never changes.
266 Atlas,
267 /// A texture of its own, drawn as [`crate::display::QuadKind::Texture`]
268 /// through an entry in `DisplayList::textures`: an image that does not
269 /// fit a `MAX_ATLAS_SIZE` page, or one that has been updated in place.
270 /// Once here, an image stays here.
271 Texture,
272}
273
274/// How an `image` node meets the pixels it shows: its two per-node rows.
275/// Carried on the node's content rather than
276/// on `NodeSpec`, so a box pays nothing for a row only an image reads.
277#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
278pub struct ImageOpts {
279 pub sampling: Sampling,
280 pub fit: ImageFit,
281}
282
283/// The `sampling` row: how a backend reads texels between pixel centres.
284#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
285pub enum Sampling {
286 /// Bilinear — a photo, a rendered frame (the default).
287 #[default]
288 Linear,
289 /// Nearest texel — pixel art, an emulator, a data grid that must stay
290 /// square under zoom.
291 Nearest,
292}
293
294impl Sampling {
295 /// Every mode, in the order the `sampling` row names them.
296 pub const ALL: [Sampling; 2] = [Sampling::Linear, Sampling::Nearest];
297
298 pub fn name(self) -> &'static str {
299 match self {
300 Sampling::Linear => "linear",
301 Sampling::Nearest => "nearest",
302 }
303 }
304}
305
306/// The `fit` row: how the pixels meet the node's box. The box itself —
307/// its layout, its hit region, its access rect — is the same in every
308/// mode; only what is painted inside it moves.
309#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
310pub enum ImageFit {
311 /// The pixels stretch to the box (the default, and what every image
312 /// did before the row existed).
313 #[default]
314 Fill,
315 /// The largest rect of the image's aspect that fits the box, centred;
316 /// the rest of the box shows what is behind it.
317 Contain,
318 /// The box is filled and the pixels that do not fit are cropped,
319 /// centred.
320 Cover,
321}
322
323impl ImageFit {
324 /// Every mode, in the order the `fit` row names them.
325 pub const ALL: [ImageFit; 3] = [ImageFit::Fill, ImageFit::Contain, ImageFit::Cover];
326
327 pub fn name(self) -> &'static str {
328 match self {
329 ImageFit::Fill => "fill",
330 ImageFit::Contain => "contain",
331 ImageFit::Cover => "cover",
332 }
333 }
334}
335
336/// A registered font: the family name shaping resolves it by, the faces
337/// it loaded into the font database (empty for installed fonts), and the
338/// weights the family is asked at for regular and bold.
339pub struct FontEntry {
340 pub family: String,
341 pub faces: Vec<cosmic_text::fontdb::ID>,
342 pub(crate) weights: crate::weights::Weights,
343}
344
345/// One family of the font database — installed or loaded — as its faces
346/// describe it, from what the database read off each face's tables when
347/// it was scanned: nothing is loaded or shaped to answer.
348/// What [`Core::system_fonts`](crate::Core::system_fonts) lists, one per
349/// family.
350#[derive(Clone, Debug, PartialEq, Eq)]
351pub struct SystemFont {
352 /// The family name [`Core::add_system_font`](crate::Core::add_system_font)
353 /// takes.
354 pub family: String,
355 /// Every face of the family says it is fixed-pitch (the `post` table's
356 /// `isFixedPitch`): a monospaced family. Measuring glyph widths instead
357 /// loads and shapes every file, and calls a symbol font whose glyphs
358 /// happen to share an advance monospaced.
359 pub monospaced: bool,
360 /// The weights its faces come in, on the CSS scale (400 regular, 700
361 /// bold) as each face's `OS/2` table says it — sorted, each once. A
362 /// variable font's face reads as its default instance.
363 pub weights: Vec<u16>,
364 /// It has an italic or an oblique face.
365 pub italic: bool,
366}
367
368/// A registered sound: the encoded file (wav/ogg/mp3/flac, whatever the
369/// driver's backend decodes), shared so the backend can hold it without a
370/// copy. The core never decodes — headless drivers have no use for PCM.
371pub struct SoundEntry {
372 pub bytes: Arc<[u8]>,
373}
374
375/// A registered fragment: the app's WGSL as it was given, which is what a
376/// backend compiles (around the core's prelude and epilogue — see
377/// `crate::fragment`) and what a C host reads back to compile itself.
378pub struct FragmentEntry {
379 /// The app's source, without the prelude or the epilogue.
380 pub source: Arc<str>,
381}
382
383/// One session's registry. The maps are secondary to the process-wide
384/// the process-wide mint (see the module doc), so a lookup that misses is a handle this
385/// session never registered or has since removed — never somebody else's
386/// entry.
387pub struct Resources {
388 session: SessionId,
389 pub(crate) images: SparseSecondaryMap<ImageId, ImageEntry>,
390 pub(crate) fonts: SparseSecondaryMap<FontId, FontEntry>,
391 pub(crate) sounds: SparseSecondaryMap<SoundId, SoundEntry>,
392 pub(crate) fragments: SparseSecondaryMap<FragmentId, FragmentEntry>,
393 /// Handles of other sessions this registry was asked for since the
394 /// last drain, each once. A `RefCell` because the resolves that find
395 /// them (`family_of` under a shaping closure, `image` under the
396 /// emitter's shared borrow) hold the registry by `&`.
397 foreign: RefCell<Vec<Foreign>>,
398 /// Frames begun by any window of the session, for aging spares.
399 frames: u64,
400 /// The images holding a spare buffer, swept each frame.
401 spared: Vec<ImageId>,
402}
403
404impl Resources {
405 /// The registry for session `session`; keys come from the process's
406 /// mint.
407 pub fn new(session: SessionId) -> Self {
408 Self {
409 session,
410 images: SparseSecondaryMap::new(),
411 fonts: SparseSecondaryMap::new(),
412 sounds: SparseSecondaryMap::new(),
413 fragments: SparseSecondaryMap::new(),
414 foreign: RefCell::new(Vec::new()),
415 frames: 0,
416 spared: Vec::new(),
417 }
418 }
419
420 /// The session this registry belongs to.
421 pub fn session(&self) -> SessionId {
422 self.session
423 }
424
425 /// The foreign handles resolved since the last call (see
426 /// [`Foreign`]); `Core::take_warnings` turns each into one line.
427 pub(crate) fn take_foreign(&self) -> Vec<Foreign> {
428 std::mem::take(&mut *self.foreign.borrow_mut())
429 }
430
431 /// A lookup missed: the mint says whether the handle is live in some
432 /// other session (recorded, once) or in none (removed — silent, the
433 /// documented behaviour). Only the miss path pays for the lock.
434 fn note_miss(&self, kind: ResourceKind, raw: u64) {
435 let owner = {
436 let m = mint();
437 match kind {
438 ResourceKind::Image => m.images.get(ImageId::from_ffi(raw)).copied(),
439 ResourceKind::Font => m.fonts.get(FontId::from_ffi(raw)).copied(),
440 ResourceKind::Sound => m.sounds.get(SoundId::from_ffi(raw)).copied(),
441 ResourceKind::Fragment => m.fragments.get(FragmentId::from_ffi(raw)).copied(),
442 }
443 };
444 let Some(owner) = owner else {
445 return;
446 };
447 if owner == self.session {
448 return;
449 }
450 let mut foreign = self.foreign.borrow_mut();
451 if foreign.len() >= MAX_FOREIGN || foreign.iter().any(|f| f.kind == kind && f.raw == raw) {
452 return;
453 }
454 foreign.push(Foreign {
455 kind,
456 raw,
457 owner,
458 here: self.session,
459 });
460 }
461
462 pub(crate) fn add_font(
463 &mut self,
464 family: String,
465 faces: Vec<cosmic_text::fontdb::ID>,
466 ) -> FontId {
467 let id = mint().fonts.insert(self.session);
468 self.fonts.insert(
469 id,
470 FontEntry {
471 family,
472 faces,
473 weights: crate::weights::Weights::CSS,
474 },
475 );
476 id
477 }
478
479 /// Reads again the weights of the registered families `families` holds
480 /// from what `db` has of them now: after a family is
481 /// registered, and after faces of it come into the database or leave.
482 /// Whether the weights of a font other than `fresh` — the one just
483 /// registered, which nothing has shaped in yet — changed: text shaped
484 /// in it was shaped at the old ones.
485 pub(crate) fn reweigh(
486 &mut self,
487 db: &cosmic_text::fontdb::Database,
488 families: &rustc_hash::FxHashSet<String>,
489 fresh: Option<FontId>,
490 ) -> bool {
491 let mut changed = false;
492 for (id, entry) in self.fonts.iter_mut() {
493 if families.contains(&entry.family) {
494 let weights = crate::weights::Weights::of(db, &entry.family);
495 changed |= Some(id) != fresh && weights != entry.weights;
496 entry.weights = weights;
497 }
498 }
499 changed
500 }
501
502 pub(crate) fn remove_font(&mut self, id: FontId) -> Option<FontEntry> {
503 let entry = self.fonts.remove(id);
504 match entry {
505 Some(_) => {
506 mint().fonts.remove(id);
507 }
508 None => self.note_miss(ResourceKind::Font, id.to_ffi()),
509 }
510 entry
511 }
512
513 /// The registered family name, if the handle is live here.
514 pub fn font_family(&self, id: FontId) -> Option<&str> {
515 let entry = self.fonts.get(id);
516 if entry.is_none() {
517 self.note_miss(ResourceKind::Font, id.to_ffi());
518 }
519 entry.map(|f| f.family.as_str())
520 }
521
522 /// The cosmic-text family a style's `FontFamily` shapes with; an unknown
523 /// or removed custom font falls back to sans-serif.
524 pub(crate) fn family_of(&self, f: FontFamily) -> cosmic_text::Family<'_> {
525 match f {
526 FontFamily::Sans => cosmic_text::Family::SansSerif,
527 FontFamily::Serif => cosmic_text::Family::Serif,
528 FontFamily::Mono => cosmic_text::Family::Monospace,
529 FontFamily::Custom(id) => match self.font_family(id) {
530 Some(name) => cosmic_text::Family::Name(name),
531 None => cosmic_text::Family::SansSerif,
532 },
533 }
534 }
535
536 /// The weights a style's `FontFamily` is asked at: a
537 /// registered family's own, the CSS ones for a generic family and for
538 /// an unknown or removed custom font (which shapes as sans-serif).
539 pub(crate) fn weights_of(&self, f: FontFamily) -> crate::weights::Weights {
540 match f {
541 FontFamily::Custom(id) => self
542 .fonts
543 .get(id)
544 .map_or(crate::weights::Weights::CSS, |entry| entry.weights),
545 _ => crate::weights::Weights::CSS,
546 }
547 }
548
549 pub fn add_image(&mut self, width: u32, height: u32, rgba: Vec<u8>) -> ImageId {
550 debug_assert_eq!(rgba.len(), (width * height * 4) as usize);
551 let id = mint().images.insert(self.session);
552 self.images.insert(
553 id,
554 ImageEntry {
555 width,
556 height,
557 rgba: std::sync::Arc::new(rgba),
558 rev: 0,
559 spare: None,
560 spare_at: 0,
561 // Past a page it has nowhere to go but its own texture;
562 // before ADR 0025 it was dropped at emission, silently.
563 backing: if width > crate::atlas::MAX_ATLAS_SIZE
564 || height > crate::atlas::MAX_ATLAS_SIZE
565 {
566 ImageBacking::Texture
567 } else {
568 ImageBacking::Atlas
569 },
570 },
571 );
572 id
573 }
574
575 /// Replaces an image's pixels in place: the
576 /// handle is unchanged, so every node declaring it shows the new
577 /// pixels next frame with no view change; the dimensions may change.
578 /// From the first update on the image is texture-backed for life.
579 /// Returns whether the pixels were taken: false for a foreign or
580 /// removed handle (noted as a miss), and for a buffer that is not
581 /// `width × height × 4` bytes, which changes nothing rather than
582 /// handing a backend a short upload it would refuse with a validation
583 /// error — the doors that take bytes from an app check the length
584 /// first and say so; this is the guard behind them.
585 pub fn update_image(&mut self, id: ImageId, width: u32, height: u32, rgba: Vec<u8>) -> bool {
586 if rgba.len() != width as usize * height as usize * 4 {
587 return false;
588 }
589 let Some(entry) = self.images.get_mut(id) else {
590 self.note_miss(ResourceKind::Image, id.to_ffi());
591 return false;
592 };
593 entry.width = width;
594 entry.height = height;
595 entry.rgba = std::sync::Arc::new(rgba);
596 entry.spare = None;
597 entry.rev = entry.rev.wrapping_add(1);
598 entry.backing = ImageBacking::Texture;
599 true
600 }
601
602 /// [`Self::update_image`] into a buffer the core recycles: `fill` is
603 /// handed `width × height × 4` bytes to write the new pixels into, and
604 /// is not called for a foreign or removed handle (noted as a miss),
605 /// or for a size whose byte count overflows. The bytes it is handed
606 /// hold an earlier frame's pixels, not zeros, so `fill` writes every
607 /// one. The buffer is the image's own when no display list still
608 /// holds it, else the one the previous update replaced, else a new
609 /// one — so a stream updated every frame, between frames or inside
610 /// them, allocates at most three times and then never again. The replaced
611 /// buffer is kept only for an image updated before
612 /// at the same size, and let go [`SPARE_FRAMES`] frames after the
613 /// last update. What every door that copies an app's bytes goes
614 /// through.
615 pub fn update_image_with(
616 &mut self,
617 id: ImageId,
618 width: u32,
619 height: u32,
620 fill: impl FnOnce(&mut [u8]),
621 ) -> bool {
622 use std::sync::Arc;
623 let Some(len) = (width as usize)
624 .checked_mul(height as usize)
625 .and_then(|n| n.checked_mul(4))
626 else {
627 return false;
628 };
629 let Some(entry) = self.images.get_mut(id) else {
630 self.note_miss(ResourceKind::Image, id.to_ffi());
631 return false;
632 };
633 if Arc::get_mut(&mut entry.rgba).is_none() {
634 // The last frame's display list (or a backend mid-upload)
635 // still reads the current buffer: write into the spare if
636 // nothing reads that any more, else into a new one. `vec!`
637 // rather than a resize, so a fresh buffer's zeros are the
638 // allocator's and not a pass over it.
639 let had = entry.spare.is_some();
640 // Unique by both counts: a `Weak` a host took of pixels it
641 // was handed makes `get_mut` refuse the buffer as well.
642 let next = match entry.spare.take() {
643 Some(spare) if Arc::strong_count(&spare) == 1 && Arc::weak_count(&spare) == 0 => {
644 spare
645 }
646 _ => Arc::new(vec![0; len]),
647 };
648 let replaced = std::mem::replace(&mut entry.rgba, next);
649 // The replaced buffer is worth keeping for a stream: an image
650 // updated before (not the pixels it was added with) whose size
651 // holds. A one-off update, or a resize, keeps nothing.
652 if entry.rev > 0 && replaced.len() == len {
653 entry.spare = Some(replaced);
654 entry.spare_at = self.frames;
655 if !had {
656 self.spared.push(id);
657 }
658 }
659 }
660 // The size, revision and backing before `fill`, so a `fill` that
661 // panics leaves stale pixels at the right length rather than a
662 // buffer whose length its size does not match.
663 entry.width = width;
664 entry.height = height;
665 entry.rev = entry.rev.wrapping_add(1);
666 entry.backing = ImageBacking::Texture;
667 let buf = Arc::get_mut(&mut entry.rgba).expect("unshared: checked or replaced above");
668 buf.resize(len, 0);
669 // A stream that shrank does not keep its old size's allocation.
670 if buf.capacity() > len.saturating_mul(2) {
671 buf.shrink_to(len);
672 }
673 fill(buf);
674 true
675 }
676
677 /// Called as each frame begins: drops the spare buffer of every image
678 /// not updated for [`SPARE_FRAMES`] frames.
679 pub(crate) fn release_spares(&mut self) {
680 self.frames += 1;
681 if self.spared.is_empty() {
682 return;
683 }
684 let (images, frames) = (&mut self.images, self.frames);
685 self.spared.retain(|&id| match images.get_mut(id) {
686 Some(entry) if entry.spare.is_some() => {
687 if frames - entry.spare_at > SPARE_FRAMES {
688 entry.spare = None;
689 false
690 } else {
691 true
692 }
693 }
694 _ => false,
695 });
696 }
697
698 pub fn remove_image(&mut self, id: ImageId) -> Option<ImageEntry> {
699 let entry = self.images.remove(id);
700 match entry {
701 Some(_) => {
702 mint().images.remove(id);
703 }
704 None => self.note_miss(ResourceKind::Image, id.to_ffi()),
705 }
706 entry
707 }
708
709 /// The pixels behind an image handle, if it is live here.
710 pub fn image(&self, id: ImageId) -> Option<&ImageEntry> {
711 let entry = self.images.get(id);
712 if entry.is_none() {
713 self.note_miss(ResourceKind::Image, id.to_ffi());
714 }
715 entry
716 }
717
718 /// The handle an identical source already has, if any. What makes a
719 /// view that calls `add_fragment` every frame cost a comparison
720 /// instead of a validation (55-73 us) and a pipeline build.
721 pub(crate) fn find_fragment(&self, source: &str) -> Option<FragmentId> {
722 self.fragments
723 .iter()
724 .find(|(_, f)| &*f.source == source)
725 .map(|(id, _)| id)
726 }
727
728 /// Registers validated WGSL, or hands back the handle an identical
729 /// source already has.
730 pub(crate) fn add_fragment(&mut self, source: &str) -> FragmentId {
731 if let Some(id) = self.find_fragment(source) {
732 return id;
733 }
734 let id = mint().fragments.insert(self.session);
735 self.fragments.insert(
736 id,
737 FragmentEntry {
738 source: Arc::from(source),
739 },
740 );
741 id
742 }
743
744 pub(crate) fn remove_fragment(&mut self, id: FragmentId) -> Option<FragmentEntry> {
745 let entry = self.fragments.remove(id);
746 match entry {
747 Some(_) => {
748 mint().fragments.remove(id);
749 }
750 None => self.note_miss(ResourceKind::Fragment, id.to_ffi()),
751 }
752 entry
753 }
754
755 /// The WGSL behind a fragment handle, if it is live here. A miss is
756 /// what makes the node draw nothing, and records a foreign handle.
757 pub fn fragment(&self, id: FragmentId) -> Option<&Arc<str>> {
758 let entry = self.fragments.get(id);
759 if entry.is_none() {
760 self.note_miss(ResourceKind::Fragment, id.to_ffi());
761 }
762 entry.map(|f| &f.source)
763 }
764
765 /// Registers a sound from its encoded file bytes.
766 pub fn add_sound(&mut self, bytes: Vec<u8>) -> SoundId {
767 let id = mint().sounds.insert(self.session);
768 self.sounds.insert(
769 id,
770 SoundEntry {
771 bytes: Arc::from(bytes),
772 },
773 );
774 id
775 }
776
777 pub fn remove_sound(&mut self, id: SoundId) -> Option<SoundEntry> {
778 let entry = self.sounds.remove(id);
779 match entry {
780 Some(_) => {
781 mint().sounds.remove(id);
782 }
783 None => self.note_miss(ResourceKind::Sound, id.to_ffi()),
784 }
785 entry
786 }
787
788 /// Whether any sound is registered: what tells an audio backend it
789 /// will be asked to play something, before it is.
790 pub fn has_sounds(&self) -> bool {
791 !self.sounds.is_empty()
792 }
793
794 /// The encoded bytes behind a sound handle, if it is live here.
795 pub fn sound(&self, id: SoundId) -> Option<&Arc<[u8]>> {
796 let entry = self.sounds.get(id);
797 if entry.is_none() {
798 self.note_miss(ResourceKind::Sound, id.to_ffi());
799 }
800 entry.map(|s| &s.bytes)
801 }
802}
803
804impl Drop for Resources {
805 /// A session's handles leave the mint with it, so the slots come back
806 /// and a process that opens and closes many sessions (a test binary)
807 /// does not keep every id it ever minted.
808 fn drop(&mut self) {
809 let mut m = mint();
810 for (id, _) in self.images.iter() {
811 m.images.remove(id);
812 }
813 for (id, _) in self.fonts.iter() {
814 m.fonts.remove(id);
815 }
816 for (id, _) in self.fragments.iter() {
817 m.fragments.remove(id);
818 }
819 for (id, _) in self.sounds.iter() {
820 m.sounds.remove(id);
821 }
822 }
823}
824
825#[cfg(test)]
826mod tests {
827 use super::*;
828 use slotmap::KeyData;
829
830 /// RG59: a reweigh reports a change only to a family registered
831 /// before — text may have been shaped in it — and never for the one
832 /// just registered, so a list registering a family per row as it
833 /// scrolls does not drop every window's shaped text each time.
834 #[test]
835 fn a_reweigh_reports_only_a_family_text_may_be_shaped_in() {
836 use crate::weights::Weights;
837 use cosmic_text::fontdb::{Database, Source};
838 let mut db = Database::new();
839 let load = |db: &mut Database, weight| {
840 let bytes = crate::testing::font_face("Kui Fresh", weight, false, true);
841 db.load_font_source(Source::Binary(std::sync::Arc::new(bytes)));
842 };
843 load(&mut db, 400);
844 let touched = std::iter::once("Kui Fresh".to_string()).collect();
845 let mut r = Resources::new(SessionId::next());
846 let id = r.add_font("Kui Fresh".into(), vec![]);
847 assert!(!r.reweigh(&db, &touched, Some(id)), "just registered");
848 assert_ne!(r.weights_of(FontFamily::Custom(id)), Weights::CSS);
849 assert!(!r.reweigh(&db, &touched, None), "nothing moved");
850 load(&mut db, 700);
851 assert!(r.reweigh(&db, &touched, None), "its Bold came");
852 assert_eq!(r.weights_of(FontFamily::Custom(id)), Weights::CSS);
853 }
854
855 #[test]
856 fn stale_handle_is_rejected_after_removal() {
857 let mut r = Resources::new(SessionId::next());
858 let id = r.add_image(1, 1, vec![0; 4]);
859 r.remove_image(id);
860 assert!(r.image(id).is_none());
861 // A new insert may reuse the slot but bumps the generation.
862 let id2 = r.add_image(1, 1, vec![0; 4]);
863 assert_ne!(id, id2);
864 assert!(r.image(id).is_none());
865 assert!(r.image(id2).is_some());
866 // Removed, not foreign: nothing to report.
867 assert!(r.take_foreign().is_empty());
868 }
869
870 /// Raw 0 is index 0, the slot the mint never fills, so it misses in
871 /// every session and is nobody's — the `fragments` scene's dead `src`
872 /// and the doors' "no image" both rest on it. Raw 1 is *not* that:
873 /// `from_ffi` reads every handle at an odd generation, so it is the
874 /// first key a fresh process hands out.
875 #[test]
876 fn raw_zero_is_dead_whatever_was_minted() {
877 let mut a = Resources::new(SessionId::next());
878 let b = Resources::new(SessionId::next());
879 let _ = a.add_image(1, 1, vec![0; 4]);
880 let _ = a.add_fragment("");
881 for r in [&a, &b] {
882 assert!(r.image(ImageId::from_ffi(0)).is_none());
883 assert!(r.fragment(FragmentId::from_ffi(0)).is_none());
884 assert!(r.take_foreign().is_empty(), "raw 0 is nobody's");
885 }
886 }
887
888 #[test]
889 fn handles_round_trip_through_u64() {
890 let mut r = Resources::new(SessionId::next());
891 let id = r.add_image(1, 1, vec![0; 4]);
892 let raw = id.data().as_ffi();
893 let back = ImageId::from(KeyData::from_ffi(raw));
894 assert_eq!(id, back);
895 assert!(r.image(back).is_some());
896 }
897
898 #[test]
899 fn two_registries_never_mint_the_same_handle() {
900 let mut a = Resources::new(SessionId::next());
901 let mut b = Resources::new(SessionId::next());
902 let ia = a.add_image(1, 1, vec![0; 4]);
903 let ib = b.add_image(2, 2, vec![0; 16]);
904 assert_ne!(ia, ib, "two first images, two handles");
905 assert_ne!(
906 a.add_font("A".into(), vec![]),
907 b.add_font("B".into(), vec![])
908 );
909 assert_ne!(a.add_sound(vec![0]), b.add_sound(vec![1]));
910 }
911
912 #[test]
913 fn a_foreign_handle_misses_and_is_reported_once() {
914 let mut a = Resources::new(SessionId::next());
915 let b = Resources::new(SessionId::next());
916 let id = a.add_image(1, 1, vec![0; 4]);
917 assert!(b.image(id).is_none(), "b never draws a's pixels");
918 assert!(b.image(id).is_none());
919 let hits = b.take_foreign();
920 assert_eq!(
921 hits,
922 [Foreign {
923 kind: ResourceKind::Image,
924 raw: id.to_ffi(),
925 owner: a.session(),
926 here: b.session(),
927 }]
928 );
929 assert!(b.take_foreign().is_empty(), "drained");
930 // The owner resolving its own handle records nothing.
931 assert!(a.image(id).is_some());
932 assert!(a.take_foreign().is_empty());
933 // Once the owner removes it, the miss is a removal everywhere.
934 a.remove_image(id);
935 assert!(b.image(id).is_none());
936 assert!(b.take_foreign().is_empty());
937 }
938
939 #[test]
940 fn removing_a_foreign_handle_touches_nothing_and_reports() {
941 let mut a = Resources::new(SessionId::next());
942 let mut b = Resources::new(SessionId::next());
943 let sound = a.add_sound(vec![1, 2, 3]);
944 assert!(b.remove_sound(sound).is_none());
945 assert!(a.sound(sound).is_some(), "a's sound is still a's");
946 assert_eq!(b.take_foreign()[0].kind, ResourceKind::Sound);
947 }
948
949 #[test]
950 fn a_dropped_registry_gives_its_slots_back() {
951 let id = {
952 let mut a = Resources::new(SessionId::next());
953 a.add_image(1, 1, vec![0; 4])
954 };
955 assert!(mint().images.get(id).is_none(), "gone with its session");
956 let b = Resources::new(SessionId::next());
957 assert!(b.image(id).is_none());
958 assert!(
959 b.take_foreign().is_empty(),
960 "a dead session's handle is just removed"
961 );
962 }
963}