starkernel: retarget REPL glyph rendering to TTF-TEXT's rasterizer (4.4j)

font_8x16.c keeps rendering everything through and including POST;
TTF-TEXT's rasterizer takes over at the interactive REPL boundary
(sk_repl()) via a new runtime mode switch, vt100_enable_ttf()
(console_fb_enable_ttf() wrapper), not a compile-time swap -- both
backends coexist in the same binary since boot/POST must stay
font_8x16.c per this item's own done-when.

TTF-TEXT (the FORTH word) isn't directly callable from vt100.c -- VM
stack arguments, different call shape than a one-glyph cell draw. Used
hal/ttf.c's VM-independent primitives directly instead (same rasterizer
TTF-TEXT itself calls underneath), added as a native C helper in
vt100.c. Lazily loads fonts:JetBrainsMono-Regular.ttf and kmallocs a
96-slot raster cache (covers all 95 printable ASCII, no eviction
thrash) on first switch.

Cell geometry changes at the switch (mode-aware cell_w()/cell_h()):
provisional 12x24 TTF cell (600/1000em * 20px = 12px exactly, using
4.4i's confirmed-uniform hmtx advance width) vs font_8x16's fixed 8x16
-- cols/rows re-derived and screen cleared at the switch point, same as
vt100_init() itself does. Final REPL text size is 4.4m's decision, not
this item's.

Also fixes the second call site 4.4i flagged: erase_line_range() now
uses one fb_fill_rect() instead of a per-cell font_8x16-specific blank
glyph draw, consistent with erase_display(2)'s full-screen case.

Three-arch verified: amd64/aarch64/riscv64 all reach ok>, POST
Failed: 0, identical dict-hashes. amd64 screendump shows real
proportional JetBrains Mono letterforms on the REPL tail, visibly
distinct from every prior font_8x16 screenshot.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-08-11 19:56:50 -04:00
co-authored by Claude Sonnet 5
parent 64c20c78eb
commit f729b91a09
18 changed files with 55116 additions and 115 deletions
+7
View File
@@ -296,6 +296,13 @@ void console_fb_init(const FramebufferInfo *info, FbPixelFormat fmt)
}
}
void console_fb_enable_ttf(void)
{
if (fb_is_available()) {
vt100_enable_ttf();
}
}
/**
* Read a single character from serial console (non-blocking)
* Returns -1 if no character available
+143 -11
View File
@@ -40,14 +40,47 @@
#include "vt100.h"
#include "framebuffer.h"
#include "starkernel/ttf.h"
#include "starkernel/capsule_generated.h"
#include "starkernel/kmalloc.h"
#include "log.h"
#include <stdint.h>
/* Maximum CSI parameters */
#define VT100_MAX_PARAMS 16
/* Underline scanline (row 14 of 16) */
/* Underline scanline (row 14 of 16) -- font_8x16 mode only; see
* draw_cursor_glyph()'s mode split. */
#define UNDERLINE_ROW 14u
/* -----------------------------------------------------------------------
* FABRIC.md item 4.4j: TTF glyph backend, active only from
* vt100_enable_ttf() onward (boot/POST stays font_8x16.c).
*
* Provisional point size and cell geometry -- item 4.4m owns the final
* REPL-strip decision. Chosen so 4.4i's confirmed hmtx result (all
* printable ASCII share one 600/1000-em advance width) divides evenly:
* 600/1000 * 20 = 12px exactly, no fractional-pixel cell width. Cell
* height is size + a fixed descender/leading allowance, not derived from
* hhea (not read by this parser -- see ttf.h) -- provisional like the
* width.
* --------------------------------------------------------------------- */
#define VT100_TTF_SIZE_PX 20u
#define VT100_TTF_CELL_W_PX 12u /* 600/1000 * VT100_TTF_SIZE_PX */
#define VT100_TTF_CELL_H_PX 24u /* provisional: size + 4px leading/descender */
#define VT100_TTF_CACHE_SLOTS 96u /* covers all 95 printable ASCII, no thrash */
#define VT100_TTF_FONT_CAPSULE "fonts:JetBrainsMono-Regular.ttf"
typedef enum {
VT_GLYPH_BITMAP = 0, /* font_8x16.c, via fb_draw_glyph() */
VT_GLYPH_TTF = 1, /* TTF-TEXT's rasterizer, via ttf_draw_glyph_cell() */
} vt_glyph_mode_t;
static vt_glyph_mode_t g_glyph_mode = VT_GLYPH_BITMAP;
static ttf_font_t g_ttf_font;
static int g_ttf_ready = 0;
static ttf_raster_cache_t g_ttf_cache;
/* -----------------------------------------------------------------------
* Parser state enum
* --------------------------------------------------------------------- */
@@ -110,9 +143,13 @@ static void apply_sgr(void);
* Internal helpers
* --------------------------------------------------------------------- */
/* Framebuffer cell dimensions (pixels) — reads from framebuffer driver */
static uint32_t cell_w(void) { return fb_cell_w(); }
static uint32_t cell_h(void) { return fb_cell_h(); }
/* Cell dimensions (pixels) — font_8x16.c's fixed 8x16 grid in bitmap mode,
* or the provisional TTF cell (4.4j) once vt100_enable_ttf() has switched
* modes. Mode-aware per 4.4i's finding that the grid was hardcoded to
* font_8x16.c specifically; this is the generalization that item called
* for. */
static uint32_t cell_w(void) { return g_glyph_mode == VT_GLYPH_TTF ? VT100_TTF_CELL_W_PX : fb_cell_w(); }
static uint32_t cell_h(void) { return g_glyph_mode == VT_GLYPH_TTF ? VT100_TTF_CELL_H_PX : fb_cell_h(); }
/* Pixel position of the top-left corner of character cell (cx, cy) */
static uint32_t px_of(uint32_t col) { return col * cell_w(); }
@@ -122,6 +159,46 @@ static uint32_t py_of(uint32_t row) { return row * cell_h(); }
static uint32_t eff_fg(void) { return g_vt.reverse ? g_vt.bg : g_vt.fg; }
static uint32_t eff_bg(void) { return g_vt.reverse ? g_vt.fg : g_vt.bg; }
/* Draw one glyph via the TTF rasterizer into the cell at pixel (cell_px,
* cell_py). Mirrors fb_draw_glyph()'s opaque-cell contract (every pixel in
* the cell gets either fg or bg, not just the glyph's "ink") by filling
* the whole cell with bg first, then stamping covered bitmap pixels with
* fg. Baseline sits VT100_TTF_SIZE_PX down from the cell's top, matching
* TTF-TEXT's own bitmap-origin convention (ttf.h's
* "glyph baseline sits at pixel (MARGIN, size_px + MARGIN)"). Bitmap
* pixels are clipped to the cell rect -- a glyph's rasterized bounds can
* slightly exceed the nominal advance width, and cells must stay opaque
* neighbors, not bleed into each other. */
static void ttf_draw_glyph_cell(uint32_t cell_px, uint32_t cell_py, uint8_t ch,
uint32_t fg, uint32_t bg)
{
ttf_bitmap_t bmp;
int rc, was_hit;
uint32_t bx, by;
uint32_t baseline_y = cell_py + VT100_TTF_SIZE_PX;
fb_fill_rect(cell_px, cell_py, cell_w(), cell_h(), bg);
if (!g_ttf_ready) return; /* shouldn't happen once VT_GLYPH_TTF is set, defensive only */
rc = ttf_raster_cache_get(&g_ttf_cache, &g_ttf_font, (uint32_t)ch,
VT100_TTF_SIZE_PX, &bmp, &was_hit);
if (rc != TTF_OK) return; /* missing/unrasterizable glyph -- leave cell blank (bg only) */
for (by = 0; by < bmp.height; by++) {
int32_t fb_y = (int32_t)baseline_y + (int32_t)by
- (int32_t)VT100_TTF_SIZE_PX - TTF_CACHE_MARGIN;
if (fb_y < (int32_t)cell_py || fb_y >= (int32_t)(cell_py + cell_h())) continue;
for (bx = 0; bx < bmp.width; bx++) {
int32_t fb_x = (int32_t)cell_px + (int32_t)bx - TTF_CACHE_MARGIN;
if (fb_x < (int32_t)cell_px || fb_x >= (int32_t)(cell_px + cell_w())) continue;
if (bmp.pixels[by * bmp.width + bx]) {
fb_put_pixel((uint32_t)fb_x, (uint32_t)fb_y, fg);
}
}
}
}
static void draw_cursor_glyph(uint8_t ch)
{
uint32_t f = eff_fg();
@@ -138,11 +215,22 @@ static void draw_cursor_glyph(uint8_t ch)
f = FB_RGB(r, g, bv);
}
fb_draw_glyph(px_of(g_vt.cx), py_of(g_vt.cy), ch, f, b);
if (g_glyph_mode == VT_GLYPH_TTF) {
ttf_draw_glyph_cell(px_of(g_vt.cx), py_of(g_vt.cy), ch, f, b);
} else {
fb_draw_glyph(px_of(g_vt.cx), py_of(g_vt.cy), ch, f, b);
}
if (g_vt.underline) {
uint32_t scale = cell_h() / 16u;
uint32_t ul_y = py_of(g_vt.cy) + UNDERLINE_ROW * scale;
/* font_8x16 mode: underline sits at UNDERLINE_ROW (row 14 of 16)
* of that font's fixed grid, scaled. TTF mode: no equivalent fixed
* row exists (this parser doesn't read hhea underline metrics --
* see ttf.h) -- placed 2px below the baseline instead, a
* provisional choice like the rest of 4.4j's TTF cell geometry. */
uint32_t scale = g_glyph_mode == VT_GLYPH_TTF ? 1u : cell_h() / 16u;
uint32_t ul_y = g_glyph_mode == VT_GLYPH_TTF
? py_of(g_vt.cy) + VT100_TTF_SIZE_PX + 2u
: py_of(g_vt.cy) + UNDERLINE_ROW * scale;
fb_fill_rect(px_of(g_vt.cx), ul_y, cell_w(), scale, f);
}
}
@@ -195,6 +283,45 @@ void vt100_init(void)
erase_display(2);
}
void vt100_enable_ttf(void)
{
if (g_glyph_mode == VT_GLYPH_TTF) return; /* already switched */
if (!g_vt.initialized) return;
if (!g_ttf_ready) {
ttf_raster_cache_slot_t *slots;
int rc;
rc = ttf_load_from_capsule(capsule_get_directory(), capsule_get_descriptors(),
capsule_get_names(), capsule_get_arena(),
VT100_TTF_FONT_CAPSULE, &g_ttf_font);
if (rc != TTF_OK) {
log_message(LOG_ERROR, "vt100: TTF font capsule load failed (rc=%d), staying on font_8x16", rc);
return;
}
slots = (ttf_raster_cache_slot_t *)kmalloc(sizeof(ttf_raster_cache_slot_t) * VT100_TTF_CACHE_SLOTS);
if (!slots) {
log_message(LOG_ERROR, "vt100: TTF cache allocation failed, staying on font_8x16");
return;
}
ttf_raster_cache_init(&g_ttf_cache, slots, VT100_TTF_CACHE_SLOTS);
g_ttf_ready = 1;
}
g_glyph_mode = VT_GLYPH_TTF;
/* Cell geometry just changed (font_8x16's fixed 8x16 vs. TTF's
* provisional 12x24, see this file's 4.4j block above) -- re-derive
* cols/rows and clear, the same as vt100_init() itself does. This only
* ever runs once, at the POST-to-REPL boundary (sk_repl()), so nothing
* of value is on screen yet to preserve. */
g_vt.cols = fb_width() / cell_w();
g_vt.rows = fb_height() / cell_h();
g_vt.cx = g_vt.cy = 0;
erase_display(2);
}
uint32_t vt100_cols(void) { return g_vt.cols; }
uint32_t vt100_rows(void) { return g_vt.rows; }
@@ -232,12 +359,17 @@ static void scroll_up(uint32_t lines)
* Erase operations
* --------------------------------------------------------------------- */
/* FABRIC.md item 4.4i found this drawing a per-cell blank glyph
* (font_8x16.c-specific, and inconsistent with erase_display(2)'s
* full-screen case just below, which already used a plain rect fill).
* 4.4j fixes both problems at once: a blank cell is visually identical to
* a filled bg rect regardless of glyph backend, so this needs no font
* involvement at all -- one fb_fill_rect() for the whole range instead of
* a per-column glyph draw. */
static void erase_line_range(uint32_t row, uint32_t c0, uint32_t c1)
{
uint32_t c;
for (c = c0; c < c1; c++) {
fb_draw_glyph(px_of(c), py_of(row), 0x20, g_vt.fg, g_vt.bg);
}
if (c1 <= c0) return;
fb_fill_rect(px_of(c0), py_of(row), (c1 - c0) * cell_w(), cell_h(), g_vt.bg);
}
static void erase_display(int mode)
+7
View File
@@ -268,6 +268,13 @@ void sk_repl_run(VM *vm)
void sk_repl(VM *vm)
{
/* FABRIC.md item 4.4j: boot and POST (both already returned by the time
* sk_repl() is called) stay on font_8x16.c/VT100 by design; the
* interactive REPL -- this function -- is the boundary where TTF-TEXT
* takes over. One-shot: console_fb_enable_ttf() no-ops on any later
* call. */
console_fb_enable_ttf();
console_println(lithos_version);
console_puts("StarForth Version "); console_println(STARFORTH_VERSION);
console_println("");