Files
ghostty-rs/crates/ghostty-font/src/shared_grid.rs
T
ddidderr 126da77a37 style: apply nightly rustfmt import grouping across the workspace
rustfmt.toml sets group_imports, imports_granularity and imports_layout,
which are nightly-only; agents formatted with stable rustfmt, which
ignored them. This is a formatting-only pass with `cargo +nightly fmt`.

Test Plan:
- cargo clippy --workspace --all-targets -- -D warnings
- cargo test --workspace
2026-10-12 05:03:37 +02:00

552 lines
18 KiB
Rust

//! This structure represents the state required to render a terminal
//! grid using the font subsystem. It is "shared" because it is able to
//! be shared across multiple surfaces.
//!
//! It is desirable for the grid state to be shared because the font
//! configuration for a set of surfaces is almost always the same and
//! font data is relatively memory intensive. Further, the font subsystem
//! should be read-heavy compared to write-heavy, so it handles concurrent
//! reads well. Going even further, the font subsystem should be very rarely
//! read at all since it should only be necessary when the grid actively
//! changes.
//!
//! SharedGrid does NOT support resizing, font-family changes, font removals
//! in collections, etc. Because the Grid is shared this would cause a
//! major disruption in the rendering of multiple surfaces (i.e. increasing
//! the font size in one would increase it in all). In many cases this isn't
//! desirable so to implement configuration changes the grid should be
//! reinitialized and all surfaces should switch over to using that one.
#![allow(clippy::collapsible_if, clippy::if_not_else, clippy::float_cmp)]
use std::{
collections::HashMap,
sync::{RwLock, RwLockReadGuard, RwLockWriteGuard},
};
use crate::{
Glyph,
Metrics,
Presentation,
SpriteFace,
Style,
atlas::{self, Atlas},
codepoint_resolver::{CodepointResolver, RenderGlyphError},
collection::{self, Collection, GetFaceError, Index, PresentationMode},
face::{Constraint, ConstraintSize, RenderOptions},
};
/// The size of a grid cell in pixels.
// TODO(integration): replace with `renderer::CellSize` once the renderer
// crate exists.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CellSize {
pub width: u32,
pub height: u32,
}
#[derive(Debug, thiserror::Error)]
pub enum InitError {
#[error(transparent)]
UpdateMetrics(#[from] collection::UpdateMetricsError),
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
GetFace(#[from] GetFaceError),
#[error(transparent)]
Render(#[from] RenderGlyphError),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Render {
pub glyph: Glyph,
pub presentation: Presentation,
}
/// The glyph cache key. Like upstream (`GlyphKey.Packed`), only the render
/// option fields that affect the identity of the render are part of the key:
/// the grid metrics and the constraint are NOT (the constraint is a function
/// of the codepoint/font, and the metrics are fixed for the grid's lifetime).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct GlyphKey {
index: Index,
glyph: u32,
cell_width: u8,
thicken: bool,
thicken_strength: u8,
constraint_width: u8,
}
impl GlyphKey {
fn new(index: Index, glyph: u32, opts: &RenderOptions) -> GlyphKey {
GlyphKey {
index,
glyph,
cell_width: opts.cell_width.unwrap_or(0) & 0b11,
thicken: opts.thicken,
thicken_strength: opts.thicken_strength,
constraint_width: opts.constraint_width & 0b11,
}
}
}
/// The lock-protected state of the grid. Callers that need direct access
/// (for example the renderer uploading the atlases) take the guard from
/// [`SharedGrid::read`] / [`SharedGrid::write`].
pub struct State {
/// Cache for codepoints to font indexes in a group.
pub codepoints: HashMap<(Style, u32, Option<Presentation>), Option<Index>>,
/// Cache for glyph renders into the atlas.
glyphs: HashMap<GlyphKey, Render>,
/// The texture atlas to store renders in. The Glyph data in the glyphs
/// cache is dependent on the atlas matching.
pub atlas_grayscale: Atlas,
pub atlas_color: Atlas,
/// The underlying resolver for font data, fallbacks, etc. The shared
/// grid takes ownership of the resolver.
pub resolver: CodepointResolver,
/// The currently active grid metrics dictating the layout of the grid.
/// This is calculated based on the resolver and current fonts.
pub metrics: Metrics,
}
pub struct SharedGrid {
/// The RwLock used to protect the shared grid. Callers are expected to use
/// this (through `read`/`write`) if they need to i.e. access the atlas
/// directly. Because callers can use this lock directly, maintainers need
/// to be extra careful to review call sites to ensure they are using the
/// lock correctly.
lock: RwLock<State>,
}
impl SharedGrid {
/// Initialize the grid.
///
/// The resolver must have a collection that supports deferred loading
/// (`collection.load_options != None`). This is because we need the load
/// options data to determine grid metrics and setup our sprite font.
///
/// SharedGrid always configures the sprite font. This struct is expected
/// to be used with a terminal grid and therefore the sprite font is
/// always necessary for correct rendering.
pub fn new(resolver: CodepointResolver) -> Result<SharedGrid, InitError> {
// We need to support loading options since we use the size data
assert!(resolver.collection.load_options.is_some());
let mut state = State {
// We set an initial capacity that can fit a good number of
// characters. This number was picked empirically based on my own
// terminal usage.
codepoints: HashMap::with_capacity(128),
glyphs: HashMap::with_capacity(128),
atlas_grayscale: Atlas::new(512, atlas::Format::Grayscale),
atlas_color: Atlas::new(512, atlas::Format::Bgra),
resolver,
// Loaded below
metrics: Metrics::calc(crate::metrics::FaceMetrics::default()),
};
// Initialize our metrics.
state.reload_metrics()?;
Ok(SharedGrid {
lock: RwLock::new(state),
})
}
/// Take the shared (read) lock.
pub fn read(&self) -> RwLockReadGuard<'_, State> {
self.lock
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
/// Take the exclusive (write) lock.
pub fn write(&self) -> RwLockWriteGuard<'_, State> {
self.lock
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
/// Returns the grid cell size.
pub fn cell_size(&self) -> CellSize {
let st = self.read();
CellSize {
width: st.metrics.cell_width,
height: st.metrics.cell_height,
}
}
/// Returns a copy of the current grid metrics.
pub fn metrics(&self) -> Metrics {
self.read().metrics
}
/// Get the font index for a given codepoint. This is cached.
///
/// This always forces loading any deferred fonts since we assume that if
/// you're looking up an index that the caller plans to use the font. By
/// loading the font in this function we can ensure thread-safety on the
/// load without complicating future calls.
pub fn get_index(
&self,
cp: u32,
style: Style,
p: Option<Presentation>,
) -> Result<Option<Index>, Error> {
let key = (style, cp, p);
// Fast path: the cache has the value. This is almost always true and
// only requires a read lock.
{
let st = self.read();
if let Some(v) = st.codepoints.get(&key) {
return Ok(*v);
}
}
// Slow path: we need to search this codepoint
let mut guard = self.write();
let st = &mut *guard;
// Try to get it, if it is now in the cache another thread beat us to it.
if let Some(v) = st.codepoints.get(&key) {
return Ok(*v);
}
// Load a value and cache it. This even caches negative matches.
let value = st.resolver.get_index(cp, style, p);
st.codepoints.insert(key, value);
if let Some(idx) = value {
// If the font is a sprite font then we don't need to preload
// because get_face doesn't work with special fonts.
if idx.special().is_none() {
// Load the face in case its deferred. If this fails then we
// would've failed to load it in the future anyways so we want
// to undo all the caching we did.
if let Err(err) = st.resolver.collection.get_face(idx) {
st.codepoints.remove(&key);
return Err(err.into());
}
}
}
Ok(value)
}
/// Returns true if the given font index has the codepoint and presentation.
pub fn has_codepoint(&self, idx: Index, cp: u32, p: Option<Presentation>) -> bool {
let st = self.read();
st.resolver.collection.has_codepoint(
idx,
cp,
match p {
Some(v) => PresentationMode::Explicit(v),
None => PresentationMode::Any,
},
)
}
/// Render a codepoint. This uses the first font index that has the codepoint
/// and matches the presentation requested. If the codepoint cannot be found
/// in any font, an null render is returned.
pub fn render_codepoint(
&self,
cp: u32,
style: Style,
p: Option<Presentation>,
opts: RenderOptions,
) -> Result<Option<Render>, Error> {
// Note: we could optimize the below to use way less locking, but
// at the time of writing this codepath is only called for preedit
// text which is relatively rare and almost non-existent in multiple
// surfaces at the same time.
// Get the font that has the codepoint
let Some(index) = self.get_index(cp, style, p)? else {
return Ok(None);
};
// Get the glyph for the font
let glyph_index = {
let mut guard = self.write();
// (The face was preloaded by `get_index`; `get_face` is a no-op
// lookup then. We need `&mut` for the possible lazy load.)
let face = guard.resolver.collection.get_face(index)?;
match face.glyph_index(cp) {
Some(g) => g,
None => return Ok(None),
}
};
// Render
Ok(Some(self.render_glyph(index, glyph_index, opts)?))
}
/// Render a glyph index. This automatically determines the correct texture
/// atlas to use and caches the result.
pub fn render_glyph(
&self,
index: Index,
glyph_index: u32,
opts: RenderOptions,
) -> Result<Render, Error> {
let key = GlyphKey::new(index, glyph_index, &opts);
// Fast path: the cache has the value. This is almost always true and
// only requires a read lock.
{
let st = self.read();
if let Some(v) = st.glyphs.get(&key) {
return Ok(*v);
}
}
// Slow path: we need to search this codepoint
let mut guard = self.write();
let st = &mut *guard;
if let Some(v) = st.glyphs.get(&key) {
return Ok(*v);
}
// Get the presentation to determine what atlas to use. (Nothing is
// inserted into the cache until the render succeeds, which gives the
// same roll-back behaviour as upstream's errdefer.)
let p = st.resolver.get_presentation(index, glyph_index)?;
let atlas: &mut Atlas = match p {
Presentation::Text => &mut st.atlas_grayscale,
Presentation::Emoji => &mut st.atlas_color,
};
let mut render_opts = opts;
// Always use these constraints for emoji.
if p == Presentation::Emoji {
render_opts.constraint = Constraint {
// Scale emoji to be as large as possible
// while preserving their aspect ratio.
size: ConstraintSize::Cover,
// Center the emoji in its cells.
align_horizontal: crate::face::Align::Center,
align_vertical: crate::face::Align::Center,
// Add a small bit of padding so the emoji
// doesn't quite touch the edges of the cells.
pad_left: 0.025,
pad_right: 0.025,
..Constraint::NONE
};
}
// Render into the atlas
let glyph = match st
.resolver
.render_glyph(atlas, index, glyph_index, render_opts)
{
Ok(g) => g,
// If the atlas is full, we resize it
Err(err) if err.is_atlas_full() => {
atlas.grow(atlas.size * 2);
st.resolver
.render_glyph(atlas, index, glyph_index, render_opts)?
}
Err(err) => return Err(err.into()),
};
// Cache and return
let render = Render {
glyph,
presentation: p,
};
st.glyphs.insert(key, render);
Ok(render)
}
}
impl State {
fn reload_metrics(&mut self) -> Result<(), collection::UpdateMetricsError> {
let collection: &mut Collection = &mut self.resolver.collection;
collection.update_metrics()?;
self.metrics = collection.metrics.expect("set by update_metrics");
// Setup our sprite font.
self.resolver.sprite = Some(SpriteFace::new(self.metrics));
Ok(())
}
/// Number of cached glyph renders (test/diagnostic helper).
pub fn glyph_cache_len(&self) -> usize {
self.glyphs.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
Face,
Library,
collection::{AddOptions, LoadOptions, SizeAdjustment},
embedded,
face::{DesiredSize, Options},
};
fn test_grid(lib: &Library) -> SharedGrid {
let mut c = Collection::new();
c.load_options = Some(LoadOptions::new(lib.clone()));
c.add(
Face::init(
lib,
embedded::REGULAR.into(),
Options::new(DesiredSize {
points: 12.0,
xdpi: 96,
ydpi: 96,
}),
)
.unwrap(),
AddOptions {
style: Style::Regular,
fallback: false,
size_adjustment: SizeAdjustment::None,
},
)
.unwrap();
SharedGrid::new(CodepointResolver::new(c)).unwrap()
}
#[test]
fn get_index() {
let lib = Library::init().unwrap();
let grid = test_grid(&lib);
// Visible ASCII.
for i in 32u32..127 {
let idx = grid.get_index(i, Style::Regular, None).unwrap().unwrap();
assert_eq!(Style::Regular, idx.style);
assert_eq!(0, idx.idx);
assert!(grid.has_codepoint(idx, i, None));
}
// Do it again without a resolver set to ensure we only hit the cache
// (upstream sets the resolver to `undefined`; we swap in an empty one).
let old_resolver = std::mem::replace(
&mut grid.write().resolver,
CodepointResolver::new(Collection::new()),
);
for i in 32u32..127 {
let idx = grid.get_index(i, Style::Regular, None).unwrap().unwrap();
assert_eq!(Style::Regular, idx.style);
assert_eq!(0, idx.idx);
}
grid.write().resolver = old_resolver;
}
#[test]
fn render_glyph_error_after_cache_insert_rolls_back_cache_entry() {
// Upstream uses a tripwire to fail `renderGlyph` after the cache entry
// was inserted. Here a failure is provoked with an invalid index
// (`get_presentation` fails); the cache must stay clean.
let lib = Library::init().unwrap();
let grid = test_grid(&lib);
let idx = grid
.get_index('A' as u32, Style::Regular, None)
.unwrap()
.unwrap();
let glyph_index = {
let st = grid.read();
st.resolver
.collection
.get_loaded_face(idx)
.unwrap()
.glyph_index('A' as u32)
.unwrap()
};
let render_opts = RenderOptions::new(grid.metrics());
assert_eq!(0, grid.read().glyph_cache_len());
let bad = Index {
style: Style::Regular,
idx: 100,
};
assert!(grid.render_glyph(bad, glyph_index, render_opts).is_err());
assert_eq!(0, grid.read().glyph_cache_len());
// And a good render is cached.
let r = grid.render_glyph(idx, glyph_index, render_opts).unwrap();
assert_eq!(Presentation::Text, r.presentation);
assert_eq!(1, grid.read().glyph_cache_len());
assert_eq!(r, grid.render_glyph(idx, glyph_index, render_opts).unwrap());
assert_eq!(1, grid.read().glyph_cache_len());
}
#[test]
fn render_codepoint_and_cell_size() {
let lib = Library::init().unwrap();
let grid = test_grid(&lib);
let size = grid.cell_size();
assert!(size.width > 0 && size.height > 0);
let r = grid
.render_codepoint(
'A' as u32,
Style::Regular,
None,
RenderOptions::new(grid.metrics()),
)
.unwrap()
.unwrap();
assert!(r.glyph.width > 0);
// Not in any font and no discovery.
assert!(
grid.render_codepoint(
0x10FFFE,
Style::Regular,
None,
RenderOptions::new(grid.metrics())
)
.unwrap()
.is_none()
);
}
#[test]
fn atlas_grows_when_full() {
let lib = Library::init().unwrap();
let grid = test_grid(&lib);
let metrics = grid.metrics();
// Render sprite glyphs (box drawing) until the 512 atlas must grow.
for cp in 0x2500u32..0x2580 {
let idx = grid.get_index(cp, Style::Regular, None).unwrap().unwrap();
grid.render_glyph(idx, cp, RenderOptions::new(metrics))
.unwrap();
}
assert!(grid.read().atlas_grayscale.size >= 512);
}
#[test]
fn init_error_without_primary_face() {
// Analogue of upstream "init error": failure in `init` must surface
// as an error (no primary font to compute metrics from).
let lib = Library::init().unwrap();
let mut c = Collection::new();
c.load_options = Some(LoadOptions::new(lib));
assert!(SharedGrid::new(CodepointResolver::new(c)).is_err());
}
}