; ============================================================================= ; term.asm - a 40x18 text terminal on the CGB LCD, with cursor and fast scroll. ; ; 4x8 font: each 8x8 background tile holds TWO 4-px characters (left+right), ; giving 40 columns. Tiles are LINE-BOUND: line-slot L owns the 20 VRAM tiles ; at tile-positions [L*20 .. L*20+19] (index = LOW(pos), VRAM bank = HIGH(pos); ; positions 256-359 live in bank 1 - hence CGB-only). assign[sr] maps each of ; the 18 screen rows to a line-slot, and the tilemap points each screen row at ; that slot's tiles. Scrolling just rotates assign[], rebuilds the one new ; bottom line, and rewrites the tilemap - O(1 line), not O(screen). ; ; The cursor is an underline OR'd into the current cell's tile (wCurMask). ; ============================================================================= INCLUDE "include/gbos.inc" DEF rSCY EQU $FF42 DEF rSCX EQU $FF43 DEF rVBK EQU $FF4F ; CGB VRAM bank select DEF rBCPS EQU $FF68 ; CGB BG palette index DEF rBCPD EQU $FF69 ; CGB BG palette data DEF TCOLS EQU 40 ; text columns DEF TROWS EQU 18 ; text rows DEF TSTRIDE EQU 64 ; bytes per line-slot in the buffer (power of 2) DEF TERM_BUF EQU $D600 ; 18*64 = 1152 B (WRAMX, above the FS caches) ; terminal state (WRAMX gap $D580-$D5FF) DEF wGlyphL EQU $D580 ; 2 DEF wGlyphR EQU $D582 ; 2 DEF wVram EQU $D584 ; 2 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? DEF wRT_sr EQU $D58A ; render_tile scratch DEF wRT_tcol EQU $D58B DEF wRT_cL EQU $D58C DEF wRT_cR EQU $D58D DEF wScratch EQU $D58E DEF wViewTop EQU $D58F ; view offset: screen row maps to logical row + this DEF wAssign EQU $D590 ; assign[18]: screen row -> line-slot ($D590-$D5A1) DEF CUR_L EQU $E0 ; cursor underline, left cell (3px) DEF CUR_R EQU $0E ; cursor underline, right cell SECTION "term", ROM0 ; ----------------------------------------------------------------------------- ; term_init - CGB palette, identity assign[], clear buffer, build, LCD on. ; ----------------------------------------------------------------------------- term_init:: xor a ld [rLCDC], a ; LCD off ld [rSCX], a ld [rSCY], a ; BG palette 0: color0 = white (bg), color1 = black (text) ld a, $80 ld [rBCPS], a ld a, $FF ld [rBCPD], a ld a, $7F ld [rBCPD], a xor a ld [rBCPD], a ld [rBCPD], a ld [rBCPD], a ld [rBCPD], a ld [rBCPD], a ld [rBCPD], a ; state: assign[sr]=sr, cursor at 0,0 (on) ld hl, wAssign ld b, TROWS xor a .mkid ld [hl+], a inc a dec b jr nz, .mkid xor a ld [wCurRow], a ld [wCurCol], a ld a, 1 ld [wCursorOn], a xor a ld [wViewTop], a ; view starts unshifted (all 18 rows) ; clear the buffer (all slots) to spaces ld hl, TERM_BUF ld de, TROWS * TSTRIDE .clr ld a, $20 ld [hl+], a dec de ld a, d or e jr nz, .clr call term_write_tilemap call term_redraw ; build all tiles (blank + cursor) ld a, %10010001 ; LCD on, BG on, tile data $8000, map $9800 ld [rLCDC], a ret ; ----------------------------------------------------------------------------- ; term_write_tilemap - point each screen row's tilemap entries at assign[]'s ; line-slot tiles. Pass 1 = indices (bank 0), pass 2 = attributes (bank 1). ; ----------------------------------------------------------------------------- term_write_tilemap:: xor a ld [rVBK], a ld c, 0 ; screen row .i_row call .slot20 ; DE = assign[c]*20 (pos start), HL = tilemap row ld b, TCOLS/2 .i_col ld a, e ld [hl+], a ; index = LOW(pos) inc de dec b jr nz, .i_col inc c ld a, c cp TROWS jr c, .i_row ld a, 1 ld [rVBK], a ld c, 0 .a_row call .slot20 ld b, TCOLS/2 .a_col ld a, d ; HIGH(pos): 0 or 1 or a jr z, .a0 ld a, $08 ; VRAM bank 1 attribute jr .aw .a0 xor a .aw ld [hl+], a inc de dec b jr nz, .a_col inc c ld a, c cp TROWS jr c, .a_row xor a ld [rVBK], a ret ; helper: C = screen row -> DE = assign[C]*20, HL = $9800 + C*32 .slot20 ld a, [wViewTop] add c ; logical row = screen row + view offset cp TROWS jr c, .lok ld a, TROWS-1 ; rows the OSK covers clamp to the cursor line .lok add LOW(wAssign) ld l, a ld a, HIGH(wAssign) adc 0 ld h, a ld a, [hl] ; L = assign[logical row] ld l, a ld h, 0 add hl, hl add hl, hl ; L*4 ld d, h ld e, l add hl, hl add hl, hl ; L*16 add hl, de ; L*20 ld d, h ld e, l ; DE = pos start ld a, c ld l, a ld h, 0 add hl, hl add hl, hl add hl, hl add hl, hl add hl, hl ; row*32 ld a, h add $98 ld h, a ; HL = $9800 + row*32 ret ; ----------------------------------------------------------------------------- ; slot_addr - A = screen row -> HL = TERM_BUF + assign[row]*64 ; ----------------------------------------------------------------------------- slot_addr: add LOW(wAssign) ld l, a ld a, HIGH(wAssign) adc 0 ld h, a ld a, [hl] ; line-slot ld l, a ld h, 0 add hl, hl add hl, hl add hl, hl add hl, hl add hl, hl add hl, hl ; *64 ld de, TERM_BUF add hl, de ret ; ----------------------------------------------------------------------------- ; glyph_ptr - A = char -> HL = FontData + (clamp(A,32,127)-32)*8 ; ----------------------------------------------------------------------------- glyph_ptr: cp 32 jr nc, .ok ld a, 32 .ok cp 128 jr c, .ok2 ld a, 32 .ok2 sub 32 ld l, a ld h, 0 add hl, hl add hl, hl add hl, hl ; *8 ld de, FontData add hl, de ret ; ----------------------------------------------------------------------------- ; build_tile - build tile for tile-position DE (E=index, D=bank) from chars ; B (left) and C (right). ORs wCurMask into the last pixel row (cursor). ; ----------------------------------------------------------------------------- build_tile:: ld a, d ld [rVBK], a ; VRAM bank for this tile ld h, 0 ld l, e add hl, hl add hl, hl add hl, hl add hl, hl ; E*16 ld a, h add $80 ld h, a ; HL = $8000 + E*16 ld a, l ld [wVram], a ld a, h ld [wVram+1], a ld a, b call glyph_ptr ld a, l ld [wGlyphL], a ld a, h ld [wGlyphL+1], a ld a, c call glyph_ptr ld a, l ld [wGlyphR], a ld a, h ld [wGlyphR+1], a ld b, 8 ; row counter ld a, [wVram] ld l, a ld a, [wVram+1] ld h, a ; HL = VRAM dest .brow push hl ld a, [wGlyphL] ld e, a ld a, [wGlyphL+1] ld d, a ld a, [de] swap a and $F0 ld c, a ; left glyph -> high nibble ld a, [wGlyphR] ld e, a ld a, [wGlyphR+1] ld d, a ld a, [de] and $0F or c ; A = combined pixel row ld e, a ; save (DE reloaded next iter) ld a, b dec a jr nz, .nocur ; last row (b==1)? add cursor underline ld a, [wCurMask] or e jr .wr .nocur ld a, e .wr pop hl ld [hl+], a ; plane 0 xor a ld [hl+], a ; plane 1 = 0 ; advance glyph pointers - MUST NOT touch HL (it holds the VRAM dest!) ld a, [wGlyphL] inc a ld [wGlyphL], a jr nz, .nl ld a, [wGlyphL+1] inc a ld [wGlyphL+1], a .nl ld a, [wGlyphR] inc a ld [wGlyphR], a jr nz, .nr ld a, [wGlyphR+1] inc a ld [wGlyphR+1], a .nr dec b jr nz, .brow ret ; ----------------------------------------------------------------------------- ; render_tile - B = screen row, C = tile column: rebuild that one tile from the ; buffer, adding the cursor underline if the cursor is in it. ; ----------------------------------------------------------------------------- render_tile: ld a, b ld [wRT_sr], a ld a, c ld [wRT_tcol], a ; chars: &buffer[assign[sr]][tcol*2] ld a, b call slot_addr ; HL = &buffer[slot][0] ld a, [wRT_tcol] add a ; tcol*2 add l ld l, a ld a, h adc 0 ld h, a ld a, [hl+] ld [wRT_cL], a ld a, [hl] ld [wRT_cR], a ; cursor mask xor a ld [wCurMask], a ld a, [wCursorOn] or a jr z, .pos ld a, [wCurRow] ld b, a ld a, [wRT_sr] cp b jr nz, .pos ld a, [wRT_tcol] add a ; left cell col ld b, a ld a, [wCurCol] cp b jr nz, .cr ld a, CUR_L ld [wCurMask], a jr .pos .cr inc b ; right cell col cp b jr nz, .pos ld a, CUR_R ld [wCurMask], a .pos ; pos = assign[sr]*20 + tcol -> DE (D=bank, E=index) ld a, [wRT_sr] add LOW(wAssign) ld l, a ld a, HIGH(wAssign) adc 0 ld h, a ld a, [hl] ld l, a ld h, 0 add hl, hl add hl, hl ld d, h ld e, l ; L*4 add hl, hl add hl, hl add hl, de ; L*20 ld a, [wRT_tcol] add l ld l, a ld a, h adc 0 ld h, a ; HL = pos ld d, h ld e, l ; DE = pos ld a, [wRT_cL] ld b, a ld a, [wRT_cR] ld c, a call build_tile xor a ld [rVBK], a ret ; render_cursor_tile - rebuild the tile at the cursor position. render_cursor_tile: ld a, [wCurRow] ld b, a ld a, [wCurCol] srl a ; /2 = tile col ld c, a jp render_tile ; ----------------------------------------------------------------------------- ; term_redraw - rebuild every tile from the buffer (init / full refresh). ; ----------------------------------------------------------------------------- term_redraw:: ld b, 0 ; screen row .row ld c, 0 ; tile col .col push bc call render_tile pop bc inc c ld a, c cp TCOLS/2 jr c, .col inc b ld a, b cp TROWS jr c, .row ret ; ----------------------------------------------------------------------------- ; term_scroll - scroll up one line: rotate assign[], blank+build the new bottom ; line, rewrite the tilemap. ; ----------------------------------------------------------------------------- ; term_scroll - scroll all 18 logical rows up one; the new line becomes logical ; row 17. The view offset decides where that lands on screen, so toggling the ; OSK never destroys backlog. term_scroll:: ld a, [wAssign] ld [wScratch], a ; freed line-slot ld hl, wAssign+1 ld de, wAssign ld b, TROWS-1 .rot ld a, [hl+] ld [de], a inc de dec b jr nz, .rot ld a, [wScratch] ld [wAssign + TROWS-1], a ; clear buffer[freed] to spaces ld l, a ld h, 0 add hl, hl add hl, hl add hl, hl add hl, hl add hl, hl add hl, hl ; *64 ld de, TERM_BUF add hl, de ld b, TCOLS .clr ld a, $20 ld [hl+], a dec b jr nz, .clr ; rebuild logical row 17 ld c, 0 .build push bc ld b, TROWS-1 call render_tile pop bc inc c ld a, c cp TCOLS/2 jr c, .build call term_write_tilemap ret ; cursor_down - advance the cursor row, scrolling when it runs past the bottom. cursor_down: ld a, [wCurRow] inc a cp TROWS jr c, .ok call term_scroll ld a, TROWS-1 .ok ld [wCurRow], a ret ; term_set_view - A = view-top offset. screen row -> logical row + A. 0 shows all ; 18 rows; OSK_ROWS shifts the view up so the cursor line clears the keyboard. ; No content is scrolled/lost - just remapped. term_set_view:: ld [wViewTop], a jp term_write_tilemap ; ----------------------------------------------------------------------------- ; 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 ; we built, no matter which process context KPutc calls us from. The stack is ; in non-banked WRAM ($Cxxx/$Axxx), so saving SVBK across the switch is safe. term_putc:: ld d, a ld a, [rSVBK] push af ld a, 1 ld [rSVBK], a ld a, d call term_putc_body pop af ld [rSVBK], a ret term_putc_body: cp 10 jr z, .newline cp 13 jr z, .cret cp 8 jr z, .bs cp 32 ret c ; ignore other control chars ; printable: buffer[assign[cur]][cc] = char ld [wRT_cL], a ld a, [wCurRow] call slot_addr ld a, [wCurCol] add l ld l, a ld a, h adc 0 ld h, a ld a, [wRT_cL] ld [hl], a ; redraw current cell WITHOUT cursor (shows the char) xor a ld [wCursorOn], a call render_cursor_tile ; advance ld a, [wCurCol] inc a cp TCOLS jr c, .adv xor a ld [wCurCol], a call cursor_down jr .show .adv ld [wCurCol], a .show ld a, 1 ld [wCursorOn], a call render_cursor_tile ret .newline xor a ld [wCursorOn], a call render_cursor_tile xor a ld [wCurCol], a call cursor_down ld a, 1 ld [wCursorOn], a call render_cursor_tile ret .cret xor a ld [wCursorOn], a call render_cursor_tile xor a ld [wCurCol], a ld a, 1 ld [wCursorOn], a call render_cursor_tile ret .bs ld a, [wCurCol] or a ret z xor a ld [wCursorOn], a call render_cursor_tile ; clear cursor at old ld a, [wCurCol] dec a ld [wCurCol], a ; blank the cell we backed into ld a, [wCurRow] call slot_addr ld a, [wCurCol] add l ld l, a ld a, h adc 0 ld h, a ld a, $20 ld [hl], a ld a, 1 ld [wCursorOn], a call render_cursor_tile ret ; ----------------------------------------------------------------------------- ; term_puts - HL = NUL-terminated string: print via term_putc. ; ----------------------------------------------------------------------------- term_puts:: ld a, [hl+] or a ret z push hl call term_putc pop hl jr term_puts ; ----------------------------------------------------------------------------- ; term_demo - drive the terminal: print enough lines to scroll, end at a prompt. ; ----------------------------------------------------------------------------- term_demo:: ld hl, .msg call term_puts ret .msg: db "GBOS terminal: cursor + fast scroll", 10 db "01 the quick brown fox jumps", 10 db "02 over the lazy dog.", 10 db "03 line three of the scroll test", 10 db "04 line four", 10 db "05 line five", 10 db "06 line six", 10 db "07 line seven", 10 db "08 line eight", 10 db "09 line nine", 10 db "10 line ten", 10 db "11 line eleven", 10 db "12 line twelve", 10 db "13 line thirteen", 10 db "14 line fourteen", 10 db "15 line fifteen", 10 db "16 line sixteen", 10 db "17 line seventeen", 10 db "18 line eighteen", 10 db "19 line nineteen", 10 db "20 line twenty", 10 db "21 line twenty-one", 10 db "22 line twenty-two", 10 db "ready$ ", 0