diff options
| author | user <user@clank> | 2026-07-18 22:19:59 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-07-18 22:19:59 +0200 |
| commit | c2b512e803285223959e0a97601c017730d1b0bb (patch) | |
| tree | 8cb28702cffcfccce8a244d7d5bfabb0424c30a3 | |
| parent | demo: boot through the CGB BIOS + double-speed ROM in the README gif (diff) | |
| download | gbos-c2b512e803285223959e0a97601c017730d1b0bb.tar.gz gbos-c2b512e803285223959e0a97601c017730d1b0bb.tar.xz gbos-c2b512e803285223959e0a97601c017730d1b0bb.zip | |
term+libc: batched write rendering - one tile render per write(), 2.3x lines
sys_write sets wBatch; render_cursor_tile - the single choke point all
of term_putc's visible mutations pass through - then marks a dirty
span (per LINE-SLOT, so spans survive scroll rotation for free)
instead of rendering. term_write_end renders every dirty tile exactly
once, then the cursor. Scrolls stay immediate (blank rebuild is cheap
and artifact-free); file-bound writes flush for free.
The enabler is in libc: puts() was a SYS_PUTC trap PER CHARACTER, so
almost no console output ever went through sys_write. It now issues
one SYS_WRITE for the whole string - one trap, one batch, and every
line-printer (echo, irc, the shell) gets the batched path.
Measured: 37-char write = 128k cycles (3.5k/char) vs ~8k/char down
the per-char trap path. Full demo passes; GIF regenerated.
| -rw-r--r-- | demo/gbos-demo.gif | bin | 330712 -> 320907 bytes | |||
| -rw-r--r-- | src/kdata.asm | 2 | ||||
| -rw-r--r-- | src/syscall.asm | 4 | ||||
| -rw-r--r-- | src/term.asm | 129 | ||||
| -rw-r--r-- | usr/libc.s | 26 |
5 files changed, 151 insertions, 10 deletions
diff --git a/demo/gbos-demo.gif b/demo/gbos-demo.gif Binary files differindex c5208f7..869065b 100644 --- a/demo/gbos-demo.gif +++ b/demo/gbos-demo.gif diff --git a/src/kdata.asm b/src/kdata.asm index 13dc6dd..c2316b8 100644 --- a/src/kdata.asm +++ b/src/kdata.asm @@ -68,3 +68,5 @@ wBootFsFresh::DS 1 ; fs_init formatted a blank disk this boot wTicks:: DS 4 ; monotonic 64 Hz system tick (TimerISR), LE wTickTog:: DS 1 ; TimerISR fires at 128 Hz in double-speed mode; ; this toggle keeps wTicks counting at 64 Hz +wBatch:: DS 1 ; inside a sys_write: defer terminal tile rendering +wBatchDirty:: DS 1 ; batched console output happened (flush needed) diff --git a/src/syscall.asm b/src/syscall.asm index 4bcb601..cab3e79 100644 --- a/src/syscall.asm +++ b/src/syscall.asm @@ -85,6 +85,8 @@ sys_write: ld a, b or a ret z ; len 0 + ld a, 1 + ld [wBatch], a ; defer terminal rendering for the whole write .loop ld a, [de] push de @@ -96,7 +98,7 @@ sys_write: inc de dec b jr nz, .loop - ret + jp term_write_end ; render each dirty tile once + the cursor ; ----------------------------------------------------------------------------- ; CurStdin / CurStdout -> A = current process PROC_STDIN / PROC_STDOUT diff --git a/src/term.asm b/src/term.asm index 46cd23e..a3bfe19 100644 --- a/src/term.asm +++ b/src/term.asm @@ -36,6 +36,10 @@ DEF ACACHE EQU 40 ; attr cache in COL_BUF's spare stride bytes: the tile ; terminal state (WRAMX gap $D580-$D5FF) DEF wVram EQU $D584 ; 2 DEF wMapTop EQU $D5BB ; BG-map ring: tilemap row where logical row 0 lives +; batched-write dirty spans, indexed by LINE-SLOT (slots survive scroll +; rotation, so a span stays valid however much the batch scrolls): +DEF wDMin EQU $D5BC ; 18 bytes: first dirty tile col per slot ($FF = clean) +DEF wDMax EQU $D5CE ; 18 bytes: last dirty tile col per slot DEF wCurRow EQU $D586 ; cursor screen row 0..17 DEF wCurCol EQU $D587 ; cursor screen col 0..39 DEF wCursorOn EQU $D588 ; draw the cursor? @@ -96,6 +100,23 @@ term_init:: ld a, $FF ld [wCSPL], a ; invalidate the color_setup memo (no packed ; color is $FF: bg/fg nibbles are 0..7) + ; dirty spans: all clean ($FF min / 0 max); no batch active + ld hl, wDMin + ld b, 18 +.dspan + ld a, $FF + ld [hl+], a + dec b + jr nz, .dspan + ld hl, wDMax + ld b, 18 +.dspan2 + xor a + ld [hl+], a + dec b + jr nz, .dspan2 + ld [wBatch], a + ld [wBatchDirty], a ld a, 1 ld [wCursorOn], a ld a, 7 @@ -689,8 +710,50 @@ render_tile: ld [rVBK], a ret -; render_cursor_tile - rebuild the tile at the cursor position. +; render_cursor_tile - rebuild the tile at the cursor position. Inside a +; batched write (sys_write sets wBatch) it renders NOTHING: it marks the +; cursor's tile in the per-slot dirty span instead, and term_write_end +; renders every dirty tile exactly once. This is the single choke point all +; of term_putc's visible mutations (char, \n, \r, \b, cursor moves) pass +; through, so batching here covers them all. render_cursor_tile: + ld a, [wBatch] + or a + jr z, .render + ; mark: span[assign[wCurRow]] grows to include wCurCol/2 + ld a, [wCurRow] + add LOW(wAssign) + ld l, a + ld a, HIGH(wAssign) + adc 0 + ld h, a + ld a, [hl] ; slot + add LOW(wDMin) + ld l, a + ld a, HIGH(wDMin) + adc 0 + ld h, a ; HL = &wDMin[slot] + ld a, [wCurCol] + srl a + ld b, a ; B = tile col + ld a, [hl] + cp b + jr c, .minok ; min < tcol: keep + ld a, b + ld [hl], a +.minok + ld de, wDMax - wDMin + add hl, de ; HL = &wDMax[slot] + ld a, [hl] + cp b + jr nc, .maxok ; max >= tcol: keep + ld a, b + ld [hl], a +.maxok + ld a, 1 + ld [wBatchDirty], a + ret +.render ld a, [wCurRow] ld b, a ld a, [wCurCol] @@ -931,6 +994,70 @@ compute_offset:: ret ; ----------------------------------------------------------------------------- +; term_write_end - flush a batched write: render every dirty span tile once, +; then the cursor tile (which also refreshes its map attr). Costs nothing +; when the batch produced no console output (file-bound writes). +; ----------------------------------------------------------------------------- +term_write_end:: + xor a + ld [wBatch], a + ld a, [wBatchDirty] + or a + ret z ; no console output in this batch + xor a + ld [wBatchDirty], a + ld a, [rSVBK] ; same banking guard as term_putc + push af + ld a, 1 + ld [rSVBK], a + ld b, 0 ; screen row +.row + ; slot = assign[row]; skip if its span is clean + ld a, b + add LOW(wAssign) + ld l, a + ld a, HIGH(wAssign) + adc 0 + ld h, a + ld a, [hl] + add LOW(wDMin) + ld l, a + ld a, HIGH(wDMin) + adc 0 + ld h, a + ld a, [hl] + cp $FF + jr z, .next + ld c, a ; C = first dirty tile col + ld a, $FF + ld [hl], a ; reset min + ld de, wDMax - wDMin + add hl, de + ld a, [hl] + ld d, a ; D = last dirty tile col + xor a + ld [hl], a ; reset max +.tile + push bc + push de + call render_tile ; B = row, C = tile col + pop de + pop bc + inc c + ld a, d + cp c + jr nc, .tile ; while last >= col +.next + inc b + ld a, b + cp TROWS + jr c, .row + call render_cursor_tile ; wBatch is 0 again: real render + map attr + pop af + ld [rSVBK], a + ret + +; ----------------------------------------------------------------------------- ; term_putc - A = character. Print at the cursor, advance, handle \n \r \b. ; ----------------------------------------------------------------------------- ; term_putc guarantees SVBK=1 so the buffer/state at $D5xx/$D6xx are the ones @@ -33,18 +33,28 @@ _putc: rst #0x30 ret - ;; void puts(const char *s /*DE*/) - write until NUL (no newline) + ;; void puts(const char *s /*DE*/) - write until NUL (no newline). + ;; ONE SYS_WRITE for the whole string instead of a SYS_PUTC per char: + ;; a single trap, and the kernel defers tile rendering for the whole + ;; run (sys_write batching), so a line paints each tile once. _puts: -1$: ld a, (de) + push de + ld h, d + ld l, e + ld b, #0 +1$: ld a, (hl) + or a + jr z, 2$ + inc hl + inc b + jr 1$ +2$: pop de + ld a, b or a ret z - push de - ld e, a - ld c, #16 ; SYS_PUTC + ld c, #3 ; SYS_WRITE (DE=buf, B=len) rst #0x30 - pop de - inc de - jr 1$ + ret ;; void nl(void) - CRLF via stdout _nl: |
