Skip to main content

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 image handle for pixels the core itself owns — a path's mask drawn
220/// from a texture of its own — unique across sessions as a registered
221/// image's is, so a backend's texture cache never confuses the two.
222pub(crate) fn mint_image(session: SessionId) -> ImageId {
223    mint().images.insert(session)
224}
225
226/// Returns a handle [`mint_image`] made.
227pub(crate) fn unmint_image(id: ImageId) {
228    mint().images.remove(id);
229}
230
231/// An RGBA image registered by the host (rendering lands in a later pass).
232pub struct ImageEntry {
233    pub width: u32,
234    pub height: u32,
235    /// Shared rather than owned so a frame's display list can hand a
236    /// backend the pixels of a texture-backed image without copying them
237    /// (`DisplayList::texture_pixels`): an update replaces the `Arc`, and
238    /// a backend mid-upload keeps the old one alive until it is done.
239    pub rgba: std::sync::Arc<Vec<u8>>,
240    /// Moves on every [`Resources::update_image`]; a backend re-uploads a
241    /// texture-backed image when the revision it uploaded is behind.
242    pub rev: u32,
243    /// Where the pixels live on the GPU.
244    pub backing: ImageBacking,
245    /// The buffer the last [`Resources::update_image_with`] replaced, kept
246    /// for the next one to write into once no display list holds it.
247    /// An update between frames finds `rgba` still shared
248    /// with the last frame's `texture_pixels`, so without it every update
249    /// was a fresh `w × h × 4` allocation and the previous one freed —
250    /// 590 µs of page faults and 170 µs of release at 1080p on Windows,
251    /// three times the copy itself. Two buffers per streamed image, and a
252    /// stream stops allocating from its fourth update. Kept only for an
253    /// image updated before, at the same size, and dropped by
254    /// [`Resources::release_spares`] once [`SPARE_FRAMES`] frames pass
255    /// with no update: an image updated once, or a stream that stopped,
256    /// holds one buffer again.
257    pub(crate) spare: Option<std::sync::Arc<Vec<u8>>>,
258    /// [`Resources::frames`] when `spare` was last set.
259    spare_at: u64,
260    /// The levels below level 0 made so far
261    /// (`docs/adr/0044-an-image-drawn-smaller-is-drawn-from-a-level.md`):
262    /// `levels[n]` is level `n + 1`, made at the first draw that wants it
263    /// or one below it and kept for every window of the session, so a
264    /// second window, or an atlas page that was reset, blits it again
265    /// without halving again. An update drops them; an updated image
266    /// draws level 0.
267    levels: std::sync::Mutex<Vec<std::sync::Arc<Level>>>,
268}
269
270/// One level of an image's chain: the image halved `n` times
271/// ([`crate::mip::halve`]), straight RGBA.
272#[derive(Debug)]
273pub struct Level {
274    pub width: u32,
275    pub height: u32,
276    pub rgba: Vec<u8>,
277}
278
279impl ImageEntry {
280    /// Whether a draw may take a level below 0 from this image: one never
281    /// updated. An update makes a stream, and a stream is drawn whole —
282    /// halving one on every update is milliseconds a frame (ADR 0044,
283    /// decision 4).
284    pub fn levels_allowed(&self) -> bool {
285        self.rev == 0
286    }
287
288    /// How many levels the chain has below level 0.
289    pub fn depth(&self) -> u8 {
290        crate::mip::depth(self.width, self.height)
291    }
292
293    /// Level `n` (1 and up) of the chain, made now if no draw has asked
294    /// for it or a level below it before, each from the one above.
295    /// `None` for `n` 0 (the entry's own pixels) or past the chain.
296    pub fn level(&self, n: u8) -> Option<std::sync::Arc<Level>> {
297        if n == 0 || n > self.depth() {
298            return None;
299        }
300        let mut chain = self.levels.lock().unwrap_or_else(|e| e.into_inner());
301        while chain.len() < n as usize {
302            let (w, h, rgba) = match chain.last() {
303                Some(l) => crate::mip::halve(l.width, l.height, &l.rgba),
304                None => crate::mip::halve(self.width, self.height, &self.rgba),
305            };
306            chain.push(std::sync::Arc::new(Level {
307                width: w,
308                height: h,
309                rgba,
310            }));
311        }
312        Some(chain[n as usize - 1].clone())
313    }
314
315    /// Forgets the chain, for pixels that changed.
316    fn drop_levels(&mut self) {
317        self.levels
318            .get_mut()
319            .unwrap_or_else(|e| e.into_inner())
320            .clear();
321    }
322}
323
324/// How many frames an image's spare buffer outlives its last update.
325/// Long enough for a stream slower than the display — a
326/// 30 fps video beside a 120 Hz animation updates every fourth frame —
327/// and short enough that one that stopped gives its buffer back.
328pub const SPARE_FRAMES: u64 = 30;
329
330/// Where a registered image's pixels are kept for drawing.
331/// The core
332/// decides on the two facts that matter — whether the image fits an atlas
333/// page, and whether its pixels were ever replaced — and the app never
334/// chooses.
335#[derive(Clone, Copy, Debug, PartialEq, Eq)]
336pub enum ImageBacking {
337    /// Blitted into the glyph atlas at first draw and drawn as
338    /// [`crate::display::QuadKind::Image`]: icons, thumbnails, anything
339    /// that fits and never changes.
340    Atlas,
341    /// A texture of its own, drawn as [`crate::display::QuadKind::Texture`]
342    /// through an entry in `DisplayList::textures`: an image that does not
343    /// fit a `MAX_ATLAS_SIZE` page, or one that has been updated in place.
344    /// Once here, an image stays here.
345    Texture,
346}
347
348/// How an `image` node meets the pixels it shows: its two per-node rows.
349/// Carried on the node's content rather than
350/// on `NodeSpec`, so a box pays nothing for a row only an image reads.
351#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
352pub struct ImageOpts {
353    pub sampling: Sampling,
354    pub fit: ImageFit,
355}
356
357/// The `sampling` row: how a backend reads texels between pixel centres.
358#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
359pub enum Sampling {
360    /// Bilinear — a photo, a rendered frame (the default).
361    #[default]
362    Linear,
363    /// Nearest texel — pixel art, an emulator, a data grid that must stay
364    /// square under zoom.
365    Nearest,
366}
367
368impl Sampling {
369    /// Every mode, in the order the `sampling` row names them.
370    pub const ALL: [Sampling; 2] = [Sampling::Linear, Sampling::Nearest];
371
372    pub fn name(self) -> &'static str {
373        match self {
374            Sampling::Linear => "linear",
375            Sampling::Nearest => "nearest",
376        }
377    }
378}
379
380/// The `fit` row: how the pixels meet the node's box. The box itself —
381/// its layout, its hit region, its access rect — is the same in every
382/// mode; only what is painted inside it moves.
383#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
384pub enum ImageFit {
385    /// The pixels stretch to the box (the default, and what every image
386    /// did before the row existed).
387    #[default]
388    Fill,
389    /// The largest rect of the image's aspect that fits the box, centred;
390    /// the rest of the box shows what is behind it.
391    Contain,
392    /// The box is filled and the pixels that do not fit are cropped,
393    /// centred.
394    Cover,
395}
396
397impl ImageFit {
398    /// Every mode, in the order the `fit` row names them.
399    pub const ALL: [ImageFit; 3] = [ImageFit::Fill, ImageFit::Contain, ImageFit::Cover];
400
401    pub fn name(self) -> &'static str {
402        match self {
403            ImageFit::Fill => "fill",
404            ImageFit::Contain => "contain",
405            ImageFit::Cover => "cover",
406        }
407    }
408}
409
410/// A registered font: the family name shaping resolves it by, the faces
411/// it loaded into the font database (empty for installed fonts), and the
412/// weights the family is asked at for regular and bold.
413pub struct FontEntry {
414    pub family: String,
415    pub faces: Vec<cosmic_text::fontdb::ID>,
416    pub(crate) weights: crate::weights::Weights,
417}
418
419/// One family of the font database — installed or loaded — as its faces
420/// describe it, from what the database read off each face's tables when
421/// it was scanned: nothing is loaded or shaped to answer.
422/// What [`Core::system_fonts`](crate::Core::system_fonts) lists, one per
423/// family.
424#[derive(Clone, Debug, PartialEq, Eq)]
425pub struct SystemFont {
426    /// The family name [`Core::add_system_font`](crate::Core::add_system_font)
427    /// takes.
428    pub family: String,
429    /// Every face of the family says it is fixed-pitch (the `post` table's
430    /// `isFixedPitch`): a monospaced family. Measuring glyph widths instead
431    /// loads and shapes every file, and calls a symbol font whose glyphs
432    /// happen to share an advance monospaced.
433    pub monospaced: bool,
434    /// The weights its faces come in, on the CSS scale (400 regular, 700
435    /// bold) as each face's `OS/2` table says it — sorted, each once. A
436    /// variable font's face reads as its default instance.
437    pub weights: Vec<u16>,
438    /// It has an italic or an oblique face.
439    pub italic: bool,
440}
441
442/// A registered sound: the encoded file (wav/ogg/mp3/flac, whatever the
443/// driver's backend decodes), shared so the backend can hold it without a
444/// copy. The core never decodes — headless drivers have no use for PCM.
445pub struct SoundEntry {
446    pub bytes: Arc<[u8]>,
447}
448
449/// A registered fragment: the app's WGSL as it was given, which is what a
450/// backend compiles (around the core's prelude and epilogue — see
451/// `crate::fragment`) and what a C host reads back to compile itself.
452pub struct FragmentEntry {
453    /// The app's source, without the prelude or the epilogue.
454    pub source: Arc<str>,
455}
456
457/// One session's registry. The maps are secondary to the process-wide
458/// the process-wide mint (see the module doc), so a lookup that misses is a handle this
459/// session never registered or has since removed — never somebody else's
460/// entry.
461pub struct Resources {
462    session: SessionId,
463    pub(crate) images: SparseSecondaryMap<ImageId, ImageEntry>,
464    pub(crate) fonts: SparseSecondaryMap<FontId, FontEntry>,
465    pub(crate) sounds: SparseSecondaryMap<SoundId, SoundEntry>,
466    pub(crate) fragments: SparseSecondaryMap<FragmentId, FragmentEntry>,
467    /// The families asked before the platform's fallback lists
468    /// (`Core::set_fallback_fonts`), in order: what the font system was
469    /// built with, kept for the one built again over a new scan.
470    pub(crate) fallback: Vec<String>,
471    /// The family `FontFamily::Mono` is shaped as by name while
472    /// `fallback` names any (backlog RG118): cosmic-text's generic
473    /// monospace family asks every monospaced face on the machine for a
474    /// character its own face lacks before it gets to the lists the app's
475    /// names lead, so the app's choice lost to whichever monospaced face
476    /// came first — Courier New's italic for Hebrew on a Mac. By name, it
477    /// asks its own face and then the app's, as a registered family does.
478    /// None with no list, where the monospaced faces are asked first as
479    /// before (`SessionState::name_mono` keeps it). With the name, the
480    /// weights its faces cover: by name cosmic-text takes a face of the
481    /// family only at a weight it has, and passes a single-weight family
482    /// over for bold — to the proportional lists — where the generic
483    /// family took any weight (backlog RG123).
484    pub(crate) mono: Option<(String, crate::weights::Weights)>,
485    /// Handles of other sessions this registry was asked for since the
486    /// last drain, each once. A `RefCell` because the resolves that find
487    /// them (`family_of` under a shaping closure, `image` under the
488    /// emitter's shared borrow) hold the registry by `&`.
489    foreign: RefCell<Vec<Foreign>>,
490    /// Frames begun by any window of the session, for aging spares.
491    frames: u64,
492    /// The images holding a spare buffer, swept each frame.
493    spared: Vec<ImageId>,
494}
495
496impl Resources {
497    /// The registry for session `session`; keys come from the process's
498    /// mint.
499    pub fn new(session: SessionId) -> Self {
500        Self {
501            session,
502            images: SparseSecondaryMap::new(),
503            fonts: SparseSecondaryMap::new(),
504            sounds: SparseSecondaryMap::new(),
505            fragments: SparseSecondaryMap::new(),
506            fallback: Vec::new(),
507            mono: None,
508            foreign: RefCell::new(Vec::new()),
509            frames: 0,
510            spared: Vec::new(),
511        }
512    }
513
514    /// The session this registry belongs to.
515    pub fn session(&self) -> SessionId {
516        self.session
517    }
518
519    /// The foreign handles resolved since the last call (see
520    /// [`Foreign`]); `Core::take_warnings` turns each into one line.
521    pub(crate) fn take_foreign(&self) -> Vec<Foreign> {
522        std::mem::take(&mut *self.foreign.borrow_mut())
523    }
524
525    /// A lookup missed: the mint says whether the handle is live in some
526    /// other session (recorded, once) or in none (removed — silent, the
527    /// documented behaviour). Only the miss path pays for the lock.
528    fn note_miss(&self, kind: ResourceKind, raw: u64) {
529        let owner = {
530            let m = mint();
531            match kind {
532                ResourceKind::Image => m.images.get(ImageId::from_ffi(raw)).copied(),
533                ResourceKind::Font => m.fonts.get(FontId::from_ffi(raw)).copied(),
534                ResourceKind::Sound => m.sounds.get(SoundId::from_ffi(raw)).copied(),
535                ResourceKind::Fragment => m.fragments.get(FragmentId::from_ffi(raw)).copied(),
536            }
537        };
538        let Some(owner) = owner else {
539            return;
540        };
541        if owner == self.session {
542            return;
543        }
544        let mut foreign = self.foreign.borrow_mut();
545        if foreign.len() >= MAX_FOREIGN || foreign.iter().any(|f| f.kind == kind && f.raw == raw) {
546            return;
547        }
548        foreign.push(Foreign {
549            kind,
550            raw,
551            owner,
552            here: self.session,
553        });
554    }
555
556    pub(crate) fn add_font(
557        &mut self,
558        family: String,
559        faces: Vec<cosmic_text::fontdb::ID>,
560    ) -> FontId {
561        let id = mint().fonts.insert(self.session);
562        self.fonts.insert(
563            id,
564            FontEntry {
565                family,
566                faces,
567                weights: crate::weights::Weights::CSS,
568            },
569        );
570        id
571    }
572
573    /// Reads again the weights of the registered families `families` holds
574    /// from what `db` has of them now: after a family is
575    /// registered, and after faces of it come into the database or leave.
576    /// Whether the weights of a font other than `fresh` — the one just
577    /// registered, which nothing has shaped in yet — changed: text shaped
578    /// in it was shaped at the old ones.
579    pub(crate) fn reweigh(
580        &mut self,
581        db: &cosmic_text::fontdb::Database,
582        families: &rustc_hash::FxHashSet<String>,
583        fresh: Option<FontId>,
584    ) -> bool {
585        let mut changed = false;
586        for (id, entry) in self.fonts.iter_mut() {
587            if families.contains(&entry.family) {
588                let weights = crate::weights::Weights::of(db, &entry.family);
589                changed |= Some(id) != fresh && weights != entry.weights;
590                entry.weights = weights;
591            }
592        }
593        changed
594    }
595
596    pub(crate) fn remove_font(&mut self, id: FontId) -> Option<FontEntry> {
597        let entry = self.fonts.remove(id);
598        match entry {
599            Some(_) => {
600                mint().fonts.remove(id);
601            }
602            None => self.note_miss(ResourceKind::Font, id.to_ffi()),
603        }
604        entry
605    }
606
607    /// The registered family name, if the handle is live here.
608    pub fn font_family(&self, id: FontId) -> Option<&str> {
609        let entry = self.fonts.get(id);
610        if entry.is_none() {
611            self.note_miss(ResourceKind::Font, id.to_ffi());
612        }
613        entry.map(|f| f.family.as_str())
614    }
615
616    /// The cosmic-text family a style's `FontFamily` shapes with; an unknown
617    /// or removed custom font falls back to sans-serif. `Mono` is its face's
618    /// family by name while the app names fallbacks (see `mono`).
619    pub(crate) fn family_of(&self, f: FontFamily) -> cosmic_text::Family<'_> {
620        match f {
621            FontFamily::Sans => cosmic_text::Family::SansSerif,
622            FontFamily::Serif => cosmic_text::Family::Serif,
623            FontFamily::Mono => self
624                .mono
625                .as_ref()
626                .map_or(cosmic_text::Family::Monospace, |(name, _)| {
627                    cosmic_text::Family::Name(name)
628                }),
629            FontFamily::Custom(id) => match self.font_family(id) {
630                Some(name) => cosmic_text::Family::Name(name),
631                None => cosmic_text::Family::SansSerif,
632            },
633        }
634    }
635
636    /// The weights a style's `FontFamily` is asked at: a
637    /// registered family's own, the pinned face's for `Mono` while it is
638    /// shaped by name (see `mono`), the CSS ones for a generic family and
639    /// for an unknown or removed custom font (which shapes as sans-serif).
640    pub(crate) fn weights_of(&self, f: FontFamily) -> crate::weights::Weights {
641        match f {
642            FontFamily::Custom(id) => self
643                .fonts
644                .get(id)
645                .map_or(crate::weights::Weights::CSS, |entry| entry.weights),
646            FontFamily::Mono => self
647                .mono
648                .as_ref()
649                .map_or(crate::weights::Weights::CSS, |&(_, weights)| weights),
650            _ => crate::weights::Weights::CSS,
651        }
652    }
653
654    pub fn add_image(&mut self, width: u32, height: u32, rgba: Vec<u8>) -> ImageId {
655        debug_assert_eq!(rgba.len(), (width * height * 4) as usize);
656        let id = mint().images.insert(self.session);
657        self.images.insert(
658            id,
659            ImageEntry {
660                width,
661                height,
662                rgba: std::sync::Arc::new(rgba),
663                rev: 0,
664                spare: None,
665                spare_at: 0,
666                levels: Default::default(),
667                // Past a page it has nowhere to go but its own texture;
668                // before ADR 0025 it was dropped at emission, silently.
669                backing: if width > crate::atlas::MAX_ATLAS_SIZE
670                    || height > crate::atlas::MAX_ATLAS_SIZE
671                {
672                    ImageBacking::Texture
673                } else {
674                    ImageBacking::Atlas
675                },
676            },
677        );
678        id
679    }
680
681    /// Replaces an image's pixels in place: the
682    /// handle is unchanged, so every node declaring it shows the new
683    /// pixels next frame with no view change; the dimensions may change.
684    /// From the first update on the image is texture-backed for life.
685    /// Returns whether the pixels were taken: false for a foreign or
686    /// removed handle (noted as a miss), and for a buffer that is not
687    /// `width × height × 4` bytes, which changes nothing rather than
688    /// handing a backend a short upload it would refuse with a validation
689    /// error — the doors that take bytes from an app check the length
690    /// first and say so; this is the guard behind them.
691    pub fn update_image(&mut self, id: ImageId, width: u32, height: u32, rgba: Vec<u8>) -> bool {
692        if rgba.len() != width as usize * height as usize * 4 {
693            return false;
694        }
695        let Some(entry) = self.images.get_mut(id) else {
696            self.note_miss(ResourceKind::Image, id.to_ffi());
697            return false;
698        };
699        entry.width = width;
700        entry.height = height;
701        entry.rgba = std::sync::Arc::new(rgba);
702        entry.spare = None;
703        entry.rev = entry.rev.wrapping_add(1);
704        entry.backing = ImageBacking::Texture;
705        entry.drop_levels();
706        true
707    }
708
709    /// [`Self::update_image`] into a buffer the core recycles: `fill` is
710    /// handed `width × height × 4` bytes to write the new pixels into, and
711    /// is not called for a foreign or removed handle (noted as a miss),
712    /// or for a size whose byte count overflows. The bytes it is handed
713    /// hold an earlier frame's pixels, not zeros, so `fill` writes every
714    /// one. The buffer is the image's own when no display list still
715    /// holds it, else the one the previous update replaced, else a new
716    /// one — so a stream updated every frame, between frames or inside
717    /// them, allocates at most three times and then never again. The replaced
718    /// buffer is kept only for an image updated before
719    /// at the same size, and let go [`SPARE_FRAMES`] frames after the
720    /// last update. What every door that copies an app's bytes goes
721    /// through.
722    pub fn update_image_with(
723        &mut self,
724        id: ImageId,
725        width: u32,
726        height: u32,
727        fill: impl FnOnce(&mut [u8]),
728    ) -> bool {
729        use std::sync::Arc;
730        let Some(len) = (width as usize)
731            .checked_mul(height as usize)
732            .and_then(|n| n.checked_mul(4))
733        else {
734            return false;
735        };
736        let Some(entry) = self.images.get_mut(id) else {
737            self.note_miss(ResourceKind::Image, id.to_ffi());
738            return false;
739        };
740        if Arc::get_mut(&mut entry.rgba).is_none() {
741            // The last frame's display list (or a backend mid-upload)
742            // still reads the current buffer: write into the spare if
743            // nothing reads that any more, else into a new one. `vec!`
744            // rather than a resize, so a fresh buffer's zeros are the
745            // allocator's and not a pass over it.
746            let had = entry.spare.is_some();
747            // Unique by both counts: a `Weak` a host took of pixels it
748            // was handed makes `get_mut` refuse the buffer as well.
749            let next = match entry.spare.take() {
750                Some(spare) if Arc::strong_count(&spare) == 1 && Arc::weak_count(&spare) == 0 => {
751                    spare
752                }
753                _ => Arc::new(vec![0; len]),
754            };
755            let replaced = std::mem::replace(&mut entry.rgba, next);
756            // The replaced buffer is worth keeping for a stream: an image
757            // updated before (not the pixels it was added with) whose size
758            // holds. A one-off update, or a resize, keeps nothing.
759            if entry.rev > 0 && replaced.len() == len {
760                entry.spare = Some(replaced);
761                entry.spare_at = self.frames;
762                if !had {
763                    self.spared.push(id);
764                }
765            }
766        }
767        // The size, revision and backing before `fill`, so a `fill` that
768        // panics leaves stale pixels at the right length rather than a
769        // buffer whose length its size does not match.
770        entry.width = width;
771        entry.height = height;
772        entry.rev = entry.rev.wrapping_add(1);
773        entry.backing = ImageBacking::Texture;
774        entry.drop_levels();
775        let buf = Arc::get_mut(&mut entry.rgba).expect("unshared: checked or replaced above");
776        buf.resize(len, 0);
777        // A stream that shrank does not keep its old size's allocation.
778        if buf.capacity() > len.saturating_mul(2) {
779            buf.shrink_to(len);
780        }
781        fill(buf);
782        true
783    }
784
785    /// Called as each frame begins: drops the spare buffer of every image
786    /// not updated for [`SPARE_FRAMES`] frames.
787    pub(crate) fn release_spares(&mut self) {
788        self.frames += 1;
789        if self.spared.is_empty() {
790            return;
791        }
792        let (images, frames) = (&mut self.images, self.frames);
793        self.spared.retain(|&id| match images.get_mut(id) {
794            Some(entry) if entry.spare.is_some() => {
795                if frames - entry.spare_at > SPARE_FRAMES {
796                    entry.spare = None;
797                    false
798                } else {
799                    true
800                }
801            }
802            _ => false,
803        });
804    }
805
806    pub fn remove_image(&mut self, id: ImageId) -> Option<ImageEntry> {
807        let entry = self.images.remove(id);
808        match entry {
809            Some(_) => {
810                mint().images.remove(id);
811            }
812            None => self.note_miss(ResourceKind::Image, id.to_ffi()),
813        }
814        entry
815    }
816
817    /// The pixels behind an image handle, if it is live here.
818    pub fn image(&self, id: ImageId) -> Option<&ImageEntry> {
819        let entry = self.images.get(id);
820        if entry.is_none() {
821            self.note_miss(ResourceKind::Image, id.to_ffi());
822        }
823        entry
824    }
825
826    /// The handle an identical source already has, if any. What makes a
827    /// view that calls `add_fragment` every frame cost a comparison
828    /// instead of a validation (55-73 us) and a pipeline build.
829    pub(crate) fn find_fragment(&self, source: &str) -> Option<FragmentId> {
830        self.fragments
831            .iter()
832            .find(|(_, f)| &*f.source == source)
833            .map(|(id, _)| id)
834    }
835
836    /// Registers validated WGSL, or hands back the handle an identical
837    /// source already has.
838    pub(crate) fn add_fragment(&mut self, source: &str) -> FragmentId {
839        if let Some(id) = self.find_fragment(source) {
840            return id;
841        }
842        let id = mint().fragments.insert(self.session);
843        self.fragments.insert(
844            id,
845            FragmentEntry {
846                source: Arc::from(source),
847            },
848        );
849        id
850    }
851
852    pub(crate) fn remove_fragment(&mut self, id: FragmentId) -> Option<FragmentEntry> {
853        let entry = self.fragments.remove(id);
854        match entry {
855            Some(_) => {
856                mint().fragments.remove(id);
857            }
858            None => self.note_miss(ResourceKind::Fragment, id.to_ffi()),
859        }
860        entry
861    }
862
863    /// The WGSL behind a fragment handle, if it is live here. A miss is
864    /// what makes the node draw nothing, and records a foreign handle.
865    pub fn fragment(&self, id: FragmentId) -> Option<&Arc<str>> {
866        let entry = self.fragments.get(id);
867        if entry.is_none() {
868            self.note_miss(ResourceKind::Fragment, id.to_ffi());
869        }
870        entry.map(|f| &f.source)
871    }
872
873    /// Registers a sound from its encoded file bytes.
874    pub fn add_sound(&mut self, bytes: Vec<u8>) -> SoundId {
875        let id = mint().sounds.insert(self.session);
876        self.sounds.insert(
877            id,
878            SoundEntry {
879                bytes: Arc::from(bytes),
880            },
881        );
882        id
883    }
884
885    pub fn remove_sound(&mut self, id: SoundId) -> Option<SoundEntry> {
886        let entry = self.sounds.remove(id);
887        match entry {
888            Some(_) => {
889                mint().sounds.remove(id);
890            }
891            None => self.note_miss(ResourceKind::Sound, id.to_ffi()),
892        }
893        entry
894    }
895
896    /// Whether any sound is registered: what tells an audio backend it
897    /// will be asked to play something, before it is.
898    pub fn has_sounds(&self) -> bool {
899        !self.sounds.is_empty()
900    }
901
902    /// The encoded bytes behind a sound handle, if it is live here.
903    pub fn sound(&self, id: SoundId) -> Option<&Arc<[u8]>> {
904        let entry = self.sounds.get(id);
905        if entry.is_none() {
906            self.note_miss(ResourceKind::Sound, id.to_ffi());
907        }
908        entry.map(|s| &s.bytes)
909    }
910}
911
912impl Drop for Resources {
913    /// A session's handles leave the mint with it, so the slots come back
914    /// and a process that opens and closes many sessions (a test binary)
915    /// does not keep every id it ever minted.
916    fn drop(&mut self) {
917        let mut m = mint();
918        for (id, _) in self.images.iter() {
919            m.images.remove(id);
920        }
921        for (id, _) in self.fonts.iter() {
922            m.fonts.remove(id);
923        }
924        for (id, _) in self.fragments.iter() {
925            m.fragments.remove(id);
926        }
927        for (id, _) in self.sounds.iter() {
928            m.sounds.remove(id);
929        }
930    }
931}
932
933#[cfg(test)]
934mod tests {
935    use super::*;
936    use slotmap::KeyData;
937
938    /// RG59: a reweigh reports a change only to a family registered
939    /// before — text may have been shaped in it — and never for the one
940    /// just registered, so a list registering a family per row as it
941    /// scrolls does not drop every window's shaped text each time.
942    #[test]
943    fn a_reweigh_reports_only_a_family_text_may_be_shaped_in() {
944        use crate::weights::Weights;
945        use cosmic_text::fontdb::{Database, Source};
946        let mut db = Database::new();
947        let load = |db: &mut Database, weight| {
948            let bytes = crate::testing::font_face("Kui Fresh", weight, false, true);
949            db.load_font_source(Source::Binary(std::sync::Arc::new(bytes)));
950        };
951        load(&mut db, 400);
952        let touched = std::iter::once("Kui Fresh".to_string()).collect();
953        let mut r = Resources::new(SessionId::next());
954        let id = r.add_font("Kui Fresh".into(), vec![]);
955        assert!(!r.reweigh(&db, &touched, Some(id)), "just registered");
956        assert_ne!(r.weights_of(FontFamily::Custom(id)), Weights::CSS);
957        assert!(!r.reweigh(&db, &touched, None), "nothing moved");
958        load(&mut db, 700);
959        assert!(r.reweigh(&db, &touched, None), "its Bold came");
960        assert_eq!(r.weights_of(FontFamily::Custom(id)), Weights::CSS);
961    }
962
963    #[test]
964    fn stale_handle_is_rejected_after_removal() {
965        let mut r = Resources::new(SessionId::next());
966        let id = r.add_image(1, 1, vec![0; 4]);
967        r.remove_image(id);
968        assert!(r.image(id).is_none());
969        // A new insert may reuse the slot but bumps the generation.
970        let id2 = r.add_image(1, 1, vec![0; 4]);
971        assert_ne!(id, id2);
972        assert!(r.image(id).is_none());
973        assert!(r.image(id2).is_some());
974        // Removed, not foreign: nothing to report.
975        assert!(r.take_foreign().is_empty());
976    }
977
978    /// Raw 0 is index 0, the slot the mint never fills, so it misses in
979    /// every session and is nobody's — the `fragments` scene's dead `src`
980    /// and the doors' "no image" both rest on it. Raw 1 is *not* that:
981    /// `from_ffi` reads every handle at an odd generation, so it is the
982    /// first key a fresh process hands out.
983    #[test]
984    fn raw_zero_is_dead_whatever_was_minted() {
985        let mut a = Resources::new(SessionId::next());
986        let b = Resources::new(SessionId::next());
987        let _ = a.add_image(1, 1, vec![0; 4]);
988        let _ = a.add_fragment("");
989        for r in [&a, &b] {
990            assert!(r.image(ImageId::from_ffi(0)).is_none());
991            assert!(r.fragment(FragmentId::from_ffi(0)).is_none());
992            assert!(r.take_foreign().is_empty(), "raw 0 is nobody's");
993        }
994    }
995
996    #[test]
997    fn handles_round_trip_through_u64() {
998        let mut r = Resources::new(SessionId::next());
999        let id = r.add_image(1, 1, vec![0; 4]);
1000        let raw = id.data().as_ffi();
1001        let back = ImageId::from(KeyData::from_ffi(raw));
1002        assert_eq!(id, back);
1003        assert!(r.image(back).is_some());
1004    }
1005
1006    #[test]
1007    fn two_registries_never_mint_the_same_handle() {
1008        let mut a = Resources::new(SessionId::next());
1009        let mut b = Resources::new(SessionId::next());
1010        let ia = a.add_image(1, 1, vec![0; 4]);
1011        let ib = b.add_image(2, 2, vec![0; 16]);
1012        assert_ne!(ia, ib, "two first images, two handles");
1013        assert_ne!(
1014            a.add_font("A".into(), vec![]),
1015            b.add_font("B".into(), vec![])
1016        );
1017        assert_ne!(a.add_sound(vec![0]), b.add_sound(vec![1]));
1018    }
1019
1020    #[test]
1021    fn a_foreign_handle_misses_and_is_reported_once() {
1022        let mut a = Resources::new(SessionId::next());
1023        let b = Resources::new(SessionId::next());
1024        let id = a.add_image(1, 1, vec![0; 4]);
1025        assert!(b.image(id).is_none(), "b never draws a's pixels");
1026        assert!(b.image(id).is_none());
1027        let hits = b.take_foreign();
1028        assert_eq!(
1029            hits,
1030            [Foreign {
1031                kind: ResourceKind::Image,
1032                raw: id.to_ffi(),
1033                owner: a.session(),
1034                here: b.session(),
1035            }]
1036        );
1037        assert!(b.take_foreign().is_empty(), "drained");
1038        // The owner resolving its own handle records nothing.
1039        assert!(a.image(id).is_some());
1040        assert!(a.take_foreign().is_empty());
1041        // Once the owner removes it, the miss is a removal everywhere.
1042        a.remove_image(id);
1043        assert!(b.image(id).is_none());
1044        assert!(b.take_foreign().is_empty());
1045    }
1046
1047    #[test]
1048    fn removing_a_foreign_handle_touches_nothing_and_reports() {
1049        let mut a = Resources::new(SessionId::next());
1050        let mut b = Resources::new(SessionId::next());
1051        let sound = a.add_sound(vec![1, 2, 3]);
1052        assert!(b.remove_sound(sound).is_none());
1053        assert!(a.sound(sound).is_some(), "a's sound is still a's");
1054        assert_eq!(b.take_foreign()[0].kind, ResourceKind::Sound);
1055    }
1056
1057    #[test]
1058    fn a_dropped_registry_gives_its_slots_back() {
1059        let id = {
1060            let mut a = Resources::new(SessionId::next());
1061            a.add_image(1, 1, vec![0; 4])
1062        };
1063        assert!(mint().images.get(id).is_none(), "gone with its session");
1064        let b = Resources::new(SessionId::next());
1065        assert!(b.image(id).is_none());
1066        assert!(
1067            b.take_foreign().is_empty(),
1068            "a dead session's handle is just removed"
1069        );
1070    }
1071}