You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
After landing procedural box-drawing in canvas (commits ace716f, b514406), every frame re-runs the fillRect/stroke calls for every box-drawing/block-element cell. This is fine for typical use (a microbench at 14pt monospace renders 10K mixed box-drawing glyphs in ~4.2 ms direct, ~2.4 M glyphs/sec on M-series Chromium), but content-heavy TUIs and large viewports will eventually want a faster path.
I tried the obvious one — caching each rendered glyph in an offscreen <canvas> and replaying with drawImage — and it lost: 0.91–0.98× of direct in the same bench. Cache lookup + drawImage(Canvas, …) is more work than the 1–4 fillRects a typical glyph compiles to, and the browser was already amortizing whatever batching is possible inside Canvas2D.
Options worth investigating, in roughly increasing scope:
1. Run-merge identical glyphs in a row
Detect runs of the same box-drawing or block-element codepoint on a single line and emit one wide fillRect instead of N. Common content where this pays off: progress bars (█████…), table rules (────…), TUI borders. Probably the highest perf-per-effort improvement and doesn't require any caching infrastructure. Lives in the renderer's row loop, not in lib/box-drawing/.
2. ImageBitmap-backed atlas, single texture
Render every (codepoint, cell-size, color) into ONE shared ImageBitmap atlas at known (sx, sy) coordinates. Drawing each cell becomes drawImage(atlas, sx, sy, w, h, dx, dy, w, h) against a single source — the browser keeps the texture bound across draws, and ImageBitmap is GPU-resident, both of which the simple per-glyph offscreen-canvas cache I tried lacked.
This is roughly what xterm.js's Canvas2D addon does for text glyphs (CharAtlas) — it's the canonical way to make caching actually win in Canvas2D.
Caveats: needs an atlas allocator, eviction policy, and re-build on font/theme/DPR change. Probably the largest non-WebGL change.
3. Path2D caching for arcs and diagonals
The fillRect-based glyphs are already cheap enough that caching can't help (#1). Arcs (╭╮╯╰) and diagonals (╱╲╳) currently rebuild a Bezier path on every draw. A shared Path2D per (corner, cell-size, lightPx) used with ctx.stroke(path) would skip the path-construction work. Smaller scope than #2; small incremental win.
4. WebGL renderer
Real atlas, real batching, one or few draw calls per frame. The proper ceiling. Tracked in #155 — putting this here for completeness.
Suggested priority
1 first (cheap, broadly applicable, zero new caching machinery), then 3 (small, contained), then 2 (real but bigger), then 4 (separate effort).
Notes
Atlas-as-OffscreenCanvas/HTMLCanvasElement (what I tested first) is not on this list because it benched slower than direct rendering. The cache pattern needs to use ImageBitmap and a single shared texture to actually beat direct rendering in Canvas2D.
Caching tradeoffs change if/when we move to WebGL — a glyph atlas there is essentially required, so investing in the atlas allocator (integrate ghostty #2) is reusable infra.
After landing procedural box-drawing in canvas (commits
ace716f,b514406), every frame re-runs thefillRect/strokecalls for every box-drawing/block-element cell. This is fine for typical use (a microbench at 14pt monospace renders 10K mixed box-drawing glyphs in ~4.2 ms direct, ~2.4 M glyphs/sec on M-series Chromium), but content-heavy TUIs and large viewports will eventually want a faster path.I tried the obvious one — caching each rendered glyph in an offscreen
<canvas>and replaying withdrawImage— and it lost: 0.91–0.98× of direct in the same bench. Cache lookup +drawImage(Canvas, …)is more work than the 1–4fillRects a typical glyph compiles to, and the browser was already amortizing whatever batching is possible inside Canvas2D.Options worth investigating, in roughly increasing scope:
1. Run-merge identical glyphs in a row
Detect runs of the same box-drawing or block-element codepoint on a single line and emit one wide
fillRectinstead of N. Common content where this pays off: progress bars (█████…), table rules (────…), TUI borders. Probably the highest perf-per-effort improvement and doesn't require any caching infrastructure. Lives in the renderer's row loop, not inlib/box-drawing/.2. ImageBitmap-backed atlas, single texture
Render every (codepoint, cell-size, color) into ONE shared
ImageBitmapatlas at known(sx, sy)coordinates. Drawing each cell becomesdrawImage(atlas, sx, sy, w, h, dx, dy, w, h)against a single source — the browser keeps the texture bound across draws, andImageBitmapis GPU-resident, both of which the simple per-glyph offscreen-canvas cache I tried lacked.This is roughly what xterm.js's Canvas2D addon does for text glyphs (
CharAtlas) — it's the canonical way to make caching actually win in Canvas2D.Caveats: needs an atlas allocator, eviction policy, and re-build on font/theme/DPR change. Probably the largest non-WebGL change.
3.
Path2Dcaching for arcs and diagonalsThe fillRect-based glyphs are already cheap enough that caching can't help (#1). Arcs (
╭╮╯╰) and diagonals (╱╲╳) currently rebuild a Bezier path on every draw. A sharedPath2Dper (corner, cell-size, lightPx) used withctx.stroke(path)would skip the path-construction work. Smaller scope than #2; small incremental win.4. WebGL renderer
Real atlas, real batching, one or few draw calls per frame. The proper ceiling. Tracked in #155 — putting this here for completeness.
Suggested priority
1 first (cheap, broadly applicable, zero new caching machinery), then 3 (small, contained), then 2 (real but bigger), then 4 (separate effort).
Notes
OffscreenCanvas/HTMLCanvasElement(what I tested first) is not on this list because it benched slower than direct rendering. The cache pattern needs to useImageBitmapand a single shared texture to actually beat direct rendering in Canvas2D.🤖 Generated with Claude Code