aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile2
-rw-r--r--src/boot.asm2
-rw-r--r--src/font.asm99
-rw-r--r--src/term.asm381
-rw-r--r--tools/genfont.py105
-rw-r--r--tools/ppmview.py16
6 files changed, 604 insertions, 1 deletions
diff --git a/Makefile b/Makefile
index 58a6bd1..b7fe95d 100644
--- a/Makefile
+++ b/Makefile
@@ -8,7 +8,7 @@ OBJS := $(patsubst src/%.asm,$(BUILD)/%.o,$(SRCS))
ASFLAGS := -I include -Wall
# MBC5 + RAM + battery; -r 3 = 32 KiB cart RAM (bump for more task banks)
-FIXFLAGS := -v -m MBC5+RAM+BATTERY -r 4 -p 0xFF -t GBOS
+FIXFLAGS := -v -C -m MBC5+RAM+BATTERY -r 4 -p 0xFF -t GBOS
.PHONY: all clean run
diff --git a/src/boot.asm b/src/boot.asm
index 9d9571e..0a5f2e3 100644
--- a/src/boot.asm
+++ b/src/boot.asm
@@ -56,6 +56,8 @@ KernelInit:
ld a, HIGH(wKernelPCB)
ld [wCurProc+1], a
call fs_init ; mount the disk (format on first boot; persists)
+ call term_init ; set up the LCD text terminal
+ call term_test ; render a test pattern
; --- spawn the init task (pid 1); it fork()s the rest ---
ld hl, TaskInit
diff --git a/src/font.asm b/src/font.asm
new file mode 100644
index 0000000..a6ff7d9
--- /dev/null
+++ b/src/font.asm
@@ -0,0 +1,99 @@
+; auto-generated by tools/genfont.py - 4x8 font (3x5 glyphs), 96 glyphs
+SECTION "font", ROM0
+FontData::
+ db $00,$00,$00,$00,$00,$00,$00,$00 ; ' '
+ db $04,$04,$04,$00,$04,$00,$00,$00 ; '!'
+ db $0A,$0A,$00,$00,$00,$00,$00,$00 ; '"'
+ db $0A,$0E,$0A,$0E,$0A,$00,$00,$00 ; '#'
+ db $04,$06,$0C,$06,$0C,$00,$00,$00 ; '$'
+ db $0A,$02,$04,$08,$0A,$00,$00,$00 ; '%'
+ db $04,$0A,$04,$0A,$06,$00,$00,$00 ; '&'
+ db $04,$04,$00,$00,$00,$00,$00,$00 ; '''
+ db $02,$04,$04,$04,$02,$00,$00,$00 ; '('
+ db $08,$04,$04,$04,$08,$00,$00,$00 ; ')'
+ db $0A,$04,$0E,$04,$0A,$00,$00,$00 ; '*'
+ db $00,$04,$0E,$04,$00,$00,$00,$00 ; '+'
+ db $00,$00,$00,$04,$08,$00,$00,$00 ; ','
+ db $00,$00,$0E,$00,$00,$00,$00,$00 ; '-'
+ db $00,$00,$00,$00,$04,$00,$00,$00 ; '.'
+ db $02,$02,$04,$08,$08,$00,$00,$00 ; '/'
+ db $04,$0A,$0A,$0A,$04,$00,$00,$00 ; '0'
+ db $04,$0C,$04,$04,$0E,$00,$00,$00 ; '1'
+ db $0C,$02,$04,$08,$0E,$00,$00,$00 ; '2'
+ db $0C,$02,$04,$02,$0C,$00,$00,$00 ; '3'
+ db $0A,$0A,$0E,$02,$02,$00,$00,$00 ; '4'
+ db $0E,$08,$0C,$02,$0C,$00,$00,$00 ; '5'
+ db $06,$08,$0C,$0A,$04,$00,$00,$00 ; '6'
+ db $0E,$02,$04,$04,$04,$00,$00,$00 ; '7'
+ db $04,$0A,$04,$0A,$04,$00,$00,$00 ; '8'
+ db $04,$0A,$06,$02,$0C,$00,$00,$00 ; '9'
+ db $00,$04,$00,$04,$00,$00,$00,$00 ; ':'
+ db $00,$04,$00,$04,$08,$00,$00,$00 ; ';'
+ db $02,$04,$08,$04,$02,$00,$00,$00 ; '<'
+ db $00,$0E,$00,$0E,$00,$00,$00,$00 ; '='
+ db $08,$04,$02,$04,$08,$00,$00,$00 ; '>'
+ db $0C,$02,$04,$00,$04,$00,$00,$00 ; '?'
+ db $04,$0A,$0E,$08,$06,$00,$00,$00 ; '@'
+ db $0C,$0A,$0E,$0A,$0A,$00,$00,$00 ; 'A'
+ db $0C,$0A,$0C,$0A,$0C,$00,$00,$00 ; 'B'
+ db $06,$08,$08,$08,$06,$00,$00,$00 ; 'C'
+ db $0C,$0A,$0A,$0A,$0C,$00,$00,$00 ; 'D'
+ db $0E,$08,$0C,$08,$0E,$00,$00,$00 ; 'E'
+ db $0E,$08,$0C,$08,$08,$00,$00,$00 ; 'F'
+ db $06,$08,$0A,$0A,$06,$00,$00,$00 ; 'G'
+ db $0A,$0A,$0E,$0A,$0A,$00,$00,$00 ; 'H'
+ db $0E,$04,$04,$04,$0E,$00,$00,$00 ; 'I'
+ db $02,$02,$02,$0A,$04,$00,$00,$00 ; 'J'
+ db $0A,$0A,$0C,$0A,$0A,$00,$00,$00 ; 'K'
+ db $08,$08,$08,$08,$0E,$00,$00,$00 ; 'L'
+ db $0A,$0E,$0E,$0A,$0A,$00,$00,$00 ; 'M'
+ db $0A,$0E,$0E,$0E,$0A,$00,$00,$00 ; 'N'
+ db $04,$0A,$0A,$0A,$04,$00,$00,$00 ; 'O'
+ db $0C,$0A,$0C,$08,$08,$00,$00,$00 ; 'P'
+ db $04,$0A,$0A,$04,$02,$00,$00,$00 ; 'Q'
+ db $0C,$0A,$0C,$0A,$0A,$00,$00,$00 ; 'R'
+ db $06,$08,$04,$02,$0C,$00,$00,$00 ; 'S'
+ db $0E,$04,$04,$04,$04,$00,$00,$00 ; 'T'
+ db $0A,$0A,$0A,$0A,$06,$00,$00,$00 ; 'U'
+ db $0A,$0A,$0A,$04,$04,$00,$00,$00 ; 'V'
+ db $0A,$0A,$0E,$0E,$0A,$00,$00,$00 ; 'W'
+ db $0A,$0A,$04,$0A,$0A,$00,$00,$00 ; 'X'
+ db $0A,$0A,$04,$04,$04,$00,$00,$00 ; 'Y'
+ db $0E,$02,$04,$08,$0E,$00,$00,$00 ; 'Z'
+ db $0C,$08,$08,$08,$0C,$00,$00,$00 ; '['
+ db $08,$08,$04,$02,$02,$00,$00,$00 ; '?'
+ db $06,$02,$02,$02,$06,$00,$00,$00 ; ']'
+ db $04,$0A,$00,$00,$00,$00,$00,$00 ; '^'
+ db $00,$00,$00,$00,$0E,$00,$00,$00 ; '_'
+ db $08,$04,$00,$00,$00,$00,$00,$00 ; '`'
+ db $0C,$0A,$0E,$0A,$0A,$00,$00,$00 ; 'a'
+ db $0C,$0A,$0C,$0A,$0C,$00,$00,$00 ; 'b'
+ db $06,$08,$08,$08,$06,$00,$00,$00 ; 'c'
+ db $0C,$0A,$0A,$0A,$0C,$00,$00,$00 ; 'd'
+ db $0E,$08,$0C,$08,$0E,$00,$00,$00 ; 'e'
+ db $0E,$08,$0C,$08,$08,$00,$00,$00 ; 'f'
+ db $06,$08,$0A,$0A,$06,$00,$00,$00 ; 'g'
+ db $0A,$0A,$0E,$0A,$0A,$00,$00,$00 ; 'h'
+ db $0E,$04,$04,$04,$0E,$00,$00,$00 ; 'i'
+ db $02,$02,$02,$0A,$04,$00,$00,$00 ; 'j'
+ db $0A,$0A,$0C,$0A,$0A,$00,$00,$00 ; 'k'
+ db $08,$08,$08,$08,$0E,$00,$00,$00 ; 'l'
+ db $0A,$0E,$0E,$0A,$0A,$00,$00,$00 ; 'm'
+ db $0A,$0E,$0E,$0E,$0A,$00,$00,$00 ; 'n'
+ db $04,$0A,$0A,$0A,$04,$00,$00,$00 ; 'o'
+ db $0C,$0A,$0C,$08,$08,$00,$00,$00 ; 'p'
+ db $04,$0A,$0A,$04,$02,$00,$00,$00 ; 'q'
+ db $0C,$0A,$0C,$0A,$0A,$00,$00,$00 ; 'r'
+ db $06,$08,$04,$02,$0C,$00,$00,$00 ; 's'
+ db $0E,$04,$04,$04,$04,$00,$00,$00 ; 't'
+ db $0A,$0A,$0A,$0A,$06,$00,$00,$00 ; 'u'
+ db $0A,$0A,$0A,$04,$04,$00,$00,$00 ; 'v'
+ db $0A,$0A,$0E,$0E,$0A,$00,$00,$00 ; 'w'
+ db $0A,$0A,$04,$0A,$0A,$00,$00,$00 ; 'x'
+ db $0A,$0A,$04,$04,$04,$00,$00,$00 ; 'y'
+ db $0E,$02,$04,$08,$0E,$00,$00,$00 ; 'z'
+ db $02,$04,$0C,$04,$02,$00,$00,$00 ; '{'
+ db $04,$04,$04,$04,$04,$00,$00,$00 ; '|'
+ db $08,$04,$06,$04,$08,$00,$00,$00 ; '}'
+ db $00,$06,$0C,$00,$00,$00,$00,$00 ; '~'
+ db $0E,$0A,$0A,$0A,$0E,$00,$00,$00 ; '?'
diff --git a/src/term.asm b/src/term.asm
new file mode 100644
index 0000000..9d13b80
--- /dev/null
+++ b/src/term.asm
@@ -0,0 +1,381 @@
+; =============================================================================
+; term.asm - a 40x18 text terminal on the CGB LCD.
+;
+; 4x8 font: each 8x8 background tile holds TWO 4-px characters (left+right).
+; The tilemap is static - one dedicated VRAM tile per screen tile-position -
+; and we (re)build the 16 tile bytes when a character changes. 360 tiles:
+; positions 0-255 in VRAM bank 0, 256-359 in bank 1 (via the tilemap attribute
+; bank bit), which is why this is CGB-only.
+; =============================================================================
+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 row in the buffer (power of 2 for addressing)
+DEF TERM_BUF EQU $D600 ; 18*64 = 1152 B (WRAMX, above the FS caches)
+
+; terminal scratch (WRAMX gap $D500-$D5FF)
+DEF wGlyphL EQU $D580
+DEF wGlyphR EQU $D582
+DEF wVram EQU $D584
+
+SECTION "term", ROM0
+
+; -----------------------------------------------------------------------------
+; row_addr - A = row -> HL = TERM_BUF + row*64
+; -----------------------------------------------------------------------------
+row_addr:
+ ld h, 0
+ ld l, a
+ 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
+
+; -----------------------------------------------------------------------------
+; term_init - CGB palette, static tilemap, clear buffer, LCD on.
+; -----------------------------------------------------------------------------
+term_init::
+ ; LCD off (so VRAM is freely writable)
+ xor a
+ ld [rLCDC], a
+ ld [rSCX], a
+ ld [rSCY], a
+
+ ; BG palette 0: color0 = white (bg), color1 = black (text)
+ ld a, $80
+ ld [rBCPS], a ; index 0, auto-increment
+ ld a, $FF
+ ld [rBCPD], a ; c0 lo (white $7FFF)
+ ld a, $7F
+ ld [rBCPD], a ; c0 hi
+ xor a
+ ld [rBCPD], a ; c1 lo (black)
+ ld [rBCPD], a ; c1 hi
+ ld [rBCPD], a ; c2..c3 = black
+ ld [rBCPD], a
+ ld [rBCPD], a
+ ld [rBCPD], a
+
+ ; --- tilemap indices (VRAM bank 0): [pos] = LOW(pos) ---
+ xor a
+ ld [rVBK], a
+ ld de, 0 ; pos
+ ld c, 0 ; row
+.mi_row
+ ; compute tilemap row start = $9800 + row*32
+ 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
+ ld b, TCOLS/2 ; 20 tile columns
+.mi_col
+ ld a, e
+ ld [hl+], a ; index = LOW(pos)
+ inc de
+ dec b
+ jr nz, .mi_col
+ inc c
+ ld a, c
+ cp TROWS
+ jr c, .mi_row
+
+ ; --- tilemap attributes (VRAM bank 1): bank bit for pos>=256 ---
+ ld a, 1
+ ld [rVBK], a
+ ld de, 0
+ ld c, 0
+.ma_row
+ ld a, c
+ ld l, a
+ ld h, 0
+ add hl, hl
+ add hl, hl
+ add hl, hl
+ add hl, hl
+ add hl, hl
+ ld a, h
+ add $98
+ ld h, a
+ ld b, TCOLS/2
+.ma_col
+ ld a, d ; HIGH(pos): 0 for 0-255, 1 for 256+
+ or a
+ jr z, .ma_bank0
+ ld a, $08 ; VRAM bank 1 for this tile
+ jr .ma_put
+.ma_bank0
+ xor a
+.ma_put
+ ld [hl+], a
+ inc de
+ dec b
+ jr nz, .ma_col
+ inc c
+ ld a, c
+ cp TROWS
+ jr c, .ma_row
+ xor a
+ ld [rVBK], a
+
+ ; clear the text buffer 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
+
+ ret
+
+; term_show - rebuild all tiles then turn the LCD on.
+term_show::
+ call term_redraw
+ ld a, %10010001 ; LCD on, BG on, tile data $8000, map $9800
+ ld [rLCDC], a
+ ret
+
+; -----------------------------------------------------------------------------
+; build_tile - build the 8x8 tile for screen tile-position DE (E=index within
+; bank, D=bank 0/1) from characters B (left) and C (right).
+; -----------------------------------------------------------------------------
+build_tile:
+ ld a, d
+ ld [rVBK], a ; select VRAM bank for this tile
+ ; VRAM dest = $8000 + E*16
+ 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
+ ; glyphL = FontData + (clamp(B)-32)*8
+ 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
+ ; 8 rows: dest[2r] = (L[r]<<4)|R[r] ; dest[2r+1] = 0
+ ld b, 8 ; row counter
+ ld a, [wVram]
+ ld l, a
+ ld a, [wVram+1]
+ ld h, a ; HL = VRAM dest
+.brow
+ push hl ; save VRAM ptr
+ ; left glyph byte
+ ld a, [wGlyphL]
+ ld e, a
+ ld a, [wGlyphL+1]
+ ld d, a
+ ld a, [de]
+ swap a
+ and $F0
+ ld c, a ; left nibble -> high
+ ; right glyph byte
+ ld a, [wGlyphR]
+ ld e, a
+ ld a, [wGlyphR+1]
+ ld d, a
+ ld a, [de]
+ and $0F
+ or c ; A = combined row
+ pop hl
+ ld [hl+], a ; plane 0
+ xor a
+ ld [hl+], a ; plane 1 = 0
+ ; advance both glyph pointers
+ 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
+
+; 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
+
+; -----------------------------------------------------------------------------
+; term_redraw - rebuild every tile from the text buffer. (LCD should be off, or
+; call during VBlank for a full frame.)
+; -----------------------------------------------------------------------------
+term_redraw::
+ ld c, 0 ; row
+.row
+ push bc
+ ld a, c
+ call row_addr ; HL = &buffer[row][0]
+ ; iterate 20 tile columns
+ ld b, 0 ; tile column
+.col
+ push bc
+ push hl
+ ; DE = pos = row*20 + tcol : maintain via a running counter is simpler, but
+ ; recompute here: pos = C(row)*20 + B(tcol)
+ ld a, c
+ ld l, a
+ ld h, 0
+ add hl, hl
+ add hl, hl ; row*4
+ ld d, h
+ ld e, l
+ add hl, hl
+ add hl, hl ; row*16
+ add hl, de ; row*20
+ ld a, b
+ add l
+ ld e, a
+ ld a, h
+ adc 0
+ ld d, a ; DE = pos
+ pop hl ; &buffer[row][0]
+ push hl
+ ; charL = buffer[row][tcol*2], charR = [+1]
+ ld a, b
+ add a ; tcol*2
+ add l
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a
+ ld a, [hl+]
+ ld b, a ; charL
+ ld a, [hl]
+ ld c, a ; charR
+ push de
+ call build_tile ; DE=pos, B=charL, C=charR
+ pop de
+ pop hl
+ pop bc
+ inc b
+ ld a, b
+ cp TCOLS/2
+ jr c, .col
+ xor a
+ ld [rVBK], a
+ pop bc
+ inc c
+ ld a, c
+ cp TROWS
+ jr c, .row
+ ret
+
+; -----------------------------------------------------------------------------
+; term_puts - HL = NUL-terminated string, B = row, C = col : write into buffer.
+; -----------------------------------------------------------------------------
+term_puts::
+ push hl
+ ld a, b
+ call row_addr ; HL = &buffer[row]
+ ld a, c
+ add l
+ ld l, a
+ ld a, h
+ adc 0
+ ld h, a ; HL = &buffer[row][col]
+ ld d, h
+ ld e, l ; DE = dest
+ pop hl ; HL = string
+.l
+ ld a, [hl+]
+ or a
+ ret z
+ ld [de], a
+ inc de
+ jr .l
+
+; -----------------------------------------------------------------------------
+; term_test - fill the screen with a test pattern and show it.
+; -----------------------------------------------------------------------------
+term_test::
+ ld hl, .s0
+ ld bc, $0000
+ call term_puts
+ ld hl, .s1
+ ld bc, $0100
+ call term_puts
+ ld hl, .s2
+ ld bc, $0200
+ call term_puts
+ ld hl, .s3
+ ld bc, $0500
+ call term_puts
+ ld hl, .s4
+ ld bc, $0A00
+ call term_puts
+ ld hl, .s5
+ ld bc, $1100
+ call term_puts
+ call term_show
+ ret
+.s0 db "GBOS TERMINAL - 40 COLUMNS x 18 ROWS", 0
+.s1 db "ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789", 0
+.s2 db "the quick brown fox jumps over the dog.", 0
+.s3 db "0123456789012345678901234567890123456789", 0
+.s4 db "row 10: some text in the middle here...", 0
+.s5 db "LAST ROW 17 - exercises bank-1 tiles!!!", 0
diff --git a/tools/genfont.py b/tools/genfont.py
new file mode 100644
index 0000000..7fe7689
--- /dev/null
+++ b/tools/genfont.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python3
+# Generate a 4x8 font (3x5 glyphs, left-aligned in a 4px cell) as RGBDS asm.
+# Output: 96 glyphs (ASCII 32..127), 8 bytes each; each byte = one 4-px row,
+# bit3=leftmost pixel of the cell, bits 3..1 = the 3px glyph, bit0 = gap.
+G = {}
+def d(ch, rows): # rows: 5 strings of width 3 ('#'/' ')
+ G[ch] = rows
+# space
+d(' ', [" "," "," "," "," "])
+# letters (uppercase; lowercase mapped to these)
+d('A',["## ","# #","###","# #","# #"])
+d('B',["## ","# #","## ","# #","## "])
+d('C',[" ##","# ","# ","# "," ##"])
+d('D',["## ","# #","# #","# #","## "])
+d('E',["###","# ","## ","# ","###"])
+d('F',["###","# ","## ","# ","# "])
+d('G',[" ##","# ","# #","# #"," ##"])
+d('H',["# #","# #","###","# #","# #"])
+d('I',["###"," # "," # "," # ","###"])
+d('J',[" #"," #"," #","# #"," # "])
+d('K',["# #","# #","## ","# #","# #"])
+d('L',["# ","# ","# ","# ","###"])
+d('M',["# #","###","###","# #","# #"])
+d('N',["# #","###","###","###","# #"])
+d('O',[" # ","# #","# #","# #"," # "])
+d('P',["## ","# #","## ","# ","# "])
+d('Q',[" # ","# #","# #"," # "," #"])
+d('R',["## ","# #","## ","# #","# #"])
+d('S',[" ##","# "," # "," #","## "])
+d('T',["###"," # "," # "," # "," # "])
+d('U',["# #","# #","# #","# #"," ##"])
+d('V',["# #","# #","# #"," # "," # "])
+d('W',["# #","# #","###","###","# #"])
+d('X',["# #","# #"," # ","# #","# #"])
+d('Y',["# #","# #"," # "," # "," # "])
+d('Z',["###"," #"," # ","# ","###"])
+# digits
+d('0',[" # ","# #","# #","# #"," # "])
+d('1',[" # ","## "," # "," # ","###"])
+d('2',["## "," #"," # ","# ","###"])
+d('3',["## "," #"," # "," #","## "])
+d('4',["# #","# #","###"," #"," #"])
+d('5',["###","# ","## "," #","## "])
+d('6',[" ##","# ","## ","# #"," # "])
+d('7',["###"," #"," # "," # "," # "])
+d('8',[" # ","# #"," # ","# #"," # "])
+d('9',[" # ","# #"," ##"," #","## "])
+# punctuation
+d('.',[" "," "," "," "," # "])
+d(',',[" "," "," "," # ","# "])
+d(':',[" "," # "," "," # "," "])
+d(';',[" "," # "," "," # ","# "])
+d('!',[" # "," # "," # "," "," # "])
+d('?',["## "," #"," # "," "," # "])
+d('-',[" "," ","###"," "," "])
+d('+',[" "," # ","###"," # "," "])
+d('=',[" ","###"," ","###"," "])
+d('*',["# #"," # ","###"," # ","# #"])
+d('/',[" #"," #"," # ","# ","# "])
+d('\\',["# ","# "," # "," #"," #"])
+d('(',[" #"," # "," # "," # "," #"])
+d(')',["# "," # "," # "," # ","# "])
+d('[',["## ","# ","# ","# ","## "])
+d(']',[" ##"," #"," #"," #"," ##"])
+d('<',[" #"," # ","# "," # "," #"])
+d('>',["# "," # "," #"," # ","# "])
+d('#',["# #","###","# #","###","# #"])
+d('$',[" # "," ##","## "," ##","## "]) # rough
+d('%',["# #"," #"," # ","# ","# #"])
+d('&',[" # ","# #"," # ","# #"," ##"])
+d("'",[" # "," # "," "," "," "])
+d('"',["# #","# #"," "," "," "])
+d('@',[" # ","# #","###","# "," ##"])
+d('_',[" "," "," "," ","###"])
+d('~',[" "," ##","## "," "," "])
+d('^',[" # ","# #"," "," "," "])
+d('|',[" # "," # "," # "," # "," # "])
+d('{',[" #"," # ","## "," # "," #"])
+d('}',["# "," # "," ##"," # ","# "])
+d('`',["# "," # "," "," "," "])
+
+def glyph_bytes(rows):
+ out=[]
+ # 5 glyph rows in the top; cell is 4px wide, glyph 3px left-aligned (bit3..1)
+ for r in range(8):
+ if r < 5:
+ s = rows[r]
+ v = 0
+ for i,px in enumerate(s): # i=0 leftmost -> bit3
+ if px=='#': v |= (1 << (3-i))
+ out.append(v)
+ else:
+ out.append(0)
+ return out
+
+lines=["; auto-generated by tools/genfont.py - 4x8 font (3x5 glyphs), 96 glyphs",
+ "SECTION \"font\", ROM0", "FontData::"]
+for code in range(32,128):
+ ch = chr(code)
+ key = ch.upper() if ch.isalpha() else ch
+ rows = G.get(key, G.get(ch, ["###","# #","# #","# #","###"])) # box for unknown
+ b = glyph_bytes(rows)
+ lines.append(" db " + ",".join("$%02X"%x for x in b) + " ; '%s'"%(ch if ch!='\\' and code!=127 else '?'))
+open("/home/user/dev/gbos/src/font.asm","w").write("\n".join(lines)+"\n")
+print("wrote src/font.inc:", (128-32), "glyphs")
diff --git a/tools/ppmview.py b/tools/ppmview.py
new file mode 100644
index 0000000..1e54bd4
--- /dev/null
+++ b/tools/ppmview.py
@@ -0,0 +1,16 @@
+#!/usr/bin/env python3
+# Render a PPM screenshot as ASCII so we can "read" the GB LCD.
+import sys
+def load(path):
+ f=open(path,'rb'); assert f.readline().strip()==b'P6'
+ w,h=map(int,f.readline().split()); f.readline()
+ d=f.read(w*h*3); return w,h,d
+w,h,d=load(sys.argv[1] if len(sys.argv)>1 else 'shot.ppm')
+y0=int(sys.argv[2]) if len(sys.argv)>2 else 0
+y1=int(sys.argv[3]) if len(sys.argv)>3 else h
+for y in range(y0,min(y1,h)):
+ row=[]
+ for x in range(w):
+ i=(y*w+x)*3; b=(d[i]+d[i+1]+d[i+2])//3
+ row.append('#' if b<128 else '.')
+ print(''.join(row))