From 49a87b4d02db821d2dce0ce112932642c4dfcba2 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Tue, 23 Jun 2026 17:07:55 +0300 Subject: [PATCH] libc: add Sprinter palette API (graphics + text planes) palette.h/video/palette.c wrap BIOS $A4 PIC_SET_PAL/PIC_GET_PAL and $A6 SET_PAL_INIT for the 8 palette pages (0-3 graphics, 4-7 text planes). gfx_palette.c and conio/text_palette.c are thin per-domain wrappers; text_palette.c maps the text "plane" 0..3 (paper/ink/blink-paper/ blink-ink) onto BIOS pages 4..7. Covered by tests/text_palette. Co-Authored-By: Claude Sonnet 4.6 --- libc/conio/text_palette.c | 42 ++++++++ libc/gfx/gfx_palette.c | 74 ++++++++++++++ libc/include/palette.h | 89 +++++++++++++++++ libc/video/palette.c | 131 ++++++++++++++++++++++++ tests/text_palette/Makefile | 5 + tests/text_palette/text_palette.c | 160 ++++++++++++++++++++++++++++++ 6 files changed, 501 insertions(+) create mode 100644 libc/conio/text_palette.c create mode 100644 libc/gfx/gfx_palette.c create mode 100644 libc/include/palette.h create mode 100644 libc/video/palette.c create mode 100644 tests/text_palette/Makefile create mode 100644 tests/text_palette/text_palette.c diff --git a/libc/conio/text_palette.c b/libc/conio/text_palette.c new file mode 100644 index 0000000..1582efd --- /dev/null +++ b/libc/conio/text_palette.c @@ -0,0 +1,42 @@ +/* + * text_palette.c — text-mode palette wrappers. + * + * Thin layer over libc/video/palette.c. Translates the text "plane + * number" 0..3 (paper/ink/blink-paper/blink-ink) into the underlying + * BIOS palette page 4..7 used by $A4. + * + * For an introduction to the four-plane text colour model see + * (top-of-file doc-block). + */ + +#include +#include +#include + +void text_pal_load(uint8_t plane, uint8_t start, uint8_t count, + const uint8_t *data) +{ + pal_load((uint8_t)(plane + 4u), start, count, data); +} + +void text_pal_set_color(uint8_t plane, uint8_t attr, + uint8_t r, uint8_t g, uint8_t b) +{ + pal_set_color((uint8_t)(plane + 4u), attr, r, g, b); +} + +void text_pal_get(uint8_t plane, uint8_t start, uint8_t count, uint8_t *data) +{ + pal_get((uint8_t)(plane + 4u), start, count, data); +} + +void text_pal_get_color(uint8_t plane, uint8_t attr, + uint8_t *r, uint8_t *g, uint8_t *b) +{ + pal_get_color((uint8_t)(plane + 4u), attr, r, g, b); +} + +void text_pal_reset(void) +{ + pal_reset(PAL_CGA); +} diff --git a/libc/gfx/gfx_palette.c b/libc/gfx/gfx_palette.c new file mode 100644 index 0000000..2a5ca79 --- /dev/null +++ b/libc/gfx/gfx_palette.c @@ -0,0 +1,74 @@ +/* + * gfx_palette.c — graphics-side palette wrappers. + * + * The "legacy" entry points (gfx_pal_load / gfx_pal_set) keep their own + * inline-asm implementation rather than thunking through pal_*; this + * avoids depending on SDCC's __sdcccall(1) stack-shuffling for nested + * calls, which empirically misbehaves here (the trampoline corrupts the + * text palette so the next text-mode print hangs). Functionally these + * are identical to the old gfx_core.c versions. + * + * The newer get / reset helpers are simple wrappers — they're either + * not on hot paths or are first introduced here, so the thunk overhead + * doesn't matter and we get to share the asm with libc/video/palette.c. + */ + +#include +#include +#include + +static uint8_t gpl_num_; +static uint8_t gpl_start_; +static uint8_t gpl_count_; +static uint16_t gpl_data_; + +void gfx_pal_load(uint8_t pal_num, uint8_t start, uint8_t count, + const uint8_t *data) +{ + gpl_num_ = pal_num; + gpl_start_ = start; + gpl_count_ = count; + gpl_data_ = (uint16_t)(uintptr_t)data; + + __asm + push ix + ld a, (_gpl_start_) + ld e, a ; E = start + ld a, (_gpl_count_) + ld d, a ; D = count (0 → 256) + ld hl, (_gpl_data_) ; HL = data + ld b, #0xFF ; mask + ld a, (_gpl_num_) ; A = palette number + ld c, #0xA4 ; BIOS PIC_SET_PAL + rst #0x08 + pop ix + __endasm; +} + +void gfx_pal_set(uint8_t pal_num, uint8_t idx, + uint8_t r, uint8_t g, uint8_t b) +{ + uint8_t entry[4]; + entry[0] = b; + entry[1] = g; + entry[2] = r; + entry[3] = 0; + gfx_pal_load(pal_num, idx, 1, entry); +} + +void gfx_pal_get(uint8_t pal_num, uint8_t start, uint8_t count, + uint8_t *data) +{ + pal_get(pal_num, start, count, data); +} + +void gfx_pal_get_color(uint8_t pal_num, uint8_t idx, + uint8_t *r, uint8_t *g, uint8_t *b) +{ + pal_get_color(pal_num, idx, r, g, b); +} + +void gfx_pal_reset(void) +{ + pal_reset(PAL_GRAPH); +} diff --git a/libc/include/palette.h b/libc/include/palette.h new file mode 100644 index 0000000..157895e --- /dev/null +++ b/libc/include/palette.h @@ -0,0 +1,89 @@ +/* + * palette.h — Sprinter palette (low-level). + * + * The Sprinter has eight 256-colour palette pages, shared by graphics and + * text modes: + * + * pal_num 0..3 — graphics palettes. Each character cell selects which + * of the four it uses through bits 7..6 of its mode byte. + * Used by graphics modes 0x81 / 0x82. + * pal_num 4..7 — text-mode planes — together they form the colour table + * of the text-mode attribute byte: + * 4 = paper (background, non-blink phase) + * 5 = ink (foreground, non-blink phase) + * 6 = paper-blink (background during blink half-cycle) + * 7 = ink-blink (foreground during blink half-cycle) + * Each plane holds 256 BGR colours indexed directly by + * the 8-bit attribute byte of the cell. + * + * Entry format — 4 bytes per colour, in Blue-Green-Red-pad order; the pad + * byte is reserved and must be 0. + * + * Notes on the underlying BIOS ($A4): + * - `count = 0` means 256 entries (full plane). + * - On write, data is AND-masked against `pal_mask` before reaching + * VRAM. pal_load/pal_set_color hard-code mask = 0xFF (no masking). + * + * Blink semantics (text planes): + * The hardware constantly alternates between planes 4↔6 (paper) and + * 5↔7 (ink). To DISABLE blink everywhere in IBM-CGA style, copy plane + * 4 → 6 and 5 → 7 (so both phases show the same colour). To ENABLE + * ZX-Spectrum style flash, swap entries 6 ↔ 7 for the desired attrs. + * The system default (PAL_CGA) leaves flash visible only for attribute + * bytes with bit 7 set. + * + * Backed by BIOS PIC_SET_PAL/PIC_GET_PAL ($A4) and SET_PAL_INIT ($A6). + * Higher-level wrappers live in (text_pal_*) and + * (gfx_pal_*); use those directly unless you need raw plane control. + */ + +#ifndef PALETTE_H +#define PALETTE_H + +#include + +/* Default-palette types for pal_reset(). Values match BIOS $A6 B-register. */ +#define PAL_GRAPH 1 /* graphics palette (planes 0..3) */ +#define PAL_SINCLAIR 2 /* Spectrum palette (text planes 4..7, ZX colours) */ +#define PAL_CGA 3 /* CGA text palette (text planes 4..7, IBM CGA) */ + +/* Load a contiguous block of palette entries. + * pal_num: 0..7 (0..3 graphics, 4..7 text) + * start: first slot (0..255) + * count: number of slots (0 means 256) + * bgr0: pointer to count entries of 4 bytes each: Blue, Green, Red, 0 */ +void pal_load(uint8_t pal_num, uint8_t start, uint8_t count, + const uint8_t *bgr0); + +/* Read a contiguous block of palette entries back into RAM. + * Same parameter shape as pal_load — bgr0 here is a write buffer of + * count*4 bytes that receives B,G,R,0 quadruples. */ +void pal_get (uint8_t pal_num, uint8_t start, uint8_t count, + uint8_t *bgr0); + +/* Convenience: set one entry from an RGB triple. */ +void pal_set_color(uint8_t pal_num, uint8_t slot, + uint8_t r, uint8_t g, uint8_t b); + +/* Convenience: read one entry into R,G,B pointers (any may be NULL). */ +void pal_get_color(uint8_t pal_num, uint8_t slot, + uint8_t *r, uint8_t *g, uint8_t *b); + +/* Restore a built-in default palette (BIOS $A6 SET_PAL_INIT). + * type: PAL_GRAPH / PAL_SINCLAIR / PAL_CGA. + * Internally: + * PAL_GRAPH → A=0, E=0, B=1 (resets graphics palette 0) + * PAL_SINCLAIR → A=0, E=0, B=2 + * PAL_CGA → A=0, E=0, B=3 (resets all text planes 4..7) + * + * Use pal_reset_at() if you need a non-zero page or a non-zero graphics + * palette index. */ +void pal_reset(uint8_t type); + +/* Full-control variant of pal_reset. + * pal_page: BIOS A register — "palette page" hardware index. + * graph_pal: BIOS E register — for PAL_GRAPH, target palette 0..3. + * type: BIOS B register — PAL_GRAPH / PAL_SINCLAIR / PAL_CGA. */ +void pal_reset_at(uint8_t type, uint8_t pal_page, uint8_t graph_pal); + +#endif diff --git a/libc/video/palette.c b/libc/video/palette.c new file mode 100644 index 0000000..23116d1 --- /dev/null +++ b/libc/video/palette.c @@ -0,0 +1,131 @@ +/* + * palette.c — Sprinter palette (BIOS $A4 PIC_SET_PAL / PIC_GET_PAL, + * BIOS $A6 SET_PAL_INIT). + * + * Direction in $A4 is selected by bit 7 of A: + * A = pal_num → write entries from RAM to VRAM + * A = 0x80 | pal_num → read entries from VRAM into RAM + * + * Lives under libc/video/ because palette control is shared between + * graphics (mode 0x81/0x82) and text (mode 0x03) — pal_num 0..3 for + * graphics, 4..7 for the four text-mode planes. The conio and gfx + * subsystems each ship thin domain-specific wrappers on top. + */ + +#include +#include + +/* Statics used to pass parameters to the inline asm — SDCC __sdcccall(1) + * gives us only HL natively, so the rest go through memory. */ +static uint8_t pal_num_; +static uint8_t pal_start_; +static uint8_t pal_count_; +static uint16_t pal_data_; + +/* ---- $A4 PIC_SET_PAL — write entries to VRAM --------------------- */ + +void pal_load(uint8_t pal_num, uint8_t start, uint8_t count, + const uint8_t *bgr0) +{ + pal_num_ = pal_num; + pal_start_ = start; + pal_count_ = count; + pal_data_ = (uint16_t)(uintptr_t)bgr0; + + __asm + push ix + ld a, (_pal_start_) + ld e, a ; E = start + ld a, (_pal_count_) + ld d, a ; D = count (0 → 256) + ld hl, (_pal_data_) ; HL = data + ld b, #0xFF ; B = mask (no AND) + ld a, (_pal_num_) ; A = palette number (bit7 = 0 → write) + ld c, #0xA4 ; BIOS PIC_SET_PAL + rst #0x08 + pop ix + __endasm; +} + +/* ---- $A4 PIC_GET_PAL — read entries from VRAM -------------------- * + * Same function id; bit 7 of A flips it to read mode. */ + +void pal_get(uint8_t pal_num, uint8_t start, uint8_t count, uint8_t *bgr0) +{ + pal_num_ = (uint8_t)(pal_num | 0x80); /* bit 7 = read */ + pal_start_ = start; + pal_count_ = count; + pal_data_ = (uint16_t)(uintptr_t)bgr0; + + __asm + push ix + ld a, (_pal_start_) + ld e, a ; E = start + ld a, (_pal_count_) + ld d, a ; D = count (0 → 256) + ld hl, (_pal_data_) ; HL = buffer + ld b, #0xFF ; B = mask + ld a, (_pal_num_) ; A = 0x80 | pal_num → read + ld c, #0xA4 ; BIOS PIC_GET_PAL (= $A4) + rst #0x08 + pop ix + __endasm; +} + +/* ---- one-colour helpers ------------------------------------------ */ + +void pal_set_color(uint8_t pal_num, uint8_t slot, + uint8_t r, uint8_t g, uint8_t b) +{ + uint8_t entry[4]; + entry[0] = b; + entry[1] = g; + entry[2] = r; + entry[3] = 0; + pal_load(pal_num, slot, 1, entry); +} + +void pal_get_color(uint8_t pal_num, uint8_t slot, + uint8_t *r, uint8_t *g, uint8_t *b) +{ + uint8_t entry[4]; + pal_get(pal_num, slot, 1, entry); + if (b) *b = entry[0]; + if (g) *g = entry[1]; + if (r) *r = entry[2]; +} + +/* ---- $A6 SET_PAL_INIT — restore a built-in default palette ------- * + * Signature: A = pal_page, E = graphics palette index (0..3), B = type. + * Type is one of PAL_GRAPH (1) / PAL_SINCLAIR (2) / PAL_CGA (3). */ + +static uint8_t reset_page_; +static uint8_t reset_graph_; +static uint8_t reset_type_; + +void pal_reset_at(uint8_t type, uint8_t pal_page, uint8_t graph_pal) +{ + reset_type_ = type; + reset_page_ = pal_page; + reset_graph_ = graph_pal; + + __asm + push ix + ld a, (_reset_graph_) + ld e, a ; E = graphics palette index (0..3) + ld a, (_reset_type_) + ld b, a ; B = type (1=GRAPH, 2=SINCLAIR, 3=CGA) + ld a, (_reset_page_) ; A = palette page (last — A is needed) + ld c, #0xA6 ; BIOS SET_PAL_INIT + rst #0x08 + pop ix + __endasm; +} + +void pal_reset(uint8_t type) +{ + pal_reset_at(type, 4, 0); + pal_reset_at(type, 5, 0); + pal_reset_at(type, 6, 0); + pal_reset_at(type, 7, 0); +} diff --git a/tests/text_palette/Makefile b/tests/text_palette/Makefile new file mode 100644 index 0000000..936647c --- /dev/null +++ b/tests/text_palette/Makefile @@ -0,0 +1,5 @@ +# Build text_palette.exe — uses lib/sprinter.lib in TINY memory mode. + +PROJ_ROOT := $(abspath $(CURDIR)/../..) +EXAMPLE := text_palette +include $(PROJ_ROOT)/app.mk diff --git a/tests/text_palette/text_palette.c b/tests/text_palette/text_palette.c new file mode 100644 index 0000000..a290131 --- /dev/null +++ b/tests/text_palette/text_palette.c @@ -0,0 +1,160 @@ +/* + * text_palette — interactive smoke test for the text-mode palette API. + * + * Walks through four palette states. Each stage clears the screen first + * and then renders 16 lines, one per ink-colour index, each line showing: + * + * + * < part 1: fg = N on bg = 0 — colour N text on the default black > + * < part 2: fg = 15 on bg = N — bright-white text on a coloured bg > + * + * Stages: + * 1) Initial CGA palette (boot default). + * 2) Grayscale ramp installed via text_pal_load() in all four text + * planes (4..7) so flash is fully suppressed; both ink (low nibble) + * and paper (mid nibble) get 16 shades — bit 7 of the attribute + * byte is folded into the paper index so we cover all 16 bg slots. + * 3) CGA defaults restored via pal_reset_at(PAL_CGA, plane, 0) for + * plane = 4..7 — explicit per-plane reset in case BIOS interprets + * the A register narrowly. + * 4) Sinclair palette via pal_reset_at(PAL_SINCLAIR, plane, 0) for + * plane = 4..7. + * + * Memory: tiny (one bank). The 1 KB palette buffer for text_pal_load() + * is static — keeps it out of the (small) stack frame. + */ + +#include +#include +#include +#include + +/* 256 entries × 4 bytes (B, G, R, 0). Static so it lives in DATA. */ +static uint8_t pal_buf[256 * 4]; + +#define ATTR_DEFAULT COLOR(COLOR_LIGHTGRAY, COLOR_BLACK) + +/* For each colour index N=0..15 print: + * - RGB triple read back from the INK plane + * - sample text in fg=N on bg=0 + * - sample text in fg=15 on bg=N + * + * The bg=N attribute uses ALL FOUR bits of the mid nibble (including the + * blink bit). In the CGA default this means N >= 8 will flash; with our + * grayscale loader the four planes are identical, so it just selects one + * of 16 paper colours. */ +static void show_palette_state(void) +{ + for (uint8_t n = 0; n < 16; n++) { + uint8_t r = 0, g = 0, b = 0; + text_pal_get_color(TEXT_PAL_INK, n, &r, &g, &b); + + textattr(ATTR_DEFAULT); + cprintf(" #%X R=%3u G=%3u B=%3u ", n, r, g, b); + + /* part 1: fg=N on bg=0 */ + textattr((uint8_t)(n & 0x0F)); + cprintf(" fg=%X on bg=0 ", n); + + /* part 2: fg=15 on bg=N (4-bit bg slot, may carry blink bit) */ + textattr((uint8_t)(((n & 0x0F) << 4) | 0x0F)); + cprintf(" white on bg=%X ", n); + + textattr(ATTR_DEFAULT); + cputs("\r\n"); + } +} + +/* Build a 16-step grayscale ramp and load it into ALL FOUR text planes + * (paper, ink, blink-paper, blink-ink). Identical paper/blink-paper and + * ink/blink-ink means there is nothing to alternate between → no flash. + * + * INK / BLINK_INK index by attr's low nibble: 16 fg grey levels + * PAPER / BLINK_PAPER index by attr's mid nibble: 16 bg grey levels + * (full 4 bits incl. bit 7) */ +static void load_grayscale_all_planes(void) +{ + /* INK and BLINK_INK: shade taken from attr & 0x0F. */ + for (uint16_t a = 0; a < 256; a++) { + uint8_t v = (uint8_t)((a & 0x0F) * 17u); /* 0,17,34,...,255 */ + pal_buf[a * 4 + 0] = v; /* B */ + pal_buf[a * 4 + 1] = v; /* G */ + pal_buf[a * 4 + 2] = v; /* R */ + pal_buf[a * 4 + 3] = 0; + } + text_pal_load(TEXT_PAL_INK, 0, 0, pal_buf); + text_pal_load(TEXT_PAL_BLINK_INK, 0, 0, pal_buf); + + /* PAPER and BLINK_PAPER: shade taken from (attr >> 4) & 0x0F. */ + for (uint16_t a = 0; a < 256; a++) { + uint8_t v = (uint8_t)(((a >> 4) & 0x0F) * 17u); + pal_buf[a * 4 + 0] = v; + pal_buf[a * 4 + 1] = v; + pal_buf[a * 4 + 2] = v; + pal_buf[a * 4 + 3] = 0; + } + text_pal_load(TEXT_PAL_PAPER, 0, 0, pal_buf); + text_pal_load(TEXT_PAL_BLINK_PAPER, 0, 0, pal_buf); +} + +/* Reset all four text-mode planes (BIOS palette pages 4..7) to a built-in + * default. We loop over the pages explicitly because the BIOS doc isn't + * crisp about whether one SET_PAL_INIT call covers all four pages or + * just the one named in A. Looping is always safe. */ +static void reset_all_text_planes(uint8_t type) +{ + for (uint8_t p = 4; p <= 7; p++) { + pal_reset_at(type, p, 0); + } +} + +static void wait_key(const char *prompt) +{ + cputs(prompt); + (void)getch(); +} + +int main(void) +{ + uint8_t prev_mode = get_videotextmode(); + set_videotextmode(TEXT_MODE_80x32); + textattr(ATTR_DEFAULT); + + /* ----- (1) Initial CGA palette ---------------------------------- */ + clrscr(); + cputs("[1] Initial CGA palette (boot default)\r\n"); + cputs("---------------------------------------\r\n"); + show_palette_state(); + wait_key("\r\nPress any key to install grayscale..."); + + /* ----- (2) Grayscale ramp in all four planes -------------------- */ + load_grayscale_all_planes(); + clrscr(); + cputs("[2] Grayscale ramp via text_pal_load() (planes 4..7)\r\n"); + cputs("-----------------------------------------------------\r\n"); + show_palette_state(); + wait_key("\r\nPress any key to restore CGA..."); + + /* ----- (3) Restore CGA defaults --------------------------------- */ + reset_all_text_planes(PAL_CGA); + clrscr(); + cputs("[3] CGA palette restored (planes 4..7)\r\n"); + cputs("---------------------------------------\r\n"); + show_palette_state(); + wait_key("\r\nPress any key to install Sinclair..."); + + /* ----- (4) Sinclair palette ------------------------------------- */ + reset_all_text_planes(PAL_SINCLAIR); + clrscr(); + cputs("[4] Sinclair/Spectrum palette (planes 4..7)\r\n"); + cputs("--------------------------------------------\r\n"); + show_palette_state(); + wait_key("\r\nPress any key to exit..."); + + /* Leave the system in a sane state. */ + reset_all_text_planes(PAL_CGA); + textattr(ATTR_DEFAULT); + clrscr(); + set_videotextmode(prev_mode); + return 0; +}