FABRIC.md item 4.4t: confine REPL text rendering to the CANVAS box

vt100's TTF-mode text grid now operates within the per-arch 640x480 box
(computed in 4.4o, pixel-verified in 4.4p) instead of the full framebuffer:
box-origin offset in px_of()/py_of(), box-derived cols/rows (53x20) set
before the 4.4q scrollback allocation depends on them, mode-aware
erase_display()/reverse-index fill, and a new box-scoped fb_scroll_rect()
alongside the existing whole-framebuffer fb_scroll_rows() (bitmap/boot mode
unaffected either way). Also clears the full framebuffer once at the
bitmap-to-TTF switch so leftover boot debris doesn't sit frozen outside the
box now that erase_display(2) is box-scoped afterward.

Three-arch QEMU boot + pixel-scanned screendumps confirm zero non-background
pixels land outside the box on amd64, aarch64, and riscv64.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-08-11 23:07:04 -04:00
co-authored by Claude Sonnet 5
parent 1f3ec3554e
commit c92be2768f
21 changed files with 91586 additions and 112 deletions
+53
View File
@@ -305,3 +305,56 @@ void fb_scroll_rows(uint32_t char_rows, uint32_t bg)
}
}
}
/**
* fb_scroll_rect — box-confined scroll (FABRIC.md item 4.4t).
*
* Same non-volatile bulk-copy rationale as fb_scroll_rows() above, but
* bounded to a caller-supplied rectangle instead of the whole framebuffer --
* needed once REPL text is confined to the 4.4o CANVAS box, since a
* whole-framebuffer scroll would drag pixels from outside the box (e.g. a
* future 4.4r border) into the text area. Takes pixel_rows directly rather
* than char_rows * a hardcoded cell height, since TTF mode's cell height
* (24px) differs from bitmap mode's (16px * scale).
*/
void fb_scroll_rect(uint32_t x, uint32_t y, uint32_t w, uint32_t h,
uint32_t pixel_rows, uint32_t bg)
{
uint32_t src_y, dst_y, px;
uint32_t packed_bg;
uint32_t *fb = (uint32_t *)(uintptr_t)g_fb.base;
uint32_t x_end, y_end;
if (!g_fb.ready || pixel_rows == 0 || w == 0 || h == 0) return;
x_end = x + w;
y_end = y + h;
if (x_end > g_fb.width) x_end = g_fb.width;
if (y_end > g_fb.height) y_end = g_fb.height;
if (x >= x_end || y >= y_end) return;
if (pixel_rows >= (y_end - y)) {
fb_fill_rect(x, y, x_end - x, y_end - y, bg);
return;
}
packed_bg = pack_pixel(bg);
/* Copy pixel rows upward, bounded to [x, x_end) */
for (dst_y = y; dst_y + pixel_rows < y_end; dst_y++) {
src_y = dst_y + pixel_rows;
uint32_t *src = fb + src_y * g_fb.stride + x;
uint32_t *dst = fb + dst_y * g_fb.stride + x;
for (px = 0; px < (x_end - x); px++) {
dst[px] = src[px];
}
}
/* Clear the newly exposed rows at the bottom of the rect */
for (dst_y = y_end - pixel_rows; dst_y < y_end; dst_y++) {
uint32_t *row = fb + dst_y * g_fb.stride + x;
for (px = 0; px < (x_end - x); px++) {
row[px] = packed_bg;
}
}
}
+80 -15
View File
@@ -80,6 +80,28 @@ static ttf_font_t g_ttf_font;
static int g_ttf_ready = 0;
static ttf_raster_cache_t g_ttf_cache;
/* -----------------------------------------------------------------------
* FABRIC.md item 4.4t: confine REPL text (TTF mode only) to the 640x480
* CANVAS box computed in 4.4o and pixel-verified in 4.4p. Per-arch raster
* top-left origin, from those items' measurements. Boot/POST (bitmap mode)
* is untouched -- g_origin_x/g_origin_y default to (0,0) and only change
* once vt100_enable_ttf() runs.
* --------------------------------------------------------------------- */
#if defined(__x86_64__) || defined(__i386__)
#define VT100_BOX_ORIGIN_X 320u
#define VT100_BOX_ORIGIN_Y 104u
#else
#define VT100_BOX_ORIGIN_X 80u
#define VT100_BOX_ORIGIN_Y 4u
#endif
#define VT100_BOX_W 640u
#define VT100_BOX_H 480u
#define VT100_BOX_COLS 53u /* 640 / VT100_TTF_CELL_W_PX, exact */
#define VT100_BOX_ROWS 20u /* 480 / VT100_TTF_CELL_H_PX, exact */
static uint32_t g_origin_x = 0; /* box top-left, TTF mode only; 0 in bitmap mode */
static uint32_t g_origin_y = 0;
/* -----------------------------------------------------------------------
* FABRIC.md item 4.4q: REPL scrollback. Text-only (not pixel snapshots --
* at 1000 lines x ~cols bytes this is tens of KB; a pixel-snapshot ring
@@ -196,9 +218,11 @@ static void apply_sgr(void);
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(); }
static uint32_t py_of(uint32_t row) { return row * cell_h(); }
/* Pixel position of the top-left corner of character cell (cx, cy),
* offset by the box origin (4.4t) -- (0,0) in bitmap mode, so boot/POST
* is unaffected. */
static uint32_t px_of(uint32_t col) { return g_origin_x + col * cell_w(); }
static uint32_t py_of(uint32_t row) { return g_origin_y + row * cell_h(); }
/* Effective fg / bg accounting for reverse video */
static uint32_t eff_fg(void) { return g_vt.reverse ? g_vt.bg : g_vt.fg; }
@@ -362,14 +386,25 @@ void vt100_enable_ttf(void)
g_glyph_mode = VT_GLYPH_TTF;
/* Cell geometry just changed (font_8x16's fixed 8x16 vs. TTF's 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();
/* FABRIC.md item 4.4t: switch to the box-confined coordinate system
* before anything below derives cols/rows/stride from it -- the
* scrollback allocation block just below depends on g_vt.cols via
* g_line_stride, so origin/cols/rows MUST be set first or every
* shadow/ring index silently corrupts instead of crashing. */
g_origin_x = VT100_BOX_ORIGIN_X;
g_origin_y = VT100_BOX_ORIGIN_Y;
g_vt.cols = VT100_BOX_COLS;
g_vt.rows = VT100_BOX_ROWS;
g_vt.cx = g_vt.cy = 0;
/* One-time full-screen clear at the bitmap-to-TTF transition: the boot/
* POST scroll left the whole framebuffer covered in font_8x16 text, and
* from here on erase_display(2) is box-scoped (see its mode-aware
* branch below) -- without this, that debris would sit frozen outside
* the box forever instead of leaving clean CANVAS around it. This is
* the one and only whole-framebuffer wipe in TTF mode; every later
* ESC[2J stays box-scoped. */
fb_fill_rect(0, 0, fb_width(), fb_height(), g_vt.def_bg);
erase_display(2);
/* FABRIC.md item 4.4q: allocate the scrollback shadow + ring buffers
@@ -380,6 +415,17 @@ void vt100_enable_ttf(void)
{
uint32_t i;
g_line_stride = g_vt.cols + 1u;
/* 4.4t: cols is now the fixed box width (53) -- verify the stride
* came out right rather than assuming the ordering above landed
* correctly (advisor-flagged failure mode: silent corruption, not
* a crash, if this ever drifts). */
if (g_line_stride != VT100_BOX_COLS + 1u) {
log_message(LOG_ERROR, "vt100: unexpected line_stride=%u (want %u), scrollback unavailable",
g_line_stride, VT100_BOX_COLS + 1u);
g_shadow = (void *)0;
g_ring = (void *)0;
return;
}
g_shadow = (char *)kmalloc((size_t)g_vt.rows * g_line_stride);
g_ring = (char *)kmalloc((size_t)VT100_SCROLLBACK_MAX_LINES * g_line_stride);
if (!g_shadow || !g_ring) {
@@ -521,7 +567,15 @@ static void scroll_up(uint32_t lines)
}
}
fb_scroll_rows(lines, g_vt.def_bg);
/* 4.4t: box-scoped scroll in TTF mode (fb_scroll_rows() is a
* whole-framebuffer blit and would drag pixels from outside the box);
* bitmap mode (boot/POST) keeps the original full-framebuffer path. */
if (g_glyph_mode == VT_GLYPH_TTF) {
fb_scroll_rect(g_origin_x, g_origin_y, VT100_BOX_W, VT100_BOX_H,
lines * cell_h(), g_vt.def_bg);
} else {
fb_scroll_rows(lines, g_vt.def_bg);
}
}
/* -----------------------------------------------------------------------
@@ -558,8 +612,14 @@ static void erase_display(int mode)
}
erase_line_range(g_vt.cy, 0, g_vt.cx + 1);
} else {
/* erase entire screen */
fb_fill_rect(0, 0, fb_width(), fb_height(), g_vt.def_bg);
/* erase entire screen -- 4.4t: box-scoped in TTF mode (a
* full-framebuffer wipe here would blank CANVAS outside the box),
* full-screen in bitmap mode as before (boot/POST unaffected). */
if (g_glyph_mode == VT_GLYPH_TTF) {
fb_fill_rect(g_origin_x, g_origin_y, VT100_BOX_W, VT100_BOX_H, g_vt.def_bg);
} else {
fb_fill_rect(0, 0, fb_width(), fb_height(), g_vt.def_bg);
}
g_vt.cx = g_vt.cy = 0;
}
}
@@ -804,8 +864,13 @@ void vt100_putc(char raw)
/* RI reverse index: scroll down (insert line at top) */
if (g_vt.cy == 0) {
/* Scroll display down: not trivially done without a
* full back-buffer; fill top row with bg instead. */
fb_fill_rect(0, 0, fb_width(), cell_h(), g_vt.def_bg);
* full back-buffer; fill top row with bg instead.
* 4.4t: box-scoped in TTF mode, full-width in bitmap mode. */
if (g_glyph_mode == VT_GLYPH_TTF) {
fb_fill_rect(g_origin_x, g_origin_y, VT100_BOX_W, cell_h(), g_vt.def_bg);
} else {
fb_fill_rect(0, 0, fb_width(), cell_h(), g_vt.def_bg);
}
} else {
g_vt.cy--;
}