# TODO / Roadmap

Открытые задачи в порядке убывания приоритета; закрытые этапы — в
«Истории» внизу.  Текущий срез libc-работ: docs/libc-roadmap.md.

## Ближайшее

- [ ] **Модульные тесты libc и libbgi под ucsim_z80** — план:
      `docs/host-tests-plan.md`.  Обвязка уже готова и обкатана
      (`testkit/`, первый потребитель — PoP/roomtest).  Не срочно, но
      обязательно: это основной продукт репозитория, а покрыт он сейчас
      только интеграционными тестами в MAME.  Начинать с `time/` и
      `stdio/` (нулевые швы), затем геометрия `libbgi/common`, затем
      фейковые ESTEX/BIOS в тестовом crt0 — они открывают `file/`, `io/`,
      `conio/` и проверку гардов (`_fd_guard`).

Порядок реализации связки: **сначала цепочка irq, потом FPS-делитель
поверх неё** (делитель — просто один слот цепи; так снимается EBUSY
на сосуществование со своим тиком приложения).

- [x] **(1) irq: цепочка кадровых обработчиков** — СДЕЛАНО 2026-07-15.
      Массив `isr_t _irq_chain[4]` + `_irq_chain_n` в W2-data (заменили
      `_irq_user`); трамплин `tr_frame` проходит слоты (один тяжёлый сейв
      на всю цепь, пустой слот пропускается); API `irq_chain_add`
      (0/-1+ENOMEM), `irq_chain_remove(h)`; `irq_install` → обёртка
      chain_add (EBUSY исчез), `irq_remove()` рвёт всю цепь; refcount
      таблицы на первом/последнем слоте; мутации под IRQ_DISABLE.
      All-modes W1-remap: трамплин копируется в W2 (`_irq_tramp_w2buf`,
      copy-safe — только jr/djnz + литерал jp 0x0038), вокруг вызова
      восстанавливает базовую W1-страницу (`_irq_app_w1_page` = IN 0xA2
      при install).  ПРОВЕРЕНО в MAME (tests/irqtest, 2 хендлера):
      tiny/big/huge — chain 97=97 → remove h2 → 48/0; **huge = код в W1,
      remap работает** (цепочка тикает синхронно).  Дизайн:
      docs/im2_isr_design.md.
      FOLLOW-UP: (а) CTC-трамплин пока требует код в W2 → в small/huge
      `irq_ctc_install` = EINVAL (нужна такая же W2-копия _irq_ctc_tramp);
      (б) small для мелких программ (BSS в W1) → irq_install EINVAL
      (safe; снять резервом area _IM2 в верхней W2).
- [x] **(2) FPS-делитель (frame pacing)** — СДЕЛАНО 2026-07-15.
      gfx_set_fps_div(n): логический кадр = ровно n кадровых интервалов
      (1=50/2=25/3=~16.7 fps); при переполнении слота — выравнивание на
      ближайший фронт (без дрейфа фазы).  Счётчик кадров _gfx_frame_tick
      через irq_chain_add(_gfx_frame_isr) — свой слот цепи (n<=1 снимает
      его через irq_chain_remove, не трогая хендлер приложения).
      gfx_wait_vsync: ветка n>=2 (счётчик+halt) перед лучевым поллингом.
      Файлы: common/_gfx_fps_state.c, _gfx_frame_isr.c, gfx_set_fps_div.c,
      правка gfx_wait_vsync.c.  Работает tiny/big/huge (цепочка all-modes);
      small для мелких программ = EINVAL.  ПРОВЕРЕНО MAME (tests/fpsdiv):
      n=1/2/3 → 20/40/60 кадров на 20 wait'ов (drift=0); n=2 с рендер-
      заглушкой ~1 кадр → период держится 2 (поглощение перерасхода,
      наивный путь дал бы ~60); huge идентично; small = EINVAL graceful.
      ПЛАН: docs/sprite-api-design.md §9е.
- [ ] **П6/железо**: MAME-смоук всех тестов после libc-сплита (conio,
      ptime, stattest, mouse, gfx_demo/gfx_d16/gfx_text/gfx_mous —
      трогался asm акселератора); затем прогон на реальном Sprinter
      (mdview2 + FILE* v2 + fdmax — подтвердить лимит 8 манипуляторов
      и зависание DSS на 9-м OPEN)
