Skip to main content

kui_core/
resources.rs

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