Files
Sprinter-SDCC/examples/mdview2/README.MD
T
snark13 a14f19b657 mdview2: поддержка кодировок CP866/CP1251/KOI8-R/UTF-8 (F8)
- Детекция при открытии (BOM + эвристика по первым 4 КБ).
- 8-битные (CP866/CP1251/KOI8-R) — общий индекс/кэш, переключение
  мгновенным ремапом глифов [128-255] на отрисовке по attr (структурные
  глифы — рамка/HR/маркеры — не ремапятся).
- UTF-8 — отдельный набор: декодирование в CP866 (кириллица + ходовые
  символы: стрелки/галка/буллет/тире/кавычки/box), свой индекс/кэш.
- Два набора (docset_t g_doc[2]) со свапом «живых» глобалов; второй
  строится ЛЕНИВО при первом F8-переходе в него (build-on-demand).
- F8: цикл CP866→CP1251→KOI8R→UTF8; метка в меню видна только когда
  переключение возможно; во время сборки 8-бит первичного F8 крутит 8-бит.
- F1-справка: секция Encoding; меню разбито на блоки (F1/F8/F10).
- Фикс: g_doc обязан быть инициализирован (SDCC z80 не обнуляет статики
  надёжно) — иначе мусорный built вёл к показу неинициализированного набора.
- Убрана отладка времени обработки из статус-бара.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 11:00:49 +03:00

