Files
Sprinter-SDCC/examples/mdview2
snark13 3a33b30c07 mdview2: статик-кэш статуса в file-scope + сентинел вместо force-флага
render_full_status форсирует перерисовку чисел через local_loading=UCHAR_MAX
(сентинел), а не отдельным force_redraw в условии. Отдельный 4-й терм + запись
флага опрокидывали render_md_status_numbers в IX-стек-фрейм (все локали в
память, +68 Б). Вынос local_* в file-scope разгрузил регистровый аллокатор
SDCC — функция осталась на регистрах. Итог даже меньше базы (28215 Б).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 23:00:22 +03:00
..

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.

# Заголовок первого уровня  
## Заголовок второго уровня  
### Заголовок третьего уровня  
#### Заголовок четвёртого уровня  

Текстовое форматирование

Жирный текст выделяется двойными звёздочками: **текст**

Курсив выделяется одиночными звёздочками *текст* или знаком подчёркивания _текст_

Встроенный код обозначается обратными кавычками

Зачёркнутый текст — двойные тильды: ~~текст~~

Ненумерованный список

Маркеры -, * или +:

  • Первый пункт списка
  • Второй пункт списка
  • Третий пункт с достаточно длинным текстом, который при необходимости будет перенесён на следующую строку с сохранением отступа

Нумерованный список

  1. Первый элемент
  2. Второй элемент
  3. Третий элемент

Цитата

Блок цитаты начинается с символа >. Несколько последовательных строк одной цитаты склеиваются в единый абзац с автоматическим переносом слов.

Блок кода (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 КБ, адреса 0x40000xBFFF).
    • Буфер файла — страницы EMM, отображаемые в W3 (0xC0000xFFFF)

Известные ограничения

Длина строки в MD-режиме — 255 ячеек

Рендер-кэш хранит каждую логическую строку как не более 255 пар (символ, атрибут) — константа MAX_CACHE_LINE_LEN. Лимит задан типами: g_ncells и cache_rec_t.lenuint8_t. Касается всех строк, но заметнее всего на горизонтально скроллируемых строках (блоки кода и строки таблиц, флаг IF_HSCROLL), которые в MD-режиме можно листать вправо.

Что происходит с более длинной строкой: при индексации gc_put() молча отбрасывает каждую ячейку после 255-й (if (g_ncells < MAX_CACHE_LINE_LEN)). В кэш попадают только первые 255 ячеек, остаток теряется — до него нельзя доскроллить и нет маркера обрезки на 255-й позиции (маркер > означает лишь «есть ещё в пределах кэша»). Переполнения буфера нет — gc_put проверяет границу.

Важно: «255 ячеек» — это отрендеренная ширина, не байты исходника. Табы в коде разворачиваются в пробелы (до TAB_STOP), а ячейки таблицы добиваются пробелами до ширины колонки + рамки — поэтому кап достигается раньше, чем 255 «полезных» символов.

RAW-режим (F2) этого лимита не имеет — он рисует прямо из файла побайтово, длинные строки видны целиком (через wrap F3 или гориз. скролл).

Идея снятия лимита (оценка, не реализовано) — расширить длину до uint16_t:

  • cache_rec_t.len uint8_t→uint16_t — структура остаётся ровно 8 байт (len съедает один pad-байт), адресация idx<<3 не меняется. Бесплатно.

  • g_ncells / g_ncells_at_spaceuint16_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

  1. Увеличение размера документов. Снять лимит 128 КБ: Достаточно разрешить работать с большим кол-вом страниц памяти, пока оттестированно на работе с 8-мю страницами по 16Кб.

Сделано: форматированные таблицы с рамкой; поддержка кодировок CP866 / CP1251 / KOI8-R / UTF-8 с автоопределением и переключением по F8 (второй набор строится лениво, по первому переключению).

  1. Ускорение рендеринга. Кэш строк экрана. Оптимизация цикла вывода символов через 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/sysenv over ESTEX $46.
  • Time: getdatetime/setdatetime + POSIX time/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 APIinitgraph / setcolor / circle / getimage / putimage / etc. on top of our gfx_* 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 items
  • docs/solid_c_compatibility.md — gap analysis vs Solid-C 2004
  • docs/im2_isr_design.md — interrupt handler design (v2)
  • docs/converted/ — source documentation (ESTEX, BIOS, architecture) converted to plain text for grep
  • docs/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 (see third_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 $C4
  • libc/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_*16 API, 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 driverrst $30h, 17 функций. Сначала тест что работает в MAME.
  • ffirst/fnext + ffblk_t struct для directory listing — реализовано, demo: ls.exe
  • getdatetime/setdatetime через ESTEX $21/$22 — libc/io/time.c, demo: time_dir_test
  • chdir/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.cputchar.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.
  • small runtime: 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.
  • small auto-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.
  • big runtime (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.
  • huge runtime (small + banked code в W3): merge W2-detect логики из crt0_small.s в crt0_banked.s. Существующий пример examples/banked/ теперь использует MEMORY=huge. Реализовано 2026-05-30.
  • --debug флаг: prepend DEBUG_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 page
  • bank_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, добавить alias
  • getmaxx() / getmaxy() — макрос на GFX_WIDTH-1 / GFX_HEIGHT-1
  • cleardevice() — alias to gfx_clear
  • getgraphmode() / setgraphmode()у нас get_videomode/set_videomode

Color/palette:

  • setcolor(c), getcolor() — current draw color
  • setbkcolor(c), getbkcolor() — background color
  • setpalette(idx, c) — палитра entry
  • getpalette(&info) — read all palette

Primitives (мы уже имеем эквиваленты — добавить BGI-имена как aliases):

  • putpixel(x, y, c) — есть как gfx_putpixel
  • getpixel(x, y) — нужно реализовать (RMW обратное — IN)
  • moveto(x, y), lineto(x, y), linerel(dx, dy) — current point + line drawing
  • line(x1, y1, x2, y2) — есть как gfx_line
  • rectangle(x1, y1, x2, y2) — есть как gfx_rect (но другой API: x1,y1,x2,y2 vs x,y,w,h!)
  • bar(x1, y1, x2, y2) — есть как gfx_fill_rect
  • bar3d(x1, y1, x2, y2, depth, topflag) — новое: rect + 3d edges
  • circle(x, y, r), arc(...), ellipse(...), pieslice(...) — новые primitives
  • fillpoly(), 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 fonts
  • gettextsettings(&info)
  • textwidth(s) / textheight(s) — measure

Image manipulation:

  • imagesize(x1, y1, x2, y2) — bytes needed for getimage
  • getimage(x1, y1, x2, y2, buf) — save rect to buffer
  • putimage(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 rect
  • getviewsettings(&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:

    • fwrite short-write должен ставить _F_ERROR
    • fgets(buf, 1, fp) — стандарт говорит "empty string", мы вернули NULL
    • mode_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's time.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_line orthogonal 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 × количество строк/столбцов экономии. Применимо ко всем композитным примитивам.