kui_core/session.rs
1//! What a window does not own alone.
2//!
3//! kui is a `Core` per window (see `docs/adr/0004-multi-window.md`), and
4//! every per-frame singleton — tree, display list, focus, hit list — stays
5//! per-window. What must not be duplicated is here: the **font database**
6//! every window shapes against, the **resource registry** behind every
7//! `FontId` / `ImageId` / `SoundId`, and the **audio store**, because the
8//! process has one audio device and not one per window. A `Session` owns
9//! them and any number of `Core`s are constructed against it
10//! (`Core::new_in`), so a font, image or sound registered in one window
11//! draws and plays in every window of the session.
12//!
13//! `Core::new()` is sugar for a private session of one, which is why
14//! nothing above the core — no test, no binding, no conformance scene —
15//! has to know a session exists.
16//!
17//! **The window set is the session's too.** Which windows exist is the
18//! union of what every live window's frame declared
19//! (`docs/adr/0004-multi-window.md`, decision 4), and a union over cores
20//! can only be kept where every core can reach it: [`WindowRegistry`]
21//! holds each core's latest declarations, the windows the diff has opened,
22//! and the ids it assigned. A core hands its declarations in at
23//! `finish_frame` and takes the resulting `Open` / `Close` commands back
24//! into its own queue, so the driver drains what it always drained.
25//!
26//! **The rule for what lives here.** A session member is a registry keyed
27//! by a process-unique handle (fonts, resources, the window registry), a
28//! revision counter beside one (`fonts_rev`), or a queue any window's
29//! driver may drain (the audio commands). Anything that is *reconciled
30//! against a frame* is one window's, because a frame is: the audio store
31//! is the session's for its device and its queue, but the `audio` nodes'
32//! mounts inside it are keyed by window and diffed against that window's
33//! frame alone (AR7 — before that, every window's `finish_frame` diffed
34//! every mount against its own tree, and a popup with no `<audio>` in it
35//! stopped the main window's loop). A removed image or fragment is the
36//! same shape the other way (AR8): every window's GPU has to hear of it,
37//! so the list of removed ids is here, beside `fonts_rev`, and each core
38//! drains what it has not yet forwarded.
39//!
40//! **What stays per window, and why.** The shaped-text cache and the glyph
41//! atlas do not move here even though they look like caches. They are one
42//! unit with a window's texture: `CachedText` stamps its positioned glyphs
43//! with the atlas epoch they were packed against, so a cache entry is only
44//! valid for the page it was built from, and `Core::output` lends that page
45//! to the renderer as `&mut GlyphAtlas`. See the ADR note in
46//! `docs/adr/0004-multi-window.md`.
47//!
48//! **Borrow discipline.** The session is `Rc<RefCell<_>>`: shared
49//! ownership with a runtime check. A `Core` takes the borrow inside a
50//! method and drops it before returning, so a re-entrant borrow is not
51//! reachable from outside; the windows of a multi-window app are driven one
52//! frame at a time on one thread, so the check never contends. The one
53//! thing a `Core` lends out of the session — the family name behind
54//! `Core::font_family` — cannot live behind that borrow, so it is read from
55//! a per-window mirror that every registration and every frame refreshes.
56//!
57//! **A handle belongs to its session.** Every session has a [`SessionId`],
58//! and the handles its registry mints are unique to the process (see
59//! `resources`), so a `FontId` / `ImageId` / `SoundId` handed to a core of
60//! another session is a miss, never an alias: it behaves as a removed
61//! handle does, and the core that drains warnings next reports it as
62//! `foreign-resource`. Two `Core::new()`s in one test are two sessions;
63//! what shares a handle is `Core::new_in(&session)`.
64
65use std::cell::{RefCell, RefMut};
66use std::rc::Rc;
67use std::sync::Arc;
68
69use cosmic_text::FontSystem;
70
71use crate::audio::AudioStore;
72use crate::resources::{FontId, FragmentId, ImageId, Resources, SessionId, SoundId};
73use crate::tree::OriginId;
74use crate::window::{WindowConfig, WindowId};
75
76/// The session's contents. Reached through [`Session::state`], one borrow
77/// at a time; the fields are borrowed disjointly the way `Core`'s own
78/// fields used to be.
79pub(crate) struct SessionState {
80 /// Which session this is, for the handles the registry mints.
81 pub(crate) id: SessionId,
82 /// The font database every window in the session shapes against — a
83 /// face loaded by one window resolves for all of them.
84 pub(crate) fonts: FontSystem,
85 /// The scan of the system's fonts `fonts` holds the faces of, so a
86 /// newer scan can be applied as what came and went since
87 /// (`Core::reload_system_fonts`).
88 pub(crate) system: std::sync::Arc<crate::text::SystemFonts>,
89 /// Bumped by every rescan of the system's fonts that changed this
90 /// session's (`apply_system_fonts`) — not by a font the app loads —
91 /// so each window can report it as one `fonts` event.
92 pub(crate) system_fonts_rev: u64,
93 pub(crate) resources: Resources,
94 pub(crate) audio: AudioStore,
95 /// Bumped by every font registration and removal, so a `Core` can tell
96 /// whether its name mirror is behind without walking the slotmap.
97 pub(crate) fonts_rev: u64,
98 /// Bumped when the weights a registered family is asked at change
99 /// under it — a face of it loaded or removed (RG59) — so each `Core`
100 /// drops the text it shaped at the old ones.
101 pub(crate) weights_rev: u64,
102 /// Whether the database's file-backed faces have been mapped once and
103 /// shared (`share_faces`, backlog DX24): done on the first family an
104 /// app names, so an app that only ever shapes the stock families never
105 /// pays for it.
106 pub(crate) faces_shared: bool,
107 /// Bumped by every image removal, so a `Core` can tell whether its
108 /// atlas still holds a slot for an image the registry no longer has
109 /// (AR8). The atlas is per window, so every core re-checks its own.
110 pub(crate) images_rev: u64,
111 /// Handles removed and not yet handed to a display list (AR8): what
112 /// a backend frees on the device. Textures and fragment pipelines
113 /// live on the device every window of the session shares, so the
114 /// list is the session's and whichever core begins a frame next
115 /// carries it — a removal through a window that closes before its
116 /// next frame is not lost with the window.
117 pub(crate) dropped: Dropped,
118 /// The declared window set and the windows it has opened.
119 pub(crate) windows: WindowRegistry,
120 /// The devtools panel's state (`docs/adr/0024`, decision 5): one
121 /// panel for the session, whichever window draws it.
122 pub(crate) devtools: crate::runtime::devtools::State,
123}
124
125impl SessionState {
126 fn new() -> Self {
127 let id = SessionId::next();
128 let (fonts, system) = crate::text::new_font_system();
129 Self {
130 id,
131 fonts,
132 system,
133 system_fonts_rev: 0,
134 resources: Resources::new(id),
135 audio: AudioStore::default(),
136 fonts_rev: 0,
137 weights_rev: 0,
138 faces_shared: false,
139 images_rev: 0,
140 dropped: Dropped::default(),
141 windows: WindowRegistry::new(),
142 devtools: crate::runtime::devtools::State::default(),
143 }
144 }
145}
146
147impl SessionState {
148 /// Unregisters an image: the registry forgets it, every core's atlas
149 /// hears (through `images_rev`) and, for a texture-backed one, the
150 /// next display list any core builds tells the backend to drop the
151 /// texture. `None` for a handle the registry did not hold, which is
152 /// noted as a miss.
153 pub(crate) fn remove_image(&mut self, id: ImageId) -> Option<crate::resources::ImageEntry> {
154 let entry = self.resources.remove_image(id)?;
155 self.images_rev += 1;
156 if entry.backing == crate::resources::ImageBacking::Texture {
157 self.dropped.images.push(id);
158 }
159 Some(entry)
160 }
161
162 /// Unregisters a fragment; the next display list any core builds
163 /// tells the backend to drop its pipelines.
164 pub(crate) fn remove_fragment(&mut self, id: FragmentId) -> bool {
165 if self.resources.remove_fragment(id).is_none() {
166 return false;
167 }
168 self.dropped.fragments.push(id);
169 true
170 }
171}
172
173/// Handles the backend has yet to hear were removed; see
174/// [`SessionState::dropped`].
175#[derive(Default)]
176pub(crate) struct Dropped {
177 /// Texture-backed images (an atlas-backed one has nothing on the
178 /// device to free).
179 pub(crate) images: Vec<ImageId>,
180 pub(crate) fragments: Vec<FragmentId>,
181}
182
183/// One frame's declaration of a window, as `Core::declare_window` recorded
184/// it: the name, the config the declaration carried, and who declared it.
185#[derive(Clone, Debug, PartialEq)]
186pub(crate) struct WindowDecl {
187 pub(crate) name: Rc<str>,
188 pub(crate) config: WindowConfig,
189 pub(crate) origin: OriginId,
190 /// The same frame declared this name again with a different config.
191 /// The first declaration is the one kept; this remembers that there
192 /// was a disagreement, for the warning the opening edge raises.
193 pub(crate) conflict: bool,
194}
195
196/// A window the registry has opened: its name, its id, and whether it is
197/// still on screen. `live` goes false when the driver reports an OS close
198/// while the name is still declared — the window stays closed until the
199/// declaration lapses and starts again (ADR 0004 decision 6).
200struct WindowEntry {
201 name: Rc<str>,
202 id: WindowId,
203 live: bool,
204}
205
206/// What one diff of the declared set decided; the core turns each into a
207/// `WindowCommand`, a `{kind:"window"}` event and, sometimes, a warning.
208pub(crate) enum WindowChange {
209 Opened {
210 id: WindowId,
211 name: Rc<str>,
212 /// The window whose slot won the declaration — a popup's owner,
213 /// and the surface its anchor is measured against.
214 owner: WindowId,
215 origin: OriginId,
216 config: WindowConfig,
217 /// Two declarations of this name disagreed about the config on the
218 /// frame it opened: `duplicate-window-config`.
219 conflict: bool,
220 },
221 Closed {
222 id: WindowId,
223 name: Rc<str>,
224 },
225}
226
227/// The declared window set, across every core of the session.
228///
229/// `slots` holds each core's latest declarations, keyed by the core's
230/// window id and kept in id order — so "the lowest declaring `WindowId`
231/// wins" is the first slot that names a window, with no rule about which
232/// frame ran first. `windows` is what the diff has opened and not yet
233/// closed, seeded with the main window, which the launcher opens and
234/// nothing here ever closes.
235pub(crate) struct WindowRegistry {
236 slots: Vec<(WindowId, Vec<WindowDecl>)>,
237 windows: Vec<WindowEntry>,
238 next_id: u32,
239}
240
241/// The main window's name: what `view(model, window)` is called with in
242/// Node, and what `Core::window_name` answers for `WindowId::MAIN`.
243pub const MAIN_WINDOW_NAME: &str = "main";
244
245impl WindowRegistry {
246 fn new() -> Self {
247 Self {
248 slots: Vec::new(),
249 windows: vec![WindowEntry {
250 name: Rc::from(MAIN_WINDOW_NAME),
251 id: WindowId::MAIN,
252 live: true,
253 }],
254 next_id: 1,
255 }
256 }
257
258 /// Replaces what the core drawing `id` declared, from its latest frame.
259 pub(crate) fn set_slot(&mut self, id: WindowId, decls: &[WindowDecl]) {
260 match self.slots.binary_search_by_key(&id, |(i, _)| *i) {
261 Ok(i) if decls.is_empty() => {
262 self.slots.remove(i);
263 }
264 Ok(i) => {
265 self.slots[i].1.clear();
266 self.slots[i].1.extend_from_slice(decls);
267 }
268 Err(_) if decls.is_empty() => {}
269 Err(i) => self.slots.insert(i, (id, decls.to_vec())),
270 }
271 }
272
273 fn remove_slot(&mut self, id: WindowId) {
274 if let Ok(i) = self.slots.binary_search_by_key(&id, |(i, _)| *i) {
275 self.slots.remove(i);
276 }
277 }
278
279 /// The name the window `id` was declared under, while it is open.
280 pub(crate) fn name_of(&self, id: WindowId) -> Option<Rc<str>> {
281 self.windows
282 .iter()
283 .find(|w| w.id == id)
284 .map(|w| w.name.clone())
285 }
286
287 /// Every window on screen, main first, in the order they opened.
288 pub(crate) fn live(&self) -> Vec<(WindowId, Rc<str>)> {
289 self.windows
290 .iter()
291 .filter(|w| w.live)
292 .map(|w| (w.id, w.name.clone()))
293 .collect()
294 }
295
296 /// Whether any window was closed by the OS and is still declared —
297 /// the state `window-declared-while-closed` reports.
298 pub(crate) fn any_closed(&self) -> bool {
299 self.windows.iter().any(|w| !w.live)
300 }
301
302 /// The names among `decls` whose window the OS closed and nothing has
303 /// stopped declaring since.
304 pub(crate) fn closed_among<'a>(
305 &'a self,
306 decls: &'a [WindowDecl],
307 ) -> impl Iterator<Item = Rc<str>> + 'a {
308 decls.iter().filter_map(|d| {
309 self.windows
310 .iter()
311 .find(|w| !w.live && w.name == d.name)
312 .map(|w| w.name.clone())
313 })
314 }
315
316 /// The driver reports that the OS closed window `id`. Its declarations
317 /// leave the union with it; its name stays known, closed, until the
318 /// declaration lapses. Returns the name if the window was open; `None`
319 /// for the main window (which closing ends the app) and for an id the
320 /// diff already closed.
321 pub(crate) fn os_closed(&mut self, id: WindowId) -> Option<Rc<str>> {
322 if id == WindowId::MAIN {
323 return None;
324 }
325 let w = self.windows.iter_mut().find(|w| w.id == id && w.live)?;
326 w.live = false;
327 let name = w.name.clone();
328 self.remove_slot(id);
329 Some(name)
330 }
331
332 /// Diffs the union of every slot against the open windows.
333 ///
334 /// A live window nobody declares any more closes, and its own
335 /// declarations leave the union with it — so a window that declared a
336 /// child closes the child in the same diff, however deep. A name in
337 /// the union with no window opens, with the config from the lowest
338 /// declaring slot (main is 0, so main wins whenever it declares) and
339 /// the first declaration within that slot's frame; any other
340 /// declaration that disagrees marks the open as a conflict. A closed
341 /// name still in the union stays closed (decision 6's edge); one that
342 /// left it is forgotten, so declaring it again opens it anew.
343 pub(crate) fn diff(&mut self, out: &mut Vec<WindowChange>) {
344 loop {
345 let union = self.union();
346 let gone: Vec<usize> = (0..self.windows.len())
347 .rev()
348 .filter(|&i| {
349 let w = &self.windows[i];
350 w.id != WindowId::MAIN && !union.iter().any(|u| u.0 == w.name)
351 })
352 .collect();
353 if gone.is_empty() {
354 for (name, config, owner, origin, conflict) in union {
355 if self.windows.iter().any(|w| w.name == name) {
356 continue;
357 }
358 let id = WindowId(self.next_id);
359 self.next_id += 1;
360 self.windows.push(WindowEntry {
361 name: name.clone(),
362 id,
363 live: true,
364 });
365 out.push(WindowChange::Opened {
366 id,
367 name,
368 owner,
369 origin,
370 config,
371 conflict,
372 });
373 }
374 return;
375 }
376 for i in gone {
377 let w = self.windows.remove(i);
378 self.remove_slot(w.id);
379 if w.live {
380 out.push(WindowChange::Closed {
381 id: w.id,
382 name: w.name,
383 });
384 }
385 }
386 }
387 }
388
389 /// The declared set: one entry per name, from the lowest declaring
390 /// slot — whose window id rides along as the owner, since the slot key
391 /// *is* the window whose frame declared it — with whether any
392 /// declaration of it disagreed.
393 fn union(&self) -> Vec<(Rc<str>, WindowConfig, WindowId, OriginId, bool)> {
394 let mut union: Vec<(Rc<str>, WindowConfig, WindowId, OriginId, bool)> = Vec::new();
395 for (slot, decls) in &self.slots {
396 for d in decls {
397 match union.iter_mut().find(|u| u.0 == d.name) {
398 Some(u) => u.4 |= u.1 != d.config,
399 None => union.push((d.name.clone(), d.config, *slot, d.origin, d.conflict)),
400 }
401 }
402 }
403 union
404 }
405}
406
407/// The font database, registries and audio store a set of windows share.
408/// Cloning one clones the handle, not the contents: that is how a second
409/// `Core` joins (`Core::new_in(&session)`).
410#[derive(Clone)]
411pub struct Session(Rc<RefCell<SessionState>>);
412
413impl Session {
414 pub fn new() -> Self {
415 Self(Rc::new(RefCell::new(SessionState::new())))
416 }
417
418 /// Whether two handles name the same session.
419 pub fn is(&self, other: &Session) -> bool {
420 Rc::ptr_eq(&self.0, &other.0)
421 }
422
423 /// This session's process-wide id — the number a `foreign-resource`
424 /// warning names.
425 pub fn id(&self) -> SessionId {
426 self.0.borrow().id
427 }
428
429 pub(crate) fn state(&self) -> RefMut<'_, SessionState> {
430 self.0.borrow_mut()
431 }
432}
433
434impl Default for Session {
435 fn default() -> Self {
436 Self::new()
437 }
438}
439
440impl std::fmt::Debug for Session {
441 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
442 f.write_str("Session")
443 }
444}
445
446/// A window's handle to the session's audio store — `Core`'s `audio`
447/// field. The process has one audio device, so the playbacks and the
448/// command queue are the session's and not this window's: whichever
449/// window's driver drains the queue applies every window's sounds. The
450/// `audio` nodes' mounts are per window inside it (see the module doc),
451/// which is why the readers here take one.
452#[derive(Clone)]
453pub struct SharedAudio(Session);
454
455impl SharedAudio {
456 pub(crate) fn new(session: &Session) -> Self {
457 Self(session.clone())
458 }
459
460 /// Drains the commands queued since the last drain.
461 pub fn take_commands(&self) -> Vec<crate::audio::AudioCommand> {
462 self.0.state().audio.take_commands()
463 }
464
465 /// The playback a keyed `audio` node of `window` is running, if it is
466 /// mounted. `Core::playback_of` asks for the core's own window.
467 pub fn playback_of(
468 &self,
469 window: WindowId,
470 key: crate::key::Key,
471 ) -> Option<crate::audio::PlaybackId> {
472 self.0.state().audio.playback_of(window, key)
473 }
474
475 /// Whether any `audio` node of `window` is mounted.
476 #[cfg(feature = "conformance")]
477 pub(crate) fn any_mounted(&self, window: WindowId) -> bool {
478 self.0.state().audio.any_mounted(window)
479 }
480}
481
482/// A window's handle to the session's resource registry — `Core`'s
483/// `resources` field. A font, image or sound registered through one is
484/// registered for every window in the session, and draws in all of them.
485#[derive(Clone)]
486pub struct SharedResources(Session);
487
488impl SharedResources {
489 pub(crate) fn new(session: &Session) -> Self {
490 Self(session.clone())
491 }
492
493 /// Registers an RGBA image (`width * height * 4` bytes).
494 pub fn add_image(&self, width: u32, height: u32, rgba: Vec<u8>) -> ImageId {
495 self.0.state().resources.add_image(width, height, rgba)
496 }
497
498 /// Drops an image from the registry. Every window's atlas forgets
499 /// its blit at that window's next frame, and a texture-backed one is
500 /// dropped from the device by the next frame any window draws.
501 pub fn remove_image(&self, id: ImageId) -> bool {
502 self.0.state().remove_image(id).is_some()
503 }
504
505 /// The pixel dimensions behind an image handle, if it is live.
506 pub fn image_size(&self, id: ImageId) -> Option<(u32, u32)> {
507 let state = self.0.state();
508 let e = state.resources.image(id)?;
509 Some((e.width, e.height))
510 }
511
512 /// Registers a sound from its encoded file bytes.
513 pub fn add_sound(&self, bytes: Vec<u8>) -> SoundId {
514 self.0.state().resources.add_sound(bytes)
515 }
516
517 /// Whether any sound is registered (see [`Resources::has_sounds`]).
518 pub fn has_sounds(&self) -> bool {
519 self.0.state().resources.has_sounds()
520 }
521
522 /// The encoded bytes behind a sound handle, if it is live. Cloning the
523 /// `Arc` rather than lending it is what lets the registry be shared:
524 /// the caller (an audio backend) holds the bytes, not the borrow.
525 pub fn sound(&self, id: SoundId) -> Option<Arc<[u8]>> {
526 self.0.state().resources.sound(id).cloned()
527 }
528
529 /// The registered family name behind a font handle, if it is live.
530 pub fn font_family(&self, id: FontId) -> Option<String> {
531 self.0.state().resources.font_family(id).map(str::to_owned)
532 }
533}