Add full compiler toolchain, libc, examples and reference docs

First substantive commit: the entire Sprinter C compiler tree on top of
the bare README+gitignore initial commit.

What's in here:
  bin/sprinter-cc        — driver script invoking SDCC + linker + mkexe
  libc/                  — Sprinter-specific libc layer over ESTEX/BIOS
                           (conio, gfx, io, mem, stdio + headers)
  runtime/               — crt0 variants (default/small/banked/minimal)
                           + heap + bank trampolines
  toolchain/             — mkexe (SprintEXE packer, C + tests)
  examples/              — 30 demo programs (gfx, file I/O, env, time, …)
  lib/Makefile           — builds the libc archive (sprinter.lib)
  docs/                  — converted Sprinter manuals + asm reference samples
  third_party/           — solid-c reference compiler dump + sdcc setup script
  release_docs/          — packaging / release notes

gitignore overhaul:
  • Drop dangerous blanket patterns: *.asm (would hide docs/samples/*.asm)
    and *.exe (case-insensitive match was hiding third_party/solid-c/*.EXE
    on macOS APFS).  Replaced with examples/*/*.{asm,exe,…} and lib/*.lib.
  • Restore tracking of toolchain/mkexe/tests/{one,big}.bin — those are
    INPUT fixtures, not build outputs.
  • Collapse the duplicated SDCC/C/Sdcc sections into one section per
    concern (build outputs / vendored / OS-junk).
  • Add .sprinter-cc-*/, build/ (catches lib/build/ too), .claude/.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 16:13:21 +03:00
parent f542608b3f
commit c71e249a4e
404 changed files with 75155 additions and 58 deletions
+106
View File
@@ -0,0 +1,106 @@
/*
* gfx_16.c — 16-colour (mode 0x82) public drawing API.
*
* Raw primitives live in gfx_raw_16.c. Single-shot wrappers do one
* W3-begin / one W3-end around their raw call; composites wrap many
* raw calls in a single begin/end pair so the W3 dance is amortised.
*
* No vertical-accelerator path: in 16-colour mode each byte spans two
* horizontal pixels, so a vertical Fill burst would also affect the
* other column's nibble — vline16 falls back to per-row RMW.
*/
#include <gfx.h>
#include <stdint.h>
extern void _gfx_w3_video_begin(void);
extern void _gfx_w3_video_end(void);
extern void _gfx_putpixel16_raw(int x, int y, uint8_t color);
extern void _gfx_hline16_raw (int x, int y, int len, uint8_t color);
extern void _gfx_vline16_raw (int x, int y, int len, uint8_t color);
extern void _gfx_clear16_raw (uint8_t color);
void gfx_clear16(uint8_t color)
{
_gfx_w3_video_begin();
_gfx_clear16_raw(color);
_gfx_w3_video_end();
}
void gfx_putpixel16(int x, int y, uint8_t color)
{
if ((unsigned)x >= GFX_WIDTH_16 || (unsigned)y >= GFX_HEIGHT_16) return;
_gfx_w3_video_begin();
_gfx_putpixel16_raw(x, y, color);
_gfx_w3_video_end();
}
void gfx_hline16(int x, int y, int len, uint8_t color)
{
_gfx_w3_video_begin();
_gfx_hline16_raw(x, y, len, color);
_gfx_w3_video_end();
}
void gfx_vline16(int x, int y, int len, uint8_t color)
{
_gfx_w3_video_begin();
_gfx_vline16_raw(x, y, len, color);
_gfx_w3_video_end();
}
void gfx_rect16(int x, int y, int w, int h, uint8_t color)
{
if (w <= 0 || h <= 0) return;
_gfx_w3_video_begin();
_gfx_hline16_raw(x, y, w, color);
_gfx_hline16_raw(x, y + h - 1, w, color);
if (h > 2) {
_gfx_vline16_raw(x, y + 1, h - 2, color);
_gfx_vline16_raw(x + w - 1, y + 1, h - 2, color);
}
_gfx_w3_video_end();
}
void gfx_fill_rect16(int x, int y, int w, int h, uint8_t color)
{
if (w <= 0 || h <= 0) return;
/* Column-major vlines — user's earlier request: 16-color rect_fill
* via vertical lines. Each column does its own per-row RMW; the
* outer begin/end wraps the whole rect so W3 is mapped once. */
_gfx_w3_video_begin();
for (int xx = 0; xx < w; xx++)
_gfx_vline16_raw(x + xx, y, h, color);
_gfx_w3_video_end();
}
void gfx_line16(int x0, int y0, int x1, int y1, uint8_t color)
{
if (y0 == y1) {
int x = x0 <= x1 ? x0 : x1;
int w = (x0 <= x1 ? x1 - x0 : x0 - x1) + 1;
gfx_hline16(x, y0, w, color);
return;
}
if (x0 == x1) {
int y = y0 <= y1 ? y0 : y1;
int h = (y0 <= y1 ? y1 - y0 : y0 - y1) + 1;
gfx_vline16(x0, y, h, color);
return;
}
/* Bresenham — single W3 setup around the whole loop. */
int dx = x1 - x0; int sx = dx < 0 ? -1 : 1; if (dx < 0) dx = -dx;
int dy = y1 - y0; int sy = dy < 0 ? -1 : 1; if (dy < 0) dy = -dy;
int err = (dx > dy ? dx : -dy) / 2;
int x = x0, y = y0;
_gfx_w3_video_begin();
for (;;) {
_gfx_putpixel16_raw(x, y, color);
if (x == x1 && y == y1) break;
int e2 = err;
if (e2 > -dx) { err -= dy; x += sx; }
if (e2 < dy) { err += dx; y += sy; }
}
_gfx_w3_video_end();
}
+118
View File
@@ -0,0 +1,118 @@
/*
* gfx_256.c — 256-colour (mode 0x81) public drawing API.
*
* Single-shot wrappers do one W3-begin / one W3-end around their raw
* call. Composites (rect, fill_rect, line) wrap a single begin/end
* around many raw calls so the W3 dance is paid once per operation,
* not once per pixel/byte.
*/
#include <gfx.h>
#include <stdint.h>
/* From gfx_raw_common.c — DI + map _gfx_bank into W3, save previous. */
extern void _gfx_w3_video_begin(void);
extern void _gfx_w3_video_end(void);
/* From gfx_raw_256.c — W3-naive primitives. */
extern void _gfx_putpixel256_raw(int x, int y, uint8_t color);
extern void _gfx_hline256_raw (int x, int y, int len, uint8_t color);
extern void _gfx_vline256_raw (int x, int y, int len, uint8_t color);
extern void _gfx_clear256_raw (uint8_t color);
void gfx_clear256(uint8_t color)
{
_gfx_w3_video_begin();
_gfx_clear256_raw(color);
_gfx_w3_video_end();
}
void gfx_putpixel256(int x, int y, uint8_t color)
{
if ((unsigned)x >= GFX_WIDTH || (unsigned)y >= GFX_HEIGHT) return;
_gfx_w3_video_begin();
_gfx_putpixel256_raw(x, y, color);
_gfx_w3_video_end();
}
void gfx_hline256(int x, int y, int len, uint8_t color)
{
_gfx_w3_video_begin();
_gfx_hline256_raw(x, y, len, color);
_gfx_w3_video_end();
}
void gfx_vline256(int x, int y, int len, uint8_t color)
{
_gfx_w3_video_begin();
_gfx_vline256_raw(x, y, len, color);
_gfx_w3_video_end();
}
void gfx_rect256(int x, int y, int w, int h, uint8_t color)
{
if (w <= 0 || h <= 0) return;
_gfx_w3_video_begin();
_gfx_hline256_raw(x, y, w, color);
_gfx_hline256_raw(x, y + h - 1, w, color);
if (h > 2) {
_gfx_vline256_raw(x, y + 1, h - 2, color);
_gfx_vline256_raw(x + w - 1, y + 1, h - 2, color);
}
_gfx_w3_video_end();
}
void gfx_fill_rect256(int x, int y, int w, int h, uint8_t color)
{
if (w <= 0 || h <= 0) return;
/* Pick the orientation with fewer accelerator bursts. Each burst
* paints up to 256 contiguous bytes (horizontal) or up to 256
* vertical pixels in one column.
* row-major (hlines): h × ceil(w/256) bursts
* col-major (vlines): w × ceil(h/256) bursts — for h ≤ 256 = w
* Vertical wins for tall-narrow rects; horizontal for short-wide. */
int h_bursts = h * ((w + 255) >> 8);
int v_bursts = w * ((h + 255) >> 8);
_gfx_w3_video_begin();
if (h_bursts <= v_bursts) {
for (int yy = 0; yy < h; yy++)
_gfx_hline256_raw(x, y + yy, w, color);
} else {
for (int xx = 0; xx < w; xx++)
_gfx_vline256_raw(x + xx, y, h, color);
}
_gfx_w3_video_end();
}
void gfx_line256(int x0, int y0, int x1, int y1, uint8_t color)
{
/* Orthogonal lines route to the accelerator. */
if (y0 == y1) {
int x = x0 <= x1 ? x0 : x1;
int w = (x0 <= x1 ? x1 - x0 : x0 - x1) + 1;
gfx_hline256(x, y0, w, color);
return;
}
if (x0 == x1) {
int y = y0 <= y1 ? y0 : y1;
int h = (y0 <= y1 ? y1 - y0 : y0 - y1) + 1;
gfx_vline256(x0, y, h, color);
return;
}
/* Bresenham — single W3-setup around the whole loop. */
int dx = x1 - x0; int sx = dx < 0 ? -1 : 1; if (dx < 0) dx = -dx;
int dy = y1 - y0; int sy = dy < 0 ? -1 : 1; if (dy < 0) dy = -dy;
int err = (dx > dy ? dx : -dy) / 2;
int x = x0, y = y0;
_gfx_w3_video_begin();
for (;;) {
_gfx_putpixel256_raw(x, y, color);
if (x == x1 && y == y1) break;
int e2 = err;
if (e2 > -dx) { err -= dy; x += sx; }
if (e2 < dy) { err += dx; y += sy; }
}
_gfx_w3_video_end();
}
+178
View File
@@ -0,0 +1,178 @@
/*
* gfx_core.c — Sprinter graphics: common state + setup/teardown API.
*
* This module owns the variables that select which graphics page is
* visible vs drawn into, which W3 bank is mapped during gfx writes,
* and the row-base CPU address. All mode-specific primitives (256-
* color in gfx_raw_256.c / gfx_256.c, 16-color in gfx_raw_16.c /
* gfx_16.c, text in gfx_text_*.c) read this state via extern.
*
* Public API:
* gfx_init / gfx_done — switch video mode, restore previous
* gfx_set_visible_page / get — ESTEX $54 SELPAGE wrapper + cached value
* gfx_set_draw_page / get — updates _gfx_addr_base for the new page
* gfx_set_bank / get — sets the W3 page byte (0x50..0x5F)
* gfx_wait_vsync — EI; HALT until next frame interrupt
* gfx_pal_load / gfx_pal_set — BIOS $A4 PIC_SET_PAL wrappers
*
* Shared state (extern from this file):
* _gfx_addr_base — 0xC000 for page 0, 0xC140 for page 1. Every
* mode-specific primitive uses this instead of a
* hard-coded 0xC000 so the same code targets the
* currently-selected draw page.
* _gfx_bank — read by _gfx_w3_video_begin in gfx_raw_common.c
* to map the right W3 page.
*/
#include <gfx.h>
#include <stdint.h>
/* From conio's videomode_raw.c — bypasses set_videotextmode's text-only
* validation so gfx_init can move INTO graphics modes. */
extern uint8_t _videomode_raw_get(void);
extern int _videomode_raw_set(uint8_t mode);
/* ---- Shared graphics state --------------------------------------- */
/* Cached values of the SELPAGE state. Page numbers are 0 or 1. */
static uint8_t _gfx_visible_page = 0;
static uint8_t _gfx_draw_page = 0;
/* The W3 page byte (0x50..0x5F) — see memory/sprinter_vram_transparency.md
* for the bit-meanings (0x50 normal, 0x54 temp, 0x58 transparent, 0x5C
* both). Read by _gfx_w3_video_begin in gfx_raw_common.c. */
uint8_t _gfx_bank = 0x50;
/* CPU address of column 0 in the current draw page. Each VRAM row is
* 1024 bytes wide — page 0 occupies bytes 0..319 (CPU 0xC000+), page 1
* occupies 320..639 (CPU 0xC140+), and the remaining 384 bytes hold
* mode descriptors / palette data we don't touch. Updated by
* gfx_set_draw_page; read by every primitive's raw helper. */
uint16_t _gfx_addr_base = 0xC000;
/* ---- gfx_init / gfx_done ----------------------------------------- */
uint8_t gfx_init(uint8_t mode, uint8_t page)
{
uint8_t prev = _videomode_raw_get();
_videomode_raw_set(mode);
_gfx_bank = 0x50;
gfx_set_visible_page(page);
gfx_set_draw_page(page);
return prev;
}
void gfx_done(uint8_t mode)
{
_videomode_raw_set(mode);
}
/* ---- Visible page (ESTEX $54 SELPAGE) ---------------------------- *
*
* Direct OUT to port 0xC9 bit 0 toggles the "screen mode page" register
* but doesn't notify DSS's display bookkeeping, leaving the screen in
* an inconsistent state (one of the swaps shows as black). Going
* through the syscall keeps DSS happy.
*/
void gfx_set_visible_page(uint8_t page)
{
_gfx_visible_page = page & 1;
__asm
push ix
ld a, (__gfx_visible_page)
ld b, a ; B = page 0/1
ld c, #0x54 ; ESTEX SELPAGE
rst #0x10
pop ix
__endasm;
}
uint8_t gfx_get_visible_page(void)
{
return _gfx_visible_page;
}
/* ---- Draw page --------------------------------------------------- */
void gfx_set_draw_page(uint8_t page)
{
_gfx_draw_page = page & 1;
/* Direct constants beat (0xC000 + (cond ? 0x140 : 0)) by 3 Z80
* instructions — SDCC doesn't fold the constant addition. */
_gfx_addr_base = _gfx_draw_page ? 0xC140 : 0xC000;
}
uint8_t gfx_get_draw_page(void)
{
return _gfx_draw_page;
}
/* ---- W3 bank (0x50..0x5F) ---------------------------------------- */
void gfx_set_bank(uint8_t bank)
{
_gfx_bank = bank;
}
uint8_t gfx_get_bank(void)
{
return _gfx_bank;
}
/* ---- Frame sync -------------------------------------------------- *
*
* Block until the next IM2 frame interrupt (50 Hz, programmed by DSS
* for keyboard / cursor maintenance). The Z80's HALT instruction
* sleeps the CPU until the next IRQ, which DSS handles and returns
* to the instruction after HALT — that's the start of the vertical
* retrace window, ideal for a tear-free page swap.
*/
void gfx_wait_vsync(void) __naked
{
__asm
ei
halt
ret
__endasm;
}
/* ---- Palette (BIOS $A4 PIC_SET_PAL) ----------------------------- */
static uint8_t pal_num_;
static uint8_t pal_start_;
static uint8_t pal_count_;
static uint16_t pal_data_;
void gfx_pal_load(uint8_t pal_num, uint8_t start, uint8_t count,
const uint8_t *data)
{
pal_num_ = pal_num;
pal_start_ = start;
pal_count_ = count;
pal_data_ = (uint16_t)(uintptr_t)data;
__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 ; mask
ld a, (_pal_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);
}
+63
View File
@@ -0,0 +1,63 @@
/*
* gfx_font.c — bitmap-font management shared by the 256- and 16-colour
* text renderers.
*
* Format (ZX-Spectrum-compatible): 256 glyphs × 8 rows × 1 byte = 2 KB,
* INTERLEAVED row-major —
* offset = row * 256 + char_code
* so row 0 of every glyph occupies bytes 0x000..0x0FF, row 1 occupies
* 0x100..0x1FF, etc. Each row byte is MSB-first (bit 7 = leftmost px).
*
* The default source is BIOS WIN_GET_ZG (fn 0xB8) — the active system
* character generator. Programs may override with gfx_set_font().
*
* Lazy initialisation: gfx_text_256.c / gfx_text_16.c call _gfx_font_ensure
* on first use so a pure-graphics program doesn't pay the BIOS call.
*
* The font pointer and the buffer are exported to the two text renderers
* via _gfx_font_ptr (extern), which always points at valid data once
* _gfx_font_ensure has been called.
*/
#include <gfx.h>
#include <stdint.h>
#define FONT_BYTES 2048
static uint8_t _gfx_font_buf[FONT_BYTES];
const uint8_t *_gfx_font_ptr = _gfx_font_buf;
static uint8_t _gfx_font_loaded = 0;
/* BIOS WIN_GET_ZG (0xB8): DE = destination, returns 2 KB. */
static void bios_get_zg(uint8_t *dest) __naked
{
(void)dest;
__asm
push ix ; BIOS clobbers IX
ex de, hl ; DE = dest (HL was arg)
xor a ; font 0 (default)
ld c, #0xB8
rst #0x08
pop ix
ret
__endasm;
}
void gfx_load_default_font(void)
{
bios_get_zg(_gfx_font_buf);
_gfx_font_ptr = _gfx_font_buf;
_gfx_font_loaded = 1;
}
void gfx_set_font(const uint8_t *font)
{
_gfx_font_ptr = font;
_gfx_font_loaded = 1;
}
/* Called from text renderers — loads the default font on first use. */
void _gfx_font_ensure(void)
{
if (!_gfx_font_loaded) gfx_load_default_font();
}
+186
View File
@@ -0,0 +1,186 @@
/*
* gfx_raw_16.c — 16-colour (mode 0x82) raw primitives.
*
* Pixel format (verified empirically 2026-05-31): each byte at
* 0xC000+xb holds two horizontal pixels —
* high nibble (bits 7-4) = pixel at x = 2*xb (LEFT, even)
* low nibble (bits 3-0) = pixel at x = 2*xb + 1 (RIGHT, odd)
*
* Row addressing matches mode 0x81: Port_Y (0x89) = y, 320 bytes/row,
* 320 × 2 = 640 pixels.
*
* Accelerator: byte-wise — one Fill burst paints up to 256 bytes =
* 512 pixels. For a solid colour the byte value is the nibble in
* both halves: b = c | (c<<4).
*
* RMW pattern for unaligned edges & vertical lines:
* read byte at (HL); mask out target nibble; OR in new nibble; write.
* ~10 instructions per pixel — slow but unavoidable since a byte
* spans two horizontal pixels.
*
* "Raw" = W3-naive: caller wraps a sequence with one
* _gfx_w3_video_begin / _gfx_w3_video_end pair.
*/
#include <gfx.h>
#include <stdint.h>
extern uint16_t _gfx_addr_base;
/* ---- Scratch shared across asm helpers ---------------------------- */
static uint8_t g16_y;
static uint8_t g16_byte; /* combined byte: nibble | (nibble<<4) */
static uint8_t g16_nibble; /* color in correct half (low or high) */
static uint8_t g16_mask; /* mask preserving the OTHER half */
static uint8_t g16_len; /* accel block size (0 = 256) */
static uint16_t g16_addr;
/* ---- Accel horizontal Fill burst ---------------------------------- *
*
* Caller has W3 mapped and DI active. Only the block-size byte uses
* SMC. Colour is preloaded into C and shipped via `ld a, c` (0x79).
* Inserting another `ld a, #n` between LD C,C and the firing LD (HL),A
* breaks the burst — the accel FSM re-reads the immediate as a fresh
* block size.
*/
static void g16_hfill_chunk(void) __naked
{
__asm
ld a, (_g16_len)
ld (_g16_h_len_imm), a
ld a, (_g16_byte)
ld c, a
ld a, (_g16_y)
out (#0x89), a
ld hl, (_g16_addr)
ld d, d ; 0x52 set block size
ld a, #0 ; 0x3E nn length (patched)
_g16_h_len_imm = . - 1
ld c, c ; 0x49 horizontal Fill
ld a, c ; 0x79 A = colour byte
ld (hl), a ; fires accel
ld b, b ; 0x40 disable
ret
__endasm;
}
/* ---- RMW one nibble at (g16_addr, g16_y) ------------------------- */
static void g16_rmw_pixel(void) __naked
{
__asm
ld a, (_g16_y)
out (#0x89), a
ld hl, (_g16_addr)
ld a, (_g16_mask)
ld b, a ; B = preserve mask
ld a, (_g16_nibble)
ld c, a ; C = new nibble (in correct half)
ld a, (hl)
and a, b ; clear target nibble
or a, c ; OR in new
ld (hl), a
ret
__endasm;
}
/* ---- Raw primitives (W3-naive, composable) ----------------------- */
void _gfx_putpixel16_raw(int x, int y, uint8_t color)
{
if ((unsigned)x >= GFX_WIDTH_16 || (unsigned)y >= GFX_HEIGHT_16) return;
g16_y = (uint8_t)y;
g16_addr = (uint16_t)(_gfx_addr_base + ((unsigned)x >> 1));
if (x & 1) {
g16_nibble = (uint8_t)(color & 0x0F);
g16_mask = 0xF0;
} else {
g16_nibble = (uint8_t)((color & 0x0F) << 4);
g16_mask = 0x0F;
}
g16_rmw_pixel();
}
void _gfx_hline16_raw(int x, int y, int len, uint8_t color)
{
if ((unsigned)y >= GFX_HEIGHT_16) return;
if (x < 0) { len += x; x = 0; }
if (x >= GFX_WIDTH_16) return;
if (x + len > GFX_WIDTH_16) len = GFX_WIDTH_16 - x;
if (len <= 0) return;
g16_y = (uint8_t)y;
uint8_t cnib = color & 0x0F;
g16_byte = (uint8_t)(cnib | (cnib << 4));
/* Leading unaligned pixel: x odd → RIGHT half of left-most byte. */
if (x & 1) {
g16_addr = (uint16_t)(_gfx_addr_base + ((unsigned)x >> 1));
g16_nibble = cnib;
g16_mask = 0xF0;
g16_rmw_pixel();
x++;
len--;
if (len <= 0) return;
}
/* Even x; emit (len/2) full bytes via accel hfill. */
int full = len >> 1;
g16_addr = (uint16_t)(_gfx_addr_base + ((unsigned)x >> 1));
while (full > 0) {
int chunk = full > 256 ? 256 : full;
g16_len = (chunk == 256) ? 0 : (uint8_t)chunk;
g16_hfill_chunk();
full -= chunk;
g16_addr += chunk;
}
/* Trailing odd-length pixel: LEFT half of the next byte. */
if (len & 1) {
g16_nibble = (uint8_t)(cnib << 4);
g16_mask = 0x0F;
g16_rmw_pixel();
}
}
void _gfx_vline16_raw(int x, int y, int len, uint8_t color)
{
if ((unsigned)x >= GFX_WIDTH_16) return;
if (y < 0) { len += y; y = 0; }
if (y >= GFX_HEIGHT_16) return;
if (y + len > GFX_HEIGHT_16) len = GFX_HEIGHT_16 - y;
if (len <= 0) return;
g16_addr = (uint16_t)(_gfx_addr_base + ((unsigned)x >> 1));
if (x & 1) {
g16_nibble = (uint8_t)(color & 0x0F);
g16_mask = 0xF0;
} else {
g16_nibble = (uint8_t)((color & 0x0F) << 4);
g16_mask = 0x0F;
}
for (int i = 0; i < len; i++) {
g16_y = (uint8_t)(y + i);
g16_rmw_pixel();
}
}
/* Clear the whole 640×256 area with `color`. Row-major hfill —
* 256 rows × 2 bursts each (256+64 bytes). */
void _gfx_clear16_raw(uint8_t color)
{
g16_byte = (uint8_t)((color & 0x0F) | ((color & 0x0F) << 4));
for (int y = 0; y < GFX_HEIGHT_16; y++) {
g16_y = (uint8_t)y;
g16_addr = _gfx_addr_base;
g16_len = 0;
g16_hfill_chunk();
g16_addr = (uint16_t)(_gfx_addr_base + 0x100);
g16_len = 64;
g16_hfill_chunk();
}
}
+175
View File
@@ -0,0 +1,175 @@
/*
* gfx_raw_256.c — 256-colour (mode 0x81) raw primitives.
*
* "Raw" = W3-naive: caller must have W3 mapped to the video bank and
* interrupts disabled (typically via _gfx_w3_video_begin/end from
* gfx_raw_common.c). Composite primitives in gfx_256.c wrap one
* begin/end around many raw calls so the W3 dance is amortised across
* the whole drawing operation.
*
* Addressing (mode 0x81): pixel (x, y) lives at CPU 0xC000 + x with
* Port_Y (0x89) = y. _gfx_addr_base is 0xC000 for draw page 0 and
* 0xC140 for draw page 1 — see gfx_core.c.
*
* Accelerator opcodes (docs/converted/accel_r.txt):
* LD D,D (0x52) enter "set block size" mode; the NEXT byte is the
* length operand and MUST follow LD A immediate (0x3E).
* LD C,C (0x49) horizontal Fill mode (LD (HL),A fills n bytes)
* LD E,E (0x5B) vertical Fill mode (auto-increments Port_Y)
* LD B,B (0x40) disable accelerator
*
* The block-length byte uses SMC — we patch the immediate at runtime,
* then run the accel sequence. HOME is RAM after DSS loads us, so
* SMC inside our own .EXE is safe.
*/
#include <gfx.h>
#include <stdint.h>
extern uint16_t _gfx_addr_base;
/* ---- Scratch shared across asm helpers --------------------------- *
* Single-threaded — no reentrancy. GFX runs with interrupts off
* between _gfx_w3_video_begin and _gfx_w3_video_end. */
static uint8_t acc_color;
static uint8_t acc_y;
static uint8_t acc_len; /* 0 means 256 — the accel convention */
static uint16_t acc_addr;
/* Putpixel scratch — separate from acc_* so a putpixel inside a
* Bresenham loop doesn't trample on outer accel state. */
static uint8_t _gfx_pp_y;
static uint16_t _gfx_pp_addr;
static uint8_t _gfx_pp_color;
/* ---- inner accel bursts ------------------------------------------ *
*
* Pre: W3 mapped to the video bank, DI active. Caller wraps a sequence
* of bursts in a single begin/end pair. */
/* Horizontal fill: acc_len bytes at acc_addr on row acc_y. */
static void hfill_chunk(void) __naked
{
__asm
;; Patch the LD A,#n immediate (operand byte) with acc_len.
ld a, (_acc_len)
ld (_hfill_len_imm), a
ld a, (_acc_y)
out (#0x89), a ; Port_Y = y
;; Pre-load colour into C and dest into HL before arming accel.
ld a, (_acc_color)
ld c, a
ld hl, (_acc_addr)
;; --- ACCEL SEQUENCE ---
ld d, d ; 0x52 set block size mode
ld a, #0 ; 0x3E nn block size (nn patched above)
_hfill_len_imm = . - 1
ld c, c ; 0x49 horizontal Fill
ld a, c ; 0x79 A = colour (NOT another ld a,#n)
ld (hl), a ; fires accel; fills acc_len bytes
ld b, b ; 0x40 disable
ret
__endasm;
}
/* Vertical fill: acc_len pixels at column acc_addr, top row acc_y.
* The accel auto-increments Port_Y as it paints down the column. */
static void vfill_chunk(void) __naked
{
__asm
ld a, (_acc_len)
ld (_vfill_len_imm), a
ld a, (_acc_y)
out (#0x89), a ; starting Y
ld a, (_acc_color)
ld c, a
ld hl, (_acc_addr)
ld d, d ; 0x52 set block size
ld a, #0 ; immediate length (patched)
_vfill_len_imm = . - 1
ld e, e ; 0x5B vertical Fill
ld a, c ; A = colour
ld (hl), a ; fires accel
ld b, b ; 0x40 disable
ret
__endasm;
}
/* ---- Raw primitives (W3-naive, composable) ----------------------- *
*
* These do NO DI/W3 setup — caller wraps a sequence with one
* _gfx_w3_video_begin / _gfx_w3_video_end pair. */
void _gfx_putpixel256_raw(int x, int y, uint8_t color)
{
if ((unsigned)x >= GFX_WIDTH || (unsigned)y >= GFX_HEIGHT) return;
_gfx_pp_y = (uint8_t)y;
_gfx_pp_addr = (uint16_t)(_gfx_addr_base + (unsigned)x);
_gfx_pp_color = color;
__asm
ld a, (__gfx_pp_y)
out (#0x89), a
ld hl, (__gfx_pp_addr)
ld a, (__gfx_pp_color)
ld (hl), a
__endasm;
}
void _gfx_hline256_raw(int x, int y, int len, uint8_t color)
{
if ((unsigned)y >= GFX_HEIGHT) return;
if (x < 0) { len += x; x = 0; }
if (x >= GFX_WIDTH) return;
if (x + len > GFX_WIDTH) len = GFX_WIDTH - x;
if (len <= 0) return;
acc_color = color;
acc_y = (uint8_t)y;
acc_addr = (uint16_t)(_gfx_addr_base + (unsigned)x);
while (len > 0) {
int chunk = len > 256 ? 256 : len;
acc_len = (chunk == 256) ? 0 : (uint8_t)chunk;
hfill_chunk();
len -= chunk;
acc_addr += chunk;
}
}
void _gfx_vline256_raw(int x, int y, int len, uint8_t color)
{
if ((unsigned)x >= GFX_WIDTH) return;
if (y < 0) { len += y; y = 0; }
if (y >= GFX_HEIGHT) return;
if (y + len > GFX_HEIGHT) len = GFX_HEIGHT - y;
if (len <= 0) return;
/* GFX_HEIGHT = 256 so a full column is a single accel burst. */
acc_color = color;
acc_y = (uint8_t)y;
acc_addr = (uint16_t)(_gfx_addr_base + (unsigned)x);
acc_len = (len == 256) ? 0 : (uint8_t)len;
vfill_chunk();
}
/* Clear the whole 320×256 area with `color`. Row-major hfill:
* 256 rows × 2 bursts each (256-byte + 64-byte) = 512 bursts. */
void _gfx_clear256_raw(uint8_t color)
{
acc_color = color;
for (int y = 0; y < GFX_HEIGHT; y++) {
acc_y = (uint8_t)y;
acc_addr = _gfx_addr_base; /* 256-byte burst */
acc_len = 0;
hfill_chunk();
acc_addr = (uint16_t)(_gfx_addr_base + 256); /* 64-byte burst */
acc_len = 64;
hfill_chunk();
}
}
+44
View File
@@ -0,0 +1,44 @@
/*
* gfx_raw_common.c — W3 page mapping primitives shared by every gfx mode.
*
* Composite primitives (gfx_line / gfx_rect / gfx_fill_rect / ...) bracket
* their inner loop with one `_gfx_w3_video_begin()` / `_gfx_w3_video_end()`
* pair and call the `*_raw` variants inside, so the W3 dance is paid once
* per drawing operation instead of once per pixel. Single-shot wrappers
* (gfx_putpixel, gfx_hline, ...) wrap the same way for their single call.
*
* Begin disables interrupts, saves the current W3 page byte, then maps
* `_gfx_bank` (the current video bank, 0x50..0x5F). End restores the
* saved page and re-enables interrupts.
*
* NOT re-entrant — saving the previous W3 byte in a static is safe only
* because GFX runs with interrupts off between begin and end.
*/
#include <stdint.h>
extern uint8_t _gfx_bank; /* current W3 video bank (gfx_core.c) */
static uint8_t _gfx_saved_w3;
void _gfx_w3_video_begin(void) __naked
{
__asm
di
in a, (#0xE2)
ld (__gfx_saved_w3), a
ld a, (__gfx_bank)
out (#0xE2), a
ret
__endasm;
}
void _gfx_w3_video_end(void) __naked
{
__asm
ld a, (__gfx_saved_w3)
out (#0xE2), a
ei
ret
__endasm;
}
+124
View File
@@ -0,0 +1,124 @@
/*
* gfx_text_16.c — bitmap-font text rendering for mode 0x82
* (640×256×16, 4 bits per pixel).
*
* Per glyph row (8 pixels): emit 4 bytes — each byte packs two
* adjacent pixels as (LEFT<<4) | RIGHT. A precomputed 4-entry pair
* table maps the 2-bit source pattern (LEFT*2 + RIGHT) directly to
* the output byte, avoiding per-pixel mask/OR.
*
* Currently requires byte-aligned x (x must be even); odd x is
* silently ignored. Text grids are typically aligned.
*/
#include <gfx.h>
#include <stdint.h>
extern uint16_t _gfx_addr_base;
extern const uint8_t *_gfx_font_ptr;
extern void _gfx_font_ensure(void);
static uint8_t txt_y;
static uint8_t txt_row;
static uint16_t txt_addr;
/* 4-byte lookup: index = LEFT*2 + RIGHT, value = (LEFT<<4)|RIGHT. */
static uint8_t txt_pair[4];
/* Render one 8-pixel row using the pair table — 4 byte writes. */
static void render_row_16(void) __naked
{
__asm
di
in a, (#0xE2)
push af
ld a, #0x50
out (#0xE2), a
ld a, (_txt_y)
out (#0x89), a
ld hl, (_txt_addr)
ld a, (_txt_row)
ld c, a ; C = rotating source bits
ld b, #4 ; 4 output bytes
rr16_loop:
;; Extract two top bits of C as index A = LEFT*2 + RIGHT.
sla c ; CY = LEFT bit; C <<= 1
ld a, #0
adc a, a ; A = LEFT
sla c ; CY = RIGHT bit; C <<= 1
adc a, a ; A = LEFT*2 + RIGHT (0..3)
;; A = txt_pair[A]; preserve HL across the lookup.
push hl
ld e, a
ld d, #0
ld hl, #_txt_pair
add hl, de
ld a, (hl)
pop hl
ld (hl), a
inc hl
djnz rr16_loop
pop af
out (#0xE2), a
ei
ret
__endasm;
}
static void build_pair_table(uint8_t fg, uint8_t bg)
{
uint8_t f = fg & 0x0F;
uint8_t b = bg & 0x0F;
/* HIGH nibble = LEFT, LOW nibble = RIGHT (sprinter_graphics convention). */
txt_pair[0] = (uint8_t)((b << 4) | b); /* 00 BG BG */
txt_pair[1] = (uint8_t)((b << 4) | f); /* 01 BG FG */
txt_pair[2] = (uint8_t)((f << 4) | b); /* 10 FG BG */
txt_pair[3] = (uint8_t)((f << 4) | f); /* 11 FG FG */
}
void gfx_putchar16(int x, int y, char c, uint8_t fg, uint8_t bg)
{
_gfx_font_ensure();
if ((unsigned)x >= GFX_WIDTH_16 || (unsigned)y >= GFX_HEIGHT_16) return;
if (x & 1) return;
build_pair_table(fg, bg);
uint8_t cc = (uint8_t)c;
uint16_t base = (uint16_t)(_gfx_addr_base + ((unsigned)x >> 1));
for (int r = 0; r < 8; r++) {
int yy = y + r;
if ((unsigned)yy >= GFX_HEIGHT_16) break;
txt_y = (uint8_t)yy;
txt_addr = base;
txt_row = _gfx_font_ptr[r * 256 + cc];
render_row_16();
}
}
void gfx_text16(int x, int y, const char *s, uint8_t fg, uint8_t bg)
{
/* Build pair table once for the whole string (FG/BG don't change). */
_gfx_font_ensure();
if (x & 1) return;
build_pair_table(fg, bg);
for (; *s; s++) {
if (x >= GFX_WIDTH_16) break;
if ((unsigned)y >= GFX_HEIGHT_16) break;
uint8_t cc = (uint8_t)*s;
uint16_t base = (uint16_t)(_gfx_addr_base + ((unsigned)x >> 1));
for (int r = 0; r < 8; r++) {
int yy = y + r;
if ((unsigned)yy >= GFX_HEIGHT_16) break;
txt_y = (uint8_t)yy;
txt_addr = base;
txt_row = _gfx_font_ptr[r * 256 + cc];
render_row_16();
}
x += 8;
}
}
+90
View File
@@ -0,0 +1,90 @@
/*
* gfx_text_256.c — bitmap-font text rendering for mode 0x81
* (320×256×256, one byte per pixel).
*
* Per glyph row (8 pixels): emit 8 bytes — FG where the source bit is
* 1, BG where 0. Port_Y is set once per row; the row writer wraps its
* own DI / W3 save+restore so callers don't need to.
*/
#include <gfx.h>
#include <stdint.h>
extern uint16_t _gfx_addr_base;
extern const uint8_t *_gfx_font_ptr;
extern void _gfx_font_ensure(void);
/* ---- Scratch shared with the asm row writer ---------------------- */
static uint8_t txt_y;
static uint8_t txt_row; /* current glyph-row bit pattern */
static uint8_t txt_fg;
static uint8_t txt_bg;
static uint16_t txt_addr; /* VRAM address for this row */
/* Render one 8-pixel row. Self-contained DI / W3 save+restore so a
* gfx_text() can be called from any context without extra ceremony. */
static void render_row_256(void) __naked
{
__asm
di
in a, (#0xE2)
push af
ld a, #0x50
out (#0xE2), a
ld a, (_txt_y)
out (#0x89), a
ld hl, (_txt_addr)
ld a, (_txt_fg)
ld d, a
ld a, (_txt_bg)
ld e, a
ld a, (_txt_row)
ld b, #8 ; 8 pixels in this row
rr256_loop:
rla ; CY = MSB, A <<= 1
jr nc, rr256_bg
ld (hl), d ; FG
jr rr256_next
rr256_bg:
ld (hl), e ; BG
rr256_next:
inc hl
djnz rr256_loop
pop af
out (#0xE2), a
ei
ret
__endasm;
}
void gfx_putchar256(int x, int y, char c, uint8_t fg, uint8_t bg)
{
_gfx_font_ensure();
if ((unsigned)x >= GFX_WIDTH || (unsigned)y >= GFX_HEIGHT) return;
txt_fg = fg;
txt_bg = bg;
uint8_t cc = (uint8_t)c;
uint16_t base = (uint16_t)(_gfx_addr_base + (unsigned)x);
for (int r = 0; r < 8; r++) {
int yy = y + r;
if ((unsigned)yy >= GFX_HEIGHT) break;
txt_y = (uint8_t)yy;
txt_addr = base;
/* Interleaved layout: row r of char cc lives at font[r*256 + cc]. */
txt_row = _gfx_font_ptr[r * 256 + cc];
render_row_256();
}
}
void gfx_text256(int x, int y, const char *s, uint8_t fg, uint8_t bg)
{
for (; *s; s++) {
if (x >= GFX_WIDTH) break;
gfx_putchar256(x, y, *s, fg, bg);
x += 8;
}
}