- [ ] Мигрировать оставшиеся examples на sprinter-cc вместо ручных
      Makefile (косметика)
- [ ] check_banks.py: разбивка size = code + const + bss per bank

## Auto-banking (memory/banking_roadmap.md)

- [ ] **Сверять `n_banks` с реальным числом банков — ошибкой сборки.**
      Сейчас число банков задаётся ДВАЖДЫ и вручную: флагами `--bank N=…`
      у `sprinter-cc` и константой `const uint8_t n_banks = N;` в
      приложении (её читает `crt0_banked` при старте, чтобы выделить
      EMM-страницы и загрузить в них образы банков).  Рассогласование не
      даёт ни ошибки компиляции, ни ошибки линковки — только зависание в
      рантайме: `_bank_pages[N]` остаётся нулём, трамплин отображает в
      окно страницу 0 и прыгает по 0xC000 в мусор (предупреждение об этом
      уже есть в шапке `runtime/bank.s`).
      **Найдено 2026-08-05** на PoP/roomtest: добавили пятый банк
      (`--bank 5=pop_ctrl.c`), забыли `n_banks` — игра доходила до конца
      загрузки ресурсов и вставала намертво в дисковом коде DSS.  Диагноз
      занял заметно больше, чем сама правка: симптом (зависание в чужом
      коде) никак не указывает на причину.
      Сверять надо не количество, а **максимальный индекс** банка: `--bank`
      нумеруются с 1 и в принципе могут идти с дырами, а `crt0_banked`
      заполняет `_bank_pages[1..n_banks]` подряд.
      Варианты, от простого к правильному:
      1. **Проверка после линковки** — вытащить значение `_n_banks` из
         `.ihx`/`.map` (это `const` в `_CODE`, адрес известен из карты) и
         сравнить с `max(N)` по флагам; не совпало — `sprinter-cc` падает
         с внятным сообщением.  Ничего не ломает в существующих
         приложениях, ставится сразу.
      2. **Генерировать самим `sprinter-cc`** — он уже так делает, когда
         банков нет (`const unsigned char n_banks = 0;`).  Правильнее по
         сути (единственный источник истины), но требует убрать константу
         из всех банковых приложений, иначе дубль символа: `tests/banked`,
         `banktest`, `banklocl`, `bankedbg`, `applications/PoP/roomtest`.

Phase 1 — file-level bin-packing (`toolchain/auto_bank.py`) — когда
проект перерастёт ~30 KB кода: парсинг размеров из .rel/.map,
first-fit-decreasing, уважение `#pragma codeseg BANKn`, перелинковка,
печать плана.  Phase 2-5 (rebalance, banks.toml, function-level) —
по потребности.

## ОБЯЗАТЕЛЬНОЕ ДЛЯ V2

### Turbo-C-style graphics API (BGI-like) — Фаза 1 ГОТОВА (256, 2026-07-08)

