Skip to content

Commit bfd0fed

Browse files
committed
glyph protocol implementation originally made in #1542
1 parent 1bab465 commit bfd0fed

13 files changed

Lines changed: 3969 additions & 0 deletions

File tree

frontends/rioterm/src/grid_emit.rs

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,13 @@ enum DecorationStyle {
323323
/// `font.sprite_index` idea.
324324
const DECORATION_FONT_ID_BASE: u32 = 0xFFFF_FF00;
325325

326+
/// Sentinel font_id for Glyph Protocol registrations. Pulled directly
327+
/// in u32 form from `sugarloaf::font::glyph_registry`; lands above the
328+
/// cursor/decoration ranges and never collides with a real font index.
329+
/// The atlas `glyph_id` for a registered cell is
330+
/// `pack_atlas_glyph_id(codepoint, version)`.
331+
use rio_backend::sugarloaf::font::glyph_registry::CUSTOM_GLYPH_FONT_ID_U32;
332+
326333
/// Sentinel font_id base for cursor sprites. Distinct from the
327334
/// decoration range so the two never collide in the atlas
328335
/// hash-key space.
@@ -1347,6 +1354,14 @@ pub fn build_row_fg(
13471354
let has_color_hints = row_hints.iter().any(|rh| rh.tag != HintTag::HyperlinkHover);
13481355
let needs_per_cell_check = has_sel || has_color_hints;
13491356

1357+
// Glyph Protocol registry — cloned once per row so the per-cell
1358+
// custom-glyph helper avoids re-acquiring the FontLibrary read
1359+
// lock on every registered cell. Arc clone is cheap; `None` when
1360+
// no program in this session has used the protocol.
1361+
let glyph_registry: Option<
1362+
rio_backend::sugarloaf::font::glyph_registry::GlyphRegistry,
1363+
> = font_library.inner.read().glyph_registry.clone();
1364+
13501365
// Phase 1: underline pass. Emit before glyphs so grayscale quads
13511366
// draw under the characters.
13521367
emit_underlines(
@@ -1379,6 +1394,93 @@ pub fn build_row_fg(
13791394
(style_set.get(sq.style_id()).flags.bits() & SHAPING_FLAG_MASK) as u8;
13801395
let (font_id, is_emoji) =
13811396
rasterizer.resolve_font(ch, run_style_flags, font_library);
1397+
1398+
// Glyph Protocol short-circuit: registered codepoints render
1399+
// directly from the registry without shaping, run-extension,
1400+
// or per-platform shaper plumbing. Each registered cell is
1401+
// its own one-cell run.
1402+
if font_id == CUSTOM_GLYPH_FONT_ID_U32 {
1403+
// The font cascade reported a custom glyph but the row
1404+
// already cloned `glyph_registry` as None — registry was
1405+
// detached between font resolution and this branch (rare,
1406+
// but harmless: render nothing).
1407+
let Some(registry) = glyph_registry.as_ref() else {
1408+
x += 1;
1409+
continue;
1410+
};
1411+
1412+
// Borrow the primary font's ascent at this size if the
1413+
// run-shaper has populated it; otherwise approximate at
1414+
// 80% of the glyph size. The approximation only fires
1415+
// when no regular text has been laid out at this size yet
1416+
// — once the user types real text the cache fills and
1417+
// subsequent registered cells use the precise ascent.
1418+
let ascent_px = rasterizer
1419+
.ascent_cache
1420+
.get(&(
1421+
rio_backend::sugarloaf::font::FONT_ID_REGULAR as u32,
1422+
size_bucket,
1423+
))
1424+
.copied()
1425+
.unwrap_or_else(|| (size_u16 as i16).saturating_mul(4) / 5);
1426+
1427+
// fg colour, mirroring the regular emit loop's
1428+
// selection / hint precedence.
1429+
let color = if !needs_per_cell_check {
1430+
cell_fg(sq, style_set, renderer, term_colors)
1431+
} else {
1432+
let is_sel = cell_in_row_sel(row_sel, x as u16);
1433+
let hint_tag = if is_sel {
1434+
None
1435+
} else {
1436+
cell_in_row_hints(row_hints, x as u16)
1437+
};
1438+
if is_sel {
1439+
cell_fg_selected(sq, style_set, renderer, term_colors)
1440+
} else if let Some(tag) = hint_tag {
1441+
cell_fg_hinted(tag, renderer)
1442+
} else {
1443+
cell_fg(sq, style_set, renderer, term_colors)
1444+
}
1445+
};
1446+
1447+
if let Some((_, slot, is_color)) = ensure_custom_glyph_by_codepoint(
1448+
grid,
1449+
registry,
1450+
ch as u32,
1451+
size_bucket,
1452+
size_u16,
1453+
cell_h,
1454+
ascent_px,
1455+
color,
1456+
) {
1457+
if slot.w != 0 && slot.h != 0 {
1458+
// Colour atlas entries are pre-painted (palette
1459+
// applied during COLR rasterisation), so the
1460+
// shader multiplies by white. Mono entries take
1461+
// the per-cell fg colour the run loop computed
1462+
// above.
1463+
let (atlas, color) = if is_color {
1464+
(CellText::ATLAS_COLOR, [255, 255, 255, 255])
1465+
} else {
1466+
(CellText::ATLAS_GRAYSCALE, color)
1467+
};
1468+
fg_scratch.push(CellText {
1469+
glyph_pos: [slot.x as u32, slot.y as u32],
1470+
glyph_size: [slot.w as u32, slot.h as u32],
1471+
bearings: [slot.bearing_x, slot.bearing_y],
1472+
grid_pos: [x as u16, y],
1473+
color,
1474+
atlas,
1475+
bools: 0,
1476+
_pad: [0, 0],
1477+
});
1478+
}
1479+
}
1480+
x += 1;
1481+
continue;
1482+
}
1483+
13821484
let run_start = x;
13831485

13841486
// Kitty Unicode placeholder shapes as a space — the cell
@@ -1840,6 +1942,87 @@ fn ensure_glyph_by_id(
18401942
Some((key, slot, is_color))
18411943
}
18421944

1945+
/// Look up or rasterise a Glyph Protocol registration into the grid
1946+
/// atlas. The atlas key combines the codepoint with the registration's
1947+
/// `version` (bumped on every register/clear) so re-registering the
1948+
/// same codepoint never serves a stale rasterisation. Each unique
1949+
/// (codepoint × version × pixel size) combination owns one atlas slot;
1950+
/// previous-version slots become unreachable and the atlas LRU evicts
1951+
/// them in due course.
1952+
///
1953+
/// `ascent_px` matches the primary font's ascent at the same size
1954+
/// bucket — Glyph Protocol payloads have no font-of-their-own, so we
1955+
/// align registered glyphs to the surrounding text baseline. A more
1956+
/// faithful rendering would walk the registered outline's bbox to
1957+
/// compute per-glyph bearings, but for icon-style PUA glyphs the
1958+
/// primary-font baseline produces the expected appearance.
1959+
///
1960+
/// `registry` is the active terminal's glyph registry, cloned once
1961+
/// per row by `build_row_fg`. Passing it in (instead of going through
1962+
/// the `FontLibrary` write lock) keeps the per-cell hot loop allocation
1963+
/// and lock free.
1964+
///
1965+
/// Returns `None` when the registration was cleared between font
1966+
/// resolution and render, or when rasterisation produces no pixels
1967+
/// (zero-area outline, malformed COLR, etc.).
1968+
#[allow(clippy::too_many_arguments)]
1969+
fn ensure_custom_glyph_by_codepoint(
1970+
grid: &mut GridRenderer,
1971+
registry: &rio_backend::sugarloaf::font::glyph_registry::GlyphRegistry,
1972+
codepoint: u32,
1973+
size_bucket: u16,
1974+
size_u16: u16,
1975+
cell_h: f32,
1976+
ascent_px: i16,
1977+
foreground_rgba: [u8; 4],
1978+
) -> Option<(GlyphKey, AtlasSlot, bool)> {
1979+
use rio_backend::sugarloaf::font::glyph_registry::pack_atlas_glyph_id;
1980+
1981+
// Fetch first so we know the registration's version. The lookup
1982+
// happens under the registry's RwLock read; the entry's payload is
1983+
// cloned out so the lock drops before we hit tiny-skia.
1984+
let entry = registry.get(codepoint)?;
1985+
let key = GlyphKey {
1986+
font_id: CUSTOM_GLYPH_FONT_ID_U32,
1987+
glyph_id: pack_atlas_glyph_id(codepoint, entry.version),
1988+
size_bucket,
1989+
};
1990+
if let Some(slot) = grid.lookup_glyph(key) {
1991+
return Some((key, slot, false));
1992+
}
1993+
if let Some(slot) = grid.lookup_glyph_color(key) {
1994+
return Some((key, slot, true));
1995+
}
1996+
1997+
let raster = rio_backend::sugarloaf::glyph_protocol::rasterize_payload(
1998+
&entry.payload,
1999+
entry.upm,
2000+
size_u16,
2001+
foreground_rgba,
2002+
)?;
2003+
2004+
let bearing_y = {
2005+
let cell_h_i16 = cell_h.round().clamp(0.0, i16::MAX as f32) as i16;
2006+
cell_h_i16
2007+
.saturating_sub(ascent_px)
2008+
.saturating_add(raster.top.clamp(i16::MIN as i32, i16::MAX as i32) as i16)
2009+
};
2010+
let raster_in = RasterizedGlyph {
2011+
width: raster.width,
2012+
height: raster.height,
2013+
bearing_x: raster.left.clamp(i16::MIN as i32, i16::MAX as i32) as i16,
2014+
bearing_y,
2015+
bytes: &raster.data,
2016+
};
2017+
2018+
let slot = if raster.is_color {
2019+
grid.insert_glyph_color(key, raster_in)?
2020+
} else {
2021+
grid.insert_glyph(key, raster_in)?
2022+
};
2023+
Some((key, slot, raster.is_color))
2024+
}
2025+
18432026
/// Platform-agnostic raw-glyph struct. Both backends populate this
18442027
/// shape and let the caller convert bearings to the grid's
18452028
/// cell-bottom-relative convention.

frontends/rioterm/src/screen/mod.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3460,6 +3460,24 @@ impl Screen<'_> {
34603460
}
34613461
}
34623462

3463+
// Attach the active terminal's glyph-protocol registry to the
3464+
// shared font library before the renderer walks the grid.
3465+
// Most terminals never emit Glyph Protocol APCs, so the
3466+
// registry stays `None` and we skip the attach entirely —
3467+
// zero cost on the hot render path. When a program in this
3468+
// session has registered, the registry is Arc-shared so
3469+
// cloning it for the attach is O(1).
3470+
if let Some(registry) = self
3471+
.context_manager
3472+
.current()
3473+
.terminal
3474+
.lock()
3475+
.glyph_registry
3476+
.clone()
3477+
{
3478+
self.sugarloaf.attach_glyph_registry(registry);
3479+
}
3480+
34633481
let (window_update, any_panel_dirty) = self.renderer.run(
34643482
&mut self.sugarloaf,
34653483
&mut self.context_manager,

0 commit comments

Comments
 (0)