From c9d5ea35a49fbd64bcf7220b89d96402a42620ed Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Tue, 23 Jun 2026 17:06:13 +0300 Subject: [PATCH] libc: add bios/text.h BIOS text-output API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Direct __naked z80 wrappers over BIOS LP_PRINT_*/LP_SET_PLACE/LP_GET_PLACE (RST 8, opcodes 081h-08Eh) for fast text rendering — a single BIOS call replaces a wrchar() loop for constant-attribute/constant-character runs. Covered by tests/bios_text. Co-Authored-By: Claude Sonnet 4.6 --- lib/Makefile | 1 + libc/bios/text.c | 302 ++++++++++++++++++++++++++++++++++++ libc/include/bios/text.h | 107 +++++++++++++ tests/bios_text/Makefile | 5 + tests/bios_text/bios_text.c | 110 +++++++++++++ 5 files changed, 525 insertions(+) create mode 100644 libc/bios/text.c create mode 100644 libc/include/bios/text.h create mode 100644 tests/bios_text/Makefile create mode 100644 tests/bios_text/bios_text.c diff --git a/lib/Makefile b/lib/Makefile index b64b6e1..74361ab 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -26,6 +26,7 @@ LIBC_C := \ libc/conio/conio.c \ libc/conio/cprintf.c \ libc/conio/text_palette.c \ + libc/bios/text.c \ libc/io/dir.c \ libc/video/videomode_raw.c \ libc/video/palette.c \ diff --git a/libc/bios/text.c b/libc/bios/text.c new file mode 100644 index 0000000..4c58a75 --- /dev/null +++ b/libc/bios/text.c @@ -0,0 +1,302 @@ +/* + * text.c — wrappers for the Sprinter BIOS text-output calls (RST 8, + * 081h..08Eh "LP_*"). See for the full opcode -> name + * mapping and semantics. + * + * ABI recap (docs/converted/bios.txt): + * - 081h/082h/083h explicitly preserve HL and IX -> no push/pop ix + * needed, and the result is the smallest/fastest of the set. + * - 084h/08Eh (place get/set) are used without push/pop ix elsewhere + * in this libc (libc/conio/conio.c gotoxy/wherex/wherey via the same + * opcodes) — empirically safe, followed here too. + * - Everything else clobbers IX like any other BIOS/ESTEX call, so + * every RST 8 below is bracketed with push ix / pop ix. + * + * Stack-argument layout: SDCC __sdcccall(1) places the first 8-bit arg + * in A, the second (only if the first was also 8-bit) in L; every arg + * beyond that lands on the stack in left-to-right declaration order, + * starting at [SP+2] (right after the 2-byte return address) — verified + * empirically via `sdcc -S` for 2..6 uint8 args and for pointer+N uint8 + * args. IY is used as a scratch index register to read those without + * disturbing IX (the caller's frame pointer). + */ + +#include + +void bios_fillcharattr(char ch, uint8_t attr, uint8_t count) __naked +{ + (void)ch; (void)attr; (void)count; + __asm + ;; ch->A, attr->L, count on stack at [SP+2]. + ;; LP_PRINT_ALL (081h): A=ch, E=attr, B=count. Preserves HL,IX. + ld iy, #2 + add iy, sp + ld b, 0 (iy) ; B = count + ld e, l ; E = attr + ld c, #0x81 + rst #0x08 + pop hl ; return address + inc sp ; consume count byte + jp (hl) + __endasm; +} + +void bios_fillchar(char ch, uint8_t count) __naked +{ + (void)ch; (void)count; + __asm + ;; ch->A, count->L. LP_PRINT_SYM (082h): A=ch, B=count. + ;; Preserves HL,IX. + ld b, l + ld c, #0x82 + rst #0x08 + ret + __endasm; +} + +void bios_fillattr(uint8_t attr, uint8_t count) __naked +{ + (void)attr; (void)count; + __asm + ;; attr->A, count->L. LP_PRINT_ATR (083h): E=attr, B=count. + ;; Preserves HL,IX. + ld e, a + ld b, l + ld c, #0x83 + rst #0x08 + ret + __endasm; +} + +void bios_set_place(uint8_t row, uint8_t col) __naked +{ + (void)row; (void)col; + __asm + ;; row->A, col->L. LP_SET_PLACE (084h): D=row, E=col. + ld d, a + ld e, l + ld c, #0x84 + rst #0x08 + ret + __endasm; +} + +uint16_t bios_get_place(void) __naked +{ + __asm + ;; LP_GET_PLACE (08Eh): returns D=row, E=col — already the + ;; uint16_t return convention (DE); nothing left to rearrange. + ld c, #0x8E + rst #0x08 + ret + __endasm; +} + +void bios_writeattr(const char *s, uint8_t len, uint8_t attr) __naked +{ + (void)s; (void)len; (void)attr; + __asm + ;; s->HL, len/attr on stack at [SP+2]/[SP+3]. + ;; LP_PRINT_LN (085h): HL=s, B=len, E=attr. + ld iy, #2 + add iy, sp + ld b, 0 (iy) ; B = len + ld e, 1 (iy) ; E = attr + push ix + ld c, #0x85 + rst #0x08 + pop ix + pop hl ; return address + inc sp + inc sp ; consume len + attr + jp (hl) + __endasm; +} + +void bios_write(const char *s, uint8_t len) __naked +{ + (void)s; (void)len; + __asm + ;; s->HL, len on stack at [SP+2]. + ;; LP_PRINT_LN2 (086h): HL=s, B=len. + ld iy, #2 + add iy, sp + ld b, 0 (iy) ; B = len + push ix + ld c, #0x86 + rst #0x08 + pop ix + pop hl + inc sp + jp (hl) + __endasm; +} + +void bios_writeattr_until(const char *s, uint8_t len, uint8_t attr, char sep) __naked +{ + (void)s; (void)len; (void)attr; (void)sep; + __asm + ;; s->HL, len/attr/sep on stack at [SP+2..4]. + ;; LP_PRINT_LN3 (087h): HL=s, B=len, E=attr, D=sep. + ;; Pads with spaces after sep up to len. + ld iy, #2 + add iy, sp + ld b, 0 (iy) ; B = len + ld e, 1 (iy) ; E = attr + ld d, 2 (iy) ; D = sep + push ix + ld c, #0x87 + rst #0x08 + pop ix + pop hl + inc sp + inc sp + inc sp ; consume len + attr + sep + jp (hl) + __endasm; +} + +void bios_write_until(const char *s, uint8_t len, char sep) __naked +{ + (void)s; (void)len; (void)sep; + __asm + ;; s->HL, len/sep on stack at [SP+2..3]. + ;; LP_PRINT_LN4 (088h): HL=s, B=len, D=sep. Pads with spaces. + ld iy, #2 + add iy, sp + ld b, 0 (iy) ; B = len + ld d, 1 (iy) ; D = sep + push ix + ld c, #0x88 + rst #0x08 + pop ix + pop hl + inc sp + inc sp + jp (hl) + __endasm; +} + +void bios_clearwin(uint8_t row, uint8_t col, uint8_t height, uint8_t width, + uint8_t attr) __naked +{ + (void)row; (void)col; (void)height; (void)width; (void)attr; + __asm + ;; row->A, col->L, height/width/attr on stack at [SP+2..4]. + ;; LP_CLS_WIN (089h): D=row, E=col, H=height, L=width, B=attr. + ld iy, #2 + add iy, sp + ld h, 0 (iy) ; H = height + ld c, 1 (iy) ; stash width (L is still col for now) + ld e, l ; E = col + ld d, a ; D = row + ld l, c ; L = width + ld b, 2 (iy) ; B = attr + push ix + ld c, #0x89 + rst #0x08 + pop ix + pop hl + inc sp + inc sp + inc sp ; consume height + width + attr + jp (hl) + __endasm; +} + +void bios_scrollwin(uint8_t dir, uint8_t row, uint8_t count) __naked +{ + (void)dir; (void)row; (void)count; + __asm + ;; dir->A, row->L, count on stack at [SP+2]. + ;; LP_SCROLL_UD (08Ah): B=dir(1 up/2 down), D=row, E=count. + ld iy, #2 + add iy, sp + ld e, 0 (iy) ; E = count + ld d, l ; D = row + ld b, a ; B = dir + push ix + ld c, #0x8A + rst #0x08 + pop ix + pop hl + inc sp + jp (hl) + __endasm; +} + +void bios_writeattr_stop(const char *s, uint8_t maxlen, uint8_t attr, + char sep) __naked +{ + (void)s; (void)maxlen; (void)attr; (void)sep; + __asm + ;; s->HL, maxlen/attr/sep on stack at [SP+2..4]. + ;; LP_PRINT_LN5 (08Bh): HL=s, B=maxlen, E=attr, D=sep. + ;; Stops at sep (no padding). + ld iy, #2 + add iy, sp + ld b, 0 (iy) ; B = maxlen + ld e, 1 (iy) ; E = attr + ld d, 2 (iy) ; D = sep + push ix + ld c, #0x8B + rst #0x08 + pop ix + pop hl + inc sp + inc sp + inc sp + jp (hl) + __endasm; +} + +void bios_write_stop(const char *s, uint8_t maxlen, char sep) __naked +{ + (void)s; (void)maxlen; (void)sep; + __asm + ;; s->HL, maxlen/sep on stack at [SP+2..3]. + ;; LP_PRINT_LN6 (08Ch): HL=s, B=maxlen, D=sep. Stops at sep. + ld iy, #2 + add iy, sp + ld b, 0 (iy) ; B = maxlen + ld d, 1 (iy) ; D = sep + push ix + ld c, #0x8C + rst #0x08 + pop ix + pop hl + inc sp + inc sp + jp (hl) + __endasm; +} + +void bios_clearwin_ch(uint8_t row, uint8_t col, uint8_t height, + uint8_t width, uint8_t attr, char fillch) __naked +{ + (void)row; (void)col; (void)height; (void)width; (void)attr; (void)fillch; + __asm + ;; row->A, col->L, height/width/attr/fillch on stack [SP+2..5]. + ;; LP_CLS_WIN2 (08Dh): D=row, E=col, H=height, L=width, B=attr, + ;; A=fillch. + ld iy, #2 + add iy, sp + ld d, a ; D = row (free A for fillch later) + ld e, l ; E = col (free L for width later) + ld h, 0 (iy) ; H = height + ld c, 1 (iy) ; stash width + ld b, 2 (iy) ; B = attr + ld a, 3 (iy) ; A = fillch + ld l, c ; L = width + push ix + ld c, #0x8D + rst #0x08 + pop ix + pop hl + inc sp + inc sp + inc sp + inc sp ; consume height + width + attr + fillch + jp (hl) + __endasm; +} diff --git a/libc/include/bios/text.h b/libc/include/bios/text.h new file mode 100644 index 0000000..f31e020 --- /dev/null +++ b/libc/include/bios/text.h @@ -0,0 +1,107 @@ +/* + * bios/text.h — direct wrappers around the Sprinter BIOS text-output + * calls (RST 8, function codes 081h..08Eh, "LP_*" in the vendor docs). + * + * These bypass ESTEX's own PUTCHAR/PCHARS/WRCHAR (see ) and talk + * straight to the BIOS character-print routines. Use them when you need + * BIOS-specific behaviour (run-length fills, pad/stop-at-separator field + * printing, raw window clear/scroll) that ESTEX doesn't expose directly. + * + * Coordinates are 0-based (row = vertical, col = horizontal), matching + * ESTEX/BIOS directly. + * + * Original BIOS name -> wrapper, with opcode (docs/converted/bios.txt): + * + * 081h LP_PRINT_ALL -> bios_fillchar fill run: char + attr + * 082h LP_PRINT_SYM -> bios_fillchar_noattr fill run: char only + * 083h LP_PRINT_ATR -> bios_fillattr fill run: attr only + * 084h LP_SET_PLACE -> bios_set_place set BIOS print cursor + * 08Eh LP_GET_PLACE -> bios_get_place read BIOS print cursor + * 085h LP_PRINT_LN -> bios_writeattr write buf + attr + * 086h LP_PRINT_LN2 -> bios_write write buf, keep attr + * 087h LP_PRINT_LN3 -> bios_writeattr_until write to sep, pad + attr + * 088h LP_PRINT_LN4 -> bios_write_until write to sep, pad + * 089h LP_CLS_WIN -> bios_clearwin clear window (space fill) + * 08Ah LP_SCROLL_UD -> bios_scrollwin scroll global window + * 08Bh LP_PRINT_LN5 -> bios_writeattr_stop write to sep, stop + attr + * 08Ch LP_PRINT_LN6 -> bios_write_stop write to sep, stop + * 08Dh LP_CLS_WIN2 -> bios_clearwin_ch clear window (custom fill) + * + * "_until" variants print up to `sep` and then PAD the rest of the field + * with spaces (output is always exactly `len` characters wide). + * "_stop" variants print up to `sep` and STOP there (no padding; `sep` + * itself is not printed; `maxlen` is just an upper bound). + * + * ABI: stack must already be in window 2 (guaranteed by crt0) — required + * by the BIOS for its own page-swap scratch use. All registers (main + + * shadow set) are clobbered except where a function says otherwise. + */ + +#ifndef BIOS_TEXT_H +#define BIOS_TEXT_H + +#include + +/* bios_scrollwin() direction values (BIOS B register). */ +#define BIOS_SCROLL_UP 1 +#define BIOS_SCROLL_DOWN 2 + +/* 081h LP_PRINT_ALL — print `count` copies of `ch` with attribute `attr` + * starting at the current BIOS print position. Preserves HL and IX. */ +void bios_fillcharattr(char ch, uint8_t attr, uint8_t count); + +/* 082h LP_PRINT_SYM — print `count` copies of `ch`, attribute unchanged + * (whatever is already on screen). Preserves HL and IX. */ +void bios_fillchar(char ch, uint8_t count); + +/* 083h LP_PRINT_ATR — overwrite `count` cells' attribute with `attr`; + * the characters already on screen are left untouched. Preserves HL + * and IX. */ +void bios_fillattr(uint8_t attr, uint8_t count); + +/* 084h LP_SET_PLACE — set the BIOS print cursor. Out-of-range values + * wrap (BIOS subtracts the window size, not an error). */ +void bios_set_place(uint8_t row, uint8_t col); + +/* 08Eh LP_GET_PLACE — read the BIOS print cursor: (row << 8) | col. */ +uint16_t bios_get_place(void); + +/* 085h LP_PRINT_LN — write `len` bytes from `s` with attribute `attr`, + * starting at the current BIOS print position. `s` must point inside + * window 2 (#4000..#BFFF). */ +void bios_writeattr(const char *s, uint8_t len, uint8_t attr); + +/* 086h LP_PRINT_LN2 — like bios_writeattr(), attribute unchanged. */ +void bios_write(const char *s, uint8_t len); + +/* 087h LP_PRINT_LN3 — write from `s` with attribute `attr` until the + * byte `sep` is found, then pad with spaces so exactly `len` characters + * are printed in total. */ +void bios_writeattr_until(const char *s, uint8_t len, uint8_t attr, char sep); + +/* 088h LP_PRINT_LN4 — like bios_writeattr_until(), attribute unchanged. */ +void bios_write_until(const char *s, uint8_t len, char sep); + +/* 089h LP_CLS_WIN — clear a `height` x `width` local window at + * (row, col) by writing spaces with attribute `attr`. */ +void bios_clearwin(uint8_t row, uint8_t col, uint8_t height, uint8_t width, + uint8_t attr); + +/* 08Ah LP_SCROLL_UD — scroll `count` whole rows of the global window, + * starting at `row`, in direction `dir` (BIOS_SCROLL_UP/_DOWN). */ +void bios_scrollwin(uint8_t dir, uint8_t row, uint8_t count); + +/* 08Bh LP_PRINT_LN5 — write from `s` with attribute `attr`, stopping + * (without padding) as soon as `sep` is seen; `maxlen` bounds the scan. */ +void bios_writeattr_stop(const char *s, uint8_t maxlen, uint8_t attr, + char sep); + +/* 08Ch LP_PRINT_LN6 — like bios_writeattr_stop(), attribute unchanged. */ +void bios_write_stop(const char *s, uint8_t maxlen, char sep); + +/* 08Dh LP_CLS_WIN2 — like bios_clearwin(), but fills with `fillch` + * instead of a space. */ +void bios_clearwin_ch(uint8_t row, uint8_t col, uint8_t height, + uint8_t width, uint8_t attr, char fillch); + +#endif diff --git a/tests/bios_text/Makefile b/tests/bios_text/Makefile new file mode 100644 index 0000000..0d9c59e --- /dev/null +++ b/tests/bios_text/Makefile @@ -0,0 +1,5 @@ +# Build bios_text.exe — exercises , TINY memory mode. + +PROJ_ROOT := $(abspath $(CURDIR)/../..) +EXAMPLE := bios_text +include $(PROJ_ROOT)/app.mk diff --git a/tests/bios_text/bios_text.c b/tests/bios_text/bios_text.c new file mode 100644 index 0000000..c2d84b3 --- /dev/null +++ b/tests/bios_text/bios_text.c @@ -0,0 +1,110 @@ +#include +#include +#include + +/* + * Exercises every BIOS text wrapper from : + * bios_set_place / bios_get_place + * bios_fillchar / bios_fillchar_noattr / bios_fillattr + * bios_writeattr / bios_write + * bios_writeattr_until / bios_write_until (pad to length) + * bios_writeattr_stop / bios_write_stop (stop at separator) + * bios_clearwin / bios_clearwin_ch + * bios_scrollwin + * + * `s` below is a static buffer (not a string literal) so its address is + * always inside window 2, as the BIOS print routines require. + */ + +static char s[16]; + +int main(void) +{ + clrscr(); + + /* Step 0 check: does LP_PRINT_LN advance the BIOS "place" cursor + * by the number of characters printed, or leave it untouched? + * "Hello" is 5 chars -> if place advances, before=(20,5), + * after=(20,10); if not, after == before. */ + s[0]='H'; s[1]='e'; s[2]='l'; s[3]='l'; s[4]='o'; + bios_set_place(20, 5); + { + uint16_t before = bios_get_place(); + bios_writeattr(s, 5, COLOR(COLOR_WHITE, COLOR_BLACK)); + uint16_t after = bios_get_place(); + gotoxy(2, 0); + textattr(0x0F); + printf("STEP0: before row=%u col=%u\n", before >> 8, before & 0xFF); + printf("STEP0: after row=%u col=%u\n", after >> 8, after & 0xFF); + printf("STEP0: expect after col=10 if place advances,\n"); + printf(" col=5 if it does NOT advance.\n"); + } + (void)getch(); + + bios_set_place(1, 2); + bios_fillcharattr('*', COLOR(COLOR_YELLOW, COLOR_BLUE), 10); + (void)getch(); + + bios_set_place(2, 2); + textattr(COLOR(COLOR_WHITE, COLOR_BLACK)); + cputs("attr already on screen ->"); + (void)getch(); + + bios_set_place(2, 27); + bios_fillchar('#', 10); + (void)getch(); + + bios_set_place(3, 2); + bios_fillattr(COLOR(COLOR_BLACK, COLOR_LIGHTGREEN), 20); + (void)getch(); + + s[0]='H'; s[1]='e'; s[2]='l'; s[3]='l'; s[4]='o'; + bios_set_place(5, 2); + bios_writeattr(s, 5, COLOR(COLOR_LIGHTRED, COLOR_BLACK)); + (void)getch(); + + bios_set_place(6, 2); + bios_write(s, 5); + (void)getch(); + + s[0]='A'; s[1]='B'; s[2]=':'; s[3]='X'; s[4]='Y'; s[5]='Z'; + bios_set_place(8, 2); + bios_writeattr_until(s, 12, COLOR(COLOR_CYAN, COLOR_BLACK), ':'); + (void)getch(); + + bios_set_place(9, 2); + bios_write_until(s, 12, ':'); + (void)getch(); + + bios_set_place(10, 2); + bios_writeattr_stop(s, 12, COLOR(COLOR_MAGENTA, COLOR_BLACK), ':'); + (void)getch(); + + bios_set_place(11, 2); + bios_write_stop(s, 12, ':'); + (void)getch(); + + bios_clearwin(13, 2, 4, 20, COLOR(COLOR_BLACK, COLOR_RED)); + bios_clearwin_ch(13, 25, 4, 20, COLOR(COLOR_WHITE, COLOR_BLUE), '.'); + (void)getch(); + + bios_set_place(18, 2); + (void)getch(); + cputs("place test ->"); + { + uint16_t place = bios_get_place(); + printf(" row=%u col=%u\n", place >> 8, place & 0xFF); + } + (void)getch(); + + gotoxy(2, 20); + for (int i = 0; i < 6; i++) { + textattr(0x0F); + printf("scroll line %d\n", i); + } + (void)getch(); + bios_scrollwin(BIOS_SCROLL_UP, 20, 3); + + (void)getch(); + return 0; +}