use std::cell::RefCell;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, LazyLock, Mutex, MutexGuard};
use slotmap::{Key as _, SlotMap, SparseSecondaryMap, new_key_type};
use crate::spec::FontFamily;
new_key_type! {
pub struct ImageId;
pub struct PainterId;
pub struct FontId;
pub struct SoundId;
pub struct FragmentId;
}
impl FragmentId {
pub fn to_ffi(self) -> u64 {
self.data().as_ffi()
}
pub fn from_ffi(raw: u64) -> Self {
Self::from(slotmap::KeyData::from_ffi(raw))
}
}
impl SoundId {
pub fn to_ffi(self) -> u64 {
self.data().as_ffi()
}
pub fn from_ffi(raw: u64) -> Self {
Self::from(slotmap::KeyData::from_ffi(raw))
}
}
impl FontId {
pub fn to_ffi(self) -> u64 {
self.data().as_ffi()
}
pub fn from_ffi(raw: u64) -> Self {
Self::from(slotmap::KeyData::from_ffi(raw))
}
}
impl ImageId {
pub fn to_ffi(self) -> u64 {
self.data().as_ffi()
}
pub fn from_ffi(raw: u64) -> Self {
Self::from(slotmap::KeyData::from_ffi(raw))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct SessionId(pub u64);
impl SessionId {
pub(crate) fn next() -> Self {
static NEXT: AtomicU64 = AtomicU64::new(1);
Self(NEXT.fetch_add(1, Ordering::Relaxed))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ResourceKind {
Image,
Font,
Sound,
Fragment,
}
impl ResourceKind {
pub fn name(self) -> &'static str {
match self {
Self::Image => "image",
Self::Font => "font",
Self::Sound => "sound",
Self::Fragment => "fragment",
}
}
fn fallback(self) -> &'static str {
match self {
Self::Image => "draws nothing",
Self::Font => "shapes as sans-serif",
Self::Sound => "plays nothing",
Self::Fragment => "draws nothing",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Foreign {
pub kind: ResourceKind,
pub raw: u64,
pub owner: SessionId,
pub here: SessionId,
}
impl Foreign {
pub fn message(&self) -> String {
let Foreign {
kind,
raw,
owner,
here,
} = *self;
let what = kind.name();
let did = kind.fallback();
format!(
"{what} handle {raw:#x} belongs to session #{} and was used in session #{}: a \
handle is only valid in the session that registered it, so this one is treated \
as removed and {did} — register the {what} through a core of this session, or \
build both cores against one `Session` (`Core::new_in`)",
owner.0, here.0
)
}
}
const MAX_FOREIGN: usize = 64;
#[derive(Default)]
struct Mint {
images: SlotMap<ImageId, SessionId>,
fonts: SlotMap<FontId, SessionId>,
sounds: SlotMap<SoundId, SessionId>,
fragments: SlotMap<FragmentId, SessionId>,
}
static MINT: LazyLock<Mutex<Mint>> = LazyLock::new(Mutex::default);
fn mint() -> MutexGuard<'static, Mint> {
MINT.lock().unwrap_or_else(|e| e.into_inner())
}
pub(crate) fn mint_image(session: SessionId) -> ImageId {
mint().images.insert(session)
}
pub(crate) fn unmint_image(id: ImageId) {
mint().images.remove(id);
}
pub struct ImageEntry {
pub width: u32,
pub height: u32,
pub rgba: std::sync::Arc<Vec<u8>>,
pub rev: u32,
pub backing: ImageBacking,
pub(crate) spare: Option<std::sync::Arc<Vec<u8>>>,
spare_at: u64,
}
pub const SPARE_FRAMES: u64 = 30;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ImageBacking {
Atlas,
Texture,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ImageOpts {
pub sampling: Sampling,
pub fit: ImageFit,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Sampling {
#[default]
Linear,
Nearest,
}
impl Sampling {
pub const ALL: [Sampling; 2] = [Sampling::Linear, Sampling::Nearest];
pub fn name(self) -> &'static str {
match self {
Sampling::Linear => "linear",
Sampling::Nearest => "nearest",
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ImageFit {
#[default]
Fill,
Contain,
Cover,
}
impl ImageFit {
pub const ALL: [ImageFit; 3] = [ImageFit::Fill, ImageFit::Contain, ImageFit::Cover];
pub fn name(self) -> &'static str {
match self {
ImageFit::Fill => "fill",
ImageFit::Contain => "contain",
ImageFit::Cover => "cover",
}
}
}
pub struct FontEntry {
pub family: String,
pub faces: Vec<cosmic_text::fontdb::ID>,
pub(crate) weights: crate::weights::Weights,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SystemFont {
pub family: String,
pub monospaced: bool,
pub weights: Vec<u16>,
pub italic: bool,
}
pub struct SoundEntry {
pub bytes: Arc<[u8]>,
}
pub struct FragmentEntry {
pub source: Arc<str>,
}
pub struct Resources {
session: SessionId,
pub(crate) images: SparseSecondaryMap<ImageId, ImageEntry>,
pub(crate) fonts: SparseSecondaryMap<FontId, FontEntry>,
pub(crate) sounds: SparseSecondaryMap<SoundId, SoundEntry>,
pub(crate) fragments: SparseSecondaryMap<FragmentId, FragmentEntry>,
pub(crate) fallback: Vec<String>,
pub(crate) mono: Option<(String, crate::weights::Weights)>,
foreign: RefCell<Vec<Foreign>>,
frames: u64,
spared: Vec<ImageId>,
}
impl Resources {
pub fn new(session: SessionId) -> Self {
Self {
session,
images: SparseSecondaryMap::new(),
fonts: SparseSecondaryMap::new(),
sounds: SparseSecondaryMap::new(),
fragments: SparseSecondaryMap::new(),
fallback: Vec::new(),
mono: None,
foreign: RefCell::new(Vec::new()),
frames: 0,
spared: Vec::new(),
}
}
pub fn session(&self) -> SessionId {
self.session
}
pub(crate) fn take_foreign(&self) -> Vec<Foreign> {
std::mem::take(&mut *self.foreign.borrow_mut())
}
fn note_miss(&self, kind: ResourceKind, raw: u64) {
let owner = {
let m = mint();
match kind {
ResourceKind::Image => m.images.get(ImageId::from_ffi(raw)).copied(),
ResourceKind::Font => m.fonts.get(FontId::from_ffi(raw)).copied(),
ResourceKind::Sound => m.sounds.get(SoundId::from_ffi(raw)).copied(),
ResourceKind::Fragment => m.fragments.get(FragmentId::from_ffi(raw)).copied(),
}
};
let Some(owner) = owner else {
return;
};
if owner == self.session {
return;
}
let mut foreign = self.foreign.borrow_mut();
if foreign.len() >= MAX_FOREIGN || foreign.iter().any(|f| f.kind == kind && f.raw == raw) {
return;
}
foreign.push(Foreign {
kind,
raw,
owner,
here: self.session,
});
}
pub(crate) fn add_font(
&mut self,
family: String,
faces: Vec<cosmic_text::fontdb::ID>,
) -> FontId {
let id = mint().fonts.insert(self.session);
self.fonts.insert(
id,
FontEntry {
family,
faces,
weights: crate::weights::Weights::CSS,
},
);
id
}
pub(crate) fn reweigh(
&mut self,
db: &cosmic_text::fontdb::Database,
families: &rustc_hash::FxHashSet<String>,
fresh: Option<FontId>,
) -> bool {
let mut changed = false;
for (id, entry) in self.fonts.iter_mut() {
if families.contains(&entry.family) {
let weights = crate::weights::Weights::of(db, &entry.family);
changed |= Some(id) != fresh && weights != entry.weights;
entry.weights = weights;
}
}
changed
}
pub(crate) fn remove_font(&mut self, id: FontId) -> Option<FontEntry> {
let entry = self.fonts.remove(id);
match entry {
Some(_) => {
mint().fonts.remove(id);
}
None => self.note_miss(ResourceKind::Font, id.to_ffi()),
}
entry
}
pub fn font_family(&self, id: FontId) -> Option<&str> {
let entry = self.fonts.get(id);
if entry.is_none() {
self.note_miss(ResourceKind::Font, id.to_ffi());
}
entry.map(|f| f.family.as_str())
}
pub(crate) fn family_of(&self, f: FontFamily) -> cosmic_text::Family<'_> {
match f {
FontFamily::Sans => cosmic_text::Family::SansSerif,
FontFamily::Serif => cosmic_text::Family::Serif,
FontFamily::Mono => self
.mono
.as_ref()
.map_or(cosmic_text::Family::Monospace, |(name, _)| {
cosmic_text::Family::Name(name)
}),
FontFamily::Custom(id) => match self.font_family(id) {
Some(name) => cosmic_text::Family::Name(name),
None => cosmic_text::Family::SansSerif,
},
}
}
pub(crate) fn weights_of(&self, f: FontFamily) -> crate::weights::Weights {
match f {
FontFamily::Custom(id) => self
.fonts
.get(id)
.map_or(crate::weights::Weights::CSS, |entry| entry.weights),
FontFamily::Mono => self
.mono
.as_ref()
.map_or(crate::weights::Weights::CSS, |&(_, weights)| weights),
_ => crate::weights::Weights::CSS,
}
}
pub fn add_image(&mut self, width: u32, height: u32, rgba: Vec<u8>) -> ImageId {
debug_assert_eq!(rgba.len(), (width * height * 4) as usize);
let id = mint().images.insert(self.session);
self.images.insert(
id,
ImageEntry {
width,
height,
rgba: std::sync::Arc::new(rgba),
rev: 0,
spare: None,
spare_at: 0,
backing: if width > crate::atlas::MAX_ATLAS_SIZE
|| height > crate::atlas::MAX_ATLAS_SIZE
{
ImageBacking::Texture
} else {
ImageBacking::Atlas
},
},
);
id
}
pub fn update_image(&mut self, id: ImageId, width: u32, height: u32, rgba: Vec<u8>) -> bool {
if rgba.len() != width as usize * height as usize * 4 {
return false;
}
let Some(entry) = self.images.get_mut(id) else {
self.note_miss(ResourceKind::Image, id.to_ffi());
return false;
};
entry.width = width;
entry.height = height;
entry.rgba = std::sync::Arc::new(rgba);
entry.spare = None;
entry.rev = entry.rev.wrapping_add(1);
entry.backing = ImageBacking::Texture;
true
}
pub fn update_image_with(
&mut self,
id: ImageId,
width: u32,
height: u32,
fill: impl FnOnce(&mut [u8]),
) -> bool {
use std::sync::Arc;
let Some(len) = (width as usize)
.checked_mul(height as usize)
.and_then(|n| n.checked_mul(4))
else {
return false;
};
let Some(entry) = self.images.get_mut(id) else {
self.note_miss(ResourceKind::Image, id.to_ffi());
return false;
};
if Arc::get_mut(&mut entry.rgba).is_none() {
let had = entry.spare.is_some();
let next = match entry.spare.take() {
Some(spare) if Arc::strong_count(&spare) == 1 && Arc::weak_count(&spare) == 0 => {
spare
}
_ => Arc::new(vec![0; len]),
};
let replaced = std::mem::replace(&mut entry.rgba, next);
if entry.rev > 0 && replaced.len() == len {
entry.spare = Some(replaced);
entry.spare_at = self.frames;
if !had {
self.spared.push(id);
}
}
}
entry.width = width;
entry.height = height;
entry.rev = entry.rev.wrapping_add(1);
entry.backing = ImageBacking::Texture;
let buf = Arc::get_mut(&mut entry.rgba).expect("unshared: checked or replaced above");
buf.resize(len, 0);
if buf.capacity() > len.saturating_mul(2) {
buf.shrink_to(len);
}
fill(buf);
true
}
pub(crate) fn release_spares(&mut self) {
self.frames += 1;
if self.spared.is_empty() {
return;
}
let (images, frames) = (&mut self.images, self.frames);
self.spared.retain(|&id| match images.get_mut(id) {
Some(entry) if entry.spare.is_some() => {
if frames - entry.spare_at > SPARE_FRAMES {
entry.spare = None;
false
} else {
true
}
}
_ => false,
});
}
pub fn remove_image(&mut self, id: ImageId) -> Option<ImageEntry> {
let entry = self.images.remove(id);
match entry {
Some(_) => {
mint().images.remove(id);
}
None => self.note_miss(ResourceKind::Image, id.to_ffi()),
}
entry
}
pub fn image(&self, id: ImageId) -> Option<&ImageEntry> {
let entry = self.images.get(id);
if entry.is_none() {
self.note_miss(ResourceKind::Image, id.to_ffi());
}
entry
}
pub(crate) fn find_fragment(&self, source: &str) -> Option<FragmentId> {
self.fragments
.iter()
.find(|(_, f)| &*f.source == source)
.map(|(id, _)| id)
}
pub(crate) fn add_fragment(&mut self, source: &str) -> FragmentId {
if let Some(id) = self.find_fragment(source) {
return id;
}
let id = mint().fragments.insert(self.session);
self.fragments.insert(
id,
FragmentEntry {
source: Arc::from(source),
},
);
id
}
pub(crate) fn remove_fragment(&mut self, id: FragmentId) -> Option<FragmentEntry> {
let entry = self.fragments.remove(id);
match entry {
Some(_) => {
mint().fragments.remove(id);
}
None => self.note_miss(ResourceKind::Fragment, id.to_ffi()),
}
entry
}
pub fn fragment(&self, id: FragmentId) -> Option<&Arc<str>> {
let entry = self.fragments.get(id);
if entry.is_none() {
self.note_miss(ResourceKind::Fragment, id.to_ffi());
}
entry.map(|f| &f.source)
}
pub fn add_sound(&mut self, bytes: Vec<u8>) -> SoundId {
let id = mint().sounds.insert(self.session);
self.sounds.insert(
id,
SoundEntry {
bytes: Arc::from(bytes),
},
);
id
}
pub fn remove_sound(&mut self, id: SoundId) -> Option<SoundEntry> {
let entry = self.sounds.remove(id);
match entry {
Some(_) => {
mint().sounds.remove(id);
}
None => self.note_miss(ResourceKind::Sound, id.to_ffi()),
}
entry
}
pub fn has_sounds(&self) -> bool {
!self.sounds.is_empty()
}
pub fn sound(&self, id: SoundId) -> Option<&Arc<[u8]>> {
let entry = self.sounds.get(id);
if entry.is_none() {
self.note_miss(ResourceKind::Sound, id.to_ffi());
}
entry.map(|s| &s.bytes)
}
}
impl Drop for Resources {
fn drop(&mut self) {
let mut m = mint();
for (id, _) in self.images.iter() {
m.images.remove(id);
}
for (id, _) in self.fonts.iter() {
m.fonts.remove(id);
}
for (id, _) in self.fragments.iter() {
m.fragments.remove(id);
}
for (id, _) in self.sounds.iter() {
m.sounds.remove(id);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use slotmap::KeyData;
#[test]
fn a_reweigh_reports_only_a_family_text_may_be_shaped_in() {
use crate::weights::Weights;
use cosmic_text::fontdb::{Database, Source};
let mut db = Database::new();
let load = |db: &mut Database, weight| {
let bytes = crate::testing::font_face("Kui Fresh", weight, false, true);
db.load_font_source(Source::Binary(std::sync::Arc::new(bytes)));
};
load(&mut db, 400);
let touched = std::iter::once("Kui Fresh".to_string()).collect();
let mut r = Resources::new(SessionId::next());
let id = r.add_font("Kui Fresh".into(), vec![]);
assert!(!r.reweigh(&db, &touched, Some(id)), "just registered");
assert_ne!(r.weights_of(FontFamily::Custom(id)), Weights::CSS);
assert!(!r.reweigh(&db, &touched, None), "nothing moved");
load(&mut db, 700);
assert!(r.reweigh(&db, &touched, None), "its Bold came");
assert_eq!(r.weights_of(FontFamily::Custom(id)), Weights::CSS);
}
#[test]
fn stale_handle_is_rejected_after_removal() {
let mut r = Resources::new(SessionId::next());
let id = r.add_image(1, 1, vec![0; 4]);
r.remove_image(id);
assert!(r.image(id).is_none());
let id2 = r.add_image(1, 1, vec![0; 4]);
assert_ne!(id, id2);
assert!(r.image(id).is_none());
assert!(r.image(id2).is_some());
assert!(r.take_foreign().is_empty());
}
#[test]
fn raw_zero_is_dead_whatever_was_minted() {
let mut a = Resources::new(SessionId::next());
let b = Resources::new(SessionId::next());
let _ = a.add_image(1, 1, vec![0; 4]);
let _ = a.add_fragment("");
for r in [&a, &b] {
assert!(r.image(ImageId::from_ffi(0)).is_none());
assert!(r.fragment(FragmentId::from_ffi(0)).is_none());
assert!(r.take_foreign().is_empty(), "raw 0 is nobody's");
}
}
#[test]
fn handles_round_trip_through_u64() {
let mut r = Resources::new(SessionId::next());
let id = r.add_image(1, 1, vec![0; 4]);
let raw = id.data().as_ffi();
let back = ImageId::from(KeyData::from_ffi(raw));
assert_eq!(id, back);
assert!(r.image(back).is_some());
}
#[test]
fn two_registries_never_mint_the_same_handle() {
let mut a = Resources::new(SessionId::next());
let mut b = Resources::new(SessionId::next());
let ia = a.add_image(1, 1, vec![0; 4]);
let ib = b.add_image(2, 2, vec![0; 16]);
assert_ne!(ia, ib, "two first images, two handles");
assert_ne!(
a.add_font("A".into(), vec![]),
b.add_font("B".into(), vec![])
);
assert_ne!(a.add_sound(vec![0]), b.add_sound(vec![1]));
}
#[test]
fn a_foreign_handle_misses_and_is_reported_once() {
let mut a = Resources::new(SessionId::next());
let b = Resources::new(SessionId::next());
let id = a.add_image(1, 1, vec![0; 4]);
assert!(b.image(id).is_none(), "b never draws a's pixels");
assert!(b.image(id).is_none());
let hits = b.take_foreign();
assert_eq!(
hits,
[Foreign {
kind: ResourceKind::Image,
raw: id.to_ffi(),
owner: a.session(),
here: b.session(),
}]
);
assert!(b.take_foreign().is_empty(), "drained");
assert!(a.image(id).is_some());
assert!(a.take_foreign().is_empty());
a.remove_image(id);
assert!(b.image(id).is_none());
assert!(b.take_foreign().is_empty());
}
#[test]
fn removing_a_foreign_handle_touches_nothing_and_reports() {
let mut a = Resources::new(SessionId::next());
let mut b = Resources::new(SessionId::next());
let sound = a.add_sound(vec![1, 2, 3]);
assert!(b.remove_sound(sound).is_none());
assert!(a.sound(sound).is_some(), "a's sound is still a's");
assert_eq!(b.take_foreign()[0].kind, ResourceKind::Sound);
}
#[test]
fn a_dropped_registry_gives_its_slots_back() {
let id = {
let mut a = Resources::new(SessionId::next());
a.add_image(1, 1, vec![0; 4])
};
assert!(mint().images.get(id).is_none(), "gone with its session");
let b = Resources::new(SessionId::next());
assert!(b.image(id).is_none());
assert!(
b.take_foreign().is_empty(),
"a dead session's handle is just removed"
);
}
}