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}