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
+85 -2
View File
@@ -5688,10 +5688,93 @@ document and committing that amendment as its own item.*
> correctly — two new words were added to the dictionary — but still cross-arch consistent,
> which is the actual parity property that matters).
- [x] **4.4t — Confine REPL text rendering to the 4.4o CANVAS box.** REPL text (TTF mode)
currently still renders across the entire framebuffer — the 640×480 box computed and
pixel-verified in 4.4o/4.4p was geometry only, never wired into `vt100`'s actual glyph
placement, cursor bounds, or scroll region. This was explicitly deferred, twice, as its own
item (flagged in both 4.4p's and 4.4q's writeups) rather than silently folded into either.
Scope: make `vt100`'s live text grid (TTF/REPL mode only) operate entirely within the box —
per-arch origin (amd64 (320,104); aarch64/riscv64 (80,4)), 640×480, giving an exact
53 cols × 20 rows (640/12, 480/24, both exact, no remainder). Boot/POST rendering
(`font_8x16`, bitmap mode) is untouched — stays full-screen, exactly as before.
Concretely: `px_of()`/`py_of()` gain a box-origin offset (0,0 in bitmap mode, the per-arch
box origin in TTF mode); `g_vt.cols`/`g_vt.rows` in `vt100_enable_ttf()` are set to the box's
53×20 directly rather than derived from `fb_width()`/`fb_height()` — and this must happen
*before* the scrollback shadow/ring `kmalloc` block in that same function, since
`g_line_stride` depends on `cols`; verify `g_line_stride == 54` at runtime rather than
assuming the ordering landed right. `erase_display(2)` must become mode-aware: box-scoped
fill in TTF mode, full-screen in bitmap mode (today it unconditionally wipes the whole
screen, which would blank CANVAS outside the box once confinement is live). `scroll_up()`'s
`fb_scroll_rows()` call is a whole-framebuffer blit — wrong once text is box-confined, since
it would drag pixels from outside the box. New `fb_scroll_rect(x, y, w, h, lines, bg)` in
`framebuffer.c`/`.h`, row-by-row copy bounded to `x..x+w`, used in TTF mode; bitmap mode
keeps `fb_scroll_rows()` unchanged. This item does **not** draw a persistent border around
the box — that is explicitly 4.4r's business (hide/show), not this item's; adding one here
would be scope creep. Input echo needs no special handling — `sk_readline()` echoes through
`console_putc()`, the same path being confined; verify this rather than adding anything.
Depends on 4.4o/4.4p (box geometry, already done) and 4.4q (scrollback, already done, whose
shadow/ring sizing this item's cols/rows change affects).
*Done when:* three-arch screendump showing REPL text bounded inside the box region, backed
by a pixel scan proving no non-background glyph pixel lands outside the box rect (measured,
not eyeballed — same discipline as 4.4p); three-arch QEMU boot + logs per CLAUDE.md. Expect
the screenshots to look different from every prior capture in this section — text starts at
the box origin, not (0,0), and only 20 rows are usable at a time.
*Refs:* §27.8, 4.4o, 4.4p, 4.4q.
> **Done 2026-08-11.** Implemented exactly as scoped: `g_origin_x`/`g_origin_y` added to
> `vt100.c` (0,0 default, matching bitmap mode); `px_of()`/`py_of()` add the origin;
> `vt100_enable_ttf()` sets `g_origin_x`/`g_origin_y`/`g_vt.cols`/`g_vt.rows` to the per-arch
> box values *before* the scrollback shadow/ring `kmalloc` block, with a runtime check that
> `g_line_stride == VT100_BOX_COLS + 1` (54) — bails to "scrollback unavailable" rather than
> silently corrupting shadow/ring indices if that ordering is ever disturbed later.
> `erase_display(2)` and the `ESC M` reverse-index top-row fill are both mode-aware now
> (box-scoped in TTF, full-screen in bitmap, unchanged). `fb_scroll_rect(x,y,w,h,pixel_rows,bg)`
> added to `framebuffer.c`/`.h` (row-by-row copy bounded to `x..x+w`, same non-volatile bulk-copy
> technique as `fb_scroll_rows()`); `scroll_up()` calls it in TTF mode, `fb_scroll_rows()`
> unchanged in bitmap mode.
>
> **One thing found and fixed beyond the item's original text, not scope creep — it's the
> same "switch cleanly into confined REPL mode" this item is about:** the first verification
> pass showed the box's content was genuinely confined (confirmed by cropping exactly the box
> rect and reading it cleanly) but the *rest of the screen* was frozen bitmap-mode boot/POST
> debris, never cleared, because `erase_display(2)` becoming box-scoped means it no longer
> clears anything outside the box either. Fixed with one `fb_fill_rect(0, 0, fb_width(),
> fb_height(), g_vt.def_bg)` at the top of `vt100_enable_ttf()`'s box-switch, before the
> (now box-scoped) `erase_display(2)` call — the one and only whole-framebuffer wipe in TTF
> mode; every later `ESC[2J]` stays box-scoped as designed.
>
> **Verification, three-arch, pixel-measured not eyeballed:** booted each arch to `ok>`,
> sent `LOG-ERROR LOG-LEVEL!` (this build's default log level traces every word execution to
> console via a HADES/ECW hotword tracer — unrelated to this item, silenced only so the
> screendump reads cleanly) then `: TB1234 0 DO I . LOOP CR ; 20 TB1234` to generate content
> and exercise wrap/scroll within the box, then a QEMU monitor `screendump`. A Python scan
> checked every pixel of each screenshot against the computed box rect
> ((320,104)-(960,584) amd64; (80,4)-(720,484) aarch64/riscv64) and counted non-background
> pixels outside it: **0 on all three architectures.** Screendumps:
> [evidence/amd64/qemu-screenshot-20260811-230119-4.4t-confinement-amd64.png](evidence/amd64/qemu-screenshot-20260811-230119-4.4t-confinement-amd64.png),
> [evidence/aarch64/qemu-screenshot-20260811-230119-4.4t-confinement-aarch64.png](evidence/aarch64/qemu-screenshot-20260811-230119-4.4t-confinement-aarch64.png),
> [evidence/riscv64/qemu-screenshot-20260811-230119-4.4t-confinement-riscv64.png](evidence/riscv64/qemu-screenshot-20260811-230119-4.4t-confinement-riscv64.png).
>
> **Real environment hazard found and worked around, recorded so it doesn't cost time again:**
> this repo's `Makefile.starkernel` builds into `build/$(ARCH)/$(TARGET)` where
> `TARGET ?= kernel` — i.e. `build/amd64/kernel/`, not `build/amd64/`. A stale
> `build/amd64/starkernel.iso` left over from hours earlier in this session at the wrong
> (non-`kernel/`) path caused three consecutive custom QEMU boots (added a monitor socket for
> screendumping, mirroring 4.4p/4.4q's technique) to reproducibly stall mid-boot during
> Hermes's migration self-test — looked exactly like a confinement-caused regression until the
> path mismatch was found via `ls -la` timestamp comparison. Not a kernel bug; a test-harness
> mistake. Always launch custom QEMU invocations against `$(BUILD_DIR)` from the Makefile
> (`build/<arch>/kernel/`), never a hand-guessed `build/<arch>/` path.
>
> Three-arch standard boot+log basis: amd64 (`logs/20260811-225236/amd64/`), aarch64
> (`logs/20260811-225514/aarch64/`), riscv64 (`logs/20260811-225903/riscv64/`) — `Failed: 0`
> all three.
- [ ] **4.4r — Toggle word: hide/show the scroll box.** A FORTH word that hides the 640×480
scroll box, revealing the rest of CANVAS beneath it for drawing; showing it again restores
the scroll-box content undisturbed, no scrollback loss. Depends on 4.4o/4.4p (geometry) and
4.4q (scrollback) all being done first.
the scroll-box content undisturbed, no scrollback loss. Depends on 4.4o/4.4p (geometry),
4.4q (scrollback), and 4.4t (confinement — the box must be real before it can be hidden)
all being done first.
*Done when:* toggling off then on again, verified by screendump, shows the CANVAS drawing
area unobstructed while off and the exact prior scroll-box content restored when back on;
three-arch QEMU boot + logs per CLAUDE.md.