B2 stage2: общий цикл inline-форматирования/переносов/склейки в одном scan_stream (mode: NONE / LIST / QUOTE / PLAIN); дублировавшиеся блоки эмиссии пробела/символа, переноса с усечением и отката стиля — в одном экземпляре. Старые имена — тонкие обёртки, API inline_scan для table-модуля не изменился. styles_map умерла: emph_to_attr(ls, ATTR_TEXT) тождественна ей. Индексатор 9839 → 9315 Б. Проверено в MAME: переносы заголовков/списков/ цитат, таблицы, inline-маркеры на границе переноса, жёсткие переносы. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MDView — Просмотрщик Markdown для Sprinter
MDView — программа для просмотра документов в формате Markdown на компьютере Sprinter (процессор Z80). Документ хранится в отдельном W3 окне и не занимает основную RAM программы.
Возможности
- Документы до 128 КБ (8 страниц EMM по 16 КБ каждая)
- До 16 384 экранных строк в индексе
- Автоматический перенос слов по ширине экрана (80 столбцов)
- Горизонтальный сдвиг для широких строк (блоки кода, таблицы)
- Статус-бар: имя файла, диапазон строк, процент прокрутки
- Спиннер в строке состояния во время загрузки и индексации
- Поддержка «мягкого» склеивания строк в абзацах и цитатах
Запуск
mdview [имя_файла.md]
Если имя файла не задано, загружается README.MD.
Управление
Клавиша Действие
───────────── ────────────────────────────────────────
Up Down Прокрутка на одну строку вверх / вниз
PgUp PgDn Прокрутка на страницу (30 строк)
Home Начало документа
End Конец документа
Left Right Горизонтальный сдвиг (только nowrap-строки)
F1 Окно справки
F8 Кодировка: CP866 → CP1251 → KOI8-R → UTF-8 → ...
F10 / Esc Выход из программы
Кодировка определяется автоматически при открытии (BOM + эвристика по
первым 4 КБ); F8 переключает её вручную, если детекция ошиблась.
8-битные кодировки (CP866/CP1251/KOI8-R) переключаются мгновенно (ремап на
отрисовке). Второй набор индекс/кэша (например UTF-8) строится лениво — при
первом переключении в него (короткая пауза со спиннером), дальше мгновенно.
Синтаксис Markdown
Заголовки
Поддерживаются уровни H1–H4. Уровни H5 и H6 отображаются как H4.
# Заголовок первого уровня
## Заголовок второго уровня
### Заголовок третьего уровня
#### Заголовок четвёртого уровня
Текстовое форматирование
Жирный текст выделяется двойными звёздочками: **текст**
Курсив выделяется одиночными звёздочками *текст* или знаком подчёркивания _текст_
Встроенный код обозначается обратными кавычками
Зачёркнутый текст — двойные тильды: ~~текст~~
Ненумерованный список
Маркеры -, * или +:
- Первый пункт списка
- Второй пункт списка
- Третий пункт с достаточно длинным текстом, который при необходимости будет перенесён на следующую строку с сохранением отступа
Нумерованный список
- Первый элемент
- Второй элемент
- Третий элемент
Цитата
Блок цитаты начинается с символа
>. Несколько последовательных строк одной цитаты склеиваются в единый абзац с автоматическим переносом слов.
Блок кода (verbatim)
Блок кода заключается в тройные обратные кавычки. Внутри блока текст отображается «как есть» без разбора Markdown:
#include <stdio.h>
#include <sprinter.h>
int main(void) {
puts("Hello, Sprinter!");
return 0;
}
Горизонтальная линия
Три или более символов ---, *** или ___ на отдельной строке:
Технические характеристики
- Платформа: Sprinter, процессор Z80 @ 21 МГц
- Кодировки: CP866 / CP1251 / KOI8-R / UTF-8 (автоопределение,
F8) - Максимальный размер файла: 128 КБ
- Максимальное число строк в индексе: 16 384
- Максимальная длина строки в рендер-кэше: 255 ячеек (см. ограничение ниже)
- Режим памяти: small
- Код программы, cтек, данные, куча — окнa W1-W2 (32 КБ, адреса 0x4000–0xBFFF).
- Буфер файла — страницы EMM, отображаемые в W3 (0xC000–0xFFFF)
Известные ограничения
Длина строки в MD-режиме — 255 ячеек
Рендер-кэш хранит каждую логическую строку как не более 255 пар (символ, атрибут)
— константа MAX_CACHE_LINE_LEN. Лимит задан типами: g_ncells и
cache_rec_t.len — uint8_t. Касается всех строк, но заметнее всего на
горизонтально скроллируемых строках (блоки кода и строки таблиц, флаг
IF_HSCROLL), которые в MD-режиме можно листать вправо.
Что происходит с более длинной строкой: при индексации gc_put() молча
отбрасывает каждую ячейку после 255-й (if (g_ncells < MAX_CACHE_LINE_LEN)).
В кэш попадают только первые 255 ячеек, остаток теряется — до него нельзя
доскроллить и нет маркера обрезки на 255-й позиции (маркер > означает лишь
«есть ещё в пределах кэша»). Переполнения буфера нет — gc_put проверяет границу.
Важно: «255 ячеек» — это отрендеренная ширина, не байты исходника. Табы в
коде разворачиваются в пробелы (до TAB_STOP), а ячейки таблицы добиваются
пробелами до ширины колонки + рамки │ — поэтому кап достигается раньше, чем
255 «полезных» символов.
RAW-режим (
F2) этого лимита не имеет — он рисует прямо из файла побайтово, длинные строки видны целиком (через wrapF3или гориз. скролл).
Идея снятия лимита (оценка, не реализовано) — расширить длину до uint16_t:
-
cache_rec_t.lenuint8_t→uint16_t— структура остаётся ровно 8 байт (len съедает один pad-байт), адресацияidx<<3не меняется. Бесплатно. -
g_ncells/g_ncells_at_space→uint16_t— главная цена по коду/скорости: 16-битная арифметика на Z80 в горячемgc_put(вызов на каждую ячейку) и в scan-циклах. Ориентир: +0.2…0.4 КБ кода + замедление индексации. -
Буфер
g_cells[MAX_CACHE_LINE_LEN*2]в near-RAM (W2) — главная цена по памяти:2 × капбайт. Сейчас 510 Б; кап 512 → +0.5 КБ, 1024 → +1.5 КБ. EMM-кэш контента (1 МБ) длинные строки тянет легко — узкое место именно near. -
viewport_x(и копия вdocset_t) + потолокmax_vx(сейчас 248) →uint16_t, иначе хранить >255 можно, а доскроллить нельзя. Плюсwidths[]/ccxв таблицах/коде.Реалистичный компромисс — кап 512–1023: хватит почти всем листингам/таблицам, цена ~+0.3 КБ кода и +0.5…1.5 КБ near-RAM. Дешёвая полумера без
uint16_t— ставить честный маркер обрезки на 255-й позиции, чтобы потеря была видна.
TODO
- Увеличение размера документов. Снять лимит 128 КБ: Достаточно разрешить работать с большим кол-вом страниц памяти, пока оттестированно на работе с 8-мю страницами по 16Кб.
Сделано: форматированные таблицы с рамкой; поддержка кодировок
CP866 / CP1251 / KOI8-R / UTF-8 с автоопределением и переключением по F8
(второй набор строится лениво, по первому переключению).
- Ускорение рендеринга. Кэш строк экрана. Оптимизация цикла вывода символов через BIOS WRCHAR (пакетный вывод, DMA).
MDView v0.2 · (c) 2026 Петров А.Г.
Sprinter C Compiler — v1.0
C toolchain for Sprinter — the Z80-based home computer by Peters Plus, running
ESTEX DSS. Host: macOS / Linux. Target: .EXE files in SprintEXE format.
Built on top of SDCC 4.5 (vendored in third_party/sdcc/). This repository adds
everything Sprinter-specific: crt0, linker integration, libc wrappers over ESTEX,
banked-call trampolines, graphics & accelerator API, mouse driver wrappers, and the
mkexe utility for producing SprintEXE images.
What you get
bin/sprinter-cc— one-line driver:sprinter-cc -o foo.exe foo.c- Memory modes:
tiny,small,big,huge,manual— see below. - stdio + conio: printf, puts, putchar, getchar, fopen/fread/..., cprintf, cputs, putch, textcolor/textbackground/textattr, gotoxy, kbhit/getch.
- Graphics: 320×256×256 and 640×256×16 modes, accelerator-backed primitives (hline / vline / rect / fill_rect / line via Bresenham, plus clear), bitmap-font text in both modes via BIOS character generator.
- File I/O: POSIX (
open/read/write/close/lseek/unlink/creat), FILE* streams (fopen/fgets/fwrite/...), directory listing (ffirst/fnext),chdir/getcwd/mkdir/rmdir,stat/fstat. - Memory: 32 KB heap (W2-resident), banking-aware page allocator (
mem_alloc_pages/bank_read/bank_write), explicit memory modes for sub-16 KB programs. - Mouse: full Sprinter driver wrapper (14 functions including custom cursor bitmaps).
- Environment:
getenv/putenv/sysenvover ESTEX$46. - Time:
getdatetime/setdatetime+ POSIXtime/localtime/mktime/asctime/ctime. - Misc:
errno/strerror/perror,atexit,setjmp/longjmp,sleep, full argv parsing in crt0.
Quick start
git clone <this repo> sprinter-c
cd sprinter-c
make sdcc # one-time: fetch SDCC 4.5 binary (~25 MB)
make all # build mkexe + libsprinter.lib + 27 examples
make floppy # pack everything into mame/v306/IMG/mc.img
cd mame/v306 && ./run_mame.sh # boot Sprinter in MAME
Compile a single program:
cat > hello.c <<EOF
#include <stdio.h>
int main(void) { puts("Hello, Sprinter!"); return 0; }
EOF
bin/sprinter-cc -o hello.exe hello.c
That's it — hello.exe is now a valid SprintEXE you can RUN HELLO from the ESTEX shell.
Memory modes
Sprinter's address space is four 16 KB windows (W0 / W1 / W2 / W3). DSS allocates pages by program size — small programs get only one page. Pick a memory mode based on what your program needs:
| Mode | Code lives in | Banking | Use when | Note |
|---|---|---|---|---|
tiny (default) |
W2 (0x8100+) | no | code+data < 14 KB | |
small |
W1-W2 (0x4100+) | no | code+data < 30 KB | |
big |
W2 + W1 banking | yes (W1) | tiny + extra code modules | |
huge |
W1-W2 + W3 banking | yes (W3) | small + extra code modules | |
manual |
user-specified | optional | special layouts | Not implemented |
sprinter-cc --memory small -o big.exe bigprog.c
sprinter-cc --memory huge -o app.exe main.c --bank 1=engine.c --bank 2=ai.c
Banked functions are declared with __banked:
void engine_tick(int dt) __banked; // lives in BANK1, automatically swapped
Examples (27 total)
| Example | What it demonstrates |
|---|---|
hello |
Hello world with stdio + conio Turbo-C-style colors |
argv |
argv parsing in crt0 |
cat |
File I/O — read & print TEST.TXT |
seek |
32-bit lseek over a 100 KB file |
ls |
Directory listing via ffirst/fnext |
filetest |
FILE* streams (fopen/fread/...) |
stattest |
stat/fstat on files and directories |
errno |
errno / strerror / perror |
mem_test |
Page allocator + bank_read/bank_write |
malloc |
Heap stress test (200+ allocations) |
banked |
Banked code in W3 (huge mode) |
bankedbg |
Banked code in W1 (big mode) |
banklocl |
Bank-local static data and BSS |
mouse |
Mouse driver in text mode |
gfx_mous |
Mouse with custom bitmap cursor in graphics mode |
gfx_demo |
320×256×256 lines / rects / accelerator |
gfx_d16 |
640×256×16 same primitives |
gfx_text |
Bitmap-font text on graphics screen |
timedir |
Date/time + directory listing |
ptime |
POSIX time API |
openenv |
open() flags + environment vars |
conio |
conio API smoke test |
attrprob |
Probe Sprinter text attribute byte layout |
strtest |
string.h test (from SDCC's z80.lib) |
stdlib |
stdlib.h test (qsort / rand / strtol / etc.) |
assrtest |
assert() |
rt_test |
Runtime helpers (sleep, setjmp, atexit) |
Headers
Standard:
<stdio.h>— puts / printf / FILE* + Sprinter-specific dec/hex helpers<stdlib.h>— atoi / atof / malloc / qsort / ... (from SDCC z80.lib)<string.h>/<ctype.h>/<math.h>— from SDCC z80.lib<unistd.h>— read / write / close / lseek / unlink<fcntl.h>— open / creat + O_RDONLY / O_CREAT / ...<errno.h>— errno + error names + strerror<sys/stat.h>— stat / fstat<setjmp.h>/<assert.h>— from SDCC
Sprinter-specific:
<conio.h>— putch / cputs / cprintf, textcolor / textbackground / textattr, kbhit / getch, clrscr, gotoxy, wherex/y<gfx.h>— gfx_init/done, palette, putpixel, hline/vline/rect/fill_rect/line, text — for both 320 and 640 modes (gfx_*16 variants)<mouse.h>— full 14-function driver wrapper + mouse_cursor_t with bitmap support<dir.h>— chdir / getcwd / mkdir / rmdir / ffirst / fnext / ffblk<time.h>— getdatetime / setdatetime + POSIX time / localtime / etc.<sprinter.h>— raw ports, ESTEX/BIOS function numbers, env API<sprinter_exit.h>— exit / _exit / atexit<sprinter_mem.h>— mem_alloc_pages / mem_free_block / bank_read / bank_write<sprinter_compat.h>— Solid-C compatibility layer (aliases + BOOL/WORD/uint types)
Toolchain commands
make all # build mkexe + lib + every example
make floppy # repack mame/v306/IMG/mc.img with all .exe files
make check # 17 mkexe unit-tests
make clean # remove all build artefacts
make sdcc # one-time: fetch SDCC 4.5 binary
sprinter-cc options
sprinter-cc -o foo.exe foo.c [more.c ...] [options]
--memory MODE tiny | small | big | huge | manual (default: tiny)
--memory-manual SPEC explicit placement (CODE=W1|W2,DATA=W1|W2|SAME,BANKED=W1|W3)
--stack-size N bytes reserved for the stack (default ~1278)
--crt0=TYPE default | minimal | banked | small
--bank N=FILE.c compile FILE.c into bank N (repeatable, max 15)
--debug enable runtime diagnostics (defines DEBUG_RT)
-I PATH extra include path
-L 0xADDR / -E / -S override load / entry / stack addresses
-Wl FLAG pass FLAG to sdldz80
--mkexe FLAG pass FLAG to mkexe (e.g. --mkexe -p --mkexe 0 for bank padding)
-v verbose
Status
What works in v1.0:
- Compile / link / pack to SprintEXE — verified on all 27 examples
- Four memory modes (tiny / small / big / huge)
- Graphics (both modes) with accelerator
- Mouse (text + graphics cursor)
- File I/O, directories, environment, time
- All headers listed above
Deferred to v2.0 (see docs/TODO.md):
- Turbo-C-style BGI graphics API —
initgraph/setcolor/circle/getimage/putimage/ etc. on top of ourgfx_*primitives - Remaining Solid-C compatibility gaps (Phase 2/3) — see
docs/solid_c_compatibility.md - Manual memory mode
- Rewrite FILE* stream API (current implementation is very primitive and doesn't use buffers)
Deferred to v3.0:
- IM2 interrupt handlers — research complete (
docs/im2_isr_design.md), implementation scheduled for v3 - Audio API (AY-3-8910 + COVOX) — requires IM2
- ISA-8 slot drivers — requires IM2 (???)
Documentation
docs/TODO.md— roadmap and open work itemsdocs/solid_c_compatibility.md— gap analysis vs Solid-C 2004docs/im2_isr_design.md— interrupt handler design (v2)docs/converted/— source documentation (ESTEX, BIOS, architecture) converted to plain text forgrepdocs/reference/,docs/samples/,docs/memory management/— original Russian docs and code samples from Peters Plus
Repository layout
bin/sprinter-cc one-line compiler driver (bash)
toolchain/mkexe/ host-side tool: .ihx -> .exe SprintEXE
toolchain/check_banks.py post-link bank size enforcer
runtime/ crt0 variants (default, minimal, small, banked)
bank trampolines, heap, heap_top
libc/include/ headers
libc/io|stdio|mem|gfx/ C and asm sources for libsprinter.lib
lib/ Makefile that archives libsprinter.lib via sdar
examples/ 27 example programs
mame/v306/ MAME binary + Sprinter ROM/HDD images + floppy script
third_party/sdcc/ vendored SDCC 4.5 (fetched via `make sdcc`)
third_party/solid-c/ reference: original Sprinter native C (for compat target)
docs/ documentation
License
This repository contains:
- Original code in
bin/,toolchain/,runtime/,libc/,lib/,examples/— MIT-licensed. third_party/sdcc/— SDCC 4.5 under GPLv2 with linking exception (seethird_party/sdcc/COPYING.txt)third_party/solid-c/— original Sprinter Solid C, used only as a reference
Credits
- Sprinter / Peters Plus — Иван Мак, Дмитрий Паринов and the original team
- SDCC — for the underlying Z80 compiler
- MAME — for the Sprinter Sp2000 emulation
For questions / patches: see CONTRIBUTING.md (TBD) or open an issue.
TODO / Roadmap
Открытые задачи в порядке убывания приоритета. По мере появления реальных программ — приоритеты будут смещаться.
Этап 5 — malloc / free + banking-aware page allocator ✅ ГОТОВО
- SDCC's
malloc/free+ нашruntime/heap.s(полностью заменяет library heap.rel, 14000-байтный heap в окне 2) libc/mem/mem_alloc.c— page allocator:mem_alloc_pages/mem_free_block/mem_get_page/mem_infoчерез ESTEX$3C/$3D/$3E+ BIOS$C4libc/mem/bank_io.c— HOME-резидентныеbank_read/bank_write/bank_load_byte/bank_store_byteсо свопом W3 внутриexamples/malloc_test/— проверка SDCC's malloc (~210 64-байтных allocations через всю heap)examples/mem_test/— проверка page allocator: 3 страницы, разные паттерны через bank_write, верификация через bank_read
Этап 6 — argv parsing + sprinter-cc wrapper ✅ ГОТОВО
- crt0 парсит ESTEX command-line из IX-prefix (inline asm в
runtime/crt0.s) - Strip leading CP/M-style space (DSS quirk)
- Передача
argc/argvв main() через HL/DE (SDCC __sdcccall(1) ABI) - argv[0] = basename .EXE через ESTEX APPINFO ($47 subfn 2)
runtime/crt0_minimal.s— opt-out для очень маленьких программruntime/crt0_banked.s— теперь тоже парсит argv (parse_argv + get_progname скопированы из crt0.s; будет factored в argv.s когда возьмёмся за libsprinter.lib)- Bash-обёртка
bin/sprinter-cc:sprinter-cc -o foo.exe foo.cодной строкой - Поддержка опций:
--memory,--memory-manual,--stack-size,--crt0=,--bank N=FILE.c,--debug,-I,-L/-E/-S,-Wl,--mkexe
Этап 8 — графика (320×256×256 + 640×256×16 + accel + bitmap font) ✅ ГОТОВО
- 8a Graphics core:
gfx_init/gfx_done/gfx_clear/gfx_putpixel/gfx_pal_load/gfx_pal_set(libc/gfx/gfx_core.c). Палитра через BIOS PIC_SET_PAL ($A4). Verified 320×256×256. - 8b Линии/прямоугольники/fill через accelerator (libc/gfx/gfx_lines.c):
gfx_hline/gfx_vlineчерез accel Fill (LD C,C / LD E,E + SMC block-size),gfx_rect/gfx_fill_rectс heuristic выбором ориентации (h/v bursts count),gfx_lineс Bresenham для диагоналей.gfx_clearтоже переписан на column-major accel (~4× быстрее). - 8c 640×256×16 mode (libc/gfx/gfx_16.c):
gfx_*16API, HIGH nibble = LEFT pixel (документация misleading), per-row RMW для vline (один байт = 2 горизонтальных пикселя). - 8d Bitmap font + gfx_text (libc/gfx/gfx_text.c): шрифт через BIOS WIN_GET_ZG ($B8), interleaved layout
font[row*256+char],gfx_text/gfx_putcharдля 320 mode,gfx_text16/gfx_putchar16для 640 mode с pair-table lookup.
См. memory/sprinter_graphics.md, sprinter_accelerator.md, sprinter_graphics_16.md, sprinter_font_format.md.
Открытые мелочи (не блокируют):
- Шрифт-quad для 640: per-cell палитра (mode 0x82 разрешает 1 из 4 палитр per 16×8 cell) — через прямой доступ к area-описания экрана 0x0300..0x039F
Auto-banking (см. memory/banking_roadmap.md для деталей)
Phase 1 — file-level bin-packing — реализовывать когда проект перерастёт ~30 KB кода.
toolchain/auto_bank.py:- Парсит размеры из
.rel-файлов (или из .map после dry-run link'а) - First-fit-decreasing bin-packing
- Уважает
#pragma codeseg BANKnкак manual override - Перелинковывает с новыми
-Wl-b_BANKn=...параметрами - Печатает план распределения
- Парсит размеры из
Phase 2-5: incremental rebalance, declarative banks.toml, function-level, call-graph-aware. Только если/когда понадобится.
Bank-local static data (mutable data в том же банке что и код) — ✅ ГОТОВО
- Пример
examples/bank_local_data/— функция в BANK1 со своим writable BSS array + const table + malloc-тест mkexe -p 0для нулевого padding банков (BSS-storage обнуляется при загрузке)- Канонический рецепт:
--codeseg BANK1 --constseg BANK1 --dataseg BANK1для bank1.c +-Wl-b_BANK1=0x1C000для линковки.--dataseg BANK1РАБОТАЕТ — раньше казалось обратное из-за trampoline bug который маскировал результат. - Критичный фикс trampoline'a в runtime/bank.s — старый
pop af; out (n), aклобберил A → все banked-функции возвращающие uint8_t тихо возвращали мусор. Новыйpop bc; out (c), bсохраняет A. - malloc из banked-функции работает прозрачно — heap живёт в W2 (HOME), W2 никогда не свапается trampoline'ом, pointer валиден из любого контекста. См. memory/bank_local_data_pattern.md.
- Документация в memory:
memory/bank_local_data_pattern.md(полный рецепт + malloc + nuances),memory/sdcc_banking.md(trampoline fix) - Опционально — расширить
check_banks.pyчтобы показывать разбивку size = code + const + bss per bank (cosmetic)
Зачем: для модулей с большим private state (level loader, audio engine, scene data). Экономит W2 heap для динамики, а статика остаётся в бэке.
Подсказки из solid-c (нативный Sprinter C — third_party/solid-c/)
После анализа solid-c'овской libc (см. memory/solid_c_findings.md) выявлены готовые паттерны для следующих недостающих функций. Приоритет от высокого к низкому:
High-priority gaps (легко портировать, большая польза)
errno+strerror/perror— табличка 32 ошибок (libc/io/errno.c)- Расширенный
open()для O_CREAT/O_TRUNC/O_APPEND/O_EXCL state machine atexit— 8-callback LIFO +exit()+_exit()(libc/io/atexit.c)setjmp/longjmp— 6-байт jmp_buf={sp,ix,pc} (libc/io/setjmp.c)sleep(seconds)— 50Hz halt-loop (libc/io/sleep.c)- ESTEX ENV API ($46, getenv/putenv) — libc/io/env.c. Учли doc-bug: реально A=0 это NOT FOUND
Medium-priority (нужно для shell-like утилит)
- Mouse driver —
rst $30h, 17 функций. Сначала тест что работает в MAME. ffirst/fnext+ ffblk_t struct для directory listing — реализовано, demo: ls.exegetdatetime/setdatetimeчерез ESTEX $21/$22 — libc/io/time.c, demo: time_dir_testchdir/getcwd/mkdir/rmdir— wrappers для ESTEX $1B-$1E — libc/io/fsdir.c- conio:
kbhit/getch/getche/cputs/clrscr/gotoxy— реализовано - conio extras:
wherex/wherey($53),wrchar/rdchar($58/$57),textmode_get/set($50/$51),clrscr_attr($56) + COLOR macros
Low-priority — ✅ FILE* stack ГОТОВО
- Минимальный unbuffered FILE* —
libc/stdio/file.c+libc/include/stdio.h. fopen/fclose/fputs/fgets/fread/fwrite/fseek/ftell/rewind/feof/ferror/clearerr/fflush + stdin/stdout/stderr как pseudo-streams. См.memory/file_star_design.mdиexamples/filetest. - fprintf / fscanf — нужна printf-через-callback machinery. Пока пользователь может
sprintf(buf, ...) + fputs(buf, fp). - Опциональный buffered mode (setvbuf, line/block buffering) — если когда-то понадобится.
POSIX time API — ✅ ГОТОВО
libc/io/posix_time.c— time/localtime/gmtime/mktime/asctime/ctime поверх getdatetime. SDCC's time.rel избегаем (нельзя override _RtcRead). См.examples/ptime.
sys/stat — ✅ ГОТОВО
libc/io/stat.c— POSIX stat/fstat. Гибрид open+fstat для файлов, ffirst+iter для папок (включая "."/".."). См.examples/stattestиmemory/estex_ffirst_dotdot.md.
assert — ✅ ГОТОВО (используем SDCC's __assert через fallback include path)
libc/stdlib — ✅ не нужно делать (см. memory/sdcc_stdlib_works.md)
Проверено через examples/stdlib_test/: SDCC z80.lib содержит работающие реализации:
atoi/atol/atof, strtol/strtoul, rand/srand, qsort/bsearch, abs/labs, div/ldiv- Полный
<string.h>(memchr/cmp/set/cpy, strcat/cmp/cpy/len/chr/spn/etc.) <ctype.h>(toupper/tolower)<math.h>(sinf/cosf/sqrtf/etc.)
Линкер автоматически тянет из z80.lib когда нужно. НЕ переписывать.
Наши Sprinter-specific обязательные модули остаются: atexit, env, errno, setjmp, putchar/puts/getchar, conio, fsdir, time, mouse, open/read/lseek/close.
Build-system: libsprinter.lib + sprinter-cc — ✅ ГОТОВО
lib/Makefile— собирает каждый libc/*.c в.rel, архивирует через sdar вlib/sprinter.lib- Включает runtime/bank.s и runtime/heap.s (auto-pulled при __banked/malloc)
bin/sprinter-cc— bash-wrapper:sprinter-cc -o foo.exe foo.cодной строкой- Поддержка опций
--crt0=default|minimal|banked,--bank N=FILE.c,-I,-L/-E/-S,-Wl,--mkexe examples/hello_sccc/— демо:hello.cсобирается за один shell-вызов, размер совпадает с ручным Makefile (925 байт)- Split
putchar.c→putchar.c+puts.cдля per-function granularity (puts override SDCC's z80.lib version) - Включён в
make all(зависимостьlibпередexamples)
Возможные улучшения (опционально):
- Мигрировать остальные examples на sprinter-cc вместо ручных Makefile (косметика)
- Дальнейшая декомпозиция libc/*.c per-function (но текущая granularity уже даёт нужный размер — линкер пакетует .rel целиком, и для большинства файлов это одна функция)
Этап 9 — memory modes для sprinter-cc
DSS выделяет страницы памяти по размеру приложения: < 16 KB → одна страница, в остальные окна подключается «страница #FF» (read=0xFF, write игнорится). Из-за этого CODE-в-W1 + DATA-в-W2 для маленькой программы молча ломается. См. memory/sprinter_memory_modes.md.
tiny: всё (CODE+DATA+стек) в W2. Default. Verified hello/argv/conio/malloc/file/etc.--memory MODEфлаг в sprinter-cc: parser + per-mode дефолты CODE_LOC/DATA_LOC, override через явные--code-loc/--data-loc. tiny работает; small/big/huge компилируются с warning'ом (runtime не готов). Реализовано 2026-05-30.--memory-manual SPEC: парситCODE=W1|W2,DATA=W1|W2|SAME,BANKED=W1|W3. Реализовано 2026-05-30.smallruntime:runtime/crt0_small.sиспользует ESTEX$3D GETMEM+$3A SETWIN2чтобы выделить и замапить W2-страницу ДО gsinit. НЕ BIOS$C4— стек на этом этапе в W1 (boot_stack в HOME), а BIOS требует стек в W2. После маппинга SP переключается на 0xBFFE, дальше стандартный flow. Реализовано 2026-05-30, verified hello.exe.smallauto-detect для >16 KB программ:crt0_small.sчитает порт0xC2(текущая страница в W2 — не0xA2! это W1). Если 0xFF — выделяет page; иначе DSS уже сделала это (программа сама вылезла в W2). Один crt0 покрывает 0..30 KB. mkexe также разрешает HOME span W1+W2 (0x4000..0xBFFF). Verified hello: small (5 KB файл, SETWIN2 path) + 32 KB файл (auto-skip). Реализовано 2026-05-30.bigruntime (tiny + banked code в W1): параметризовалиcrt0_banked.s+bank.sчерез.ifdef BANK_W1— другой banking port (0xA2 vs 0xE2), другой load-addr (0x4000 vs 0xC000). sprinter-cc prepend'итBANK_W1 = 1при--memory big, передаётmkexe -B 0x4000. Примерexamples/banked_big/. Реализовано 2026-05-30.hugeruntime (small + banked code в W3): merge W2-detect логики изcrt0_small.sвcrt0_banked.s. Существующий примерexamples/banked/теперь использует MEMORY=huge. Реализовано 2026-05-30.--debugфлаг: prependDEBUG_RT = 1в crt0 +-DDEBUG_RTв sdcc. Открывает symbol_w2_self_allocated(uint8_t) — runtime diagnostic кто аллоцировал W2. Реализовано 2026-05-30.
Дизайн-решения по libc и crt0
Одна sprinter.lib работает для всех memory mode — .rel-члены relocatable, SDLD делает dead-code elimination per-member (без графики не подтягивает gfx_core.rel и т.д.). Verified hello vs malloc_test через map-файлы.
gfx.lib отдельно — НЕ нужен: dead-code elimination уже работает.
libc_banked (libc в bank вместо HOME) — идея на потом, когда HOME (16 KB) забит user-кодом + libc в huge mode. Реализуется через --codeseg BANK0 при компиляции libc; trade-off: trampoline ~30 циклов на каждый libc-вызов. Триггер: реальная программа упрётся в HOME budget.
HW-зависимые модули — sprinter_home.lib отдельно. Часть libc физически не может быть забанкована в W3, потому что она РАБОТАЕТ с W3:
gfx_*— пишет в видеопамять0xC000+после swap W3 на video pagebank_io(mem_alloc_pages/bank_read/bank_write) — swap'ит W3 черезOUT (0xE2)- Будущие ISR — прерывание может прийти когда W3 на чём угодно
В huge mode эти модули ДОЛЖНЫ остаться в HOME (W1). Когда будем делать libc_banked, одновременно выделяем sprinter_home.lib (HOME-only) из sprinter.lib (bankable). Финальная схема:
sprinter_home.lib HOME-only: gfx, bank_io, ISR shims
sprinter.lib bankable: printf, malloc, string, conio, stdio, env, ...
sprinter_banked.lib тот же sprinter.lib но --codeseg BANK0 (для huge)
Триггер: реализация --memory huge runtime.
crt0 — по одному на mode:
crt0.s— текущий, для tiny/big: SP=0xBFFE, парсит argv (W2-ресурс уже выделен DSS).crt0_minimal.s— текущий, для tiny без argv.crt0_small.s— новый, step 3: для small/huge, аллоцирует W2 черезmem_alloc_pagesДО gsinit, маппит в порт0xA2, потом стандартный flow.crt0_banked.s— текущий, для big: trampoline-таблица для W3 банков, CODE в W2.crt0_banked_small.s— новый: huge = small (W2-alloc) + banked (W3 trampolines).
sprinter-cc подбирает crt0 по --memory mode (сейчас --crt0= это override).
- Настраиваемый размер стека: флаг
sprinter-cc --stack-size BYTES. Wrapper генерируетheap_top.sс___sdcc_heap_end = stack_top + 1 - stack_size, отдельный .rel линкуется per-program. Default ≈1278 байт (heap_top=0xBB00) изruntime/heap_top.s. Реализовано 2026-05-30.
Интерфейс: sprinter-cc --memory [tiny|small|big|huge|manual] [--memory-manual SPEC] [--stack-size N] foo.c. --memory-manual имеет смысл только с --memory manual.
Known issues / quirks
- ESTEX $46 ENV API: ✅ работает. Док-ция в
DiskSyscalls.txt v1.6ошибочно описывает return-status — A=0 это NOT FOUND, не FOUND. Зафиксировано вmemory/sprinter_platform.md.
ОБЯЗАТЕЛЬНЫЕ ЗАДАЧИ ДЛЯ V2 (после релиза v1)
Turbo-C-style graphics API (BGI-like) — MUST для v2
Расширить наш gfx_* API до уровня Turbo-C <graphics.h> (BGI) для MS-DOS.
Программисты привыкшие к Turbo-C должны переносить графический код 1-в-1.
Что должно быть (на основе Borland BGI):
Setup/teardown:
initgraph()/closegraph()— у нас сейчасgfx_init/gfx_done, добавить aliasgetmaxx()/getmaxy()— макрос на GFX_WIDTH-1 / GFX_HEIGHT-1cleardevice()— alias to gfx_cleargetgraphmode()/setgraphmode()— у нас get_videomode/set_videomode
Color/palette:
setcolor(c),getcolor()— current draw colorsetbkcolor(c),getbkcolor()— background colorsetpalette(idx, c)— палитра entrygetpalette(&info)— read all palette
Primitives (мы уже имеем эквиваленты — добавить BGI-имена как aliases):
putpixel(x, y, c)— есть как gfx_putpixelgetpixel(x, y)— нужно реализовать (RMW обратное — IN)moveto(x, y),lineto(x, y),linerel(dx, dy)— current point + line drawingline(x1, y1, x2, y2)— есть как gfx_linerectangle(x1, y1, x2, y2)— есть как gfx_rect (но другой API: x1,y1,x2,y2 vs x,y,w,h!)bar(x1, y1, x2, y2)— есть как gfx_fill_rectbar3d(x1, y1, x2, y2, depth, topflag)— новое: rect + 3d edgescircle(x, y, r),arc(...),ellipse(...),pieslice(...)— новые primitivesfillpoly(),drawpoly()— полигоныfloodfill(x, y, border_color)— заливка
Text on graphics screen:
outtext(s)/outtextxy(x, y, s)— есть как gfx_text (alias)settextstyle(font, dir, size)— multiple bitmap fontsgettextsettings(&info)textwidth(s)/textheight(s)— measure
Image manipulation:
imagesize(x1, y1, x2, y2)— bytes needed for getimagegetimage(x1, y1, x2, y2, buf)— save rect to bufferputimage(x, y, buf, op)— paste back with COPY_PUT/XOR_PUT/AND_PUT/OR_PUT/NOT_PUT
Clipping/viewport:
setviewport(x1, y1, x2, y2, clip)— drawing clip rectgetviewsettings(&info)clearviewport()setactivepage(p)/setvisualpage(p)— двойная буферизация (Sprinter имеет 2 screen)
Line style:
setlinestyle(style, pattern, thickness)— SOLID_LINE / DOTTED_LINE / etc.getlinesettings(&info)
Acceptance: перенос типичной Turbo-C BGI программы (рисующей с использованием moveto/lineto/circle/bar/setcolor) должен работать без существенных правок.
Notes:
- BGI fonts (TRIPLEX/SANS_SERIF/GOTHIC) — у нас один BIOS font, остальные нужно добавить (как bitmap data в lib)
- imagesize/getimage/putimage — самые востребованные для game/animation
- Active/visual page (двойная буферизация) — Sprinter поддерживает 2 graphics pages, нужен API switching
См. также examples/ Turbo C 2.x BGIDEMO как reference что нужно.
IM2 Interrupt Handlers — MUST для v2
User-задаваемые ISR через Z80 IM 2 mode. Нужны для:
- Timer ticks (50 Hz frame counter, плавная анимация)
- Music playback (AY, COVOX)
- Real-time games (input + game logic + render в interrupt-driven)
- Async keyboard / mouse handling
Status: ОТЛОЖЕНО до v2. Полный research + design в docs/im2_isr_design.md.
Решение по архитектуре: реализовать как отдельный memory mode --memory im2
(вместо того чтобы лезть во все существующие crt0). Detail'и в design-doc.
Резюме research'а (полный текст в docs/im2_isr_design.md):
- Vector 0xFF — frame + keyboard + CBL. Disambiguation по портам 0x19 / 0xFE
- Mouse hardware-IRQ не приходит (на текущей плате)
- Vector table / ISR / stack ОБЯЗАНЫ быть в W2 (0x8000..0xBFFF)
- DSS имеет свой IM 2 handler — нужно chain'иться (иначе клавиатура / SYSTIME ломаются)
Прочие крупные пункты для v2
-
FILE API rewrite — buffered streams — текущая реализация в
libc/stdio/file.cэто provisional unbuffered shim (каждый fputc/fgetc = один read/write syscall). Нужна полноценная buffered семантика как в Solid-C:typedef struct { uint flags; // +0..1 file status flags int level; // +2..3 empty/fill level of buffer char *curp; // +4..5 current active pointer int fd; // +6..7 underlying low-level fd char *buffer; // +8..9 data transfer buffer char hold; // +10 ungetc byte if no buffer short token; // +11..12 reserved char dummy; // +13 reserved } FILE;stdin/stdout/stderr — fd-маркеры
0 / -1 / -2. Отрицательные для stdout/stderr выбраны намеренно: ESTEX OPEN может вернуть positive small fd (1, 2, …) для обычного файла → если бы stdout=1, реальный fd=1 сталкивался бы с идентификатором. fd=0 для stdin безопасно (ESTEX 0 не возвращает). Сами fd не передаются в syscall'ы — диспетчеризация по флагам_F_CONIN/_F_CONOUT.Принтер-потоки (stdaux/stdprn) НЕ реализуем — Sprinter принтерной API не имеет.
Альтернатива — взять реализацию из third_party/solid-c (sources в
SRC/CLIB/); там есть готовый buffered FILE + fopen/fread/fwrite/ fseek/setvbuf и т.д. Адаптировать к нашим open/read/write/lseek.При rewrite заодно решить deferred issues stdio-review:
fwriteshort-write должен ставить_F_ERRORfgets(buf, 1, fp)— стандарт говорит "empty string", мы вернули NULLmode_to_flags— break-out на '+' (cosmetic)
-
Audio API — AY-3-8910 + COVOX через прерывания (требует IM2)
-
ISA-8 slot support — ZX-Bus карты (sound, network, etc.) — требует IM2 + чтения portов
Прочие задачи (v1 backlog, не блокирующие)
- #9: text I/O split (Turbo-C style) — stdio (puts/printf/putchar) теперь fast no-attr через PCHARS/PUTCHAR. conio (cputs/cprintf/putch) применяет attr через textcolor/textbackground/textattr. KEEP_EXIST_ATTR → conio fallback на fast path. Verified в hello.exe. См.
memory/text_output_api_split.md. Реализовано 2026-05-31. - Mouse API полный (резидентный driver, RST 30h) — все 14 функций обёрнуты (init/show/hide/refresh/read/goto/bounds/text_cursor/load_cursor/get_cursor/get/set_sensitivity/video_mode_changed). См.
memory/mouse_api.md. Verified в MAME 2026-05-31. Sensitivity = divider (меньше = быстрее). - Interrupt handlers — IM 2 vector table в HOME для user ISR'ов
- Поддержка
restore SP on EXIT(паттерн из z88dk +pps) — проверить нужно ли - CI: автоматически запускать MAME с
-aviwriteдля screenshot-сравнения, чтобы тесты примеров проходили без человека
Идеи на потом
- Поддержка
<setjmp.h>(есть в SDCC stdlib — нужно протестировать что наш crt0 совместим) <time.h>через ESTEX SYSTIME ($21) и CMOS BIOS-функции- ZX Spectrum-совместимый режим как отдельный target (для портирования спектрумовских программ)
- Поддержка ZX-Bus карт (sound, network, etc.) — нужны драйверы
- Profile-guided optimization tools (hot/cold detection) для крупных программ
Linker duplicate-symbol warnings (благоприятные, отфильтрованы)
Когда мы сознательно overrides'им SDCC z80.lib функции собственной версией в sprinter.lib, sdldz80 пишет ?ASlink-Warning-Definition of public symbol '...' found more than once. Линкер берёт первое найденное определение (наше), поэтому поведение корректное — warning только noise.
Текущие overrides:
_puts— наша версия через PCHARS+\r\n vs SDCC posix puts___sdcc_heap— наш heap в W2 vs SDCC's стандартный_asctime,_localtime(и возможно другие из time) — нашposix_time.cчерез ESTEX SYSTIME vs SDCC'stime.relкоторый зависит от_RtcRead
Текущее решение: bin/sprinter-cc отфильтровывает warning-блок (warning + 2 follow-up Library: строки) из вывода sdcc. Через -v (verbose) всё показывается. Реализовано через awk-pipe.
Возможные улучшения:
- Перейти на explicit
--nostdlib+ ручной список нужных модулей из z80.lib (string, math, stdlib без override'нутых) — убрать ИСТОЧНИК warning'ов, не маскировать - Или: переименовать наши
_puts→_puts_sprinter+ alias через linker flag (не уверен что SDCC поддерживает) - Или: оставить как сейчас (рабочее и benign) — приоритет низкий
TODO: проверить на реальном железе
-
Port_Y banking trick (
docs/part2/SprinterGraphics programming.txt): доку утверждает что послеOUT (0x89), Yадреса 0xC000+0x400*N в окне W3 маппятся на строки Y..Y+15 (одно программирование → 16 строк). Empirical 2026-06-01 в MAME 0.283 этот trick не работает — пиксели по адресам выше 0xC000+row_width уходят в невидимую область. Каноническийdocs/samples/plasma2.asmтоже не использует banking, переустанавливает Port_Y per row. План: 1. Получить доступ к реальному Sprinter 2. Запустить тест dual-write (_gfx_putpixel_raw+ второй write в0xD000+x) 3. Если на железе видны двойные линии → бага MAME, открыть issue с минимальным репро 4. Если на железе тоже одна линия → документ неверный, удалить упоминание из доки и просто оставить текущую реализацию (Port_Y per pixel) 5. Если banking работает на железе → внедрить кэширование Port_Y в_gfx_putpixel_raw(sentinel out-of-range, см. memory/gfx_port_y_banking.md)Связанный выигрыш для Bresenham (60-pixel диагональ) — около 8× меньше OUT (0x89) операций, для `gfx_fill_rect 320x256` — 16× меньше. Не блокирует release v1.
GFX: расширения по docs/part2/accelerator_doc.txt
После прочтения детального accelerator doc выявлены незакрытые направления. Сейчас в коде используется только горизонтальный/вертикальный Fill mode.
Quick wins для текущих primitives
- Заменить SMC на
LD A, (var)для block-size. Документ явно разрешаетLD A, (HL),LD A, (BC),LD A, (DE)(но неLD A, r). Это уберёт SMC complexity вgfx_lines.c:hfill_chunk/vfill_chunkиgfx_16.c:g16_hfill_chunk. Запрещено только register-to-register. - Кэширование block-size. Документ показывает что accel запоминает
block size между bursts (см.
Horizontal_Line_Fill: устанавливают size +LD B,Bотключение, потом включают Fill mode и используют сохранённый size). Дляgfx_fill_rectс 100 одинаковыми строками — установить size 1 раз, а не 100.
Bank-prefix modes (port 0xE2 bits)
Документ показывает три варианта банка видеостраницы помимо стандартного 0x50:
| Bank byte | Effect |
|---|---|
| 0x50 | Normal write — пишется в shadow + видимый |
| 0x54 | "no copy in main shadow RAM" |
| 0x58 | "FF is transparent" — байт 0xFF при write оставляет background |
| 0x5C | both |
Bank 0x58 объясняет почему mouse cursor рисуется с 0xFF-прозрачностью. Это путь к sprite-blending через accel block copy:
gfx_set_bank_transparent(on)или флаг вgfx_set_bankдля выбора 0x50/0x58 при отрисовке sprite'ов- Использовать в новом
gfx_blit()чтобы по факту получать transparent sprites через accel-копию
Block copy mode (sprite blit'ы)
LD L,L (horizontal) и LD A,A (vertical) — режим копирования блока через
256-байтную accel memory. Это базис для blit'ов.
gfx_blit(src_data, x, y, w, h)— копирование sprite'а (произвольный размер, через accel)gfx_blit_transparent(src, x, y, w, h)— с использованием bank 0x58
См. Draw_Restangle_Data в accelerator_doc.txt как референс.
AND / OR / XOR operations через accel
Документ показывает что accel поддерживает логические операции с блоками данных. Применения:
-
XOR — инверсия области (выделение selection в UI)
-
OR / AND — masking, alpha-style blending
-
См. пример в accelerator_doc.txt: "256 bytes block coding via XOR"
-
gfx_xor_rect/gfx_or_rect/gfx_and_rect— примитивы логических операций над прямоугольником -
gfx_invert_rect(x, y, w, h)— alias на xor с 0xFF
Bitmap fonts разных размеров
Сейчас gfx_text / gfx_putchar хардкоженно работают с 8×8 шрифтом
(BIOS WIN_GET_ZG возвращает 256×8 байт). Для будущих UI / титульников
нужны:
gfx_set_font_size(w, h)— переключить ширину/высоту glyph'аgfx_set_font_data(ptr, w, h, advance)— заменить указатель на пользовательский шрифт + размеры- Поддержка proportional (advance != w) шрифтов — добавить array advance[256] на ширину каждого glyph'а
- Big-font режимы: 8×16, 16×16, 16×8 (для титульников)
- Возможно отдельный API
gfx_text_ex(x, y, str, font_id)где font_id выбирает один из загруженных шрифтов - Anti-alias 2-bit шрифты (бит фон / бит граница / 2-бит alpha?) — far future, для smooth UI
Финальный этап оптимизаций (не сейчас)
-
gfx_lineчерез accel для пологих диагоналей — Bresenham для линии с |dy| << |dx| (или наоборот) выдаёт длинные runs одинакового Y (или X): пиксель, пиксель, пиксель, шаг Y, пиксель, пиксель... Каждый такой run — это готовый аргумент дляgfx_hline(илиvline).
План исследования: посчитать длину runs как функцию от наклона; решить минимальный run length, при котором выгоднее accel hline чем N×putpixel (overhead accel ~20µs, putpixel ~5µs — accel выгоднее при run ≥ 4-5 px); для крутых диагоналей (dx ≈ dy) оставить Bresenham, для пологих — run-length-based fill. Сейчасgfx_lineorthogonal cases уже через accel — оптимизировать только косые. -
gfx_fill_rectс одним W3-swap на всю операцию — сейчас каждый внутреннийgfx_hline/gfx_vlineделает свой DI/save-W3/restore-W3/EI. Можно сделать internal_fill_rect_innerкоторый держит W3 замапленным и DI весь цикл; ~20µs × количество строк/столбцов экономии. Применимо ко всем композитным примитивам.