Архитектура: `graphics.h` — mode-agnostic слой (libc/bgi/*.c в
sprinter.lib), режим задаёт driver-архив; выбор линковкой через
`sprinter-cc --gfx 256` (16 — позже, тем же leaf-split'ом; см.
memory/bgi_two_lib_design).

**Фаза 1 (256, реализовано и проверено в MAME — tests/bgitest):**
initgraph/closegraph/graphresult/cleardevice, setcolor/getcolor/
setbkcolor/getbkcolor, getmaxx/getmaxy/getmaxcolor, putpixel/getpixel,
moveto/moverel/getx/gety, line/lineto/linerel, rectangle, bar, circle,
outtext/outtextxy.  initgraph грузит EGA-палитру 0..15.  Пакетные
примитивы (circle) — одна W3-скобка на весь примитив (raw-плот), иначе
на порядок медленнее.

**Фаза 2a/2b/2c ГОТОВЫ (2026-07-08, проверено в MAME tests/bgitest):**
- 2a: arc, ellipse, drawpoly (Q7-тригонометрия, БЕЗ 32-бит).
- 2b: setfillstyle/getfillsettings + 10 паттернов Borland, bar (с
  паттерном), bar3d, fillpoly (scanline min/max), fillellipse (isqrt).
- 2c: floodfill (scanline span; медленный — self-bracket чтение, но
  корректный), pieslice, sector.

**Фаза 2d-1/2/3 ГОТОВЫ (2026-07-08, MAME tests/bgitest):**
- 2d-1: getimage/putimage/imagesize (COPY/XOR/OR/AND/NOT_PUT), raw-блит
  в одной W3-скобке (_gfx_getpixel256_raw).
- 2d-2: setlinestyle/getlinesettings (SOLID/DOTTED/CENTER/DASHED/USERBIT
  + NORM/THICK) — line/lineto/linerel/rectangle/drawpoly.
- 2d-3: settextstyle/gettextsettings/textwidth/textheight — масштаб
  1..10, HORIZ/VERT, прозрачный фон (свой scaled-рендер поверх 8×8).

**Фаза 2d (осталось):** setviewport/clearviewport + клиппинг (инвазивно —
трогает все примитивы), settextjustify, setaspectratio (пиксели 320×256
неквадратные — круги визуально эллиптичны), setfillpattern (USER_FILL),
setactivepage/setvisualpage (2 страницы в gfx уже есть).  Оптимизации:
floodfill на raw-чтении; ellipse/arc в одной W3-сессии.  Потом drv16 +
sprinter_gfx16.lib (--gfx 16).
Acceptance: типичная BGI-программа переносится без существенных
правок.  Референс: Turbo C 2.x BGIDEMO.

### IM2 Interrupt Handlers — Phase 1 ГОТОВ (2026-07-06, libc/irq, tests/irqtest; Phase 2: CBL/ISA/цепочки)

User-ISR через IM 2 — timer ticks, музыка (AY/COVOX), real-time
игры, async input.  Phase 1 реализован БЕЗ отдельного memory mode:
libc/irq (<irq.h>: irq_install/irq_remove), таблица в BSS с runtime-
выравниванием, jp-заглушка внутри таблицы, чейн к DSS всегда;
работает в tiny/big, в small/huge — EINVAL.  Детали и отличия от
исходного плана: docs/im2_isr_design.md.

### Прочее v2

- [ ] **Audio API** — AY-3-8910 + COVOX (требует IM2)
- [ ] **ISA-8 slot support** — ZX-Bus карты (требует IM2)

## GFX: расширения по accelerator_doc.txt

Quick wins:
- [ ] block-size через `LD A,(nn)` вместо SMC (док разрешает LD A,(HL/BC/DE))
- [ ] кэширование block-size между burst'ами (accel помнит размер)

Новые возможности:
- [x] ~~пакетное чтение/запись массива байт через акселератор~~ —
      сделано 2026-07-11 (Фаза A спрайтового дизайна,
      docs/sprite-api-design.md): leaf `_bgi_copy_rows_raw` (accel
      block-copy LD L,L, до 256 байт/burst, размер блока армируется
      один раз) + ядро `gfx_blit_part` (клиппинг, полосы ≤256);
      putimage(COPY_PUT) и getimage переведены (регресс tests/bgi_img
      1:1 с per-pixel эталоном, подрежимы банков на accel-пути —
      tests/gfxbanks).
- [ ] **вертикальный copy-leaf `_bgi_copy_cols_raw`** (режим LD A,A —
      вертикальная копия, Port_Y двигается сам как у vfill).  Анализ
      2026-07-12: для СПРАЙТОВ требует column-major хранения (читать
      линейный буфер вертикально нельзя — Port_Y не действует вне
      видеоокна; референс docs/samples/balls пишет колонками и потому
      выводит спрайт ТРАНСПОНИРОВАННЫМ — незаметно только на
      симметричном шаре) → формат несовместим с getimage, не делать.
      А вот **вертикальный HEAL формат-независим** (экран→экран) и
      выгоден для узких высоких областей: L-полоска 2×16 при
      горизонтальном движении спрайта = 2 burst'а вместо 16 (8×
      меньше оверхеда); выбор ориентации по форме — как в
      _gfx_rectfill256.  Нюанс: между read и write колонки Port_Y
      надо вернуть на y0 (стоп → OUT → ре-арм, ~20Т/колонку).
      Делать вместе с L-strip оптимизацией heal.
- [ ] **паттерны через акселератор + FF-прозрачность** (идея 2026-07-11):
      всё, что сейчас рисуется по-пиксельно из-за «дырок», можно гнать
      burst'ами через банк 0x58 — дырки паттерна кодируются 0xFF и
      отбрасываются железом на записи:
      - стилизованные линии (_bgi_styled_line: DOTTED/DASHED/CENTER/
        USERBIT сейчас per-pixel): построить 16-байтовый шаблон строки
        из 16-бит маски (бит=цвет, 0=0xFF) и повторять accel-copy;
      - fill-паттерны (_bgi_fill_span, LTSLASH_FILL и пр.): 8-байтовые
        строки-шаблоны 8×8 паттерна тем же способом;
      - ВНИМАНИЕ: на MAME 0.283 запись FF через 0x58 портит теневое
        ОЗУ (частичный скип, см. sprite-api-design «Результаты Фазы 0»)
        — включать после подтверждения полного подавления на железе,
        либо через 0x5C + пере-heal.
- [x] ~~gfx_blit / putsprite / gfx_heal / movesprite~~ — Фаза B
      сделана 2026-07-11 (docs/sprite-api-design.md §3): GFX_BANK_*
      константы + gfx_blit/gfx_blit_part/gfx_heal в gfx.h,
      putsprite/movesprite в graphics.h; проверено tests/sprites
      (прозрачность, клиппинг 4 краёв, heal src==dst, чистый след
      movesprite, атлас).  Осталась Фаза C — пример-курсор.
- [x] ~~**managed-движок спрайтов v2**~~ — СДЕЛАН 2026-07-12/13
      (sprite.h: retained-модель + batch-пасс, см. пункт batch-пасса
      ниже) и расширен: W0-атласы (.atl, atlas_load/atlas_sprite_init,
      ISR-стаб — §9в), авто-анимация кадровая + tween (sprite_anim/
      sprite_moveto/sprite_update тикер — §9г), gfx_pal_sync/fload/
      fsave.  Замерен бюджет: ~26К тактов на спрайт 16×16, лимит 14
      спрайтов на стабильные 48 fps (§9д); демо examples/space,
      examples/rpgwalk (+rpgprof — профайлер кадра).
- [ ] `gfx_xor_rect` / `gfx_or_rect` / `gfx_and_rect` / `gfx_invert_rect`
- [ ] шрифты ≠ 8×8: gfx_set_font_data(ptr,w,h,advance), proportional,
      8×16/16×16, отдельный font_id API; font-quad для 640×256
      (per-cell палитра через дескрипторы 0x0300..0x039F)

Оптимизации (не сейчас):
- [x] ~~stride-арифметика в _bgi_copy_rows_raw — вон из горячего
      цикла~~ — сделано 2026-07-12 (профиль examples/balls vs
      docs/samples/balls): универсальный leaf — SMC-патч страйдов в
      8-битные add/adc-цепочки при входе (140Т → 73Т/строку, BC/push/
      pop/ex ушли); heal — отдельный `_bgi_heal_rows_raw` без адресной
      арифметики вообще (77Т/строку против 204Т; референсный уровень).
      Шар 16×16: ~9.6 → ~6.6 кТ (heal 16×173 + блит 16×236).  Регресс:
      blitw/bgi_img/sprites/balls — 1:1.  Остатки разрыва с asm-
      референсом — C-обвязка вызовов (batch-пасс, см. ниже) и спец-
      blit-leaf (dstride 0: −30Т/строку — делать по замеру).
- [x] ~~**batch-пасс для спрайтов** (движок v2, §9.1)~~ — СДЕЛАНО
      2026-07-12/13: движок v2 (sprite.h: sprite_init/update/flip,
      retained per-page dirty), два прохода heal→blit под одной
      W3-скобкой/банком, спец-leaf'ы `_bgi_blit_rows_raw` (dstride 0,
      SMC sstride) и `_bgi_heal_rows_raw`, лин-ядра ≤64×64, ОДИН DI на
      спрайт (санкция 2026-07-12), noclip-ядра + funcptr-диспетч
      `_gfx_blit_fn`/`_gfx_heal_fn` (см. sprite-api-design §9б).
      Замер dev-MAME 16 шаров uncapped: clip 50 → noclip 60-61 fps
      (+20-22%); с vsync-капом кадр < 20 мс.  Исходный уровень был
      24 fps.  (Диагноз разрыва с asm-референсом — per-call C-обвязка,
      не W3-скобка — и путь оптимизации описаны в sprite-api-design
      §9-9б.)  На железе перепроверить снятие src[0]-фикса (§9а).
- [ ] клип fast-path «прямоугольник целиком на экране» в gfx_blit_part
      (4 сравнения вместо полного пути) — делается в _gfx_blit_full/
      _gfx_heal_full вместе с batch-пассом.
- [ ] размер: общий clip-хелпер для gfx_blit_part (528 Б) и gfx_heal
      (362 Б) — клиппинг сейчас продублирован; кандидат −300..400 Б.
- [ ] gfx_line через accel для пологих диагоналей (runs ≥ 4-5 px)
- [ ] композитные примитивы с одним W3-swap на операцию
- [ ] ~~**спрайт-анимация: heal только открывшейся L-полоски**~~ —
      ОТВЕРГНУТО 2026-07-12 для ПРОЗРАЧНЫХ спрайтов (наш случай).
      L-полоска (bbox старой позиции минус новой) корректна ТОЛЬКО для
      непрозрачного full-box спрайта: тогда зону перекрытия целиком
      перекрашивает новый блит.  У прозрачного спрайта (0xFF через
      0x5C) в перекрытии дырки нового кадра НЕ перекрывают старые
      непрозрачные пиксели → на хвосте остаётся «полумесяц» старого
      изображения ВНУТРИ bbox-перекрытия, куда L-полоска не достаёт.
      Точный «новооткрытый» набор = old_opaque AND NOT new_opaque —
      это heal-с-маской по форме, а не по bbox, на Z80 дороже самого
      heal.  Подтверждено чтением референса docs/samples/balls:
      restore_bg лечит ПОЛНЫЙ 16×16 (ld b,16 + 16 байт/строку),
      L-полоску не использует.  Итог: heal остаётся full-box; экономия
      только через batch-пасс (амортизация обвязки), не через L-полоску.
      L-полоска годна лишь для непрозрачных тайлов фона — не спрайтов.
      Дисциплина «heal ВСЕ → блит ВСЕ» по-прежнему обязательна (слияние
      heal+блит per-sprite выкусывает соседа на перекрытии — проверено
      examples/balls 2026-07-11).
- [ ] **span-примитивы для узких прямоугольников**: при узкой стороне
      ≤ 8 линий chunked-rectfill проигрывает простому циклу
      _bgi_hspan_raw/_bgi_vspan_raw (подготовка+precompute ~340Т уходят
      впустую; break-even n≈9 — docs/accel-fill-budget.md).  Варианты:
      fast-path в диспетчере _gfx_rectfill256 («узкая сторона ≤ 8 →
      цикл span'ов») или просто задокументировать рецепт для
      пользователя.  Делать по результатам профиля — если узкие
      прямоугольники реально встречаются в горячем коде.

## Прочий backlog

- [ ] factoring parse_argv из crt0/crt0_banked в общий argv.s
- [ ] `restore SP on EXIT` (паттерн z88dk +pps) — проверить нужность
- [x] ~~CI: MAME с -aviwrite для screenshot-сравнения без человека~~ —
      `toolchain/mame_interactive.py`: авто-запуск .exe вводом с
      эмуляцией клавиатуры + Lua-таймер для скриншотов/выхода; сравнение
      визуальное (Claude читает скриншот), не автоматический diff.
      Полный справочник: docs/mame-autotest.md.
- [ ] linker duplicate-symbol warnings: сейчас фильтруются в
      sprinter-cc (наши overrides _puts/___sdcc_heap/_asctime/…);
      радикально — --nostdlib с ручным списком модулей z80.lib
- [ ] ZX Spectrum-совместимый target; ZX-Bus драйверы; PGO-tools

## Проверить на реальном железе

- [ ] **Port_Y banking trick** (адреса 0xC000+0x400*N → строки
      Y..Y+15): в MAME 0.283 НЕ работает (2026-06-01).  На железе:
      dual-write тест → если работает, кэшировать Port_Y в putpixel
      (~8× меньше OUT для Брезенхэма); если нет — вычистить из доков.
- [ ] **Подрежимы видеостраниц #50..#5F** (tests/gfxbanks +
      tests/sprites, дизайн docs/sprite-api-design.md): перепрогнать
      на железе и сверить со скриншотами MAME.  Три открытых вопроса: (1) 0x58 — полное
      подавление FF-записи (док/master-MAME) или FF протекает в
      теневое ОЗУ (MAME 0.283)?  (2) FF в VRAM — цвет 255 (MAME) или
      display-подстановка фона из ОЗУ (тогда дешёвое стирание
      FF-заливкой, tests/fferase)?  (3) accel-путь = CPU-пути (в ПЛМ
      пути физически разные).
- [ ] fdmax: лимит манипуляторов и зависание 9-го OPEN — MAME vs железо.
- [ ] CBL: щелчок перед первым проигрыванием звука за сессию (tests/
      cbltest, tests/cblwav) — воспроизводится ТОЛЬКО на первом запуске
      программы за сессию MAME, не зависит от содержимого потока
      (тон/тишина/речь одинаково).  Похоже на разовый прогрев звуковой
      подсистемы MAME при первой активации канала — на реальном железе
      скорее всего отсутствует.  Проверить и закрыть либо описать как
      реальный аппаратный эффект.
- [ ] **CBL: помехи в звуке при движении мыши** (tests/cblstream,
      2026-07-07) — при потоковом воспроизведении движение мыши даёт
      слышимые артефакты.  Гипотеза (НЕ подтверждена): мышь может
      делить SIO-канал/детект-бит с клавиатурой (порт 0x19 бит 0,
      см. _irq_tramp.c), и трамплин классифицирует байты мыши как
      «клавиатура» → сразу chain на DSS (0x0038), пропуская проверку
      CBL-хука для ЭТОГО прерывания — при движении мыши часть тиков,
      которые должны были обслужить CBL-насос, уходят мимо, кольцо
      недоливается.  Нужно исследование (MAME-дамп/лог трамплина).
- [ ] **kbd_raw: частые Rx-overrun в MAME при тапах** (roomtest,
      2026-07-22) — замеры watchpoint-счётчиками: при стабильном
      удержании клавиши overrun'ов ноль, но каждый быстрый тап (5 байт:
      E0 74 + E0 F0 74 при FIFO 3) даёт overrun.
      **Уточнение 2026-08-01 (по коду драйвера, прежняя гипотеза
      НЕВЕРНА):** dev-MAME per-byte INT ДАЁТ —
      `mame/sources/MAME/src/mame/sinclair/sprinter.cpp`
      `on_kbd_data()` ставит `m_irqs->in_set<1>()` на каждый принятый
      байт.  Но тут же заводит `m_irq_off_timer` на **32 такта CPU**, и
      `irq_off()` снимает линию — то есть импульс, пришедшийся на наше
      DI-окно, теряется НАСОВСЕМ (байт остаётся в FIFO до следующего
      IRQ или кадрового 50 Гц).  А DI-окна у нас длинные: ядра
      акселератора держат `di` на весь блит (`libbgi/bgi256/
      _bgi_blit_cols_raw.c`), это сотни микросекунд против 32 тактов.
      **Замерено 2026-08-01 (PoP roomtest, счётчики в MAME): обе
      «наши» гипотезы отпали.**  (а) `kbd_raw_poll()` из главного цикла
      не меняет ничего (9/10 с ним и без); (б) снятие `di` в accel-ядрах
      УРОНИЛО машину — режим «акселератор при EI» тут недоступен; и сама
      длина DI ни при чём (в замороженном кадре без блитов потерь
      БОЛЬШЕ).  Байт теряется ДО чтения порта: на 49 прочитанных байт
      только 28 входов в клавиатурную ветку, т.е. ~44 % импульсов
      запроса не обслужены и 3-байтовый FIFO переполняется.  Главный
      подозреваемый — `m_irq_off_timer` в sprinter.cpp: он ОДИН на два
      источника (экран + клавиатура), `irq_off()` гасит обе линии, так
      что кадровое прерывание способно обрезать клавиатурный импульс.
      **ЗАКРЫТО в рабочем объёме 2026-08-01:** лечится ПЛОТНЫМ опросом —
      `kbd_raw_poll` повешен idle-хуком графики (`gfx_set_idle_hook`,
      новый API libbgi) на ожидание кадра, где процессор всё равно
      простаивает ~2/3 периода.  35 нажатий с зажатым Shift → 35
      дошедших make против 9 из 10 без хука.  Пользователь на ручной
      проверке отмечает, что редкие пропуски всё же ощущаются — остаток
      отложен до финальной полировки, следующий шаг описан там же.
      Полный протокол — applications/PoP/roomtest/TASKS.md, KBD-1.
      Recovery-политика уже терпима к частым overrun'ам (селективный
      wipe, docs/kbd-games.md), но аккорды «держу →, тапнул ↑» и
      «держу Shift, тапаю ←» остаются уязвимы.

## Known quirks (зафиксированы, обходы в libc)

- ESTEX $46 ENV: A=0 это NOT FOUND (док врёт) — memory/sprinter_platform
- ESTEX WRITE $14: на успехе DE=0, не счётчик; успех = CF=0 & A=0 —
  memory/estex_write_de_quirk
- DSS: 8 манипуляторов, 9-й OPEN вешает систему; _fd_guard в libc —
  memory/dss_fd_limit
- SDCC z80 `if(n!=g)g=n;` пишет (n-g) — memory/sdcc_z80_cmp_store_a_bug

---

# История — закрытые этапы

## Этап 10 — libc: сплит + FILE v2 + Solid-C (2026-07-05/06) ✅

Полный план/итоги: docs/libc-roadmap.md.  Кратко:
- вся libc разложена «1 функция = 1 модуль» (~250 модулей, wildcard-
  сборка, DCE на уровне файлов): gfx_text −4.4 КБ, timedir/ls/stattest
  −3 КБ и т.д.; правила asm-связок: docs/libc-split-asm-cases.md
- **FILE* v2 (B+)**: ленивый буфер 512 на чтение/запись с
  автопереключением, таблица OPEN_MAX=8, flush-on-exit, ungetc,
  fprintf/vfprintf, fdopen/freopen/fclosall/fgetpos/fsetpos; горячие
  пути fgetc/fputc/fgets на asm (fgets 100 КБ: 144с unbuffered-оценка
  → ~1 с).  Дизайн: docs/file-buffering-design.md
- **scanf/fscanf/sscanf** — своё C-ядро (в SDCC z80 нет)
- **Solid-C совместимость закрыта**: <dos.h> (даты/диски/absread),
  errno-алиасы, <sprinter_solid.h> — docs/solid_c_compatibility.md
- гигиена: stale .rel чистка, все 43 теста в make all, размерный
  регресс (make size-check), контракт заголовков docs/libc-headers.md
- справочник API: docs/libc-reference.md

## Этап 9 — memory modes (tiny/small/big/huge/manual) ✅ 2026-05-30

`--memory MODE` в sprinter-cc; crt0-семейство (crt0/minimal/small/
banked); small: ESTEX GETMEM+SETWIN2 до gsinit, auto-detect W2 по
порту 0xC2; big/huge: параметризация crt0_banked/bank.s через
BANK_W1; --debug, --stack-size.  Детали: memory/memory_modes_
implemented, memory/sprinter_memory_modes.

Дизайн-решения: одна sprinter.lib на все режимы (DCE per-member);
gfx.lib отдельно не нужен; libc_banked + sprinter_home.lib — идея
на потом (триггер: HOME забит user-кодом).

## Этап 8 — графика ✅

320×256×256 + 640×256×16, акселератор (Fill h/v, SMC block-size),
Брезенхэм, bitmap font (WIN_GET_ZG, interleaved), gfx_text.
memory/sprinter_graphics*, sprinter_accelerator, sprinter_font_format.

## Bank-local data ✅

--codeseg/--constseg/--dataseg BANKn + mkexe -p 0; фикс трамплина
(pop bc/out (c),b — сохраняет A); malloc из банка прозрачен (heap в
W2).  memory/bank_local_data_pattern.

## Этапы 5-7 и ранняя libc ✅

- malloc/free (SDCC + runtime/heap.s в W2), page allocator
  (mem_alloc_pages, ESTEX $3C-$3E + BIOS $C4), bank_read/bank_write
- crt0 argv-парсинг (IX-prefix, CP/M-space quirk, APPINFO basename),
  sprinter-cc wrapper со всеми опциями
- errno+strerror/perror, open state-machine, atexit, setjmp/longjmp,
  sleep, ENV API ($46), ffirst/fnext, getdatetime/setdatetime,
  chdir/getcwd/mkdir/rmdir, conio (полный), mouse (RST 30h, 14 ф-й),
  POSIX time API, sys/stat, assert
- text I/O split (stdio fast / conio attr) — memory/text_output_api_split
- SDCC stdlib НЕ переписываем — memory/sdcc_stdlib_works