800 lines
51 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
- **Режим памяти:** small
- Код программы, cтек, данные, куча — окнa W1-W2 (32 КБ, адреса 0x40000xBFFF).
- Буфер файла — страницы EMM, отображаемые в W3 (0xC0000xFFFF)
## TODO
1. **Увеличение размера документов.** Снять лимит 128 КБ: Достаточно
разрешить работать с большим кол-вом страниц памяти, пока оттестированно
на работе с 8-мю страницами по 16Кб.
Сделано: форматированные таблицы с рамкой; поддержка кодировок
CP866 / CP1251 / KOI8-R / UTF-8 с автоопределением и переключением по `F8`
(второй набор строится лениво, по первому переключению).
4. **Ускорение рендеринга.** Кэш строк экрана. Оптимизация цикла вывода
символов через 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
```sh
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:
```sh
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 |
```sh
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`:
```c
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
```sh
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 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 ✅ ГОТОВО
- [x] SDCC's `malloc`/`free` + наш `runtime/heap.s` (полностью заменяет library heap.rel, 14000-байтный heap в окне 2)
- [x] `libc/mem/mem_alloc.c` — page allocator: `mem_alloc_pages`/`mem_free_block`/`mem_get_page`/`mem_info` через ESTEX `$3C/$3D/$3E` + BIOS `$C4`
- [x] `libc/mem/bank_io.c` — HOME-резидентные `bank_read`/`bank_write`/`bank_load_byte`/`bank_store_byte` со свопом W3 внутри
- [x] `examples/malloc_test/` — проверка SDCC's malloc (~210 64-байтных allocations через всю heap)
- [x] `examples/mem_test/` — проверка page allocator: 3 страницы, разные паттерны через bank_write, верификация через bank_read
## Этап 6 — argv parsing + sprinter-cc wrapper ✅ ГОТОВО
- [x] crt0 парсит ESTEX command-line из IX-prefix (inline asm в `runtime/crt0.s`)
- [x] Strip leading CP/M-style space (DSS quirk)
- [x] Передача `argc`/`argv` в main() через HL/DE (SDCC __sdcccall(1) ABI)
- [x] argv[0] = basename .EXE через ESTEX APPINFO ($47 subfn 2)
- [x] `runtime/crt0_minimal.s` — opt-out для очень маленьких программ
- [x] `runtime/crt0_banked.s` — теперь тоже парсит argv (parse_argv + get_progname скопированы из crt0.s; будет factored в argv.s когда возьмёмся за libsprinter.lib)
- [x] Bash-обёртка `bin/sprinter-cc`: `sprinter-cc -o foo.exe foo.c` одной строкой
- [x] Поддержка опций: `--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) ✅ ГОТОВО
- [x] **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.
- [x] **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× быстрее).
- [x] **8c** 640×256×16 mode (libc/gfx/gfx_16.c): `gfx_*16` API, HIGH nibble = LEFT pixel (документация misleading), per-row RMW для vline (один байт = 2 горизонтальных пикселя).
- [x] **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 в том же банке что и код) — ✅ ГОТОВО
- [x] Пример `examples/bank_local_data/` — функция в BANK1 со своим writable BSS array + const table + malloc-тест
- [x] `mkexe -p 0` для нулевого padding банков (BSS-storage обнуляется при загрузке)
- [x] Канонический рецепт: `--codeseg BANK1 --constseg BANK1 --dataseg BANK1` для bank1.c + `-Wl-b_BANK1=0x1C000` для линковки. **`--dataseg BANK1` РАБОТАЕТ** — раньше казалось обратное из-за trampoline bug который маскировал результат.
- [x] **Критичный фикс trampoline'a в runtime/bank.s** — старый `pop af; out (n), a` клобберил A → все banked-функции возвращающие uint8_t тихо возвращали мусор. Новый `pop bc; out (c), b` сохраняет A.
- [x] **malloc из banked-функции работает прозрачно** — heap живёт в W2 (HOME), W2 никогда не свапается trampoline'ом, pointer валиден из любого контекста. См. memory/bank_local_data_pattern.md.
- [x] Документация в 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 (легко портировать, большая польза)
- [x] **`errno` + `strerror`/`perror`** — табличка 32 ошибок (libc/io/errno.c)
- [x] **Расширенный `open()`** для O_CREAT/O_TRUNC/O_APPEND/O_EXCL state machine
- [x] **`atexit`** — 8-callback LIFO + `exit()` + `_exit()` (libc/io/atexit.c)
- [x] **`setjmp`/`longjmp`** — 6-байт jmp_buf={sp,ix,pc} (libc/io/setjmp.c)
- [x] **`sleep(seconds)`** — 50Hz halt-loop (libc/io/sleep.c)
- [x] **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**.
- [x] **`ffirst`/`fnext` + ffblk_t struct** для directory listing — реализовано, demo: ls.exe
- [x] **`getdatetime`/`setdatetime`** через ESTEX $21/$22 — libc/io/time.c, demo: time_dir_test
- [x] **`chdir`/`getcwd`/`mkdir`/`rmdir`** — wrappers для ESTEX $1B-$1E — libc/io/fsdir.c
- [x] **conio: `kbhit`/`getch`/`getche`/`cputs`/`clrscr`/`gotoxy`** — реализовано
- [x] **conio extras**: `wherex`/`wherey` ($53), `wrchar`/`rdchar` ($58/$57), `textmode_get/set` ($50/$51), `clrscr_attr` ($56) + COLOR macros
### Low-priority — ✅ FILE* stack ГОТОВО
- [x] **Минимальный 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 — ✅ ГОТОВО
- [x] `libc/io/posix_time.c` — time/localtime/gmtime/mktime/asctime/ctime поверх getdatetime. SDCC's time.rel избегаем (нельзя override _RtcRead). См. `examples/ptime`.
### sys/stat — ✅ ГОТОВО
- [x] `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 — ✅ ГОТОВО
- [x] `lib/Makefile` — собирает каждый libc/*.c в `.rel`, архивирует через sdar в `lib/sprinter.lib`
- [x] Включает runtime/bank.s и runtime/heap.s (auto-pulled при __banked/malloc)
- [x] `bin/sprinter-cc` — bash-wrapper: `sprinter-cc -o foo.exe foo.c` одной строкой
- [x] Поддержка опций `--crt0=default|minimal|banked`, `--bank N=FILE.c`, `-I`, `-L`/`-E`/`-S`, `-Wl`, `--mkexe`
- [x] `examples/hello_sccc/` — демо: `hello.c` собирается за один shell-вызов, размер совпадает с ручным Makefile (925 байт)
- [x] Split `putchar.c``putchar.c` + `puts.c` для per-function granularity (puts override SDCC's z80.lib version)
- [x] Включён в `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](../../.claude/projects/-Volumes-SAM8-Projects-DIY-Z80-Sprinter-C-Compiler/memory/sprinter_memory_modes.md).
- [x] **`tiny`**: всё (CODE+DATA+стек) в W2. Default. Verified hello/argv/conio/malloc/file/etc.
- [x] **`--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.
- [x] **`--memory-manual SPEC`**: парсит `CODE=W1|W2,DATA=W1|W2|SAME,BANKED=W1|W3`. Реализовано 2026-05-30.
- [x] **`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.
- [x] **`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.
- [x] **`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.
- [x] **`huge` runtime** (small + banked code в W3): merge W2-detect логики из `crt0_small.s` в `crt0_banked.s`. Существующий пример `examples/banked/` теперь использует MEMORY=huge. Реализовано 2026-05-30.
- [x] **`--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).
- [x] **Настраиваемый размер стека**: флаг `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:
```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, не блокирующие)
- [x] **#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.
- [x] **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 × количество строк/столбцов экономии. Применимо ко всем композитным примитивам.