49 Commits

Author SHA1 Message Date
snark13 52636a5d6e libbgi: выделить графику BGI в отдельную библиотеку + тест спрайтов
Графика вынесена из libc/ в новую библиотеку libbgi/:
  - common/  — mode-agnostic математика и состояние (один исходник,
    .rel попадает в оба driver-архива);
  - bgi256/ + bgi16/ — mode-specific leaf'ы (raw-плот/чтение/спаны);
  - include/ — graphics.h + gfx.h; _bgi.h — внутренний заголовок.
Собираются lib/bgi256.lib (и bgi16.lib в Фазе 2); выбор режима
линковкой через sprinter-cc --gfx 256|16.  libc/ теперь без графики.

tests/bgi_img — тест спрайтов getimage/putimage/imagesize (5 операций
COPY/XOR/OR/AND/NOT + XOR-round-trip + self-check imagesize).
Проверен автотестом в MAME.

Примечание: make size-check пока красный (gfx_dbuf/gfx_demo выросли
после реорга) — закрыть по завершении миграции libbgi.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 14:18:33 +03:00
snark13 3bf50f7ff7 docs: единый справочник по автотестам в MAME; убрать метод AUTORUN.BAT
- docs/mame-autotest.md — исчерпывающий документ: запуск, ввод команд,
  скриншоты, завершение сессий, анализ, раскладка клавиатуры, все квирки.
  Одного этого документа достаточно, чтобы работать с MAME в режиме
  автотестирования.
- mame_interactive.py теперь единственный инструмент: авто-запускает exe
  вводом пути (a:\<exe>+Enter), --step опционален (доп. ввод в программу),
  умные дефолты снимков/таймаута.
- удалён mame_auto_test.py (старый метод через AUTORUN.BAT chainload) и
  все упоминания AUTORUN.BAT в доках; интерактивный ввод его заменил.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 16:41:45 +03:00
snark13 657c6d2955 libc: BGI Фаза 2d-3 — settextstyle (масштаб и направление текста)
settextstyle/gettextsettings/textwidth/textheight: DEFAULT_FONT 8×8,
целочисленный масштаб 1..10, HORIZ/VERT (поворот 90° CCW), прозрачный
фон. Свой scaled-рендер (_bgi_draw_scaled) читает глиф через leaf
_bgi_font_rows (interleaved системный шрифт) и рисует блоки size×size
raw в одной W3-скобке. outtext/outtextxy переведены на него. Проверено
в MAME (tests/bgitest): размеры 1..4 + вертикальный текст.

Доки обновлены (Ф2d-1/2/3 готовы; осталось viewport/клиппинг).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 16:18:48 +03:00
snark13 b26560c409 libc: BGI Фаза 2d-2 — setlinestyle (стили и толщина линий)
setlinestyle/getlinesettings: SOLID/DOTTED/CENTER/DASHED/USERBIT +
NORM/THICK. _bgi_styled_line — Брезенхэм с 16-битной маской (пропуск
пикселя по биту) и дублированием ±1 перпендикулярно оси для THICK;
SOLID+NORM идёт быстрым путём (accel _bgi_lineseg). line/lineto/linerel/
rectangle/drawpoly переведены на него. Проверено в MAME (tests/bgitest).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 16:14:03 +03:00
snark13 25cb7ba554 libc: BGI Фаза 2d-1 — getimage/putimage/imagesize (спрайты)
Растровые образы: imagesize (4 байта заголовка w,h + w*h пикселей),
getimage (захват прямоугольника), putimage с COPY/XOR/OR/AND/NOT_PUT.
Блит идёт raw в одной W3-скобке — добавлен _gfx_getpixel256_raw в gfx +
leaf _bgi_read_raw в drv256. Проверено в MAME (tests/bgitest): захват
спрайта, 3 COPY-копии, XOR/OR/COPY поверх фона.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 16:09:51 +03:00
snark13 dc37a14010 libc: BGI Фаза 2c — floodfill/pieslice/sector
floodfill — скан-строчная заливка области до границы (self-bracket
чтение: корректно, но медленно; raw-оптимизация в TODO).
pieslice/sector — залитые сектора круга/эллипса: границу (центр→дуга→
центр) прогоняем через _bgi_poly_edge и заливаем min/max по строкам,
как fillpoly (для >180° возможен перелив — упрощение). Проверено в
MAME (tests/bgitest): floodfill круга, круговая диаграмма, штрих-сектор.

Доки/справочник/память обновлены (Ф2a-c готовы; Ф2d = images/viewport/
text-style/line-style — осталось).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 16:04:04 +03:00
snark13 ecb419efda libc: BGI Фаза 2b — заливки (setfillstyle/bar/bar3d/fillpoly/fillellipse)
setfillstyle/getfillsettings + 10 стандартных 8×8 паттернов Borland.
bar теперь честно учитывает стиль заливки; bar3d (3D-брусок), fillpoly
(scanline min/max по строкам через брезенхэмовский проход рёбер),
fillellipse (полуширина строки через целочисленный _bgi_isqrt — без
32-бит). Общий _bgi_fill_span (SOLID/EMPTY/паттерн) с клипом, поверх
raw-hline в одной W3-скобке. Проверено в MAME (tests/bgitest): solid/
hatch бары, bar3d со slash, синий fillellipse, xhatch-треугольник.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 15:58:06 +03:00
snark13 5a48f7fafb libc: BGI Фаза 2a — arc/ellipse/drawpoly
Дуги/эллипсы через целочисленную тригонометрию Q7 (_bgi_trig.c, ×128) +
общий рисователь полилинией (_bgi_arc_draw.c). arc(x,y,st,end,r),
ellipse(x,y,st,end,xr,yr), drawpoly(n,pts). Проверено в MAME (tests/
bgitest): окружность/эллипс/дуга/полигон рисуются корректно.

ВАЖНО: тригонометрию считаем в int (Q7), НЕ через (long)…>>8 — первая
версия на 32-бит арифметике рисовала эллипс прямоугольником (SDCC/z80
криво собирает 32-бит; см. memory/avoid_32bit_arith_z80). Q7 даёт
радиус×значение ≤ 255×128 < 32767 — всё влезает в 16 бит.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 15:50:00 +03:00
snark13 e761d21505 libc: graphics.h — Turbo-C BGI-слой, Фаза 1 (режим 256)
Функционально-совместимый с Turbo-C <graphics.h> поверх gfx.h.
Архитектура: mode-agnostic математика (libc/bgi/*.c → sprinter.lib) +
driver-leaf'ы per-режим (libc/bgi/drv256/*.c → sprinter_gfx256.lib).
Режим выбирается линковкой: sprinter-cc --gfx 256 (16 — позже, тем же
leaf-split'ом). Один код работает в любом режиме без правок.

API: initgraph/closegraph/graphresult/cleardevice, set/get color+bkcolor,
getmaxx/y/color, put/getpixel, moveto/moverel/getx/gety, line/lineto/
linerel, rectangle, bar, circle, outtext/outtextxy. initgraph грузит
EGA-палитру 0..15. Пакетные примитивы (circle) — одна W3-скобка на
примитив (иначе на порядок медленнее). Проверено в MAME (tests/bgitest).

Попутно: gfx_getpixel256 в libc/gfx. size-check без регресса, baseline
обновлён. Детали: memory/bgi_two_lib_design, docs/TODO.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 15:40:49 +03:00
snark13 72466f7cad toolchain: скриптовый интерактивный ввод в DSS через MAME
mame_interactive.py печатает произвольный текст в командную строку DSS,
дёргая поля AT/PS-2-клавиатуры :kbd:ms_naturl через Lua set_value
(at_keyboard сам генерит scancode'ы → SIO Z84C015 → DSS). Раньше
инъекция шла в ZX-матрицу :IO_LINE*, которую DSS не читает — отсюда
«нет эффекта». Полная раскладка char→(port,mask,shift) с авто-Shift.

Квирки: attotime.seconds целое (субсекунды через attoseconds/1e18),
клавишу держать коротко (~0.06с, иначе автоповтор), дискета без
AUTORUN.BAT → приглашение C:\>. Проверено end-to-end: dir<Enter> и
запуск теста набором a:\rt_test.exe<Enter> (Shift для ':' и '\').

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 12:18:37 +03:00
snark13 d652f89240 toolchain: автотест .exe в MAME без участия человека
AUTORUN.BAT chainload из system.bat (правится пользователем один раз)
+ mame_auto_test.py: кладёт .exe и сгенерированный AUTORUN.BAT на
дискету, гоняет MAME с Lua-таймингом (register_periodic +
manager.machine.time) для скриншотов и выхода по таймауту.

Natural keyboard (Lua natkeyboard:post/post_coded, -autoboot_command)
и прямая инъекция через ioport.fields[...]:set_value() не работают на
этом драйвере — перепробовано разными способами; AUTORUN.BAT chainload
оказался единственным надёжным путём запуска без участия человека.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 11:56:54 +03:00
snark13 46bd9ad0f1 gfx/time: vsync через polling бита кадра, sleep/delayms без halt-подсчёта
gfx_wait_vsync() и sleep() раньше предполагали, что КАЖДОЕ прерывание
на векторе 0xFF — кадровый тик; с CBL/клавиатурой на том же векторе
это уже не так.

gfx_wait_vsync(): вместо halt — polling бита 5 порта 0xFE (реальная
позиция луча, см. MAME kbd_fe_r), доступного пока включён CBL bit7
порта 0x004E. Разделяемое владение портом с CBL через
_cbl_port_ref/unref (тот же ref-counting паттерн, что у IM2-таблицы) —
cbl_close() возвращает "немой" режим вместо полного выключения, если
gfx ещё держит ссылку. Fallback на halt при таймауте.

sleep()/delayms(): калиброванный busy-wait по духу delayms.asm вместо
подсчёта halt-пробуждений. Калибровка одна на кадр (не на секунду —
не переполняет uint16_t и не требует умножения/32-бит арифметики),
общий движок libc/time/_sleep_calib.c для обеих функций. Fallback на
старое поведение при EBUSY (фрейм-хук занят другим irq_install()).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 10:47:57 +03:00
snark13 5086c47f0f libc: IM2 Phase 2b — звук CBL/COVOX через callback fill(), без кольца libc
CBL уже имеет аппаратный буфер 256 Б (2×128, двойная буферизация на
стороне железа) — держать поверх него ещё одно кольцо в libc было бы
лишней копией. cbl_open(freq, fmt, pump_mode, underrun_mode, fill)
регистрирует callback, вызываемый из ISR за очередным блоком; он сам
пропихивает данные приложения (откуда угодно) через cbl_push_otir()/
cbl_push_accel() — без промежуточного буфера.

- два насоса: OTIR (порт 0x4F) и ACCEL (акселератор, спец-страница
  EMM 0xFD@0xC000); OTIR+16-бит запрещён (EINVAL) — по исходнику MAME
  порт данных физически не может собрать 16-бит сэмпл из пары байт;
- форматы CBL_FMT_MONO8/16/STEREO8/16, частоты 7.8..109к;
- CBL_UNDERRUN_APP (по умолчанию, недолив не наша забота) /
  CBL_UNDERRUN_SILENCE (буфер тишины malloc'ится только в этом режиме);
- tests/cbltest: матрица 64 комбинации (2 насоса × 8 форматов × 4
  частоты); tests/cblwav: banked-стрим речи с дискеты (физстраницы
  кэшированы заранее — mem_get_page нельзя звать из fill()/ISR);
  tests/cblstream: единственный случай с собственным кольцом уровня
  приложения (диск нельзя читать из fill()).

Verified в MAME 2026-07-07 — все три теста работают.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 21:21:54 +03:00
snark13 8a952b99eb irq: Phase 2a — CTC-таймер на отдельном векторе 0x06 (Z84C015)
- irq_ctc_install(handler, div2, div3) / irq_ctc_remove: канал 2 CTC
  делит видеотакт 875 кГц (1 тик = 1 знакоместо), канал 3 считает от
  него и прерывает; f = 875000/(div2*div3), div 0 = 256. Пресет
  IRQ_CTC_VSYNC_DIV2/3 = 112*160 — точное начало кадра ~48.8 Гц БЕЗ
  примеси клавиатуры (вектор 0x06 отделён от общего 0xFF)
- CTC-трамплин: полный сейв -> handler -> EI/RETI; RETI обязателен
  (daisy chain Z84C015 снимает IUS только по опкоду RETI); к DSS не
  чейнится — личное прерывание
- общая IM2-таблица под счётчиком ссылок (_irq_table.c): кадровый и
  CTC-хендлеры независимы, последний unref возвращает I/IM 1;
  atexit-уборка глушит CTC обязательно (иначе кГц-прерывания душат
  шелл после выхода)
- порты/слова по docs/samples: CH0=0x10/CH2=0x12/CH3=0x13,
  0x57/0xD7/вектор в CH0, стоп 0x03
- irqtest: CTC-vsync параллельно с кадровым + произвольная частота;
  MAME: frame 48 Гц, ctc(vsync) 49 Гц (parallel frame жив),
  ctc(50x50) 350 Гц точно по формуле, remove/выход чистые

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 09:15:04 +03:00
snark13 5184415fc4 irq: фикс Phase 1 после отладки — DSS работает в IM 1, чейн всегда на 0x0038
Первая версия висла на первом прерывании. Две причины (verified по
docs/samples/sprinterIntLib.asm, SIO_CTC_KEY.asm и исходникам MAME):

- DSS работает в IM 1 (обработчик 0x0038); I=0x3F — наследие Spectrum
  ROM, НЕ таблица: чтение [I<<8|0xFF] давало мусор (0x00BF) и прыжок в
  никуда. Чейн из трамплина теперь ВСЕГДА jp 0x0038 (interrupted-PC на
  стеке = имитация RST 38); irq_remove безусловно восстанавливает IM 1
- CBL-фильтр по биту 7 порта 0xFE убран: при выключенном CBL бит
  подтянут к 1 (MAME kbd_fe_r: data |= 0xE0) — каждый кадр ложно
  уходил в чейн, user-handler не вызывался бы. Вернуть в Phase 2
  вместе с поддержкой CBL

Попутно подтверждено: порт 0x19 = SIO-A RR0 (Z84C015), бит 0 = Rx
Available; вектора встроенной периферии SIO 0x10..0x1E / CTC 0x06
(заливка 257×H ловит любые); внешний вектор 0xFF.

irqtest: диагностика I до установки + фаза без ESTEX; прогон в MAME:
~49 Гц, клавиатура жива, remove останавливает тики, чистый выход.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 09:05:28 +03:00
snark13 a6fe50e247 libc: IM2 user-ISR, Phase 1 — libc/irq (irq_install/irq_remove) + irqtest
Проще исходного плана (docs/im2_isr_design.md обновлён): отдельный
--memory im2 не понадобился.

- <irq.h>: irq_install(handler) — вызов ~50 Гц только на КАДРОВЫХ
  прерываниях (клавиатура bit0:0x19 и CBL bit7:0xFE отфильтровываются);
  штатный обработчик DSS чейнится ВСЕГДА (SMC-jp, адрес из старой
  IM2-таблицы по регистру I) — клавиатура/SYSTIME/мышь живут
- вектор-таблица: 513 Б BSS + runtime-выравнивание; Sprinter шлёт
  только вектор 0xFF, поэтому jp-заглушка лежит внутри самой таблицы
  по смещению H — без linker-областей и правок crt0
- трамплин: полный сейв обоих наборов+IX/IY вокруг user-handler'а
  (ex af,af' как .db 0x08 — апостроф ломает препроцессор SDCC)
- tiny/big: работает (код в W2); small/huge: EINVAL по проверке
  адресов; irq_remove идемпотентен и висит на atexit (выход без
  снятия = I в памяти умершего процесса = крах шелла); old_I==0 → IM1
- tests/irqtest: тики за 3 с против time() (~50 Гц), живая клавиатура
  под handler'ом, остановка после remove, чистый выход
- docs: im2_isr_design (статус+дельты), libc-reference (<irq.h>), TODO

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 21:21:16 +03:00
snark13 110f69fb2e docs: П6 (MAME-смоук) закрыт — все тесты зелёные
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 21:11:28 +03:00
snark13 8531b25e75 runtime: фикс banked-режимов — _bank_pages переезжает из _DATA в _CODE
Регрессия от 961cfb7 (gsinit зануляет _DATA, 2026-07-04): crt0_banked
заполняет таблицу физических страниц _bank_pages ДО gsinit, а тот её
стирал — трамплины banked-вызовов читали нули и прыгали в незамапленную
страницу.  Висли ВСЕ banked-программы (banked/bankedbg/banklocl/
banktest); найдено MAME-смоуком.  Тот коммит перенёс crt0-приватные
_estex_* в _CODE, но _bank_pages в runtime/bank.s пропустил.

- runtime/bank.s: _bank_pages → .area _CODE (RAM, всегда замаплен —
  это же условие нужно и трамплину); +16 Б _CODE у программ с bank.s
- app.mk: exe теперь зависит от runtime/*.s — правка crt0/bank.s
  перелинковывает тесты без make clean (фикс иначе не подхватывался)
- эталон размеров обновлён (+16 Б у banked/bankedbg/banklocl/
  banktest/openenv — size-check поймал ровно их)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 21:06:20 +03:00
snark13 7187752b29 docs: справочник libc API, правила проекта в CLAUDE.md, актуализация TODO (П7)
- docs/libc-reference.md — сводный справочник по всем заголовкам:
  сигнатуры + описание + особенности ABI и квирки
- CLAUDE.md — сборка/проверка (make, size-check, MAME-workflow),
  правила libc (1 функция = 1 модуль, internal _-модули, русские
  комментарии, без = 0, asm-связки), ABI-шпаргалка, структура репо
- docs/TODO.md переписан: открытые задачи наверху (MAME/железо,
  auto-banking, v2: BGI/IM2/audio, gfx-расширения, Port_Y),
  закрытые этапы 5-10 сжаты в «Историю»; снят протухший пункт
  «FILE API rewrite для v2» (сделан в v1), fprintf/fscanf и др.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:47:33 +03:00
snark13 a4c8c79428 сборка: гигиена (П5) — stale .rel, все тесты в make all, размерный регресс
- lib/Makefile: stale .rel удаляются сверкой списка модулей перед
  упаковкой; штамп build/.modules триггерит перелинковку при любом
  изменении состава исходников (при смене списка архив сносится —
  на exFAT гранулярность mtime грубая, сравнение времён ненадёжно)
- top-level TESTS: все каталоги tests/ теперь собираются make all
  (43 программы; banktest переименован из banked.exe — конфликт имён
  с tests/banked); mdview2 добавлен в APPS
- размерный регресс: toolchain/size_check.py сверяет _CODE всех
  программ с docs/size_baseline.tsv; make size-check / size-baseline
- заголовки: контракт затенения SDCC задокументирован в
  docs/libc-headers.md; новый string.h (include_next + strlwr/strupr);
  из sprinter_compat.h убраны макросы min/max — конфликтовали с
  функциями из stdlib.h, и в Solid-C min/max тоже функции

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 16:50:39 +03:00
snark13 60373930fb libc: Solid-C совместимость (П3) + rename/isatty (П4); scanf-семейство
- <dos.h>: getdate/gettime/setdate/settime (структуры Turbo-C, обёртки
  над getdatetime), getdisk/setdisk (ESTEX $02/$01), absread/abswrite
  (BIOS $55/$56, rst 8 — номера найдены в solid-c DOS.ASM; сектор 0 =
  boot логического диска)
- scanf/fscanf/sscanf: своё C-ядро _scanf_core (%d %u %x %o %c %s,
  модификатор l, ширина, подавление '*', %%); в SDCC z80 scanf нет,
  asm solid-c не портируем из-за чужого ABI; 22 хост-теста ядра
- хвост П2: fdopen/freopen/fclosall/fgetpos/fsetpos поверх таблицы
  FILE; парсер режима и выдача слота вынесены в _file_mode/_file_slot
- rename() — ESTEX RENAME $10; isatty(fd) = fd <= 0 (tty только
  псевдо-fd 0/-1/-2: из CLI DSS манипуляторы идут с 1 — verified,
  fd 1 не резерв, под Flex Navigator его держит навигатор)
- errno.h: Solid-C имена ошибок (EZERO/EINVFNC/ENOFILE/...) как алиасы
- <sprinter_solid.h> — зонтичный заголовок для портирования;
  ltell/_setargv в sprinter_compat.h; div/ldiv — из SDCC (проверено)
- tests/solidt — smoke всех П3/П4 API, зелёный в MAME (вкл. absread
  boot-сектора с сигнатурой 55AA)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 16:35:58 +03:00
snark13 057dd615ba libc: квирки DSS — возврат WRITE и лимит манипуляторов; тесты fdmax/fbench
- ESTEX WRITE ($14) на успехе возвращает DE=0, а НЕ счётчик записанного
  (вопреки докам; solid-c в своём fflush тоже отключил сравнение по
  счётчику) — write() теперь судит по CF/A: CF=0&A=0 → n,
  CF=0&A!=0 → ENOSPC/-1
- DSS выдаёт 8 манипуляторов (fd 2..9; fd 1 держит шелл под запущенный
  exe), а 9-й OPEN не возвращает 06h — ВЕШАЕТ систему; предохранитель
  _fd_guard: счётчик в open()/close(), отказ EMFILE без захода в DSS
- tests/fdmax — эмпирика лимита (8 хендлов, затем EMFILE=6);
  tests/fbench — бенчмарк буферизации (floor 512-байтными read,
  оценка небуферизованного по 1-байтным, fgetc/fgets/fputc)
- filetest расширен: raw-probe возврата write, сценарий r+
  (чтение-запись-чтение с инвалидацией буфера), ungetc, fprintf

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 16:09:29 +03:00
snark13 48d552bf3a libc: FILE* v2 — буферизация потоков (вариант B+)
- единый ленивый буфер BUFSIZ=512 на чтение и запись с
  автопереключением направления (_F_DIROUT, _file_sync: запись
  сбрасывается write()-ом, readahead откатывается lseek-ом)
- статическая таблица OPEN_MAX=8 слотов вместо malloc для FILE;
  _fclosall через atexit — exit() сбрасывает несброшенную запись
- fread/fwrite: мелкое через буфер (memcpy), блоки >= BUFSIZ — мимо
  буфера одним syscall; горячие пути fgetc/fputc и сканер строк
  fgets (LDI до '\n') — на asm, SDCC на эти цепочки генерит ~90
  инструкций с IX-фреймом
- новое: ungetc (1 байт через hold, работает и на stdin),
  fprintf/vfprintf (vsprintf+fwrite), fflush(NULL) = все потоки
- фиксы stdio-review: fwrite ставит _F_ERROR при короткой записи
  (issue 3), fgets(n=1) возвращает пустую строку (issue 4)
- замер (MAME, HDD, 100 КБ): небуферизованная оценка ~144 с →
  fgetc 5 с (×29), fgets ~1 с; дизайн и отвергнутые варианты —
  docs/file-buffering-design.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 16:09:13 +03:00
snark13 4a081501d8 libc: сплит «1 функция = 1 модуль» — вся библиотека, wildcard-сборка
- bios/conio/env/errno/gfx/io/mem/mouse/stdio/stdlib/string/sys/time/
  video разложены по модулям: общие статики и helpers — в internal
  _-модулях (_conio.h/_mouse.h/_gfx.h/_palette.h/_atexit.h/_time.h)
- lib/Makefile: LIBC_C = wildcard libc/*/*.c — гранулярность файлов
  = гранулярность DCE линкера
- эффект _CODE: gfx_text 6986→2568 Б, gfx_mous −1745, gfx_demo/d16
  −542; ранее timedir −3270, ls −3098, stattest −2995
- комментарии оставшихся модулей переведены на русский; puts: убран
  мёртвый pchars; videomode_raw разложен на get/set
- docs/libc-split-asm-cases.md — правила asm-связок между модулями;
  docs/libc-roadmap.md — план этапа
- восстановлен examples/mdview/SAMPLE.MD (нужен make floppy)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 16:08:58 +03:00
snark13 46553f4e07 mdview2: фоновая сборка второго набора кодировки в паузах между клавишами; версия v1.0 (b3)
- индексатор порезан на резюмируемые шаги index_begin/index_step/index_finish;
  межшаговое состояние в статиках модуля, в docset_t не входит
- bg_build_start/bg_step: второй набор (UTF-8 при 8-битном первичном и
  наоборот) строится в idle главного цикла; холдаун после клавиш, спиннер
  погашен (g_bg_building) — фон незаметен
- F8 до готовности докручивает начатое фоном (ветка resume в build_doc),
  а не строит заново; общий setup вынесен в doc_setup
- кодировка в статус-баре показывается сразу (детект/F8), не дожидаясь
  конца индексации
- побочный фикс: UTF-конвертация впереди проверки останова — >4КБ абзац
  больше не обрывает конвертацию остатка
- README.md (новый, v1.0 b3), CHANGELOG.md; дискета: README/DEMO/CHANGES
  в трёх кодировках (пути автодетекта)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 19:33:34 +03:00
snark13 ae23d2dea2 mdview2: HEX-режим (F4) — дамп оригинального файла; версия v1.0 (b1)
Новый модуль mdview2_hex.c (WITH_HEX в conf): формат
' 0x012340 │ 16×hex │ 16 print', 30 строк, один атрибут.

- Дамп всегда ОРИГИНАЛЬНОГО файла (orig_file_phys), не активного буфера;
  ряд выровнен на 16 → один bank_read на ряд (не пересекает EMM-страницу),
  fb()/W3 не используются.
- Printable по текущей кодировке: CP866 как есть, CP1251/KOI8 через
  g_remap, UTF-8 — глиф на позиции лид-байта (continuation → '.') через
  новый utf_cp_glyph(), выделенный из конвертера enc-модуля.
- Навигация: ±16 / ±480 / Home / End; одна строка — аппаратный scroll()
  + подрисовка одного ряда (как MD/RAW); процент в статусе.
- F4 — тумблер HEX ↔ прежний вид; F2 из HEX уводит в MD; позиция при
  всех переходах через view_pos/view_reanchor (map_off orig ↔ active).
- F8 в HEX: hex-колонка неизменна, printable перерисовывается в новой
  кодировке; позиция не двигается.
- Help: версия v1.0 (b1), строка F4.

exe 25725 → 27893 (+2168). Проверено в MAME.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 13:19:08 +03:00
snark13 e5866d6ba4 mdview2: единая позиция при переключениях F2 (MD↔RAW) и F8 (8bit↔UTF-8)
Валюта позиции — байт-offset активного буфера:
- line_at_off(off): обратный перевод offset → MD-строка (бинарный поиск
  по seg_off, оффсеты сегментов монотонны);
- map_off(off, from, to): пропорциональный перенос позиции между
  буферами разного размера — один цикл restoring-деления, без
  __mullong/__divulong, точность from/65536;
- raw_pos()/raw_reanchor(off) в RAW-модуле; raw_seed_from через
  reanchor, raw_home стал приватным (только клавиша Home).

F2 RAW→MD: top_line = line_at_off(raw_pos()) — точное позиционирование.
F8 между готовыми наборами: view_pos → map_off → view_reanchor вместо
восстановления сохранённой позиции набора. Ленивая сборка — по-прежнему
с начала (в RAW с raw_reanchor(0) и откатом при неудаче).

Попутно: F8 в RAW-режиме больше не рисует MD-вид поверх RAW —
перерисовка по g_view.

exe 25100 → 25725 (+625). Проверено в MAME.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 22:00:24 +03:00
snark13 adf667c087 mdview2: слить inline_scan + scan_join_stream в scan_stream — exe 25616 → 25100
B2 stage2: общий цикл inline-форматирования/переносов/склейки в одном
scan_stream (mode: NONE / LIST / QUOTE / PLAIN); дублировавшиеся блоки
эмиссии пробела/символа, переноса с усечением и отката стиля — в одном
экземпляре. Старые имена — тонкие обёртки, API inline_scan для
table-модуля не изменился. styles_map умерла: emph_to_attr(ls, ATTR_TEXT)
тождественна ей.

Индексатор 9839 → 9315 Б. Проверено в MAME: переносы заголовков/списков/
цитат, таблицы, inline-маркеры на границе переноса, жёсткие переносы.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 21:26:13 +03:00
snark13 ffd179e064 mdview2: оптимизация размера — exe 28215 → 25616 (−2599 Б)
Раунд 1 (−1411): --max-allocs 100000 в Makefile (−848 кода) + снятие
всех нулевых инициализаторов file-scope переменных (_INITIALIZER
583→24; _DATA теперь зануляется в crt0).

Раунд 2 (−1188, индексатор 11019→9839): дедупликации в mdview2_index.c:
- classify_line: копия HR-проверки → вызов is_hr_raw;
- next_line() поверх row_end() вместо 9 копий «домотать до \n»;
- inline_marker: emph_to_attr(ls, base) вычисляется один раз (at);
- set_{nowrap,blank,code,hscroll}_cur → set_cur_flags(mask): строка
  code-блока делает один idx_put вместо трёх.

Проверено в MAME (README/UTF8TEST: маркеры, списки, цитаты, таблицы,
code-блоки, F8).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 21:15:43 +03:00
snark13 961cfb786d toolchain: gsinit зануляет _DATA (C-семантика статиков) + sprinter-cc --max-allocs
Все четыре crt0 (default/small/minimal/banked): gsinit теперь зануляет
_DATA и _BSS через общий zero_area, затем копирует _INITIALIZER.
Явные `= 0` у глобалов/статиков больше не нужны (они жгли байты
_INITIALIZER в образе). crt0-приватные переменные, записываемые ДО
gsinit (_estex_startup_ix и др.), перенесены из _DATA в _CODE (RAM).

sprinter-cc: новая опция --max-allocs N → SDCC --max-allocs-per-node
(агрессивнее аллокация регистров, меньше/быстрее код ценой времени
компиляции).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 21:15:42 +03:00
snark13 e1450ba7b4 mdview2: render_menu — объявление num[] в начало функции
Косметика (позиция декларации), на размер не влияет.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 23:10:46 +03:00
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
snark13 fabbc8129c mdview2: percent на 16-битной арифметике (убрать __divulong)
calc_md_pct/calc_raw_pct тянули 32-битное деление __divulong (+__muluint2ulong)
ради показа процента в статусе. Оба дают операнды ≤16 бит (≤18432 / ≤1024),
переполняет только *100. Новый pct16() масштабирует оба вниз и считает долю
циклом-вычитанием — 66 Б, НОЛЬ подтянутых арифм-хелперов. Точность ±1%
(на границах точно), для индикатора прокрутки незаметно.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 23:00:14 +03:00
snark13 977e2d3d4a mdview2: вернуть floppy к обычному README-диску (тест-каркас отработал)
Тест-файлы лимита 256 КБ (TABLES/LINES/HUGE) проверены в MAME и сняты с
диска. Сами файлы и генератор остаются в testfiles/ как архив для повтора.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 22:21:11 +03:00
snark13 a43b4d6703 mdview2 testfiles: LINES.MD ~250 КБ (лимит 18432) + HUGE.MD >256 КБ (кламп)
- LINES.MD: 22000->25000 строк (~250 КБ -> 16 страниц -> max_lines 18432),
  чтобы обрыв был ровно на заявленном лимите.
- HUGE.MD: ~340 КБ (проза ×4) для проверки клампа файлов >256 КБ.
- floppy кладёт HUGE.MD на диск.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 22:13:36 +03:00
snark13 e3342b63f3 mdview2: чинить детект лимита строк + кламп файлов >256 КБ
1. Лимит строк не показывал предупреждение: g_trunc_cause=TRUNC_LINES
   ставился в emit_seg, но главный цикл выходит по n_lines<max_lines ДО
   вызова emit_seg в переполненном состоянии (для code-block — один
   emit_seg на строку). Теперь ловим после цикла по признаку p<file_size
   (остановились, файл не кончился).
2. Файл >256 КБ больше не отвергаем экраном ошибки, а КЛАМПим: читаем
   первые 256 КБ, индексатор дописывает строку TRUNC_FILE (File too large
   - truncated at 256 KB). Приоритет ниже content/lines. Текст ошибки -2
   поправлен (был 'size > 128K').

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 22:13:29 +03:00
snark13 c88f127057 mdview2 testfiles: LINES.MD как fenced code block (обойти склейку в параграф)
Вьювер склеивает подряд идущие непустые строки в один абзац (markdown
soft-wrap), из-за чего простые строки сворачивались в ~2752 экранных и
лимит 18432 не достигался. Завернул содержимое в code fence (verbatim,
1:1 строка-источник = экранная строка) -> 22000 строк > 18432.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 22:04:55 +03:00
snark13 386837fc25 mdview2: целевые тестовые файлы путей обрыва (testfiles/) на диск
testfiles/gen_testfiles.py генерирует:
  BIG.MD    — ~170 КБ прозы (CP866), успешный рендер большого файла
  TABLES.MD — неровные таблицы, пробивает кап контент-кэша (Content cache exhausted)
  LINES.MD  — ~22000 коротких строк, пробивает лимит 18432 (Line limit reached)
floppy кладёт на диск TABLES.MD + LINES.MD (ASCII, напрямую из testfiles/)
вместо BIG.MD. ВРЕМЕННО для проверки лимита 256 КБ.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 21:55:17 +03:00
snark13 40ab5b9b48 mdview2: make run/floppy кладёт большой тестовый файл BIG.MD на диск
make run пересобирает образ через floppy, затирая прежний диск. Теперь
floppy генерирует BIG.MD (README+READMEBG ×2 ~216 КБ → CP866) и кладёт
его рядом с README/UTF8TEST — образ всегда содержит файл >128 КБ для
проверки лимита 256 КБ. Состав переопределяется: make floppy BIG_SRCS=...

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 20:52:13 +03:00
snark13 bc3483c2dd mdview2: лимит файла 256 КБ, обрыв с сообщением вместо потери строк
- MAX_PAGES 8->16 (256 КБ), MAX_INDEX_PAGES/MAX_CACHE_DIR_PAGES 8->9
  (18432 строк), кап контент-кэша 64->40 стр./набор (80 на оба).
  Бюджет worst-case (UTF-8 Latin+BOM): ~150 из 215 EMM-страниц.
- При исчерпании контент-кэша (cache_reserve==0) или лимита строк
  индексация обрывается и последняя строка заменяется предупреждением
  (IF_TRUNC_MSG, рисуется вживую с ATTR_WARN — жёлтый по красному),
  без хвоста пустых строк. Раньше переполнение молча давало len=0
  (пустые строки) без какого-либо индикатора.
- help: лимиты обновлены (256 КБ / 18432 строк).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 20:48:35 +03:00
snark13 733746572c mdview2: полировка статус-бара/справки/детекции + libc min/max
- status-bar: dirty-tracking (числа/процент/кодировка перерисовываются
  только при изменении), поле кодировки сдвинуто к DIV1_X-10 (8 симв.)
- md_key: HOME/END не перерисовывают экран, если позиция не меняется
- help: версия v1.0(a3), добавлены F2/F3 (RAW/Wrap), компактные секции
- enc: детекция по 5 частотным буквам и сэмплу 1КБ; ENC_UNSUPPORTED (UTF16/32)
- libc: добавлены min()/max() (naked, <stdlib.h>) + сборка в lib/Makefile

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 20:43:04 +03:00
snark13 1b78dda125 mdview2: статус-бар в отдельный модуль, упростить alloc_set_storage
- mdview2_status.c: вынести статус-бар/меню/спиннер из ядра
- mdview2.c: убрать retry-цикл в alloc_set_storage (fail-fast вместо
  ложной устойчивости — при нехватке EMM под индекс контент тоже не влезет)
- mdview2.h: дополнить экспортами статус-модуля
- mdview2_md.c / mdview2_raw.c: зачистка после расщепления
- mdview/mdview.c: переименовать scroll_* → md_scroll_* (симметрия)
- docs/fast_ram.md, docs/turboc.txt: добавить справочные доки
- examples/mdview2/README.MD, READMEBG.MD: обновить описание

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 23:19:00 +03:00
snark13 05916a3cc6 mdview2: вынести md_key() в mdview2_md.c (симметрия с raw_key)
MD-навигация после загрузки (стрелки/PgUp/PgDn/Home/End/←/→) вынесена из
switch в main() в md_key(scan) — peer к raw_key(): возвращает 1/0, сама
перерисовывает область+статус. main() теперь симметричен для обоих видов:
F-клавиши (F1/F8/F10, для RAW ещё F2/F3) разбираются в цикле, навигация
делегируется md_key()/raw_key().

HPAN_STEP вынесен в mdview2.h (был продублирован в ядре и raw). load_key()
(навигация во время прогрессивной загрузки, bounded по drawable_lines)
остаётся в ядре — у неё нет RAW-аналога.

Поведение сохранено (F1 теперь без лишнего render_updated_status — show_help
и так перерисовывает всё). Размер: exe 28115→28173 (+58 Б — стоимость
границы функции, как у raw_key). Сборка чистая.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 11:21:51 +03:00
snark13 ee87ae1bd9 mdview2: переименовать view→md и причесать секции ядра
- mdview2_view.c → mdview2_md.c: модуль уже содержит отрисовку MD-документа
  из рендер-кэша + скролл + статус-бар, т.е. это парный к mdview2_raw.c вид
  (MD ↔ RAW). Переименование делает пару явной.
- mdview2.c: обновлён устаревший заголовок-комментарий («Фаза 0 — копия
  mdview.c») на описание ядра + карту модулей; убраны осиротевшие после
  выноса комментарии; нормализованы баннеры секций (рендер-кэш / примитивы
  экрана+EMM / загрузка файла / doc-slots / loading-loop / точка входа).
- mdview2.h: освежён заголовок-комментарий под текущую раскладку модулей.

Только переименование и комментарии/баннеры — поведение и размер не
меняются (exe 28115, как до). Сборка чистая.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 11:15:28 +03:00
snark13 858f748e7a mdview2 RAW: чинить дубликат строки при скролле у конца файла
В RAW-режиме offset ряда под экраном (raw_bot) велся инкрементально в
предположении, что экран всегда полон контентом. После Wrap → конец файла
→ Unwrap контент кончается в середине экрана, raw_bot рассинхронизировался
(raw_scroll_up1 делал raw_bot = raw_prev(raw_bot)), и guard raw_bot >=
file_size в scroll-down ложно проходил → дубликат последней строки внизу.

Фикс: убран хрупкий raw_bot. raw_scroll_down1 проходит VIEW_H рядов от
raw_top и скроллит вниз только если контент реально уходит за нижний край
(иначе внизу была бы пустая строка). Кнопка «вниз» теперь работает лишь
когда под экраном есть контент; иначе доступна только «вверх».

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 11:07:07 +03:00
snark13 c0dd1621e6 mdview2: расщепление монолита на модули (6 файлов, размер-нейтрально)
Вынос подсистем из mdview2.c в отдельные C-модули для читаемости и
навигации. mdview2.c: 2740 → 1062 строк (-61%); ядро теперь чистая
инфраструктура (cache-пул, EMM/fb, загрузка файла, doc-slot, loading-loop,
build_doc, main).

Модули (через EXTRA_SRCS, общий интерфейс в mdview2.h):
- mdview2_help.c   — диалог справки F1
- mdview2_table.c  — выровненная отрисовка таблиц
- mdview2_enc.c    — кодировки CP866/CP1251/KOI8R + UTF-8 конвертер
- mdview2_view.c   — отрисовка области/статус-бара/меню + прокрутка
- mdview2_index.c  — парсер/индексатор markdown (сердце приложения)
  (mdview2_raw.c был выделен ранее)

Чистый перенос static→extern; приватное состояние подсистем осталось
приватным. Размер: exe 28090 → 28112 (+22 Б / +0.08% — codegen-шум на
двух сильно связанных модулях enc/index). Сборка чистая, smoke-тест ОК.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 10:53:04 +03:00
snark13 68d5be6e47 mdview2: RAW-просмотр исходника (F2/F3) + инкрементальная UTF-8 конвертация
Новый модуль mdview2_raw.c (+ mdview2.h с общими константами/атрибутами и
extern'ами) — первый шаг разбиения монолита. Опционален через mdview2_conf.h
(#define WITH_RAW): при 0 — пустой объектник, нулевой расход (проверено:
размер как до RAW).

RAW-просмотр (без markdown-форматирования):
- работает по активному буферу (8-бит как есть / UTF-8 декодированный), 1 байт
  = 1 ячейка, \t→пробел, ремап CP1251/KOI8 на отрисовке;
- два под-режима: wrap (перенос кратно 80) и hscroll (одна строка + ←/→);
- прокрутка на 1 строку через аппаратный scroll + отрисовка одной строки;
  вывод char-буфером (bios_write_until по фону ATTR_TEXT), без win_rest/scratch;
- индекс/кэш markdown не используются, 0 доп. EMM.

Клавиши/меню:
- F2 — тумблер RAW↔MD (запоминает под-режим RAW);
- F3 — Wrap/Unwrap (только в RAW), метка показывает целевой режим;
- меню перестроено: блоки по 8 кол (col i*8), номера всех 10 клавиш без 'F'
  (' 1'..' 9','10'), текст-функция 6 симв. сразу за номером и только когда
  функция доступна; F8 сокращён до CodePg.

Инкрементальная UTF-8→CP866 конвертация: вместо полного прохода перед
индексацией — чанками впереди позиции чтения (CONV_MARGIN), первый экран
появляется быстро. Конвертер читает оригинал через cv_read (своя W3-страница),
index_lines докручивает конвертацию; progress_tick рисует по текущему виду
(MD/RAW), без мелькания чужого вида.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 23:31:54 +03:00
snark13 17639ed62d mdview2: B2 stage 1 — вынос inline_marker (-870 байт)
Разбор inline-маркеров (\X, [x], `, **, ~~, *, _, \t) был продублирован
в inline_scan() и scan_join_stream(). Вынесен в общий inline_marker():
возвращает 1 если токен обработан, 0 если обычный символ/пробел (его кладёт
вызывающий). attr = emph_to_attr(ls, base); для join base=ATTR_TEXT, что
тождественно прежнему styles_map[ls]. В scan_join маркеры пропускаются при
soft_break (синтетический пробел склейки кладёт ветка пробела).

scan_join_stream 3794→2329, inline_scan 1842→700, inline_marker +1436.
_CODE 22246→21376. Рендер идентичный (проверено в MAME).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 14:33:17 +03:00
snark13 6d515e4b9a mdview2: уменьшение размера кода (пакет A + B1, -1038 байт)
A.1: таблицы ремапа cp1251/koi8r 256→128 (старшие байты; младшие в
     ремапе не используются — win_rest_remap трогает только ch>=0x80).
A.2: conv_emit_cp switch → таблица структур utf_sym_t {utf8, cp866}
     (читаемо, добавление символа = одна строка; … и BOM — спецветки).
A.3: common_* цепочки сравнений → таблицы детекции + in_set10.
B1: удалён мёртвый код в scan_join_stream — условия
    `if(!soft_break)...else q++` во всех непробельных ветках
    (там soft_break всегда 0, т.к. ch!=' ').

_CODE (mdview2.c): 23284 → 22246 байт. Поведение не менялось.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 14:22:08 +03:00
441 changed files with 63102 additions and 7282 deletions
+2 -2
View File
@@ -3,7 +3,7 @@
# =========================================================================== # ===========================================================================
# `build/` directories anywhere in the tree # `build/` directories anywhere in the tree
# (top-level build/, lib/build/, toolchain/*/build/, ...) # (top-level build/, libc/build/, libbgi/build/, toolchain/*/build/, ...)
build/ build/
# sprinter-cc per-example intermediate directory # sprinter-cc per-example intermediate directory
@@ -40,7 +40,7 @@ tests/*/*.cdb
tests/*/*.mem tests/*/*.mem
tests/*/*.rst tests/*/*.rst
# libc archive (built from libc/, see lib/Makefile) # libc + libbgi archives (built by libc/Makefile + libbgi/Makefile)
lib/*.lib lib/*.lib
# Host-built mkexe binary + test outputs (input fixtures *.bin/*.ihx kept) # Host-built mkexe binary + test outputs (input fixtures *.bin/*.ihx kept)
+75
View File
@@ -0,0 +1,75 @@
# Sprinter C-Compiler — правила проекта
Target-слой SDCC 4.5 (z80) для компьютера Sprinter Sp2000: crt0,
линковка, libc, mkexe. Общение и комментарии — на русском.
## Сборка и проверка
```
make # tools + lib + libbgi + все тесты (45) + examples
make -C libc # только libc → lib/sprinter.lib
make -C libbgi # только BGI → lib/bgi256.lib (и bgi16.lib в Фазе 2)
make floppy # упаковать все .exe в mame/v306/IMG/mc.img
make size-check # размерный регресс: _CODE vs docs/size_baseline.tsv
make size-baseline # принять текущие размеры эталоном
```
Графика (BGI) — отдельная библиотека libbgi/ (см. ниже). Программа,
использующая graphics.h, собирается с `--gfx 256` (или `--gfx 16` в
Фазе 2): sprinter-cc подлинкует lib/bgi256.lib и добавит -I libbgi/include.
Одиночный тест: `cd tests/<имя> && make run` (пакует ТОЛЬКО этот exe
+ EXTRA_DATA на дискету и запускает MAME). Тесты в MAME гоняет
пользователь — готовь дискету и проси прогнать.
После правок libc/libbgi: пересборка от чистого листа (`make -C libc clean`
/ `make -C libbgi clean`) не обязательна — stale .rel чистятся
автоматически; `make size-check` обязателен (рост _CODE без причины —
регрессия).
## Правила libc
- **1 публичная функция = 1 .c-модуль** (линкер тянет .rel целиком —
гранулярность файлов = гранулярность DCE). Никакой группировки
«используются вместе». Internal-хелперы — тоже по одному на модуль
(`_`-префикс); общие статики — в отдельные data-модули
(`_xxx_state.c`); internal-заголовки (`_file.h`, `_gfx.h`, …) —
рядом с исходниками, НЕ в libc/include.
- Имя файла = имя функции. libc/Makefile собирает wildcard'ом —
ничего регистрировать не надо.
- Комментарии — на русском; шапка модуля объясняет что/зачем + ABI.
- File-scope переменные НЕ инициализировать `= 0` (crt0 зануляет
_DATA; см. memory/sdcc_static_storage_gotcha).
- asm-связки между модулями: `call/jp _global` — ок; `jr/djnz` через
границу и fall-through — НЕЛЬЗЯ (docs/libc-split-asm-cases.md).
- Заголовки: сначала пробовать include_next-паттерн; полная замена
SDCC-заголовка обязана дублировать его контракт
(docs/libc-headers.md).
- Справочник API — docs/libc-reference.md (обновлять при добавлении
функций).
## ABI и платформа (кратко; детали в memory/)
- SDCC `__sdcccall(1)`: arg1 → HL (8-бит → A), arg2 → DE, остальные
на стеке (callee-pops в __naked); **возврат int/ptr в DE**, uint8 в A.
IX callee-saved (в __naked с IX — push/pop обязательны).
- ESTEX (rst #0x10): CF=1 — ошибка, код в A → `call __errno_set`;
все регистры клобберятся (IX сохранять); стек обязан быть в W2.
- BIOS (rst #0x08): строки/буферы в #4000-#BFFF.
- Квирки: ESTEX WRITE возвращает DE=0 на успехе (судить по CF/A);
лимит 8 файловых манипуляторов, 9-й OPEN ВЕШАЕТ DSS (_fd_guard);
ENV $46: A=0 = NOT FOUND.
- Перед обвинением компилятора/железа — подтвердить артефактом
(сгенерированный .asm в libc/build/ или libbgi/build/, дамп, репро) — см.
memory/defer_unexplained_quirks.
## Структура
- `libc/<area>/*.c` — модули libc (ядро, БЕЗ графики); `libc/include/` — публичные заголовки libc
- `libbgi/` — графика BGI (отдельная библиотека): `common/` — mode-agnostic (один исходник, .rel в обеих driver-библиотеках), `bgi256/` + `bgi16/` — mode-specific leaf'ы (реальные реализации, без обёрток); `include/` — graphics.h + gfx.h; `_bgi.h` — внутренний заголовок. Собирает `lib/bgi256.lib``bgi16.lib` в Фазе 2). Выбор режима линковкой: `--gfx 256` / `--gfx 16`.
- `runtime/` — crt0-семейство, heap, bank (bank.s собирается per-build)
- `bin/sprinter-cc` — обёртка компилятора; `toolchain/mkexe` — упаковщик
- `tests/` — по одному API/фиче; `examples/` — реальные приложения
- `docs/` — дизайн-доки; `docs/TODO.md` — roadmap
- `third_party/solid-c/` — нативный Sprinter C (референс, CP866;
их ABI несовместим — только как образец)
+25 -9
View File
@@ -2,7 +2,7 @@
# #
# make build host tools, libc archive, all tests, all apps # make build host tools, libc archive, all tests, all apps
# make tools build only host tools (mkexe) # make tools build only host tools (mkexe)
# make lib build lib/sprinter.lib (libc archive used by sprinter-cc) # make lib build lib/sprinter.lib (libc) + lib/bgi256.lib (libbgi)
# make tests build all libc feature tests under tests/ # make tests build all libc feature tests under tests/
# make examples build all real applications under examples/ # make examples build all real applications under examples/
# make floppy package every .exe + test fixtures into mame/v306/IMG/mc.img # make floppy package every .exe + test fixtures into mame/v306/IMG/mc.img
@@ -13,12 +13,16 @@
# Most heavy lifting is delegated to sub-Makefiles. # Most heavy lifting is delegated to sub-Makefiles.
# Small libc-feature tests (one program per .c-language feature or libc API). # Small libc-feature tests (one program per .c-language feature or libc API).
TESTS := hello banked bankedbg strtest cat seek malloc mem_test argv errno \ TESTS := hello hello2 simple banked bankedbg banktest strtest cat seek \
rt_test openenv ls conio attrprob timedir mouse banklocl stdlib \ malloc mem_test argv errno rt_test openenv ls conio conio2 \
assrtest ptime stattest filetest gfx_demo gfx_d16 gfx_text gfx_mous attrprob timedir mouse banklocl stdlib assrtest ptime stattest \
filetest fdmax fbench solidt irqtest cbltest cblwav dec_test gets stest2 winrest \
bios_text text_palette \
gfx_demo gfx_dbuf
# gfx_d16 / gfx_text / gfx_mous — 16-цветные; убраны до Фазы 2 (bgi16.lib
# ещё не собирается). Вернуть мигрированными на BGI --gfx 16.
# Larger end-user applications under examples/. # Larger end-user applications under examples/.
APPS := mdview APPS := mdview mdview2
MAME_DIR := mame/v306 MAME_DIR := mame/v306
FLOPPY_IMG := $(MAME_DIR)/IMG/mc.img FLOPPY_IMG := $(MAME_DIR)/IMG/mc.img
@@ -31,9 +35,11 @@ ALL_EXES := $(TEST_EXES) $(APP_EXES)
DATA_FILES := \ DATA_FILES := \
tests/cat/test.txt \ tests/cat/test.txt \
tests/seek/big.txt \ tests/seek/big.txt \
tests/cblwav/speech.pcm \
examples/mdview/SAMPLE.MD examples/mdview/SAMPLE.MD
.PHONY: all tools lib tests examples check clean sdcc floppy $(TESTS) $(APPS) .PHONY: all tools lib tests examples check clean sdcc floppy \
size-check size-baseline $(TESTS) $(APPS)
all: tools lib tests examples all: tools lib tests examples
@@ -41,7 +47,8 @@ tools:
$(MAKE) -C toolchain/mkexe $(MAKE) -C toolchain/mkexe
lib: lib:
$(MAKE) -C lib $(MAKE) -C libc
$(MAKE) -C libbgi
check: tools check: tools
$(MAKE) -C toolchain/mkexe check $(MAKE) -C toolchain/mkexe check
@@ -66,9 +73,18 @@ floppy: tests examples tests/seek/big.txt
@echo "Floppy ready: $(FLOPPY_IMG)" @echo "Floppy ready: $(FLOPPY_IMG)"
@echo "Run: cd $(MAME_DIR) && ./run_mame.sh" @echo "Run: cd $(MAME_DIR) && ./run_mame.sh"
# Размерный регресс: сверить _CODE всех программ с docs/size_baseline.tsv.
size-check:
python3 toolchain/size_check.py
# Принять текущие размеры как эталон (после осознанных изменений).
size-baseline:
python3 toolchain/size_check.py --update
clean: clean:
$(MAKE) -C toolchain/mkexe clean $(MAKE) -C toolchain/mkexe clean
$(MAKE) -C lib clean $(MAKE) -C libc clean
$(MAKE) -C libbgi clean
@for t in $(TESTS); do $(MAKE) -C tests/$$t clean; done @for t in $(TESTS); do $(MAKE) -C tests/$$t clean; done
@for a in $(APPS); do $(MAKE) -C examples/$$a clean; done @for a in $(APPS); do $(MAKE) -C examples/$$a clean; done
+12 -2
View File
@@ -51,14 +51,24 @@ CC_FLAGS += $(EXTRA_FLAGS)
all: $(EXAMPLE).exe all: $(EXAMPLE).exe
$(EXAMPLE).exe: $(SOURCES) $(MKEXE) $(LIB) # runtime/*.s (crt0-семейство, bank.s, heap.s) собираются per-build
# внутри sprinter-cc — без этой зависимости их правка не перелинкует
# уже собранный exe (кусало: фикс bank.s не подхватился).
RUNTIME_DEPS := $(wildcard $(PROJ_ROOT)/runtime/*.s)
$(EXAMPLE).exe: $(SOURCES) $(MKEXE) $(LIB) $(RUNTIME_DEPS)
$(SPRINTER_CC) $(CC_FLAGS) -o $@ $(SOURCES) $(SPRINTER_CC) $(CC_FLAGS) -o $@ $(SOURCES)
$(MKEXE): $(MKEXE):
$(MAKE) -C $(PROJ_ROOT)/toolchain/mkexe $(MAKE) -C $(PROJ_ROOT)/toolchain/mkexe
# $(LIB) = lib/sprinter.lib (libc). Графика (bgi256.lib) собирает
# libbgi/Makefile; гоним и его — иначе standalone `make` в тесте с
# --gfx 256 не найдёт bgi256.lib при линковке. Оба инкрементальные,
# на повторном запуске ничего не пересобирают.
$(LIB): $(LIB):
$(MAKE) -C $(PROJ_ROOT)/lib $(MAKE) -C $(PROJ_ROOT)/libc
$(MAKE) -C $(PROJ_ROOT)/libbgi
clean: clean:
rm -rf .sprinter-cc-* $(EXAMPLE).exe rm -rf .sprinter-cc-* $(EXAMPLE).exe
+23 -3
View File
@@ -34,6 +34,9 @@
# --bank N=FILE.c compile FILE.c as bank N; repeatable; pulls crt0_banked # --bank N=FILE.c compile FILE.c as bank N; repeatable; pulls crt0_banked
# automatically and adds -Wl-b_BANKN=0x{N}C000 # automatically and adds -Wl-b_BANKN=0x{N}C000
# --mkexe FLAG extra mkexe flag (repeatable; e.g. --mkexe -p --mkexe 0) # --mkexe FLAG extra mkexe flag (repeatable; e.g. --mkexe -p --mkexe 0)
# --max-allocs N SDCC --max-allocs-per-node (default: SDCC's 3000).
# Higher values give smaller/faster code at the cost of
# compile time; 10000..100000 is a reasonable range.
# --debug enable runtime diagnostics — defines DEBUG_RT for both # --debug enable runtime diagnostics — defines DEBUG_RT for both
# sdcc (-DDEBUG_RT) and the crt0 assembly (prepended # sdcc (-DDEBUG_RT) and the crt0 assembly (prepended
# `DEBUG_RT = 1`). Exposes runtime introspection symbols # `DEBUG_RT = 1`). Exposes runtime introspection symbols
@@ -61,6 +64,7 @@ CHECK_BANKS="$PROJ_ROOT/toolchain/check_banks.py"
RUNTIME="$PROJ_ROOT/runtime" RUNTIME="$PROJ_ROOT/runtime"
LIB_DIR="$PROJ_ROOT/lib" LIB_DIR="$PROJ_ROOT/lib"
INC_DIR="$PROJ_ROOT/libc/include" INC_DIR="$PROJ_ROOT/libc/include"
BGI_INC_DIR="$PROJ_ROOT/libbgi/include" # gfx.h, graphics.h (BGI)
# ------- Defaults ------------------------------------------------------------ # ------- Defaults ------------------------------------------------------------
OUT="" OUT=""
@@ -82,6 +86,8 @@ SOURCES=()
LD_EXTRA=() LD_EXTRA=()
MKEXE_EXTRA=() MKEXE_EXTRA=()
BANK_SPECS=() # entries like "1=engine.c" BANK_SPECS=() # entries like "1=engine.c"
MAX_ALLOCS="" # if set, passed to sdcc as --max-allocs-per-node
GFX_MODE="" # "256" → link BGI graphics.h driver sprinter_gfx256.lib
# ------- Parse args ---------------------------------------------------------- # ------- Parse args ----------------------------------------------------------
usage() { usage() {
@@ -105,6 +111,8 @@ while [[ $# -gt 0 ]]; do
-Wl) LD_EXTRA+=("$2"); shift 2;; -Wl) LD_EXTRA+=("$2"); shift 2;;
--bank) BANK_SPECS+=("$2"); shift 2;; --bank) BANK_SPECS+=("$2"); shift 2;;
--mkexe) MKEXE_EXTRA+=("$2"); shift 2;; --mkexe) MKEXE_EXTRA+=("$2"); shift 2;;
--max-allocs) MAX_ALLOCS="$2"; shift 2;;
--gfx) GFX_MODE="$2"; shift 2;;
--debug) DEBUG_RT=1; shift;; --debug) DEBUG_RT=1; shift;;
-v) VERBOSE=1; shift;; -v) VERBOSE=1; shift;;
-h|--help) usage 0;; -h|--help) usage 0;;
@@ -116,6 +124,17 @@ done
[[ -z "$OUT" ]] && { echo "sprinter-cc: -o NAME is required" >&2; exit 1; } [[ -z "$OUT" ]] && { echo "sprinter-cc: -o NAME is required" >&2; exit 1; }
[[ ${#SOURCES[@]} -eq 0 ]] && { echo "sprinter-cc: no input files" >&2; exit 1; } [[ ${#SOURCES[@]} -eq 0 ]] && { echo "sprinter-cc: no input files" >&2; exit 1; }
# ------- BGI graphics driver selection (--gfx) -------------------------------
# graphics.h — mode-agnostic слой; конкретный видеорежим задаёт driver-
# архив. Одновременно только один. drv16 пока не реализован.
GFX_LD=()
case "$GFX_MODE" in
"") ;; # graphics.h не используется
256) GFX_LD=("-lbgi256");; # libbgi/bgi256 → lib/bgi256.lib
16) echo "sprinter-cc: --gfx 16 ещё не реализован (пока только 256)" >&2; exit 1;;
*) echo "sprinter-cc: --gfx: ожидается 256 (или 16), дано: $GFX_MODE" >&2; exit 1;;
esac
# ------- Resolve memory mode → CODE_LOC / DATA_LOC --------------------------- # ------- Resolve memory mode → CODE_LOC / DATA_LOC ---------------------------
# tiny : CODE in W2 (0x8100), DATA auto after code (= W2) # tiny : CODE in W2 (0x8100), DATA auto after code (= W2)
# small : CODE in W1 (0x4100), DATA in W2 (0x8000) — crt0 must alloc W2 # small : CODE in W1 (0x4100), DATA in W2 (0x8000) — crt0 must alloc W2
@@ -270,8 +289,9 @@ run "$SDASZ80" -o "$HEAP_TOP_REL" "$HEAP_TOP_SRC"
# 2. user sources → .rel (HOME) # 2. user sources → .rel (HOME)
USER_RELS=() USER_RELS=()
CC_FLAGS=(-mz80 --no-std-crt0 --std-c99 --opt-code-size -I "$INC_DIR" "${USER_INCS[@]}") CC_FLAGS=(-mz80 --no-std-crt0 --std-c99 --opt-code-size -I "$INC_DIR" -I "$BGI_INC_DIR" "${USER_INCS[@]}")
[[ $DEBUG_RT -eq 1 ]] && CC_FLAGS+=(-DDEBUG_RT) [[ $DEBUG_RT -eq 1 ]] && CC_FLAGS+=(-DDEBUG_RT)
[[ -n "$MAX_ALLOCS" ]] && CC_FLAGS+=(--max-allocs-per-node "$MAX_ALLOCS")
for src in "${SOURCES[@]}"; do for src in "${SOURCES[@]}"; do
rel="$WORK/$(basename "$src" .c).rel" rel="$WORK/$(basename "$src" .c).rel"
run "$SDCC" "${CC_FLAGS[@]}" -c -o "$rel" "$src" run "$SDCC" "${CC_FLAGS[@]}" -c -o "$rel" "$src"
@@ -336,12 +356,12 @@ for f in "${LD_EXTRA[@]}"; do LINK_FLAGS+=("$f"); done
if [[ $VERBOSE -eq 1 ]]; then if [[ $VERBOSE -eq 1 ]]; then
run "$SDCC" "${LINK_FLAGS[@]}" -o "$IHX" \ run "$SDCC" "${LINK_FLAGS[@]}" -o "$IHX" \
"$CRT0_REL" "$HEAP_TOP_REL" "${USER_RELS[@]}" "${BANK_RELS[@]}" \ "$CRT0_REL" "$HEAP_TOP_REL" "${USER_RELS[@]}" "${BANK_RELS[@]}" \
"-L$LIB_DIR" "-lsprinter" "-L$LIB_DIR" "${GFX_LD[@]}" "-lsprinter"
else else
# Drop the warning line + its two follow-up "Library:" lines. # Drop the warning line + its two follow-up "Library:" lines.
run "$SDCC" "${LINK_FLAGS[@]}" -o "$IHX" \ run "$SDCC" "${LINK_FLAGS[@]}" -o "$IHX" \
"$CRT0_REL" "$HEAP_TOP_REL" "${USER_RELS[@]}" "${BANK_RELS[@]}" \ "$CRT0_REL" "$HEAP_TOP_REL" "${USER_RELS[@]}" "${BANK_RELS[@]}" \
"-L$LIB_DIR" "-lsprinter" 2>&1 \ "-L$LIB_DIR" "${GFX_LD[@]}" "-lsprinter" 2>&1 \
| awk ' | awk '
/^\?ASlink-Warning-Definition of public symbol/ { skip = 3 } /^\?ASlink-Warning-Definition of public symbol/ { skip = 3 }
skip > 0 { skip--; next } skip > 0 { skip--; next }
+200 -434
View File
@@ -1,436 +1,202 @@
# TODO / Roadmap # TODO / Roadmap
Открытые задачи в порядке убывания приоритета. По мере появления реальных программ — приоритеты будут смещаться. Открытые задачи в порядке убывания приоритета; закрытые этапы — в
«Истории» внизу. Текущий срез libc-работ: docs/libc-roadmap.md.
## Этап 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` - [ ] **П6/железо**: MAME-смоук всех тестов после libc-сплита (conio,
- [x] `libc/mem/bank_io.c` — HOME-резидентные `bank_read`/`bank_write`/`bank_load_byte`/`bank_store_byte` со свопом W3 внутри ptime, stattest, mouse, gfx_demo/gfx_d16/gfx_text/gfx_mous —
- [x] `examples/malloc_test/` — проверка SDCC's malloc (~210 64-байтных allocations через всю heap) трогался asm акселератора); затем прогон на реальном Sprinter
- [x] `examples/mem_test/` — проверка page allocator: 3 страницы, разные паттерны через bank_write, верификация через bank_read (mdview2 + FILE* v2 + fdmax — подтвердить лимит 8 манипуляторов
и зависание DSS на 9-м OPEN)
## Этап 6 — argv parsing + sprinter-cc wrapper ✅ ГОТОВО - [ ] Мигрировать оставшиеся examples на sprinter-cc вместо ручных
Makefile (косметика)
- [x] crt0 парсит ESTEX command-line из IX-prefix (inline asm в `runtime/crt0.s`) - [ ] check_banks.py: разбивка size = code + const + bss per bank
- [x] Strip leading CP/M-style space (DSS quirk)
- [x] Передача `argc`/`argv` в main() через HL/DE (SDCC __sdcccall(1) ABI) ## Auto-banking (memory/banking_roadmap.md)
- [x] argv[0] = basename .EXE через ESTEX APPINFO ($47 subfn 2)
- [x] `runtime/crt0_minimal.s`opt-out для очень маленьких программ Phase 1 — file-level bin-packing (`toolchain/auto_bank.py`)когда
- [x] `runtime/crt0_banked.s` — теперь тоже парсит argv (parse_argv + get_progname скопированы из crt0.s; будет factored в argv.s когда возьмёмся за libsprinter.lib) проект перерастёт ~30 KB кода: парсинг размеров из .rel/.map,
- [x] Bash-обёртка `bin/sprinter-cc`: `sprinter-cc -o foo.exe foo.c` одной строкой first-fit-decreasing, уважение `#pragma codeseg BANKn`, перелинковка,
- [x] Поддержка опций: `--memory`, `--memory-manual`, `--stack-size`, `--crt0=`, `--bank N=FILE.c`, `--debug`, `-I`, `-L`/`-E`/`-S`, `-Wl`, `--mkexe` печать плана. Phase 2-5 (rebalance, banks.toml, function-level) —
по потребности.
## Этап 8 — графика (320×256×256 + 640×256×16 + accel + bitmap font) ✅ ГОТОВО
## ОБЯЗАТЕЛЬНОЕ ДЛЯ V2
- [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× быстрее). ### Turbo-C-style graphics API (BGI-like) — Фаза 1 ГОТОВА (256, 2026-07-08)
- [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. Архитектура: `graphics.h` — mode-agnostic слой (libc/bgi/*.c в
sprinter.lib), режим задаёт driver-архив; выбор линковкой через
См. memory/sprinter_graphics.md, sprinter_accelerator.md, sprinter_graphics_16.md, sprinter_font_format.md. `sprinter-cc --gfx 256` (16 — позже, тем же leaf-split'ом; см.
memory/bgi_two_lib_design).
Открытые мелочи (не блокируют):
- [ ] Шрифт-quad для 640: per-cell палитра (mode 0x82 разрешает 1 из 4 палитр per 16×8 cell) — через прямой доступ к area-описания экрана 0x0300..0x039F **Фаза 1 (256, реализовано и проверено в MAME — tests/bgitest):**
initgraph/closegraph/graphresult/cleardevice, setcolor/getcolor/
## Auto-banking (см. `memory/banking_roadmap.md` для деталей) setbkcolor/getbkcolor, getmaxx/getmaxy/getmaxcolor, putpixel/getpixel,
moveto/moverel/getx/gety, line/lineto/linerel, rectangle, bar, circle,
Phase 1 — file-level bin-packing — реализовывать когда проект перерастёт ~30 KB кода. outtext/outtextxy. initgraph грузит EGA-палитру 0..15. Пакетные
примитивы (circle) — одна W3-скобка на весь примитив (raw-плот), иначе
- [ ] `toolchain/auto_bank.py`: на порядок медленнее.
- Парсит размеры из `.rel`-файлов (или из .map после dry-run link'а)
- First-fit-decreasing bin-packing **Фаза 2a/2b/2c ГОТОВЫ (2026-07-08, проверено в MAME tests/bgitest):**
- Уважает `#pragma codeseg BANKn` как manual override - 2a: arc, ellipse, drawpoly (Q7-тригонометрия, БЕЗ 32-бит).
- Перелинковывает с новыми `-Wl-b_BANKn=...` параметрами - 2b: setfillstyle/getfillsettings + 10 паттернов Borland, bar (с
- Печатает план распределения паттерном), bar3d, fillpoly (scanline min/max), fillellipse (isqrt).
- 2c: floodfill (scanline span; медленный — self-bracket чтение, но
Phase 2-5: incremental rebalance, declarative `banks.toml`, function-level, call-graph-aware. Только если/когда понадобится. корректный), pieslice, sector.
## Bank-local static data (mutable data в том же банке что и код) — ✅ ГОТОВО **Фаза 2d-1/2/3 ГОТОВЫ (2026-07-08, MAME tests/bgitest):**
- 2d-1: getimage/putimage/imagesize (COPY/XOR/OR/AND/NOT_PUT), raw-блит
- [x] Пример `examples/bank_local_data/` — функция в BANK1 со своим writable BSS array + const table + malloc-тест в одной W3-скобке (_gfx_getpixel256_raw).
- [x] `mkexe -p 0` для нулевого padding банков (BSS-storage обнуляется при загрузке) - 2d-2: setlinestyle/getlinesettings (SOLID/DOTTED/CENTER/DASHED/USERBIT
- [x] Канонический рецепт: `--codeseg BANK1 --constseg BANK1 --dataseg BANK1` для bank1.c + `-Wl-b_BANK1=0x1C000` для линковки. **`--dataseg BANK1` РАБОТАЕТ** — раньше казалось обратное из-за trampoline bug который маскировал результат. + NORM/THICK) — line/lineto/linerel/rectangle/drawpoly.
- [x] **Критичный фикс trampoline'a в runtime/bank.s** — старый `pop af; out (n), a` клобберил A → все banked-функции возвращающие uint8_t тихо возвращали мусор. Новый `pop bc; out (c), b` сохраняет A. - 2d-3: settextstyle/gettextsettings/textwidth/textheight — масштаб
- [x] **malloc из banked-функции работает прозрачно** — heap живёт в W2 (HOME), W2 никогда не свапается trampoline'ом, pointer валиден из любого контекста. См. memory/bank_local_data_pattern.md. 1..10, HORIZ/VERT, прозрачный фон (свой scaled-рендер поверх 8×8).
- [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) **Фаза 2d (осталось):** setviewport/clearviewport + клиппинг (инвазивно —
трогает все примитивы), settextjustify, setaspectratio (пиксели 320×256
Зачем: для модулей с большим private state (level loader, audio engine, scene data). Экономит W2 heap для динамики, а статика остаётся в бэке. неквадратные — круги визуально эллиптичны), setfillpattern (USER_FILL),
setactivepage/setvisualpage (2 страницы в gfx уже есть). Оптимизации:
## Подсказки из solid-c (нативный Sprinter C — `third_party/solid-c/`) floodfill на raw-чтении; ellipse/arc в одной W3-сессии. Потом drv16 +
sprinter_gfx16.lib (--gfx 16).
После анализа solid-c'овской libc (см. `memory/solid_c_findings.md`) выявлены готовые паттерны для следующих недостающих функций. Приоритет от **высокого** к низкому: Acceptance: типичная BGI-программа переносится без существенных
правок. Референс: Turbo C 2.x BGIDEMO.
### High-priority gaps (легко портировать, большая польза)
- [x] **`errno` + `strerror`/`perror`** — табличка 32 ошибок (libc/io/errno.c) ### IM2 Interrupt Handlers — Phase 1 ГОТОВ (2026-07-06, libc/irq, tests/irqtest; Phase 2: CBL/ISA/цепочки)
- [x] **Расширенный `open()`** для O_CREAT/O_TRUNC/O_APPEND/O_EXCL state machine
- [x] **`atexit`** — 8-callback LIFO + `exit()` + `_exit()` (libc/io/atexit.c) User-ISR через IM 2 — timer ticks, музыка (AY/COVOX), real-time
- [x] **`setjmp`/`longjmp`** — 6-байт jmp_buf={sp,ix,pc} (libc/io/setjmp.c) игры, async input. Phase 1 реализован БЕЗ отдельного memory mode:
- [x] **`sleep(seconds)`** — 50Hz halt-loop (libc/io/sleep.c) libc/irq (<irq.h>: irq_install/irq_remove), таблица в BSS с runtime-
- [x] **ESTEX ENV API** ($46, getenv/putenv) — libc/io/env.c. Учли doc-bug: реально A=0 это NOT FOUND выравниванием, jp-заглушка внутри таблицы, чейн к DSS всегда;
работает в tiny/big, в small/huge — EINVAL. Детали и отличия от
### Medium-priority (нужно для shell-like утилит) исходного плана: docs/im2_isr_design.md.
- [ ] **Mouse driver**`rst $30h`, 17 функций. **Сначала тест что работает в MAME**.
- [x] **`ffirst`/`fnext` + ffblk_t struct** для directory listing — реализовано, demo: ls.exe ### Прочее v2
- [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 - [ ] **Audio API**AY-3-8910 + COVOX (требует IM2)
- [x] **conio: `kbhit`/`getch`/`getche`/`cputs`/`clrscr`/`gotoxy`** — реализовано - [ ] **ISA-8 slot support** — ZX-Bus карты (требует IM2)
- [x] **conio extras**: `wherex`/`wherey` ($53), `wrchar`/`rdchar` ($58/$57), `textmode_get/set` ($50/$51), `clrscr_attr` ($56) + COLOR macros
## GFX: расширения по accelerator_doc.txt
### Low-priority — ✅ FILE* stack ГОТОВО
Quick wins:
- [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`. - [ ] block-size через `LD A,(nn)` вместо SMC (док разрешает LD A,(HL/BC/DE))
- [ ] fprintf / fscanf — нужна printf-через-callback machinery. Пока пользователь может `sprintf(buf, ...) + fputs(buf, fp)`. - [ ] кэширование block-size между burst'ами (accel помнит размер)
- [ ] Опциональный buffered mode (setvbuf, line/block buffering) — если когда-то понадобится.
Новые возможности:
### POSIX time API — ✅ ГОТОВО - [ ] `gfx_blit` / `gfx_blit_transparent` — block copy (LD L,L / LD A,A),
- [x] `libc/io/posix_time.c` — time/localtime/gmtime/mktime/asctime/ctime поверх getdatetime. SDCC's time.rel избегаем (нельзя override _RtcRead). См. `examples/ptime`. прозрачность через bank 0x58 («FF is transparent»)
- [ ] `gfx_xor_rect` / `gfx_or_rect` / `gfx_and_rect` / `gfx_invert_rect`
### sys/stat — ✅ ГОТОВО - [ ] шрифты ≠ 8×8: gfx_set_font_data(ptr,w,h,advance), proportional,
- [x] `libc/io/stat.c` — POSIX stat/fstat. Гибрид open+fstat для файлов, ffirst+iter для папок (включая "."/".."). См. `examples/stattest` и `memory/estex_ffirst_dotdot.md`. 8×16/16×16, отдельный font_id API; font-quad для 640×256
(per-cell палитра через дескрипторы 0x0300..0x039F)
### assert — ✅ ГОТОВО (используем SDCC's __assert через fallback include path)
Оптимизации (не сейчас):
## libc/stdlib — ✅ не нужно делать (см. memory/sdcc_stdlib_works.md) - [ ] gfx_line через accel для пологих диагоналей (runs ≥ 4-5 px)
- [ ] композитные примитивы с одним W3-swap на операцию
Проверено через `examples/stdlib_test/`: SDCC z80.lib содержит работающие реализации:
- `atoi/atol/atof, strtol/strtoul, rand/srand, qsort/bsearch, abs/labs, div/ldiv` ## Прочий backlog
- Полный `<string.h>` (memchr/cmp/set/cpy, strcat/cmp/cpy/len/chr/spn/etc.)
- `<ctype.h>` (toupper/tolower) - [ ] factoring parse_argv из crt0/crt0_banked в общий argv.s
- `<math.h>` (sinf/cosf/sqrtf/etc.) - [ ] `restore SP on EXIT` (паттерн z88dk +pps) — проверить нужность
- [x] ~~CI: MAME с -aviwrite для screenshot-сравнения без человека~~
Линкер автоматически тянет из z80.lib когда нужно. **НЕ переписывать**. `toolchain/mame_interactive.py`: авто-запуск .exe вводом с
эмуляцией клавиатуры + Lua-таймер для скриншотов/выхода; сравнение
Наши Sprinter-specific обязательные модули остаются: atexit, env, errno, setjmp, putchar/puts/getchar, conio, fsdir, time, mouse, open/read/lseek/close. визуальное (Claude читает скриншот), не автоматический diff.
Полный справочник: docs/mame-autotest.md.
## Build-system: libsprinter.lib + sprinter-cc — ✅ ГОТОВО - [ ] linker duplicate-symbol warnings: сейчас фильтруются в
sprinter-cc (наши overrides _puts/___sdcc_heap/_asctime/…);
- [x] `lib/Makefile` — собирает каждый libc/*.c в `.rel`, архивирует через sdar в `lib/sprinter.lib` радикально — --nostdlib с ручным списком модулей z80.lib
- [x] Включает runtime/bank.s и runtime/heap.s (auto-pulled при __banked/malloc) - [ ] ZX Spectrum-совместимый target; ZX-Bus драйверы; PGO-tools
- [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) - [ ] **Port_Y banking trick** (адреса 0xC000+0x400*N → строки
- [x] Включён в `make all` (зависимость `lib` перед `examples`) Y..Y+15): в MAME 0.283 НЕ работает (2026-06-01). На железе:
dual-write тест → если работает, кэшировать Port_Y в putpixel
Возможные улучшения (опционально): (~8× меньше OUT для Брезенхэма); если нет — вычистить из доков.
- [ ] Мигрировать остальные examples на sprinter-cc вместо ручных Makefile (косметика) - [ ] fdmax: лимит манипуляторов и зависание 9-го OPEN — MAME vs железо.
- [ ] Дальнейшая декомпозиция libc/*.c per-function (но текущая granularity уже даёт нужный размер — линкер пакетует .rel целиком, и для большинства файлов это одна функция) - [ ] CBL: щелчок перед первым проигрыванием звука за сессию (tests/
cbltest, tests/cblwav) — воспроизводится ТОЛЬКО на первом запуске
## Этап 9 — memory modes для sprinter-cc программы за сессию MAME, не зависит от содержимого потока
(тон/тишина/речь одинаково). Похоже на разовый прогрев звуковой
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). подсистемы MAME при первой активации канала — на реальном железе
скорее всего отсутствует. Проверить и закрыть либо описать как
- [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. - [ ] **CBL: помехи в звуке при движении мыши** (tests/cblstream,
- [x] **`--memory-manual SPEC`**: парсит `CODE=W1|W2,DATA=W1|W2|SAME,BANKED=W1|W3`. Реализовано 2026-05-30. 2026-07-07) — при потоковом воспроизведении движение мыши даёт
- [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. делить SIO-канал/детект-бит с клавиатурой (порт 0x19 бит 0,
- [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. см. _irq_tramp.c), и трамплин классифицирует байты мыши как
- [x] **`huge` runtime** (small + banked code в W3): merge W2-detect логики из `crt0_small.s` в `crt0_banked.s`. Существующий пример `examples/banked/` теперь использует MEMORY=huge. Реализовано 2026-05-30. «клавиатура» → сразу chain на DSS (0x0038), пропуская проверку
- [x] **`--debug` флаг**: prepend `DEBUG_RT = 1` в crt0 + `-DDEBUG_RT` в sdcc. Открывает symbol `_w2_self_allocated` (uint8_t) — runtime diagnostic кто аллоцировал W2. Реализовано 2026-05-30. CBL-хука для ЭТОГО прерывания — при движении мыши часть тиков,
которые должны были обслужить CBL-насос, уходят мимо, кольцо
### Дизайн-решения по libc и crt0 недоливается. Нужно исследование (MAME-дамп/лог трамплина).
**Одна `sprinter.lib`** работает для всех memory mode — `.rel`-члены relocatable, SDLD делает dead-code elimination per-member (без графики не подтягивает `gfx_core.rel` и т.д.). Verified hello vs malloc_test через map-файлы. ## Known quirks (зафиксированы, обходы в libc)
**`gfx.lib` отдельно — НЕ нужен**: dead-code elimination уже работает. - ESTEX $46 ENV: A=0 это NOT FOUND (док врёт) — memory/sprinter_platform
- ESTEX WRITE $14: на успехе DE=0, не счётчик; успех = CF=0 & A=0 —
**`libc_banked` (libc в bank вместо HOME)** — идея на потом, когда HOME (16 KB) забит user-кодом + libc в `huge` mode. Реализуется через `--codeseg BANK0` при компиляции libc; trade-off: trampoline ~30 циклов на каждый libc-вызов. Триггер: реальная программа упрётся в HOME budget. memory/estex_write_de_quirk
- DSS: 8 манипуляторов, 9-й OPEN вешает систему; _fd_guard в libc —
**HW-зависимые модули — `sprinter_home.lib` отдельно.** Часть libc физически не может быть забанкована в W3, потому что она РАБОТАЕТ с W3: memory/dss_fd_limit
- `gfx_*` — пишет в видеопамять `0xC000+` после swap W3 на video page - SDCC z80 `if(n!=g)g=n;` пишет (n-g) — memory/sdcc_z80_cmp_store_a_bug
- `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 ## Этап 10 — libc: сплит + FILE v2 + Solid-C (2026-07-05/06) ✅
sprinter.lib bankable: printf, malloc, string, conio, stdio, env, ...
sprinter_banked.lib тот же sprinter.lib но --codeseg BANK0 (для huge) Полный план/итоги: docs/libc-roadmap.md. Кратко:
``` - вся libc разложена «1 функция = 1 модуль» (~250 модулей, wildcard-
Триггер: реализация `--memory huge` runtime. сборка, DCE на уровне файлов): gfx_text 4.4 КБ, timedir/ls/stattest
−3 КБ и т.д.; правила asm-связок: docs/libc-split-asm-cases.md
**crt0 — по одному на mode:** - **FILE* v2 (B+)**: ленивый буфер 512 на чтение/запись с
- `crt0.s` — текущий, для **tiny/big**: SP=0xBFFE, парсит argv (W2-ресурс уже выделен DSS). автопереключением, таблица OPEN_MAX=8, flush-on-exit, ungetc,
- `crt0_minimal.s` — текущий, для tiny без argv. fprintf/vfprintf, fdopen/freopen/fclosall/fgetpos/fsetpos; горячие
- `crt0_small.s`**новый, step 3**: для **small/huge**, аллоцирует W2 через `mem_alloc_pages` ДО gsinit, маппит в порт `0xA2`, потом стандартный flow. пути fgetc/fputc/fgets на asm (fgets 100 КБ: 144с unbuffered-оценка
- `crt0_banked.s` — текущий, для **big**: trampoline-таблица для W3 банков, CODE в W2. → ~1 с). Дизайн: docs/file-buffering-design.md
- `crt0_banked_small.s`**новый**: huge = small (W2-alloc) + banked (W3 trampolines). - **scanf/fscanf/sscanf** — своё C-ядро (в SDCC z80 нет)
- **Solid-C совместимость закрыта**: <dos.h> (даты/диски/absread),
sprinter-cc подбирает crt0 по `--memory` mode (сейчас `--crt0=` это override). errno-алиасы, <sprinter_solid.h> — docs/solid_c_compatibility.md
- [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. - гигиена: stale .rel чистка, все 43 теста в make all, размерный
регресс (make size-check), контракт заголовков docs/libc-headers.md
Интерфейс: `sprinter-cc --memory [tiny|small|big|huge|manual] [--memory-manual SPEC] [--stack-size N] foo.c`. `--memory-manual` имеет смысл только с `--memory manual`. - справочник API: docs/libc-reference.md
## Known issues / quirks ## Этап 9 — memory modes (tiny/small/big/huge/manual) ✅ 2026-05-30
- **ESTEX $46 ENV API**: ✅ работает. Док-ция в `DiskSyscalls.txt v1.6` ошибочно описывает return-status — A=0 это NOT FOUND, не FOUND. Зафиксировано в `memory/sprinter_platform.md`. `--memory MODE` в sprinter-cc; crt0-семейство (crt0/minimal/small/
banked); small: ESTEX GETMEM+SETWIN2 до gsinit, auto-detect W2 по
## ОБЯЗАТЕЛЬНЫЕ ЗАДАЧИ ДЛЯ V2 (после релиза v1) порту 0xC2; big/huge: параметризация crt0_banked/bank.s через
BANK_W1; --debug, --stack-size. Детали: memory/memory_modes_
### Turbo-C-style graphics API (BGI-like) — **MUST для v2** implemented, memory/sprinter_memory_modes.
Расширить наш `gfx_*` API до уровня **Turbo-C `<graphics.h>`** (BGI) для MS-DOS. Дизайн-решения: одна sprinter.lib на все режимы (DCE per-member);
Программисты привыкшие к Turbo-C должны переносить графический код 1-в-1. gfx.lib отдельно не нужен; libc_banked + sprinter_home.lib — идея
на потом (триггер: HOME забит user-кодом).
**Что должно быть** (на основе Borland BGI):
## Этап 8 — графика ✅
Setup/teardown:
- `initgraph()` / `closegraph()`у нас сейчас `gfx_init`/`gfx_done`, добавить alias 320×256×256 + 640×256×16, акселератор (Fill h/v, SMC block-size),
- `getmaxx()` / `getmaxy()` — макрос на GFX_WIDTH-1 / GFX_HEIGHT-1 Брезенхэм, bitmap font (WIN_GET_ZG, interleaved), gfx_text.
- `cleardevice()` — alias to gfx_clear memory/sprinter_graphics*, sprinter_accelerator, sprinter_font_format.
- `getgraphmode()` / `setgraphmode()`у нас get_videomode/set_videomode
## Bank-local data ✅
Color/palette:
- `setcolor(c)`, `getcolor()` — current draw color --codeseg/--constseg/--dataseg BANKn + mkexe -p 0; фикс трамплина
- `setbkcolor(c)`, `getbkcolor()` — background color (pop bc/out (c),b — сохраняет A); malloc из банка прозрачен (heap в
- `setpalette(idx, c)` — палитра entry W2). memory/bank_local_data_pattern.
- `getpalette(&info)` — read all palette
## Этапы 5-7 и ранняя libc ✅
Primitives (мы уже имеем эквиваленты — добавить BGI-имена как aliases):
- `putpixel(x, y, c)` — есть как gfx_putpixel - malloc/free (SDCC + runtime/heap.s в W2), page allocator
- `getpixel(x, y)` — нужно реализовать (RMW обратное — IN) (mem_alloc_pages, ESTEX $3C-$3E + BIOS $C4), bank_read/bank_write
- `moveto(x, y)`, `lineto(x, y)`, `linerel(dx, dy)` — current point + line drawing - crt0 argv-парсинг (IX-prefix, CP/M-space quirk, APPINFO basename),
- `line(x1, y1, x2, y2)` — есть как gfx_line sprinter-cc wrapper со всеми опциями
- `rectangle(x1, y1, x2, y2)` — есть как gfx_rect (но другой API: x1,y1,x2,y2 vs x,y,w,h!) - errno+strerror/perror, open state-machine, atexit, setjmp/longjmp,
- `bar(x1, y1, x2, y2)` — есть как gfx_fill_rect sleep, ENV API ($46), ffirst/fnext, getdatetime/setdatetime,
- `bar3d(x1, y1, x2, y2, depth, topflag)` — новое: rect + 3d edges chdir/getcwd/mkdir/rmdir, conio (полный), mouse (RST 30h, 14 ф-й),
- `circle(x, y, r)`, `arc(...)`, `ellipse(...)`, `pieslice(...)` — новые primitives POSIX time API, sys/stat, assert
- `fillpoly()`, `drawpoly()` — полигоны - text I/O split (stdio fast / conio attr) — memory/text_output_api_split
- `floodfill(x, y, border_color)` — заливка - SDCC stdlib НЕ переписываем — memory/sdcc_stdlib_works
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:` строки) из вывода `sdc
c`. Через `-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 × количество строк/столбцов экономии. Применимо ко всем композитным примитивам.
+195
View File
@@ -0,0 +1,195 @@
# Fast RAM (Быстрое ОЗУ / «КЭШ-ОЗУ») на Sprinter
Сводка по результатам изучения документации платформы. Источники:
- `docs/converted/Architecture.txt` — официальное «Описание архитектуры» (раздел
«Распределение основной памяти»).
- `docs/converted/ARHITECT.txt` — ранняя редакция того же документа (про загрузку
конфигураций ППЛМ).
- `docs/converted/IvanMak.txt` / `docs/converted/Parinov.txt` / `docs/converted/Forum.txt`
и `docs/part2/forum.txt` — форумные ответы Дениса Паринова (Sprinter Team) и
руководство Ивана Мака (раздел «7 КЭШ-ОЗУ»).
- `docs/part2/accelerator_doc.txt` — ограничение акселератора.
- `docs/samples/sprinterIntLib.asm` — практический пример temporary-off / restore.
> **Терминология.** В документации одно и то же ОЗУ называется тремя именами:
> **Fast RAM**, **Быстрое ОЗУ** и **«КЭШ-ОЗУ»**. Это *не* кэш в формальном смысле
> (нет автоматического заполнения/вытеснения) — это отдельный массив статической
> памяти, в котором процессор работает на полной частоте **без тактов ожидания**.
> Имя «КЭШ» — историческое, по аналогии с кэшем на КР537РУ10 в Pentagon-128.
---
## 1. Что это и зачем
* **Объём:** 64 КБ статической памяти (SRAM), отдельной от основного DRAM-SIMM
(4 МБ) и от видео-ОЗУ (256 КБ).
* **Скорость:** процессор обращается к Fast RAM на полной тактовой частоте
(21 МГц) **без wait-state'ов**. Основное ОЗУ (DRAM) требует тактов ожидания,
поэтому код и данные в Fast RAM исполняются/читаются заметно быстрее.
* **Назначение:** разместить «горячий» код или данные (внутренние циклы,
таблицы, буферы), которые критичны по скорости.
* **Системная роль:** Fast RAM также используется механизмом
переконфигурирования ППЛМ — именно в неё BIOS грузит данные новой
конфигурации и флаг `ACEX_30K_LOADING` (старое имя `FLEX_10K_LOADING`) перед
программным сбросом. Поэтому к Fast RAM нельзя относиться как к «своей» памяти,
которая всегда сохраняется (см. §5).
---
## 2. Карта физических страниц
Память делится на 16 КБ-блоки с однобайтовым физическим номером:
| Тип памяти | Физические номера страниц |
|---------------|---------------------------|
| Основное ОЗУ | `#00..#4F`, видео-область `#50..#5F`, ... |
| ПЗУ (ROM) | `#E0..#EF` |
| **Fast RAM** | `#F0..#FF` |
> Хотя диапазон номеров Fast RAM — `#F0..#FF` (16 значений), **реально
> используются только биты 1 и 2** номера страницы. То есть адресуются 4
> страницы × 16 КБ = **64 КБ**: `#F0`, `#F2`, `#F4`, `#F6`.
---
## 3. Как включать Fast RAM
Есть **два способа** подключить Fast RAM в адресное пространство Z80.
### Способ A. Pentagon-style через порт `#FB` / `#7B` (в окно 0)
Включается «как кэш в Pentagon»: подключает 16 КБ Fast RAM в **окно 0**
(`#0000..#3FFF`) вместо ПЗУ. Переключение — *побочный эффект чтения порта*
(значение в `A` после `IN` — мусор, важен сам факт обращения):
```asm
DI
IN A,(#FB) ; включить Fast-RAM — 16 КБ в окно 0 (#0000..#3FFF)
; ... ваш код / работа с Fast RAM ...
IN A,(#7B) ; выключить Fast-RAM (вернуть ПЗУ в окно 0)
EI
```
* `IN A,(#FB)`**включить**.
* `IN A,(#7B)`**выключить**.
> **Конфликт портов.** Порт `#FB` (и `#4F`) — это также порт COVOX/Blaster-а.
> Вывод (`OUT`) в `#FB` управляет звуком, а *чтение* (`IN`) — переключает
> Fast RAM. Не путать направления обращения.
### Способ B. Как ПЗУ — через PAGE0 (`#82`) + порт `#1FFD`
Fast RAM-страница (`#F0..#FF`) выбирается в PAGE0 и подключается на место ПЗУ
в окно 0 через спец-порт `#1FFD`:
```asm
; выбрать физическую страницу Fast RAM в PAGE0
LD A, #F0 ; номер страницы Fast RAM
OUT (#82), A ; PAGE0 = страница в окно 0
LD A,1 ; 1 → ОЗУ (выбранная страница) в #0000..#3FFF
LD BC,#1FFD
OUT (C),A
; ...
LD A,0 ; 0 → вернуть ПЗУ в #0000..#3FFF
LD BC,#1FFD
OUT (C),A
```
* Порты PAGE: `PAGE0=#82`, `PAGE1=#A2`, `PAGE2=#C2`, `PAGE3=#E2`.
**Чтение** порта PAGE возвращает текущий номер страницы.
* Эти адреса портов формально могут отличаться в других конфигурациях ППЛМ —
правильнее запрашивать их у BIOS и сверять (см. `docs/part2/bios_doc.txt`,
~строка 1033).
---
## 4. Преимущества
1. **Скорость без wait-state.** Главное и единственное предназначение — код и
данные исполняются на полной частоте 21 МГц без тактов ожидания, в отличие от
основного DRAM.
2. **Идеально для горячих участков.** Внутренние циклы, lookup-таблицы,
временные буферы рендера — то, к чему обращаются интенсивно и многократно.
3. **Отдельный массив.** Не отнимает страницы основного 4 МБ ОЗУ и не пересекается
с видео-областью.
---
## 5. Ограничения и подводные камни ⚠️
Это **самая важная часть** — Fast RAM небезопасна в обращении и легко даёт
«молча не работает».
1. **Акселератор НЕ работает с Fast RAM.**
Акселератор поддерживает пересылку блоков только для основного ОЗУ и
видео-ОЗУ. Пересылку **ROM и FastRAM он не поддерживает**. То есть нельзя
использовать accel-Fill/Copy для заполнения или копирования в/из Fast RAM —
только обычные `LD`-циклы процессора.
2. **Содержимое не сохраняется между процессами.**
Fast RAM может быть использована другими программами. При запуске любого
процесса через DSS (а также самим механизмом переконфигурирования ППЛМ)
**содержимое Fast RAM может быть затёрто**. Нельзя рассчитывать на
персистентность данных между вызовами системы.
3. **Перед вызовами DSS и BIOS Fast RAM надо ОТКЛЮЧАТЬ.**
Системные функции рассчитывают на стандартную карту памяти (ПЗУ в окне 0).
Вызывать `RST 10h` (ESTEX/DSS) или `RST 8` (BIOS) при включённой Fast RAM в
окне 0 — нельзя.
4. **Прерывания.**
Fast RAM (способ A) подключается в окно 0, перекрывая ПЗУ и системный вектор.
Если используются прерывания, программа **обязана установить свой обработчик
по адресу `#0038`**. На практике работу с Fast RAM ведут с `DI`, а на время
ожидания кадра/`halt` Fast RAM временно выключают и восстанавливают (см. §6).
5. **Окно 0 занято под DSS.**
В нашем C-toolchain'е окно 0 (`#0000..#3FFF`) — это ESTEX/DSS система
(см. `release_docs/ru/platform_reference.md`). Подключение Fast RAM в окно 0
вытесняет именно её, что усиливает требование п.3.
6. **Конфликт `#FB` с COVOX.** См. §3, способ A.
---
## 6. Канонический паттерн temporary-off / restore
Из реального резидента (`docs/samples/sprinterIntLib.asm`): перед `ei: halt`
(ожидание кадрового прерывания) Fast RAM временно выключается, после —
восстанавливается прежнее состояние:
```asm
_intWaitVsyncSys
call memCacheOffTemporary ; временно выключаем Fast RAM
ei
halt
jp memCacheRestoryState ; восстанавливаем прежнее состояние подключения
```
Идея паттерна: библиотека хранит флаг «было ли Fast RAM включено», умеет
безопасно его снять на время системных операций (прерывания, DSS/BIOS) и вернуть
обратно. При интеграции в C-toolchain эту логику следует обернуть так же:
сохранять состояние, отключать вокруг любого `RST`/`halt`, восстанавливать.
---
## 7. Выводы для нашего C-toolchain (SDCC + target-слой)
* **Из коробки сейчас не используется.** В `runtime/`, `lib/`, `libc/` обращений
к Fast RAM нет (порт `#FB`/`#7B` нигде не задействован под эту задачу).
* **Где могло бы пригодиться:** разместить «горячую» функцию или таблицу в
Fast RAM для ускорения. Но 64 КБ перекрывают окно 0, конфликтуют с DSS и не
переживают системные вызовы — это узкоспециализированный, ручной режим, не
кандидат на общий механизм линковки.
* **Реалистичный сценарий:** короткий самодостаточный inner-loop без вызовов
системы, с `DI`, со своим вектором `#0038`, скопированный в Fast RAM обычным
`LD`-циклом (не акселератором), исполняемый из окна 0, с гарантированным
восстановлением карты памяти перед любым `RST`.
* **Несовместимость с акселератором** означает, что для графики/блочных операций
Fast RAM бесполезна — там выигрывает accel по основному/видео-ОЗУ.
Если будем добавлять поддержку — делать это отдельным opt-in механизмом
(по аналогии с banked-режимами), с обязательной обёрткой off/restore вокруг всех
точек входа в систему.
+161
View File
@@ -0,0 +1,161 @@
# FILE*: буферизация — анализ solid-c и варианты (2026-07-06)
Статус: **вариант B+ РЕАЛИЗОВАН 2026-07-06** (решение пользователя).
Единый ленивый буфер BUFSIZ=512 на чтение/запись с автопереключением
направления (_F_DIROUT), статическая таблица OPEN_MAX=8 слотов
(без malloc для FILE), _fclosall через atexit, ungetc через hold,
fprintf/vfprintf через vsprintf+fwrite, fflush(NULL) = все потоки.
Внутренности: libc/file/_file.h (+_file_sync/_file_buf/_file_slots/
_fclosall). Фактическая цена: filetest 7411→9929 Б _CODE (доля
только-читающих потребителей ~+1.3 КБ, включая malloc); программы
без FILE* не платят. Верификация: MAME filetest + fdmax + fbench.
Ниже — исходный анализ, на основании которого принималось решение.
## Как сделано в solid-c (SRC/CLIB/STDIO.ASM)
Структура FILE — 14 байт, статический массив `_iob[8]` (без malloc
для самих FILE; псевдопотоки stdout/stderr/stdaux/stdprn лежат ПЕРЕД
массивом и адресуются отрицательными индексами — трюк, нам не нужен):
flags(2), level(2), curp(2), fd(2), buffer(2), hold(1), token(2), dummy(1)
Механика:
- **Буфер 512 Б, ленивый malloc** при первом буферизуемом fgetc/fputc
(флаг `_F_BUF` = «буфер наш, free при fclose»). Программа без
файлового I/O не платит ничего.
- **Чтение** (`_fgetc`): `level == 0``read(fd, buffer, 512)`,
`curp = buffer`; отдача — `*curp++`, `level--`; ставится `_F_IN`.
- **Запись** (`_fputc`): `*curp++ = c`, `level++`; при `level == 512`
fflush (один `write` всего буфера); ставится `_F_OUT`.
- **Полудуплекс**: fputc при взведённом `_F_IN` — ОШИБКА (не
авто-flush); направление сбрасывает только fflush.
- **fflush входного потока**: `lseek(fd, -level, SEEK_CUR)` — откат
непрочитанного readahead, буфер инвалидируется. Выходного —
`write(buffer, level)`.
- **fseek/ftell** = fflush + голый lseek/ltell по fd (после flush
позиция fd совпадает с логической позицией потока).
- **ungetc**: буфер не пуст → `*--curp = c`; пуст/отсутствует → символ
в поле `hold`, `curp` указывает на hold.
- **fclosall через atexit** — сброс буферов при exit.
- Консольные потоки минуют буфер (RST-вызовы напрямую).
- **fread/fwrite — ПОБАЙТОВЫЙ цикл** через _fgetc/_fputc: большие блоки
платят call+IY-доступ за каждый байт. Это слабое место порта.
## Варианты для нас
**A. Полный порт solid-c** (буфер на чтение и запись).
Плюсы: ускоряются и писатели через fputc/fprintf. Минусы: полудуплекс
(«запись после чтения без fflush — ошибка») — источник тонких багов;
обязателен flush в exit (сцепка atexit+file); больше кода во всех
модулях; наши блочные fread/fwrite пришлось бы защищать от деградации.
**B. Буферизовать ТОЛЬКО чтение (рекомендую).**
FILE += `buf(2), level(2), curp(2), hold(2)`; буфер 512 Б лениво.
- fgetc: hold → буфер → refill. fgets остаётся циклом по fgetc
(теперь дешёвым).
- fread: сначала хвост буфера (memcpy), остаток ≥ 512 — прямой read()
в ptr одним syscall (обходя буфер), мелкий остаток — refill.
- Запись НЕ буферизуется — как сейчас: fputc = write(1 байт),
fputs/fwrite = один write() на блок. Нечего терять при аварии,
fflush остаётся no-op по данным, полудуплекса нет.
- Согласование позиций: перед write/fseek/ftell на потоке с readahead —
`lseek(fd, -(level), SEEK_CUR)` + инвалидация буфера (один общий
хелпер `_file_sync`). ftell = lseek(0,CUR) level (без syscall не
выйдет — lseek и так syscall).
- ungetc: через hold, работает и до первого заполнения буфера.
Плюсы: решает главную боль (парсеры), запись остаётся простой и
надёжной, никакого flush-on-exit, r+ работает через _file_sync.
Минусы: fputc-писатели остаются медленными (редкий паттерн — fputs/
fwrite блочные и так быстрые).
**B+. Единый буфер на чтение И запись с АВТОпереключением направления
(предложение 2026-07-06, кандидат в целевой дизайн).**
Схема solid-c, но без ловушки: направление переключает сама библиотека.
- флаг направления в FILE: буфер сейчас «readahead» или «накопитель
записи»;
- fputc при направлении «чтение»: `_file_sync` (отмотка fd на -level,
буфер пуст) → режим записи → накопление; сброс write() при
заполнении;
- fgetc при направлении «запись»: flush (write(buf, level)) → режим
чтения → refill;
- fwrite больших блоков: flush + прямой write мимо буфера; мелких —
memcpy в буфер. fread симметрично;
- fseek/ftell/fclose: flush-или-sync по направлению; ftell = позиция
fd level (чтение) / + level (запись);
- **обязателен реестр открытых потоков**: поле next в FILE
(регистрация в fopen, снятие в fclose) + _fclosall через atexit —
стандарт требует flush всех потоков в exit(); без этого
`fputs(...); exit(1);` теряет данные;
- цена-семантика: ошибки записи становятся ОТЛОЖЕННЫМИ (вылезают при
flush/fclose, не в момент fputc) — проверять результат fclose;
- цена-код: ~+350–500 Б против ~+200–300 у B (тянется только
использующими FILE*).
**C. Оставить небуферизованным** («большие файлы читаются целиком в
EMM», паттерн mdview). Для приложений-парсеров среднего размера
неудобно; отвергается самим существованием П2-пункта.
**D. Полная стандартная буферизация + setvbuf** — отвергнуто ранее
решением file_star_design (минимальный FILE*).
## Что взять у solid-c при варианте B
- ленивый malloc 512 Б + флаг «буфер наш»;
- откат readahead lseek'ом (механика их fflush-на-вход) — как
`_file_sync` перед write/fseek/ftell;
- ungetc с hold-байтом;
- консольные потоки мимо буфера (у нас уже так).
Чего НЕ брать: побайтовые fread/fwrite, полудуплекс, статический
`_iob[]` с отрицательными индексами, буферизацию записи.
## Лимит открытых файлов и статическая таблица FILE (2026-07-06)
Факты: solid-c — OPEN_MAX = 8, статический массив из 8 FILE-структур
(+5 псевдопотоков перед ним), fdopen отвергает fd > 8. DSS-доки:
FCB строятся «в рабочих областях ДОС», код ошибки 06h = «Too many
open files» (наш EMFILE = 6 совпадает).
**ПОДТВЕРЖДЕНО тестом fdmax (MAME, DSS 1.71.57, 2026-07-06)**:
пользователю доступно 8 манипуляторов, fd 2..9 (fd 1 держит шелл DSS
под запущенный .exe). КРИТИЧНО: 9-й OPEN не возвращает 06h — он
ВЕШАЕТ систему. Поэтому в libc/io добавлен предохранитель _fd_guard
(счётчик в open/close, отказ EMFILE на 9-м open без захода в DSS) —
таблица fopen и guard вместе закрывают и высокий, и низкий уровень.
Следствие для B+: вместо malloc-FILE + связного списка-реестра —
**статическая таблица из 8 слотов** (свободный слот: flags == 0):
- реестр для flush-on-exit бесплатен: _fclosall = цикл по таблице;
- fopen без malloc — единственный отказ синхронен с отказом DSS
(EMFILE), утечка «fclose без free» невозможна;
- цена: ~112128 Б BSS (в exe не входит), только у программ с fopen;
- буферы НЕ статические — остаются ленивыми malloc 512 Б (потолок
8×512 = 4 КБ heap в худшем случае);
- объявить FOPEN_MAX 8 в stdio.h; если fdmax покажет лимит DSS < 8 —
уменьшить таблицу.
## Семантика инвалидации (вариант B) — контрольный сценарий
Буфер — только кэш опережающего чтения. Правило: **любая запись и
любой fseek обнуляют буфер; запись всегда идёт напрямую в файл после
отмотки позиции** (`_file_sync`: `lseek(fd, -level, SEEK_CUR)` +
`level = 0`). Буфер при записи НЕ патчится — write-through с правкой
окна отвергнут как сложный ради редкого паттерна.
Сценарий «r+, чтение-запись-чтение» (обсуждено 2026-07-06):
read 512 в буфер → 10×fgetc (логическая поз. 10, fd на 512) →
первый fputc: sync отматывает fd на 10, буфер пуст, 10×write ложатся
на 10..19 → fseek(0) → fgetc перечитывает буфер С ДИСКА и видит
записанные байты. Протечка старой копии невозможна — она уничтожена
в момент первой записи.
## Оценка/проверка
Бенчмарк до/после: цикл fgets по tests/seek/big.txt с замером ptime
(тест tests/fbench), плюс filetest-регресс в MAME. Ожидание: чтение
~512× меньше syscall'ов; код file-модулей +200–300 Б (тянется только
использующими FILE*).
+229 -2
View File
@@ -1,6 +1,233 @@
# IM2 Interrupt Handlers — Design Document # IM2 Interrupt Handlers — Design Document
**Status:** РЕАЛИЗАЦИЯ ОТЛОЖЕНА (до пост-релизной версии). Обязательная фича для v2. **Status: Phase 1 (2026-07-06), Phase 2a CTC (2026-07-07), Phase 2b CBL
(2026-07-07, редизайн на callback без кольца — см. ниже) РЕАЛИЗОВАНЫ**
(libc/irq: irq_install/irq_remove + трамплин, irq_ctc_install/remove;
libc/cbl: cbl_open/close/push_otir/push_accel + fill-callback; тесты
tests/irqtest, tests/cbltest, tests/cblwav, tests/cblstream).
Callback-редизайн **verified в MAME 2026-07-07**: все три CBL-теста
(cbltest — матрица 64 комбинации; cblwav — banked-стрим, голос слышен;
cblstream — собственное кольцо приложения) работают.
## Результаты verification (2026-07-06, по docs/samples и исходникам MAME)
Первая версия зависала на первом же прерывании после irq_install.
Причины и проверенные факты:
- **DSS работает в IM 1** (обработчик на 0x0038). I=0x3F — наследие
Spectrum ROM, НЕ признак IM 2-таблицы. Доказательства:
`docs/samples/sprinterIntLib.asm` (intRestoreDefaultInterrupt: `ld i,a`
+ `im 1` безусловно, «обязательно перед функциями дос и биос») и
`docs/samples/SIO_CTC_KEY.asm` (выход: `LD I,A` + `IM 1`). Чтение
«таблицы DSS» по [I<<8+0xFF] давало мусор (0x00BF) и было причиной
зависания. Фикс: чейн из трамплина ВСЕГДА на 0x0038 (jp с
interrupted-PC на стеке = имитация RST 38); irq_remove всегда
восстанавливает IM 1 (I — только регистр).
- **CBL-фильтр по биту 7 порта 0xFE убран из трамплина**: в MAME при
выключенном CBL бит 7 всегда = 1 (sprinter.cpp kbd_fe_r:
`data |= 0xE0`), и каждое кадровое прерывание ложно классифицировалось
как CBL — user-handler не вызывался бы никогда. Признак «#fe.bit7=1»
(sprinterIntLib.asm) имеет смысл только при активном CBL — вернуть в
Phase 2 вместе с поддержкой CBL.
- **Порт 0x19 = SIO-A RR0 (Z84C015), бит 0 = «Rx Character Available»** —
семантика клавиатурного пробника верна (подтверждено
sprinterIntLib.asm: `in a,(COM_A); bit 0,a; Z → кадровое`).
- **Вектора встроенной периферии Z84C015**: SIO — 0x10..0x1E, CTC — 0x06
(базовые вектора задаются записью в WR2 SIO-B / CTC ch0). Штатно их
прерывания выключены (сэмплы включают/выключают их сами); наша
заливка 257×H перехватывает любой вектор на трамплин, а чейн на
0x0038 безопасен для любого источника.
- Внешний вектор действительно 0xFF (MAME sprinter.cpp:
`set_irq_acknowledge_callback` → 0xff).
Отличия реализации от плана ниже:
- **отдельный `--memory im2` НЕ понадобился**: таблица — статический
буфер 513 Б в BSS с выравниванием в рантайме; т.к. внешний вектор —
только 0xFF, значимы лишь байты [0xFF]/[0x100], и 3-байтовый
`jp _irq_tramp` лежит ВНУТРИ таблицы по смещению H (H = старший
байт её адреса, < 0xC0 — не пересекается). Никаких linker-областей
и правок crt0;
- работает в tiny/big (код и данные в W2); в small/huge irq_install
возвращает EINVAL (проверка адресов трамплина/буфера);
- **чейн к DSS — ВСЕГДА и всегда на 0x0038** (и клавиатура, и кадр):
не нужно знать, что DSS делает в своём ISR — SYSTIME/клавиатура/мышь
живут. User-handler зовётся только на кадровых (фильтр: бит 0
порта 0x19 → мимо); финальный jp — SMC-операнд;
- W3-порт в трамплине НЕ сохраняется: gfx держит DI на время свопов,
а user-handler'у banking запрещён; DSS свои окна сохраняет сам;
- irq_remove вешается на atexit (выход без снятия = IM 2/I указывают в
память умершего процесса = крах шелла); восстановление — всегда IM 1.
## Phase 2a — CTC-таймер (РЕАЛИЗОВАН 2026-07-07)
`irq_ctc_install(handler, div2, div3)` / `irq_ctc_remove()` — вектор
0x06, ОТДЕЛЬНЫЙ от кадрового 0xFF (решает проблему «кадр и клавиатура
неразличимы»). Механика (по docs/samples/sprinterIntLib.asm и
SIO_CTC_KEY.asm):
- CTC Z84C015: канал 2 тактируется видеотактом **875 кГц (1 тик =
1 знакоместо)** и работает делителем; канал 3 считает от канала 2 и
прерывает. f = 875000/(div2*div3), div 0 = 256;
- пресет IRQ_CTC_VSYNC_DIV2/3 = 112×160 (2 пикс. линии × 160 = 320
линий) — точное начало кадра ~48.8 Гц;
- порты: CH0=0x10, CH2=0x12, CH3=0x13; control-слова 0x57 (ch2:
counter, int off) / 0xD7 (ch3: counter, int on); базовый вектор
блока пишется в CH0 (0 → вектор ch3 = 0x06); стоп = 0x03 (reset);
- **RETI обязателен** в CTC-трамплине: daisy chain Z84C015 снимает IUS
только по опкоду RETI — с RET следующее прерывание не придёт;
- CTC-путь НЕ чейнится к DSS (личное прерывание); кадровые/клавиатурные
0xFF идут своим путём параллельно;
- общая IM2-таблица под счётчиком ссылок (_irq_table.c): кадровый и
CTC-хендлеры ставятся/снимаются независимо, последний возвращает
I/IM 1; atexit-уборка глушит CTC ОБЯЗАТЕЛЬНО (иначе после выхода
прерывания кГц-частоты душат шелл).
## Phase 2b — CBL/COVOX audio (РЕАЛИЗОВАН 2026-07-07, редизайн без кольца)
`libc/include/cbl.h`: `cbl_open(freq_code, fmt, pump_mode,
underrun_mode, fill)` / `cbl_close()` / `cbl_push_otir(src,n)` /
`cbl_push_accel(src,n)` / `cbl_requests()` / `cbl_underruns()`. По
`docs/samples/Пример для CBL.asm`, разделу «Звук через COVOX-Blaster»
в `docs/converted/Forum.txt`, официальной документации "5.3
COVOX-Blaster" (see below) и `docs/converted/accel_r.txt`:
**Архитектурный редизайн (2026-07-07)**: у CBL УЖЕ ЕСТЬ собственный
аппаратный буфер 256 Б, разбитый на ДВЕ половины по 128 Б (double
buffering целиком на стороне железа — см. официальную доку: "Блок ОЗУ
256 байт условно разбит на две банки по 128 байт, и бит 7 порта #FE
указывает какая из банок ОЗУ выводится в ЦАП... используется программой
вывода для определения, нужно ли подгружать следующие 128 байт").
Держать ЕЩЁ ОДНО кольцо в libc поверх этого — лишний второй буфер и
лишняя копия. Библиотека теперь НЕ хранит кольца: при cbl_open()
регистрируется callback `fill(n)`, который ISR вызывает НАПРЯМУЮ, а
callback сам пропихивает n байт ИЗ ДАННЫХ ПРИЛОЖЕНИЯ (статический
массив, банковая EMM-страница, файл — что угодно) прямо в CBL через
`cbl_push_otir()`/`cbl_push_accel()` — без промежуточной копии в libc.
Пример из офиц. документации делает ровно это: `OUTI` читает прямо из
HL, указывающего в банковую страницу с WAV-данными, без всякого
стейджинга.
`fill(n)` вызывается ИЗ ISR — ОБЯЗАН быть быстрым: никаких
ESTEX/BIOS/gfx-вызовов (тот же констрейнт, что у `irq_install()`-
хендлера). В частности, диск (read() — ESTEX) читать из fill() НЕЛЬЗЯ
— см. tests/cblstream ниже, где под это заведено собственное кольцо
уровня приложения. Возвращает ненулевое при успехе, 0 — недолив.
- **Control-порт 0x004E — 16-битный** (`ld bc,#0x004E` / `out (c),a`,
не 8-битный `out (n),a`). Биты: 7 = CBL on, 6 = stereo, 5 = 16-bit,
4 = interrupt enable, 3..0 = код частоты (8=7.8125 кГц…F=109.375 кГц,
0/1 — legacy-режим без сэмплирования). `cbl_open` шлёт
`0x90|fmt|freq` (on + int + формат + частота) под DI, `cbl_close`
шлёт `0`.
- **Формат — CBL_FMT_MONO8/MONO16/STEREO8/STEREO16** (2026-07-07):
биты 5(16-бит)/6(stereo) напрямую соответствуют константам, можно
OR'ить с freq. Блок запроса — 128 Б для 8-бит, 256 Б для 16-бит
(НЕ зависит от моно/стерео — см. Forum.txt: «для каждых 128 байт
(256 в режиме 16 бит)»); тишина — 0x80 (8-бит unsigned) или 0x0000
(16-бит signed). 8-бит сэмплы центр 0x80, 16-бит центр 0x0000,
stereo — чередование L/R. Runtime-размер блока — `_cbl_block`
(128 или 256), заполняется в `cbl_open` по fmt.
- **Два насоса, выбор — CBL_PUMP_OTIR/CBL_PUMP_ACCEL** (2026-07-07):
- **OTIR** (`_cbl_pump_otir`) — блок в порт 0x4F через `otir`; B=младший
байт `_cbl_block` (128 остаётся 128, 256 идёт как 0 — Z80 OTIR:
B=0 значит 256 итераций). Проверено в MAME (звук слышен, 0
underrun).
- **ACCEL** (`_cbl_pump_accel`) — запись через акселератор в
спец-страницу EMM 0xFD, замапленную в окно W3 на 0xC000 (Forum.txt:
«запись данных в COVOX-Blaster... 128/256 байт с адреса 0xC000»).
Последовательность (по accel_r.txt + рабочему прецеденту
libc/gfx/_gfx_hfill256.c): `LD D,D` (режим размера блока) →
immediate `LD A,n` (SMC-патч, 0 значит 256 — тот же трюк, что и в
OTIR) → `LD L,L` (режим "копирование блока") → `LD A,(HL)` /
`LD (DE),A` (блочное чтение источника → блочная запись в
0xC000@стр.0xFD) → `LD B,B` (выкл). Акселератор НЕ продвигает
HL/DE сам — advance после пересылки делается вручную (`add hl,
(block)`). W3 сохраняется/восстанавливается вокруг переключения
(как `bank_read`/`bank_write`); доп. DI/EI не нужны — весь насос
целиком уже внутри ISR (прерывания замаскированы до EI/RETI
трамплина). **Verified в MAME 2026-07-07** — tests/cbltest, все
32 accel-комбинации матрицы прошли без ошибок/underrun.
- Общий каприз с CBL-примером из docs/samples: там для установки
размера блока акселератора используется `LD C,128` СРАЗУ ЗА
`LD D,D` — это противоречит и accel_r.txt («далее следует команда
типа LD A,dat»), и нашему же подтверждённому на gfx констрейнту
(CLAUDE.md: «block-size ОБЯЗАН быть immediate операндом LD A,n»).
Мы взяли ВЕРИФИЦИРОВАННЫЙ вариант (LD A,n), а не пример — вероятно,
у автора размер уже был установлен раньше (заметка в accel_r.txt:
«если размер блока был установлен ранее, его можно не
устанавливать»), и `LD C,128` в примере готовит BC для последующего
`ADD HL,BC`, а не для акселератора.
- **Data-порт 0x4F** (для OTIR-насоса), блок 128/256 байт.
- **Признак запроса блока — бит 7 порта 0xFE — валиден ТОЛЬКО когда
CBL реально активен.** При выключенном CBL MAME (`kbd_fe_r`)
подтягивает этот бит к 1 всегда (`data |= 0xE0`) — поэтому трамплин
проверяет бит 0xFE.7 не напрямую, а через индирекцию
`_irq_cbl_hook`: пока `cbl_open` не установил хук, кадровые
прерывания даже не читают порт 0xFE (см. Phase 1 — по этой же
причине первая версия фильтра была убрана). Официальная документация
описывает тот же бит как "старший бит счётчика" адреса аппаратного
буфера — по нему же программа определяет, какую половину доливать.
- **Насос (`_cbl_pump_otir`/`_cbl_pump_accel`) теперь тривиален**:
`_cbl_reqs++; ok = _cbl_fill ? _cbl_fill(_cbl_block) : 0; if (!ok) {
_cbl_undr++; ...}`. Обычные (не `__naked`) Си-функции — можно, т.к.
трамплин уже сохраняет ВЕСЬ контекст (оба регистровых набора + IX/IY)
вокруг вызова хука, что бы функция ни наделала с регистрами.
- **Поведение при недоливе — CBL_UNDERRUN_APP/SILENCE** (4-й параметр
cbl_open, 2026-07-07): по умолчанию (`APP`, 0) недолив — не забота
библиотеки, буфер тишины НЕ аллоцируется вовсе, в CBL доигрывает то,
что уже лежало в его аппаратном буфере. `SILENCE` (1) — насос сам
пропихивает тишину (`_cbl_silence`, malloc'ится В cbl_open() ТОЛЬКО
в этом режиме, размером `_cbl_block`, залит 0x80/0x0000 по формату) —
тот же приём, что раньше был жёстко вшит в насос, теперь опционален.
`cbl_underruns()` считает недоливы в ОБОИХ режимах — это только
диагностика.
- **CBL-путь НЕ чейнится к DSS** — личное прерывание CBL, полный сейв
контекста (основной набор + теневой AF/BC/DE/HL + IX/IY, т.к. `fill()`
может клобберить что угодно) → `EI`/`RETI` напрямую, без 0x0038.
- `cbl_open` держит те же анти-повторные гарантии, что и irq/ctc:
занятый хук → EBUSY, `atexit(cbl_close)` регистрируется один раз,
`_irq_table_ref()`/`_irq_table_unref()` для общей IM2-таблицы.
- **tests/cbltest**: полная матрица (2 насоса × 8 форматов × 4 частоты
= 64 комбинации) пилообразного тона, ~1 с каждая; `fill_tone()`
всегда возвращает 1 (period-64 тон никогда не "кончается") —
CBL_UNDERRUN_APP без буфера тишины достаточно. Main не поллит
ничего — просто ждёт halt()'ом. OTIR+16-бит (16/64) пропускаются
заранее (cbl_open вернул бы EINVAL).
- **tests/cblwav**: потоковая речь (78 КБ) с ДИСКЕТЫ через banked EMM
(диск слишком медленный для realtime — клип предзагружается в RAM
ДО cbl_open). `fill_speech()` делает `bank_read()` из уже загруженной
страницы в стейджинг и `cbl_push_otir()` — физические номера страниц
кэшированы в массиве ЗАРАНЕЕ (на этапе загрузки, в main-контексте):
`mem_get_page()` — BIOS-вызов, сам управляет EI/DI, и звать его ИЗ
fill() (то есть из ISR) нельзя — его `ei` при возврате может
преждевременно снять маску прерываний, пока мы ещё внутри ISR.
- **tests/cblstream**: без banked-предзагрузки — чтение с диска
ОДНОВРЕМЕННО с воспроизведением; рассчитан на быстрый носитель (HDD).
Единственный из трёх тестов, где приложению НУЖНО собственное кольцо:
`read()` — ESTEX-вызов, а `fill()` зовётся из ISR, где ESTEX/BIOS под
запретом — поэтому диск читается ТОЛЬКО в main (в кольцо уровня
приложения), а `fill_stream()` лишь копирует уже готовые байты и
пропихивает `cbl_push_otir()`. НЕ входит в общую сборку (`make`/
`make floppy`) — только `cd tests/cblstream && make run`, свой образ
диска.
ISA-вектора, цепочки нескольких хендлеров на одном векторе — не
реализовано (см. «Phase 2 (когда понадобится)» ниже).
**Щелчок перед первым звуком за сессию (2026-07-07, A/B/C-стенд)**:
на РЕАЛЬНОМ файле (tests/cblwav, потоковая речь) перед началом
воспроизведения был слышен щелчок/призвук. Диагностика через
tests/cblwav (banked-стрим с пилой, затем с чистой тишиной вместо
речи, в одном запуске) показала: щелчок слышен ТОЛЬКО на первом
запуске программы после старта MAME и НЕ зависит от содержимого потока
(тон / тишина / речь — одинаково). Проверенное на macOS (`afplay`)
воспроизведение исходного speech.pcm — чистое, артефактов в самом
файле нет. Разбивка `cbl_open()` на две записи в порт 0x004E (сначала
код частоты, потом enable, с паузой) не повлияла — отменена. Вывод:
это одноразовый прогрев звуковой подсистемы MAME при первой активации
канала CBL за сессию эмулятора, не баг протокола/драйвера; на реальном
железе, скорее всего, отсутствует (см. docs/TODO.md).
Этот документ собирает всё, что мы знаем о прерываниях Sprinter и план реализации user-задаваемых ISR через Z80 IM 2 mode. Когда возьмёмся за реализацию — читать этот файл, чтобы не повторять research. Этот документ собирает всё, что мы знаем о прерываниях Sprinter и план реализации user-задаваемых ISR через Z80 IM 2 mode. Когда возьмёмся за реализацию — читать этот файл, чтобы не повторять research.
@@ -168,7 +395,7 @@ User's ISR НЕ должен:
## Phase 2 (когда понадобится) ## Phase 2 (когда понадобится)
- CBL/COVOX prerequisite handler (для audio playback) - ~~CBL/COVOX prerequisite handler~~ — реализован, см. «Phase 2b» выше
- ISA interrupt handler (для ZX-Bus карт) - ISA interrupt handler (для ZX-Bus карт)
- Multiple user handler chain (e.g. tick + sound) - Multiple user handler chain (e.g. tick + sound)
+54
View File
@@ -0,0 +1,54 @@
# Заголовки libc: контракт затенения SDCC (2026-07-06)
`libc/include` стоит в -I ПЕРЕД заголовками SDCC, поэтому наш файл с
именем стандартного заголовка «затеняет» SDCC-шный. Два разрешённых
паттерна:
## 1. Цепочка `#include_next` — только ДОБАВЛЯЕМ
Наш заголовок первым делом делает `#include_next <имя>` (берёт
SDCC-версию) и дальше только добавляет Sprinter-расширения. Ничего
из стандартной части не переобъявлять — malloc/strlen/… должны
приходить из SDCC, иначе разъедутся прототипы с z80.lib (уже кусало:
полный shadow stdlib.h терял malloc/free).
| Заголовок | Что добавляем |
|---|---|
| `stdlib.h` | min/max (функции, int16_t — как `int min()` в Solid-C) |
| `string.h` | strlwr/strupr (CP866-регистры) |
## 2. Полная замена — обязаны продублировать контракт SDCC
Наш заголовок полностью замещает SDCC-шный. Он ОБЯЗАН объявить всё,
что программы берут из z80.lib, с точными SDCC-сигнатурами:
| Заголовок | Обязан объявлять (из z80.lib) | Наше |
|---|---|---|
| `stdio.h` | printf, sprintf, vprintf, vsprintf | FILE* API (буферизованный B+), puts/putchar/getchar (наши, ESTEX), scanf-семейство, dec*/hex*, gets |
| `time.h` | struct tm, time_t, time, mktime, gmtime, localtime, asctime, ctime — **раскладка struct tm и __TIME_UNSIGNED=1 должны совпадать с SDCC ABI** (см. шапку time.h) | datetime_t, getdatetime/setdatetime, DOW_* |
При апгрейде SDCC сверять сигнатуры этих двух заголовков с
`third_party/sdcc/share/sdcc/include/`.
## 3. Свои заголовки (SDCC-аналога нет — затенения нет)
conio.h, dir.h, dos.h, errno.h, fcntl.h, mouse.h, palette.h,
sprinter*.h, unistd.h, bios/*.
Графика вынесена из libc в отдельную библиотеку libbgi/: её публичные
заголовки gfx.h (mode-agnostic BGI_GFX) и graphics.h (BGI API) живут в
libbgi/include/ и пробрасываются через -I libbgi/include (sprinter-cc
добавляет его автоматически). Внутренний заголовок графики —
libbgi/_bgi.h (слияние старых libc/gfx/_gfx.h и libc/bgi/_bgi.h).
## Правила
- новый стандартный заголовок — сначала пробовать паттерн 1
(include_next); паттерн 2 — только если надо переопределить
реализацию (как puts/putchar на ESTEX);
- в заголовках паттерна 2 — комментарий, какие декларации обслуживают
z80.lib;
- internal-заголовки libc (`_conio.h`, `_file.h`, …) живут
РЯДОМ с исходниками в libc/<area>/, не в libc/include. Internal
графики — в libbgi/_bgi.h (корень libbgi/, подключается из common/ и
bgi256/bgi16/ как `#include "../_bgi.h"`).
+371
View File
@@ -0,0 +1,371 @@
# libc — справочник API (2026-07-06)
Сводка по заголовкам: сигнатура + одна строка + особенности ABI.
Детали дизайна: docs/libc-headers.md (контракт затенения SDCC),
docs/file-buffering-design.md (FILE*), docs/solid_c_compatibility.md.
Общие соглашения:
- ошибки: возврат -1/NULL/EOF + `errno` (код DSS as-is, см. errno.h);
- SDCC `__sdcccall(1)`: 1-й аргумент HL (8-битный — A), 2-й — DE,
остальные на стеке; **int/указатель возвращается в DE**;
- строки для BIOS-вызовов (rst 8) должны лежать в #4000#BFFF;
- стек при любых ESTEX/BIOS-вызовах — в W2 (обеспечено crt0).
## <stdio.h> — полная замена SDCC (контракт: printf-семейство из z80.lib)
Из SDCC z80.lib: `printf sprintf vprintf vsprintf`.
Консоль (ESTEX, без атрибутов — быстрый путь; цветной вывод — conio):
| Сигнатура | Описание |
|---|---|
| `int putchar(int c)` | символ через PUTCHAR $5B; '\n'→CR LF |
| `int getchar(void)` | блокирующий WAITKEY $30, ASCII |
| `char puts(const char *s)` | строка + '\n' (посимвольно через putchar) |
| `char *gets(char *buf)` | строка с консоли, без контроля длины |
| `void dec8/dec16/dec32(v)` | десятичная печать без ведущих нулей |
| `void hex8/hex16/hex32(v)` | hex-печать фиксированной ширины |
FILE* (буферизованный, вариант B+ — единый ленивый буфер BUFSIZ=512
на чтение/запись с автопереключением; таблица `OPEN_MAX=8` слотов;
exit() сбрасывает всё через atexit; **ошибки записи отложенные —
проверять fclose**):
| Сигнатура | Описание |
|---|---|
| `FILE *fopen(path, mode)` | "r/w/a" + '+', 'b/t' игнорируются |
| `FILE *fdopen(fd, mode)` | завернуть готовый fd (закрывать fclose!) |
| `FILE *freopen(path, mode, fp)` | переоткрыть тот же FILE* |
| `int fclose(FILE*)` / `void fclosall(void)` | сброс+закрытие / все потоки |
| `int fflush(FILE*)` | сброс записи / откат readahead; NULL = все |
| `int fgetc/fputc(...)` | горячий путь на asm; getc/putc — макро-алиасы |
| `char *fgets(buf, n, fp)` | до '\n' (сохраняется); блочный LDI-сканер |
| `int fputs(s, fp)` | без '\n'; через fwrite |
| `size_t fread/fwrite(p, sz, n, fp)` | блоки ≥ 512 идут мимо буфера |
| `int ungetc(c, fp)` | 1 байт putback (и на stdin) |
| `int fseek(fp, off, whence)` / `long ftell(fp)` | ftell без побочных эффектов |
| `void rewind(fp)` | fseek(0) + сброс EOF/ERROR |
| `int fgetpos/fsetpos(fp, &pos)` | fpos_t = long |
| `int feof/ferror(fp)`, `void clearerr(fp)` | флаги потока |
| `int fprintf/vfprintf(fp, fmt, ...)` | vsprintf в статический буфер 256 |
| `int scanf/fscanf/sscanf(...)` | %d %u %x %o %c %s, `l`, ширина, %*, %% |
| `int rename(old, new)` | ESTEX RENAME $10 |
`stdin/stdout/stderr` — консольные псевдопотоки (fd 0/-1/-2), не
буферизуются; freopen на них меняет только FILE*-операции (printf
идёт в ESTEX напрямую).
## <stdlib.h> — include_next + добавки
Из SDCC: malloc/free/calloc/realloc (heap в W2), atoi/atol/strtol/
strtoul, rand/srand, qsort/bsearch, abs/labs, div/ldiv, exit-типы.
Наше: `int16_t min(a,b)`, `int16_t max(a,b)` (функции, как в Solid-C).
## <string.h> — include_next + добавки
Из SDCC: mem*/str* полностью. Наше: `char *strlwr/strupr(char *s)`
in-place регистр, латиница + кириллица CP866.
## <time.h> — полная замена (struct tm в SDCC-ABI, __TIME_UNSIGNED=1)
| Сигнатура | Описание |
|---|---|
| `void getdatetime(datetime_t*)` | RTC как есть (ESTEX SYSTIME $21) |
| `int setdatetime(const datetime_t*)` | установка RTC ($22) |
| `time_t time(time_t*)` | Unix-эпоха из RTC |
| `mktime/gmtime/localtime/asctime/ctime` | POSIX поверх RTC (без TZ) |
`datetime_t`: day/month/year(полный)/hour/minute/second/dow (1=Вс).
## <unistd.h>, <fcntl.h> — fd-уровень (манипуляторы DSS)
| Сигнатура | Описание |
|---|---|
| `int open(path, flags)` | O_RDONLY/WRONLY/RDWR + O_CREAT/TRUNC/EXCL/APPEND (ESTEX $11/$0A/$0B) |
| `int creat(path, mode)` | open(W|CREAT|TRUNC); mode игнорируется |
| `int read/write(fd, buf, n)` | ESTEX $13/$14. **Квирк WRITE: DE-возврат ненадёжен, успех = CF=0&A=0** (см. memory/estex_write_de_quirk) |
| `int close(fd)` | ESTEX $12 |
| `long lseek(fd, off, whence)` | 32-битная позиция (MOVE_FP $15) |
| `int unlink(path)` | удалить (DELETE $0E) |
| `int isatty(fd)` | fd <= 0 (файловые манипуляторы DSS с 1) |
| `int mkdir/rmdir/chdir(path)` | ESTEX $1B/$1C/$1D |
| `char *getcwd(buf, size)` | буфер 256 байт, size игнорируется |
| `void sleep(seconds)` | калиброванный busy-wait (см. ниже) |
| `void delayms(uint16_t ms)` | то же, гранулярность — миллисекунды (см. ниже) |
**Лимит: 8 одновременных манипуляторов**; 9-й OPEN вешает DSS —
libc отказывает сама (EMFILE, предохранитель _fd_guard).
**`sleep()` (2026-07-07, редизайн)**: старая версия считала halt-
пробуждения (50 = 1 c), предполагая, что КАЖДОЕ прерывание — кадровый
тик; с CBL/клавиатурой на векторе 0xFF это уже не так (CBL прерывает
намного чаще кадра — sleep() возвращался бы раньше срока). Теперь:
лениво, один раз калибруется кратковременным `irq_install()` против
РЕАЛЬНОГО кадрового тика (трамплин зовёт хук только на настоящих
кадровых прерываниях — клавиатура/CBL уходят в свои ветки раньше),
считая, сколько итераций тесного цикла умещается в один КАДР (не в
секунду — калибровка на секунду переполняла `uint16_t`, был баг:
`sleep(5)` отрабатывал быстрее секунды, найден пользователем на
реальном прогоне и исправлен). `sleep(seconds)` — вложенный цикл:
внешний по секундам, внутренний ровно 50 раз калиброванный busy-wait —
без единого прерывания и БЕЗ умножения/32-битной арифметики (по духу
`docs/samples/delayms.asm`, который тоже калибрует на 1 мс, а не на
1 с). Погрешность ~5-10% (калибровочный и рабочий циклы не тактово-
идентичны). Fallback на старое "50 halt" поведение, если фрейм-хук уже
занят другим `irq_install()`-клиентом (EBUSY).
**Cтоит дороже по размеру** (~+580 Б) — тянет весь модуль `irq`
(install/remove/трамплин/IM2-таблицу), даже если программа больше
ничего из irq не использует.
**`delayms(ms)`** — тот же движок (`libc/time/_sleep_calib.c`), общий с
`sleep()`: калибровка одна на двоих (первый вызов ЛЮБОЙ из функций
калибрует, вторая просто использует готовое). Производная величина
"итераций на 1 мс" — одно 16-битное деление (`__divuint`, НЕ
`__mullong`) на константу 20, вычисляется один раз при калибровке;
`delayms(ms)` — простой цикл `ms` раз, без умножения вообще. Fallback
при EBUSY — грубый (одно кадровое `halt`), точной альтернативы для
миллисекундной гранулярности без калибровки нет.
**Квирк общий для sleep()/delayms()**: калибровка сама стоит ~20-40 мс
(синхронизация на границу кадра + сам замер) — ПЕРВЫЙ вызов ЛЮБОЙ из
двух функций в программе превысит запрошенное время на эту величину;
для `delayms()` с маленьким `ms` это заметно (`delayms(5)` на первом
вызове может растянуться на ~25-45 мс). Все последующие вызовы точны.
Оставлено как есть по решению пользователя — не стали усложнять API
отдельным calibrate()-примитивом.
## <errno.h>
`errno` (int), коды = коды DSS (EOK..EUNKERR) + POSIX-имена
(ENOENT/EBADF/EMFILE/…) + алиасы Solid-C (EZERO/EINVFNC/ENOFILE/…).
`const char *strerror(int)`, `void perror(const char*)`.
## <dos.h> — DOS-слой Solid-C
| Сигнатура | Описание |
|---|---|
| `void getdate/gettime(&d)` | struct date/time (Turbo-C; ti_hund=0) |
| `int setdate/settime(&d)` | RMW полного datetime |
| `uint8_t getdisk(void)` | текущий диск, 0=A (ESTEX $02) |
| `int setdisk(uint8_t)` | смена диска; возврат = число дисков ($01) |
| `int absread/abswrite(disk, sect, cnt, buf)` | секторы ЛОГИЧЕСКОГО диска (BIOS $55/$56); буфер в #4000#BFFF; abswrite минует ФС! |
## <dir.h>
`int ffirst(pattern, ffblk_t*, attrib)` / `int fnext(ffblk_t*)`
поиск по шаблону (ESTEX $19/$1A). Квирк: "."/".." находятся только
итерацией "*.*" (memory/estex_ffirst_dotdot). FA_*-атрибуты.
## <sys/stat.h>
`int stat(path, struct stat*)` / `int fstat(fd, ...)` — st_mode
(S_ISREG/S_ISDIR), st_size, st_mtime (Unix-эпоха).
## <conio.h> — текстовый экран с атрибутами (Turbo-C стиль)
Клавиатура: `kbhit getch getche getkey` (+KEY_* коды позиций),
`char *cgets(buf)`.
Вывод с атрибутом: `putch cputs cprintf` (~10× медленнее stdio-пути;
'\n' НЕ транслируется — писать "\r\n").
Атрибуты: `textcolor textbackground textattr`, `set/get_text_attr`,
COLOR_*-enum, `COLOR(fg,bg)`, COLOR_BLINK; `set/get_putch_raw_mode`.
Экран: `clrscr clrscr_attr gotoxy home() wherex wherey wherexy scroll
wrchar rdchar`; режимы `gettextmode/settextmode` (0x02=40×32,
0x03=80×32).
Порты/IRQ: `inp outp enable() disable()`.
Текстовая палитра: `text_pal_load/set_color/get/get_color/reset`
(план 0..3 → страница BIOS 4..7).
## <bios/text.h> — быстрый BIOS-вывод (rst 8, place-based)
`bios_set_place/get_place`, `bios_write[attr][_until|_stop]`,
`bios_fillchar/fillattr/fillcharattr`, `bios_clearwin[_ch]`,
`bios_scrollwin`. Строка s — в #4000#BFFF; place продвигается.
## <gfx.h> — графика, mode-agnostic API (BGI_GFX); живёт в libbgi/include
Рисование (putpixel/line/bar/circle/…) вынесено в BGI — см. `<graphics.h>`
и driver-библиотеки lib/bgi256.lib / lib/bgi16.lib (выбор режима линковкой:
`sprinter-cc --gfx 256` / `--gfx 16`). В gfx.h остались только функции
БЕЗ BGI-аналога (mode-agnostic, живут в libbgi/common/, .rel в обеих
driver-библиотеках):
Setup: `gfx_init(mode,page)→prev`, `gfx_done(prev)`.
Страницы/банк: `gfx_set/get_visible_page`, `gfx_set/get_draw_page`
(double buffering), `gfx_set/get_bank` (0x50..0x5F, 0x58 = FF-
прозрачность).
`gfx_wait_vsync()` (2026-07-07, редизайн): ждёт переход бита 5 порта
0xFE из 1 в 0 — реальное аппаратное состояние луча (Y>256 → начало
кадра, см. MAME sprinter.cpp kbd_fe_r), а не прерывание — поэтому не
путается с клавиатурой/CBL/CTC, деляющими вектор 0xFF. Бит доступен
только пока включён `cbl_mode()` (bit7 порта 0x004E) — если приложение
уже играет через `cbl_open()`, бит достаётся бесплатно; иначе
`gfx_wait_vsync()` лениво занимает bit7 "немым" кодом частоты через
`_cbl_port_ref()`/`_cbl_port_unref()` (см. `<cbl.h>`, `_cbl_port.c`) —
разделяемое владение портом 0x004E, безопасное при любом порядке
использования с реальным CBL-звуком. Фолбэк на одно кадровое
прерывание (`halt`), если бит не ведёт себя как ожидается за разумное
число попыток.
Шрифт: `gfx_load_default_font`, `gfx_set_font(ptr)` (interleaved
font[row*256+char]) — грузится лениво при первом использовании BGI-текста.
Палитра: `gfx_pal_load/set/get/get_color/reset` (страницы 0..3).
Константы режимов: `GFX_MODE_320x256x256` (0x81), `GFX_MODE_640x256x16`
(0x82); размеры `GFX_WIDTH/HEIGHT` (320/256), `GFX_WIDTH_16/HEIGHT_16`
(640/256). Рисование через `<graphics.h>` (BGI).
## <graphics.h> — Turbo-C BGI (функц. совместимость), Фаза 1, режим 256
Слой поверх `<gfx.h>` со «текущим» цветом/позицией. Режим задаётся
driver-либой на линковке: `sprinter-cc --gfx 256` (→ 320×256×256; 16 —
позже). API mode-agnostic: код не меняется при смене режима.
Setup: `initgraph()` (без аргументов — режим фиксирован либой; грузит
EGA-палитру 0..15, цвет=WHITE, фон=BLACK, CP=(0,0)),
`closegraph()`, `graphresult()`, `cleardevice()`.
Границы/цвет: `getmaxx/getmaxy` (319/255), `getmaxcolor` (255),
`setcolor/getcolor`, `setbkcolor/getbkcolor`. Константы BLACK..WHITE.
Точки: `putpixel(x,y,c)`, `getpixel(x,y)`.
Позиция/линии: `moveto/moverel/getx/gety`, `lineto/linerel` (двигают
CP), `line(x1,y1,x2,y2)` (не двигает).
Фигуры: `rectangle` (контур), `bar` (заливка стилем), `circle`.
Дуги (Ф2a): `arc`, `ellipse(x,y,st,end,xr,yr)`, `drawpoly(n,pts)`.
Текст 8×8: `outtextxy(x,y,s)`, `outtext(s)` (двигает CP).
Заливки (Ф2b): `setfillstyle(pattern,color)`/`getfillsettings` (10
паттернов Borland: SOLID/EMPTY/LINE/…/HATCH/XHATCH/…), `bar3d`,
`fillpoly(n,pts)`, `fillellipse(x,y,xr,yr)`.
Заливка областей (Ф2c): `floodfill(x,y,border)` (медленно, но верно),
`pieslice(x,y,st,end,r)`, `sector(x,y,st,end,xr,yr)`.
Спрайты (Ф2d): `imagesize/getimage/putimage` (COPY/XOR/OR/AND/NOT_PUT;
формат буфера: uint16 w,h + w*h байт).
Стиль линий (Ф2d): `setlinestyle(style,upattern,thick)`/`getlinesettings`
(SOLID/DOTTED/CENTER/DASHED/USERBIT + NORM/THICK) — на line/rectangle/
drawpoly.
Стиль текста (Ф2d): `settextstyle(font,dir,size)`/`gettextsettings`,
`textwidth`/`textheight` — масштаб 1..10, HORIZ/VERT, прозрачный фон
(только DEFAULT_FONT 8×8).
Реализация: libbgi/common/*.c (mode-agnostic math + BGI public API +
BGI_GFX) + libbgi/bgi256/*.c (256-цветные leaf'ы) → lib/bgi256.lib
(Фаза 2 добавит libbgi/bgi16/*.c → lib/bgi16.lib; common .rel одни и
те же в обоих архивах). Leaf'ы — реальные реализации (БЕЗ обёрток:
putpixel/getpixel полностью inline, _bgi_plot_raw/_bgi_hspan_raw/...
поглощают акселераторный asm). Пакетные примитивы — одна W3-скобка
на примитив; тригонометрия/эллипсы целочисленные (Q7/isqrt, БЕЗ 32-бит).
Cross-lib: libbgi всегда линкуется с libc; _cbl_port_ref/unref
объявлены extern в libbgi/_bgi.h (сверять с libc/cbl/_cbl.h).
Ф2d (осталось): setviewport/клиппинг, settextjustify, setaspectratio —
см. docs/TODO.md.
## <irq.h> — user-ISR кадрового прерывания (IM 2)
| Сигнатура | Описание |
|---|---|
| `int irq_install(isr_t h)` | h зовётся ~50 Гц на кадровых прерываниях; DSS-обработчик чейнится всегда (клавиатура/SYSTIME/мышь живы). 0 / -1+errno (EBUSY повтор, EINVAL — код не в W2: только tiny/big) |
| `void irq_remove(void)` | вернуть таблицу DSS; идемпотентно; висит на atexit |
| `int irq_ctc_install(h, div2, div3)` | периодический таймер CTC (вектор 0x06): f = 875000/(div2×div3), div 0=256; пресет кадра IRQ_CTC_VSYNC_DIV2/3 (112×160, ~48.8 Гц); независим от кадрового; трамплин завершает RETI |
| `void irq_ctc_remove(void)` | глушит CTC (обязательно; atexit подстрахует) |
| `IRQ_DISABLE()/IRQ_ENABLE()` | di/ei — скобки для чтения shared-переменных из main |
Handler'у нельзя: ESTEX/BIOS-вызовы, gfx_*/своп окон, акселератор,
banked-функции; только volatile-глобалы и быстрая работа (<1 мс).
## <cbl.h> — потоковый звук CBL/COVOX (вектор 0xFF, свой ISR)
Без собственного кольца (2026-07-07): у CBL уже есть аппаратный буфер
256 Б (2×128, двойная буферизация на стороне железа — см. официальную
доку "5.3 COVOX-Blaster"); библиотека просто зовёт `fill()` приложения
ИЗ ISR, а оно само пропихивает данные (откуда угодно) через
`cbl_push_otir/accel` — без промежуточной копии.
| Сигнатура | Описание |
|---|---|
| `int cbl_open(freq_code, fmt, pump_mode, underrun_mode, fill)` | включить CBL (`0x90\|fmt\|freq`), зарегистрировать callback; 0 / -1+errno (EBUSY повтор, EINVAL — freq/fmt/pump_mode/underrun_mode плохие или OTIR+16-бит, ENOMEM — буфер тишины) |
| `void cbl_close(void)` | выключить CBL, снять хук, освободить буфер тишины (если был); висит на atexit |
| `void cbl_push_otir(const void *src, uint16_t n)` | пропихнуть n байт через `otir` в порт 0x4F; звать ИЗ fill() |
| `void cbl_push_accel(const void *src, uint16_t n)` | то же акселератором (страница EMM 0xFD@0xC000); единственный путь для 16-бит |
| `uint16_t cbl_requests(void)` | счётчик запросов блока ISR (~fs/(сэмплов в блоке) в секунду) |
| `uint16_t cbl_underruns(void)` | счётчик недоливов (`fill` вернул 0 или не задан) |
| `CBL_FREQ_7K8 .. CBL_FREQ_109K` | коды частоты (биты 3..0 control-порта) |
| `CBL_FMT_MONO8/MONO16/STEREO8/STEREO16` | формат (биты 5/6 control-порта); блок 128 Б (8-бит) или 256 Б (16-бит), не зависит от моно/стерео; тишина 0x80 (8-бит) / 0x0000 (16-бит, знаковый) |
| `CBL_PUMP_OTIR / CBL_PUMP_ACCEL` | способ выдачи — см. ниже |
| `CBL_UNDERRUN_APP / CBL_UNDERRUN_SILENCE` | поведение при недоливе — см. ниже |
`typedef int (*cbl_fill_fn)(uint16_t n);` — callback, зовётся ИЗ ISR за
очередным блоком (n = `_cbl_block`, 128/256). **Обязан быть быстрым**
— никаких ESTEX/BIOS/gfx-вызовов (тот же констрейнт, что у
`irq_install()`-хендлера); вернуть ненулевое, если реально пропихнул n
байт. Диск (read()) читать из fill() НЕЛЬЗЯ — см. tests/cblstream,
где под это заведено кольцо уровня приложения.
Два насоса (3-й параметр `cbl_open`):
- **OTIR** (`_cbl_pump_otir`) — `cbl_push_otir()` в порт 0x4F; базовый.
**НЕ умеет 16-бит**`cbl_open(..., MONO16/STEREO16, CBL_PUMP_OTIR,
...)` вернёт EINVAL: по исходнику MAME (sprinter.cpp) порт данных
ВСЕГДА кладёт байт как есть в один слот, не собирая пару байт в
16-бит значение и не сверяясь с 16-бит флагом вообще.
- **ACCEL** (`_cbl_pump_accel`) — `cbl_push_accel()` через акселератор
в спец-страницу EMM 0xFD, замапленную в окно W3 на 0xC000 (см.
docs/converted/accel_r.txt, Forum.txt); размер блока патчится SMC
(`LD D,D` + immediate `LD A,n` + `LD L,L` — тот же паттерн, что и в
libc/gfx/_gfx_hfill256.c); единственный путь для 16-бит.
**Verified в MAME 2026-07-07** (tests/cbltest, вся accel-половина
матрицы прошла без ошибок/underrun).
Поведение при недоливе (4-й параметр `cbl_open`):
- **CBL_UNDERRUN_APP** (по умолчанию, 0) — не забота библиотеки, буфер
тишины НЕ аллоцируется, в CBL доигрывает то, что уже лежало в его
аппаратном буфере.
- **CBL_UNDERRUN_SILENCE** (1) — насос сам пропихивает тишину; буфер
(128/256 Б по формату) аллоцируется malloc'ом ВНУТРИ `cbl_open()`
только в этом режиме.
`cbl_underruns()` считает недоливы в обоих режимах — диагностика,
поведение не меняет.
Приватное прерывание, к DSS не чейнится; бит 7 порта 0xFE (запрос
блока) читается только пока `cbl_open` не закрыт (при выключенном CBL
бит всегда 1 — MAME-квирк).
**Разделяемое владение портом 0x004E** (2026-07-07): бит 5 порта 0xFE
(позиция луча — см. `<gfx.h>` `gfx_wait_vsync()`) доступен только пока
включён bit7 порта 0x004E, независимо от того, играет ли реальный
звук. `_cbl_port_ref()`/`_cbl_port_unref()` (internal, `_cbl_port.c`)
дают gfx-модулю занять bit7 "немым" кодом частоты (не заводящим таймер
CBL — без звука/прерываний), не мешая реальной `cbl_open()`-сессии,
если она уже идёт (и наоборот — `cbl_close()` возвращает "немой" режим
вместо полного выключения порта, если gfx его ещё держит).
## <palette.h> — низкий уровень (BIOS $A4/$A6)
`pal_load pal_get pal_set_color pal_get_color` (страница 0..7,
записи B,G,R,0), `pal_reset(type)` / `pal_reset_at(type,page,graph)`;
PAL_GRAPH/PAL_SINCLAIR/PAL_CGA.
## <mouse.h> — драйвер RST 30h
`mouse_init show hide refresh read(mouse_state_t*) goto bounds_x/y
text_cursor load_cursor/get_cursor(mouse_cursor_t*) set_sensitivity
get_sensitivity_x/y video_mode_changed`. Sensitivity = делитель
(меньше = быстрее). Solid-C алиасы ms_* включены.
## <sprinter.h> — платформа
Константы портов (PORT_PAGE_W0..W3, PORT_RGADR, PORT_RGMOD), номера
всех ESTEX-функций (ESTEX_*), BIOS EMM ($C0..$C7); `__sfr`-доступ и
inline `sprinter_page_w0..w3(page)`; ENV: `getenv putenv sysenv`.
## <sprinter_mem.h> — EMM-страницы и банковый I/O
`mem_alloc_pages(n)→blk_id, mem_free_block, mem_get_page(blk,idx),
mem_info(&total,&free)` (реализации _bios/_estex; макро-выбор
MEM_MANAGE_MODE_*). HOME-резидентный доступ к чужим страницам:
`bank_load_byte/store_byte/read/write` (своп W3 внутри; *_w1 —
вариант через окно W1).
## <sprinter_exit.h>
`atexit` (LIFO, 8 слотов), `exit` (хендлеры+сброс FILE), `_exit`.
## <sprinter_compat.h> / <sprinter_solid.h>
Типы (BYTE/BOOL/WORD/uint/FD/f_point), TRUE/FALSE/OK/ERROR,
`setmem movmem` (порядок аргументов!), `strerr seek tell ltell
remove _ffirst _setargv abort()`, isascii. `<sprinter_solid.h>`
зонтичный: один include для портирования Solid-C программ.
+122
View File
@@ -0,0 +1,122 @@
# libc — план работ (на рассмотрение, 2026-07-06)
Анализ после сплита четвёрки лидеров (time/stat/file/conio, итог 19.6 КБ
суммарно по _CODE приложений). Ниже — что ещё стоит сделать, по приоритетам.
---
## П1. Досплит остальной libc — СДЕЛАНО 2026-07-06
Всё из таблицы ниже посплитано (кроме dec_print — осознанно оставлен).
gfx: 40 модулей (внутренний заголовок `libc/gfx/_gfx.h`, скретчи в
data-модулях `_gfx_state/_gfx_w3_state/_gfx_acc256/_gfx_g16_state/
_gfx_font_state`, helpers `_gfx_hfill256/_gfx_hfill16/_gfx_rmw16/
_gfx_text16`); video/palette → 7 модулей + `_palette.h`;
conio/text_palette → 5 модулей. Эффект (_CODE, Б):
gfx_demo 3769→3227, gfx_d16 3869→3327, gfx_text 6986→2568,
gfx_mous 7287→5542. Не-gfx тесты не изменились.
Правило было: 1 публичная функция = 1 модуль, state/helpers — в отдельные
internal-модули, комментарии на русском, без `= 0`.
| Файл | Ф-ий | _CODE | Замечания |
|---|---|---|---|
| gfx/gfx_256.c + gfx_16.c | 13+13 | 1277+1159 Б | самый жирный кусок; резать по примитивам (putpixel/line/hline/vline/rect/fill/clear/text). Учесть SMC-паттерны акселератора и кэш Port_Y — state в data-модули |
| gfx/gfx_raw_16 / raw_256 / raw_common / core / palette / font / text_* | ~40 | ~2.7 КБ | вместе с предыдущим — весь gfx |
| mouse/mouse.c | 16 | 308 Б | state mb_*/mc_* → data-модули |
| bios/text.c | 14 | 264 Б | все `__naked`, сплит чистый |
| io/open.c | 3+3 | 216 Б | open/creat/close; asm-хелперы `_estex_*_raw` у единственных потребителей |
| errno/errno.c | 2 | 680 Б | strerror + таблица строк неразделимы (один модуль); perror — отдельно, зовёт strerror |
| video/palette.c | 6 | 348 Б | |
| conio/text_palette.c | 5 | 140 Б | |
| io/read.c | 2 | 60 Б | read + write — обязательно врозь (write-only приложения) |
| io/fsdir.c | 4 | 80 Б | mkdir/rmdir/chdir/getcwd |
| env/env.c | 3 | 78 Б | getenv/putenv/sysenv + общий env_buf → data-модуль |
| mem/mem_bios, mem_estex, bank_io_w1/w3 | 34 каждый | ~430 Б | |
| sys/atexit.c | 3 | 106 Б | atexit/exit/_exit; общий стек хендлеров → data-модуль (exit тянется всегда из crt0, выигрыш небольшой но правильный) |
| io/dir.c | 2 | 53 Б | ffirst/fnext |
| stdlib/minmax.c | 2 | 27 Б | min/max врозь |
| stdio/hex_print.c | 3 | 38 Б | сплит чистый (call/jp по именам) — см. docs/libc-split-asm-cases.md |
| stdio/dec_print.c | 3 | 176 Б | НЕ резать (общее тело); открытое решение: вариант «3 независимых цикла» — п. отложен |
Ожидаемый эффект: графические приложения −1–2 КБ, mouse/BIOS-text — сотни байт.
## П2. FILE* — отложенные баги и недостающее
Из шапки бывшего file.c («PROVISIONAL», stdio-review issues 3/4/5):
- [x] fwrite: короткая запись ставит _F_ERROR (issue 3) — сделано 2026-07-06
- [x] fgets(n=1): возвращает пустую строку по стандарту (issue 4) — сделано 2026-07-06
- [x] mode_to_flags: проверено — парсер сканирует весь хвост режима, «rb+» работает (issue 5, уже был исправлен)
- [x] **fprintf/vfprintf** — сделаны 2026-07-06 (vsprintf в статический 256-байтовый буфер + fwrite)
- [x] ungetc — 1-байтный putback через поле hold; работает и на stdin — сделано 2026-07-06
- [x] Буферизация FILE v2 — **реализован вариант B+** (2026-07-06): единый ленивый буфер BUFSIZ=512 на чтение и запись с автопереключением направления, статическая таблица OPEN_MAX=8 слотов, _fclosall через atexit, fflush(NULL) = все потоки. Дизайн: docs/file-buffering-design.md. Цена: filetest (использует всё) 7411→9929 Б _CODE; не-FILE программы не платят ничего. **Ждёт MAME-прогона: filetest, fdmax (лимит DSS), fbench (замер скорости)**
- [ ] fdopen/freopen/fclosall/fgetpos/fsetpos — по мере надобности (Solid-C категория C)
## П3. Solid-C совместимость — ЗАКРЫТ 2026-07-06
Всё сделано (детали в docs/solid_c_compatibility.md): getdisk/setdisk,
getdate/gettime/setdate/settime + <dos.h>, ltell/_setargv, errno-алиасы,
<sprinter_solid.h>, div из SDCC (проверено), absread/abswrite (BIOS
$55/$56 — номера найдены в solid-c DOS.ASM), **scanf/fscanf/sscanf**
(своё C-ядро _scanf_core, 22 хост-теста), fdopen/freopen/fclosall/
fgetpos/fsetpos (хвост П2). bdos/brk/ioctl — отказ решением.
Тест tests/solidt ждёт MAME-прогона.
## П4. Недостающие POSIX-мелочи — ЗАКРЫТ 2026-07-06
- [x] rename() — ESTEX RENAME $10 (HL=старое, DE=новое), libc/io/rename.c
- [x] isatty(fd) — fd < 2 (манипуляторы DSS с 2; консольные псевдо-fd 0/-1/-2)
## П5. Заголовки и гигиена сборки — ЗАКРЫТ 2026-07-06
- [x] Контракт затенения — **docs/libc-headers.md**: include_next
(stdlib.h + новый string.h со strlwr/strupr) vs полная замена
(stdio.h, time.h — обязаны дублировать сигнатуры z80.lib);
из sprinter_compat.h убраны макросы min/max (конфликтовали с
функциями из stdlib.h; в Solid-C это тоже функции)
- [x] libc/Makefile: stale .rel чистятся сверкой списка перед упаковкой;
штамп .modules триггерит перелинковку при смене состава (и сносит
архив — mtime на exFAT грубый)
- [x] Все extra-тесты в top-level TESTS (43 программы: + hello2, simple,
banktest (переименован из banked.exe), conio2, dec_test, gets,
stest2, winrest, bios_text, text_palette, gfx_dbuf) и mdview2 в APPS
- [x] Размерный регресс: toolchain/size_check.py + docs/size_baseline.tsv;
`make size-check` (выход 1 при росте) / `make size-baseline`
## П6. Верификация после сплита — ЗАКРЫТ 2026-07-06 (MAME; железо — отдельно)
- [ ] `make floppy` + прогон в MAME ключевых тестов (conio, filetest, ptime,
stattest, mouse, gfx_demo) — линковка прошла, но поведение надо
подтвердить на эмуляторе
- [ ] Потом на железе (mdview2 и так ждёт проверки на железе — совместить)
## П7. Документация — ЗАКРЫТ 2026-07-06
- [x] **docs/libc-reference.md** — справочник API по всем заголовкам
- [x] docs/TODO.md переписан: открытое наверху, закрытые этапы (5-10)
в «Истории»; протухшие пункты (FILE rewrite «для v2») сняты
- [x] **CLAUDE.md** создан: сборка/проверка, правила libc (1 ф-я =
1 модуль, `_`-модули, русские комментарии, без `= 0`,
asm-правила), ABI-шпаргалка, квирки, структура
## П8. Смежное (не libc, из TODO.md — чтобы не потерялось)
- auto-banking Phase 1 (toolchain/auto_bank.py) — когда проект перерастёт ~30 КБ
- IM2 ISR v2 (docs/im2_isr_design.md) — отложено решением 2026-06-01
- font-quad для 640×256 (per-cell палитра)
- factoring parse_argv из crt0/crt0_banked в общий argv.s
- check_banks.py: разбивка code/const/bss per bank (косметика)
---
## Предлагаемый порядок
1. **П1-лайт**: io/env/errno/atexit/minmax/mem/dir/fsdir (мелкие, час работы,
выигрыш для всех CLI-приложений) + mouse + bios/text.
2. **П6**: MAME-смоук — подтвердить, что сплит ничего не сломал в рантайме,
до того как менять что-то ещё.
3. **П1-gfx**: разбор графики (самый большой кусок, отдельный заход).
4. **П2**: баги FILE* (3 шт.) + fprintf + ungetc.
5. **П3/П4**: solid-c остатки + rename/isatty.
6. **П5/П7**: гигиена сборки и документация — фоном, по кусочку.
+52
View File
@@ -0,0 +1,52 @@
# libc split: asm-связки между функциями
Журнал случаев, найденных при разбиении libc на «1 публичная функция = 1 модуль»
(2026-07-05). Сюда записывается каждый обнаруженный переход `jr _func` или
`jr/jp/call` на метку **внутри другой функции** — такие связки нельзя разрывать
механически, разбираем каждую отдельно.
## Правила (справка)
| Паттерн | Через границу модулей |
|---|---|
| `call/jp _func` (публичная C-функция) | работает — метка глобальная (`::`) |
| `call/jp _label` на метку в чужой функции | работает, только если метка объявлена `_label::` |
| `jr` / `djnz` в другой модуль | **запрещено** — ±128 байт, разложение модулей не гарантировано |
| fall-through (без перехода, в надежде на соседство) | **не работает никогда** |
## Случаи
### 1. stdio/dec_print.c — dec8/dec16/dec32: разделяемое тело (НЕ разрывать)
Статус: **оставлены в одном файле, решение отдельно.**
- `dec8``jp __dec_entry3` — прыжок в середину тела `dec32`;
- `dec16``jp __dec_entry5` — то же;
- метки уже глобальные (`__dec_entry3::`, `__dec_entry5::`) — линковаться будет,
но выигрыша от сплита нет: dec8 всё равно притянет модуль с телом dec32;
- внутри хвоста: `_dec_get_d16` **fall-through** в `_dec_emit_or_skip`,
`_dec_get_d32``jr _dec_emit_or_skip`, общий флаг `_dec_flag`.
Это осознанный дизайн из solid-c: тройка делит per-digit код. Варианты на потом:
(а) оставить как есть (176 Б тянутся целиком — терпимо);
(б) развести на 3 независимых цикла — dec8 станет ~40 Б, но исходник длиннее
и суммарно в exe, использующем dec8+dec32, станет хуже. Решение отложено.
### 2. stdio/hex_print.c — чист
`hex16``call _hex8` / `jp _hex8` (tail), `hex32``_hex16`: переходы по
именам публичных функций. Разъезжается на hex8.c/hex16.c/hex32.c без правок.
`_hex8_digit` — self-call внутри hex8, не мешает.
### 3. conio/conio.c — jp _clrscr_attr (чист)
`clrscr``jp _clrscr_attr` — tail-call публичной функции, работает через
модули как есть.
### 4. mem/mem_bios.c, mem/mem_estex.c — jp __errno_set (чист)
Tail-call публичного internal-хелпера `_errno_set` — кросс-модульный уже сейчас.
---
Все прочие `jr`-переходы в libc (проверены все `jr`, включая условные формы,
2026-07-05) ведут на метки внутри своей же функции — сплиту не мешают.
+242
View File
@@ -0,0 +1,242 @@
# Автотестирование в MAME
Единый справочник: как запускать программы Sprinter в эмуляторе MAME
**без участия человека**, вводить команды, снимать скриншоты, завершать
сессию и анализировать результат. Если нужно что-то про автотесты в
MAME — смотреть сюда.
Весь механизм собран в одном инструменте: **`toolchain/mame_interactive.py`**.
---
## 1. TL;DR
```bash
# собрать .exe (пример)
make -C tests/bgitest
# запустить в MAME, снять экран, выйти по таймауту
python3 toolchain/mame_interactive.py tests/bgitest/bgitest.exe \
--snap 12,14 --timeout 16
```
Инструмент сам:
1. проверит, что нет висящих копий MAME;
2. положит `.exe` на дискету A: (`mame/v306/IMG/mc.img`);
3. запустит MAME с драйвером `sprinter`;
4. дождётся приглашения `C:\>` и **напечатает `a:\bgitest.exe` + Enter**
(эмулируя нажатия клавиш);
5. снимет скриншоты в указанные секунды эмулированного времени;
6. завершит сессию по таймауту;
7. выведет пути к PNG-скриншотам.
Скриншоты лежат в `mame/v306/snap_auto/sprinter/` (`0000.png`, `0001.png`,
…). Их читает Claude визуально (инструментом Read) — текст с экрана
программно НЕ распознаётся.
---
## 2. Инструмент: `mame_interactive.py`
```
python3 toolchain/mame_interactive.py [exe] [--data f ...] \
[--launch-at T] [--step "T:TEXT" ...] [--snap t1,t2,...] [--timeout N]
```
| Аргумент | Назначение |
|----------|-----------|
| `exe` | `.exe` кладётся на A: и **авто-запускается** (печатается `a:\<exe>`+Enter в момент `--launch-at`). Без `exe` работаем на голой командной строке. |
| `--data f ...` | доп. файлы на дискету A: (данные для теста). |
| `--launch-at T` | секунда, когда печатается запуск `exe` (по умолчанию **8**). |
| `--step "T:TEXT"` | в момент `T` сек напечатать `TEXT`. Можно много раз — диалог с уже запущенной программой. В `TEXT`: `\n`=Enter, `\t`=Tab; заглавные и символы через Shift — автоматически. |
| `--snap t1,t2,...` | секунды эмуляции для скриншотов. По умолчанию: `launch_at+4` и `+6` (для `exe`), либо сразу после последнего ввода. |
| `--timeout N` | секунд эмуляции до принудительного выхода. По умолчанию — чуть позже последнего скриншота. |
**Важно:** все времена — это **секунды эмулированного времени от старта
машины** (не от нажатий, их «нет»). Загрузка DSS до `C:\>` занимает
~7 секунд, поэтому `--launch-at 8` и скриншоты с ~12 с.
### Типовые рецепты
```bash
# 1. Запустить тест и снять результат (самый частый случай)
python3 toolchain/mame_interactive.py tests/rt_test/rt_test.exe \
--snap 12,14 --timeout 16
# 2. Набрать команду на голой командной строке (без exe)
python3 toolchain/mame_interactive.py --step "8:dir\n" \
--snap 10,11 --timeout 12
# 3. Запустить программу и ответить на её ввод (например, выбор пункта меню)
python3 toolchain/mame_interactive.py tests/menu/menu.exe \
--step "13:2\n" --snap 15 --timeout 17
# 4. Тест с файлом-данными на дискете
python3 toolchain/mame_interactive.py examples/mdview2/mdview2.exe \
--data doc.md --step "13:mdview2 doc.md\n" --snap 16 --timeout 18
```
---
## 3. Как это работает внутри
### 3.1 Запуск MAME
Нативный `mame.arm` (arm64) в `mame/v306/`, драйвер `sprinter`, BIOS
v3.06. Полный набор аргументов зашит в `COMMON_ARGS` внутри скрипта:
две дискеты (A: наш `mc.img`, B: DSS 1.71u), два HDD-образа (система +
медиа), CD-ROM, ZX-Bus карта NeoGS, `-video opengl -window`,
`-snapshot_directory`, `-autoboot_script <сгенерированный .lua>`.
### 3.2 Ввод с клавиатуры — ключевой момент
У Sprinter в MAME **две** клавиатуры:
- `IO_LINE0..7` — легаси ZX-Spectrum-матрица (порт `0xFE`). DSS её для
командной строки **НЕ читает**.
- `root:kbd:ms_naturl`**настоящая AT/PS-2 клавиатура**, подключённая
последовательно к SIO Z84C015 (`sprinter.cpp:2037`). Именно её DSS
читает как поток scancode'ов.
Поэтому **не работают** (проверено многократно): `natkeyboard:post`,
`-autoboot_command`, а также `set_value` по полям `:IO_LINE*`. Всё это
бьёт в ZX-матрицу, которую DSS игнорирует.
**Работает** — прямое управление полями AT-клавиатуры из Lua:
```lua
manager.machine.ioport.ports[":kbd:ms_naturl:P1.4"].fields["D"]:set_value(1) -- нажать
... подождать ~0.06с ...
manager.machine.ioport.ports[":kbd:ms_naturl:P1.4"].fields["D"]:set_value(0) -- отпустить
```
`at_keyboard` сам сгенерит make/break scancode'ы → SIO → DSS.
Скрипт хранит раскладку `char → (порт, битовая маска)` (словарь `PHYS` +
`SHIFTED` для Shift-символов) и разворачивает строку в список
timed-событий `(время, порт, маска, значение)`. Backslash `\` в
AT-клавиатуре есть (поле `P2.1`/0x4) — путь `a:\name.exe` вводится
полностью.
### 3.3 Тайминг (Lua)
Генерируется `_interactive_gen.lua`, который через
`emu.register_periodic` на каждом кадре сверяет **эмулированное время** и
проигрывает события ввода, снимает скриншоты и завершает сессию.
Время берётся как `t.seconds + t.attoseconds/1e18`, потому что
`attotime.seconds`**целое** (дробную часть отбрасывает); если считать
по нему, все события схлопнутся в 1-секундную сетку.
Старт отсчёта — `emu.add_machine_reset_notifier` (НЕ `emu.register_start`
— он deprecated).
### 3.4 Скриншоты
`manager.machine.video:snapshot()` пишет PNG в каталог из
`-snapshot_directory` (`mame/v306/snap_auto/sprinter/`). Скрипт перед
запуском чистит этот каталог, потом печатает пути к готовым файлам.
Claude открывает их инструментом Read (визуальный анализ).
### 3.5 Завершение сессии
Два рубежа, чтобы MAME гарантированно не «завис»:
- в Lua: при `elapsed >= timeout``manager.machine:exit()` (чистый
выход);
- снаружи: shell-обёртка `timeout <N+8> ./mame.arm …` как страховка.
---
## 4. Предпосылки (окружение)
- **MAME**: `mame/v306/mame.arm` + образы в `mame/v306/IMG/`
(`mc.img` — наш перезаписываемый A:, `dss171u.img`, HDD `.chd`, CD
`.iso`). Каталог `mame/` целиком в `.gitignore` — поэтому инструмент
живёт в `toolchain/`, а не рядом с MAME.
- **Загрузка должна доходить до `C:\>`.** `system.bat` на системном
диске НЕ должен автоматически запускать Flex Navigator или приложение —
иначе мы не попадём на командную строку и ввод уйдёт в чужую программу.
(Это файл на HDD-образе, вне репозитория; правится один раз.)
- **Нет висящих копий MAME.** Несколько одновременных инстансов пишут в
один `mc.img` и дают недостоверный результат. Скрипт проверяет это сам
(`pgrep`), но при ручных запусках MAME — проверяйте `ps aux | grep mame`.
---
## 5. Как выбирать времена
- **Загрузка до `C:\>`:** ~7 секунд → `--launch-at 8` безопасно.
- **Набор пути `a:\name.exe`:** ~13 символов × 0.14с ≈ 1.8с → команда
уходит около 9.8с, программа стартует ~10с.
- **Скриншот:** давайте программе дорисоваться. Быстрая программа —
снимать с ~12с; если рисует долго/по частям, снимайте несколько кадров
(`--snap 12,16,20`) и смотрите, где картинка «дособралась».
- **Диалог с программой (`--step`):** времена шагов ставьте ПОСЛЕ старта
программы (например, запуск на 8с, ответ на ввод на 13–15с).
---
## 6. Анализ результата
- Скриншоты — **единственный** способ проверки: программного чтения
текстового/графического VRAM нет, OCR нет. Claude читает PNG глазами
(инструмент Read с картинкой).
- Лог MAME фильтруется по строкам `[interactive]` (моменты снимков и
выхода) — видно, в какие секунды сделаны кадры.
- Если картинка «не дособралась» — снять более поздний кадр (увеличить
`--snap`/`--timeout`).
---
## 7. Раскладка клавиатуры (справочно)
Раскладка снята дампом ioport-полей `:kbd:ms_naturl:*` живой машины.
Она зашита в `PHYS`/`SHIFTED` внутри `mame_interactive.py`. Поддержаны:
буквы (a–z, A–Z через Shift), цифры, пробел, Enter (`\n`), Tab (`\t`),
и символы ``- = [ ] \ ; ' , . / ` `` плюс их Shift-версии
`! @ # $ % ^ & * ( ) _ + { } | : " < > ? ~`.
Если понадобится клавиша вне списка — снять её поле дампом (пример
Lua-пробы ниже) и добавить в `PHYS`:
```lua
-- дамп всех полей клавиатуры в лог
for tag, port in pairs(manager.machine.ioport.ports) do
if tostring(tag):find("kbd") then
for fname, field in pairs(port.fields) do
print(string.format("%s mask=0x%x %q", tag, field.mask, fname))
end
end
end
```
---
## 8. Квирки и грабли (все, на которые уже наступали)
- **`attotime.seconds` — целое.** Субсекундный тайминг только через
`seconds + attoseconds/1e18`.
- **Автоповтор (typematic).** Клавишу держать коротко (~0.06с). Если
держать ~1с — `d` превратится в `dddddd`.
- **Слипание scancode'ов.** Между символами ~0.14с.
- **Не та клавиатура.** Ввод — только в `:kbd:ms_naturl`, НЕ в
`:IO_LINE*`, НЕ через `natkeyboard`/`-autoboot_command`.
- **Загрузка мимо `C:\>`.** Если `system.bat` что-то автозапускает —
ввод уходит в чужую программу; вернуть чистую командную строку.
- **Висящие копии MAME.** Всегда проверять перед запуском.
- **macOS-специфика (справочно):** известный баг MAME
(mamedev/mame#10612 — потеря ввода в fullscreen при движении мыши на
старте) к нам НЕ относится: работаем в `-window`, ввод скриптовый.
---
## 9. На будущее (заметки, ещё не в инструменте)
- **Быстрый накопитель.** Тестам, которым важна скорость диска (напр.
потоковое чтение), имеет смысл копировать файлы с медленной дискеты A:
на HDD `C:\TEMP` перед запуском.
- **Видео+звук.** MAME умеет писать AVI (`-aviwrite`) — для тестов с
анимацией/звуком, где скриншотов мало. Пока не подключено к скрипту.
- **Ручная отладка ввода.** Запуск MAME с `-console` даёт интерактивный
Lua-REPL — удобно нащупывать поля/тайминги вживую перед скриптованием.
- **Второй видеорежим/варианты BIOS** — при необходимости менять
`COMMON_ARGS`.
```
+50
View File
@@ -0,0 +1,50 @@
# Эталон размеров _CODE (байт); обновление: python3 toolchain/size_check.py --update
argv 3431
assrtest 3847
attrprob 4091
banked 1056
bankedbg 1067
banklocl 4832
banktest 3767
bgi_img 6218
bgitest 4196
bios_text 4470
cat 915
cblstream 6277
cbltest 6407
cblwav 6486
conio 4634
conio2 3946
dec_test 860
errno 6216
fbench 9040
fdmax 6337
filetest 11624
gets 509
gfx_d16 3319
gfx_dbuf 2544
gfx_demo 3219
gfx_mous 5542
gfx_text 2560
hello 4176
hello2 4251
irqtest 5881
ls 4919
malloc 4462
mdview 19122
mdview2 27029
mem_test 4558
mouse 4394
openenv 6127
ptime 5896
rt_test 4906
seek 4177
simple 963
solidt 12381
stattest 7700
stdlib 6643
stest2 3644
strtest 1340
text_palette 5039
timedir 5471
winrest 4481
1 # Эталон размеров _CODE (байт); обновление: python3 toolchain/size_check.py --update
2 argv 3431
3 assrtest 3847
4 attrprob 4091
5 banked 1056
6 bankedbg 1067
7 banklocl 4832
8 banktest 3767
9 bgi_img 6218
10 bgitest 4196
11 bios_text 4470
12 cat 915
13 cblstream 6277
14 cbltest 6407
15 cblwav 6486
16 conio 4634
17 conio2 3946
18 dec_test 860
19 errno 6216
20 fbench 9040
21 fdmax 6337
22 filetest 11624
23 gets 509
24 gfx_d16 3319
25 gfx_dbuf 2544
26 gfx_demo 3219
27 gfx_mous 5542
28 gfx_text 2560
29 hello 4176
30 hello2 4251
31 irqtest 5881
32 ls 4919
33 malloc 4462
34 mdview 19122
35 mdview2 27029
36 mem_test 4558
37 mouse 4394
38 openenv 6127
39 ptime 5896
40 rt_test 4906
41 seek 4177
42 simple 963
43 solidt 12381
44 stattest 7700
45 stdlib 6643
46 stest2 3644
47 strtest 1340
48 text_palette 5039
49 timedir 5471
50 winrest 4481
+25
View File
@@ -130,6 +130,31 @@ These will be in `docs/solid_c_diff.md`:
To make porting easier, add a single `<sprinter_solid.h>` that includes all the standard headers (`stdio.h`, `string.h`, `conio.h`, etc.) — Solid-C programs can `#include <sprinter_solid.h>` and have most functions available. To make porting easier, add a single `<sprinter_solid.h>` that includes all the standard headers (`stdio.h`, `string.h`, `conio.h`, etc.) — Solid-C programs can `#include <sprinter_solid.h>` and have most functions available.
## Status 2026-07-06 — Phase 1/2/3 ЗАКРЫТЫ
Всё из категорий A/B/C реализовано или закрыто решением:
- **A (алиасы)**: все на месте в sprinter_compat.h (+ ltell, _setargv
добавлены 2026-07-06); div/ldiv — из SDCC z80.lib (проверено).
- **B**: getdisk/setdisk (ESTEX $02/$01, libc/io), getdate/gettime/
setdate/settime (обёртки над getdatetime, структуры Turbo-C в
<dos.h>), остальное было готово ранее.
- **C**: fdopen/freopen/fclosall/fgetpos/fsetpos — реализованы поверх
таблицы FILE v2 (libc/file); ungetc — есть (FILE v2);
**absread/abswrite — BIOS $55/$56** (rst 8, A=диск, HL:IX=сектор,
DE=буфер, B=счётчик; найдено в solid-c DOS.ASM) — реализованы в
libc/io; **scanf/fscanf/sscanf — реализованы** (своё C-ядро
_scanf_core с семантикой Solid-C: %d %u %x %o %c %s, l, ширина, %*;
в SDCC z80 scanf нет); isatty — fd < 2 (см. memory/dss_fd_limit).
bdos/bdosh/intdos — НЕ экспонируем (решение: типизированные
обёртки); brk/sbrk — НЕ нужны (heap SDCC); ioctl — скип.
- **errno**: Solid-C имена (EZERO/EINVFNC/ENOFILE/…) — алиасы в errno.h.
- **Зонтичный заголовок**: <sprinter_solid.h>.
- Тест: tests/solidt (MAME).
## History ## History
- 2026-07-06 — Phase 1/2/3 закрыты: dos.h (даты/диски/сектора),
scanf-семейство, fdopen/freopen/fclosall/fgetpos/fsetpos,
rename/isatty (П4), errno-алиасы, sprinter_solid.h, тест solidt
- 2026-06-01 — initial gap analysis vs Solid-C v2004 - 2026-06-01 — initial gap analysis vs Solid-C v2004
+284
View File
@@ -0,0 +1,284 @@
Функция puts()
Функция puts() записывает символьную строку в стандартный
поток данных (т.е. выводит ее на экран). Функция puts()
возвращает код символа «\п».
int puts(const char *string);
После выполнения функции puts() курсор переводится на
новую строку.
Функция putchar()
Функция putchar() записывает символ в стандартный поток
данных (т.е. выводит его на экран). Функция putchar() возвращает
выведенный на экран символ.
int putchar(int ch);
Функция gets()
Функция gets() считывает символьную строку стандартного
входного потока и помещает ее по адресу, заданному указателем
buffer; прием строки заканчивается, если функция обнаруживает
символ конца строки «\п», данный символ удаляется и
заменяется нуль-терминатором «\0».
char *gets (char*buffer);
Функция gets() возвращает указатель на считанную строку.
Функция getchar()
Функция getchar() считывает символ из стандартного
входного потока.
int getchar(void);
Функция getchar() возвращает считанный символ.
=== Функции консольного ввода
char *cgets(char *str)
- помещает в буфер, на начало которого
указывает str, строку символов со стандартного ввода.
Запись символов начинается с str[l]; str[0] должен содержать
максимальное число символов, которое должно быть прочитано
и записано в строку. Функция возвращает указатель на начало
буфера str.
int getch(void)
- выполняет ввод символа с клавиатуры.
Turbo С не выполняет «эхо» ввода. В этой связи полезна для
организации интерфейса с пользователем, при котором нажатие
той или иной клавиши вызывает немедленную реакцию программы
без отображения введённого символа на экране.
int getche(void)
- выполняет небуферизуемый ввод символа
с клавиатуры. Turbo С «эхоирует» ввод на экране. Перевод
строки происходит при достижении правой вертикальной
границы текущего активного окна.
int kbhit(void)
- проверяет, пуст ли буфер клавиатуры.
Если в буфере есть символы, функция возвращает ненулевое
значение, в противном случае она возвращает О. Является
удобным средством предотвращения «зацикливания» или «по-
висания» при ожидании невозможного в данный момент события.
Кроме того, осуществляется проверка нажатия комбинации
клавиш «Ctrl-Break», что позволяет выполнить аварийное завершение
программы.
int ungetch(int ch)
- записывает непосредственно в буфер
клавиатуры символ ch. Он будет доступен при выполнении следующей
операции чтения с консоли (функциями файла
«conio.h»). Разрешает помещать только один символ, который
не должен совпадать с константой EOF, описанной в файле
«stdio.h». В случае успеха функция возвращает ch; в противном
случае возвращается -1.
=== Функции консольного вывода
void textmode(int newmode)
- изменяет текущий текстовый
режим. Новый режим указывается единственным параметром
newmode и может задаваться либо числом, либо с использованием
символических констант, значения которых определяет
перечислимый тип text_modes
Функции консольного вывода используют понятие активного
окна экрана. Активное окно - это прямоугольная область
экрана, в границах которой в данный момент работают функции.
Описание активного окна (или, как часто говорят, фрейм)
хранится во внутренней структурной переменной Turbo С. Установку
параметров активного текстового окна выполняет функция
window ().
void window(int l_t_col, int l_t_row, int r_b_col, int r_b_row)
- описывает активное текстовое окно: первая пара
аргументов задает столбец и строку левого верхнего угла, вторая
пара - правого нижнего угла. Строки и столбцы нумеруются,
начиная от 1. Поэтому, например, координаты левого верх-
него и правого нижнего углов экрана в режимах «25 строк х 80
столбцов» задаются парами (1,1) и (80,25). Ось X направлена
слева направо, а ось Y направлена сверху вниз. Следует обратить
внимание на то, как в Turbo С задаются координаты углов,
сначала столбец, затем строка.
Фрейм окна Turbo С имеет следующую структуру:
struct text_info {
unsigned char winleft; /* столбец, строка */
unsigned char wintop; /* левого верхнего угла */
unsigned char winright; /* столбец, строка */
unsigned char winbottom;/* правого верхнего угла */
unsigned char attribute; /* атрибуты */
unsigned char normattr; /* окна */
unsigned char screenheight; /* полная высота экрана */
unsigned char screenwidth; /* полная ширина экрана */
unsigned char curx; /* строка, столбец */
unsigned char сuгу /* текущей позиции курсора */
}
void gettextinfo(struct text_info *r)
- заполняет поля
структурной переменной по шаблону text_info, на которую
ссылается. Шаблон структуры text_info, описывающей текущее
окно экрана, содержится в заголовочном файле «conio.h».
Функция window() инициализирует поля координат фрейма
окна. Функции textcolor(), textbackground(), textattr() и
другие управляют цветом отображаемых символов окна.
void textattr(int newattr)
- устанавливает атрибут для
функций, работающих с текстовыми окнами. Атрибут хранится в
поле attribute структурной переменной по шаблону text_info,
доступной через функцию gettextinfo()
void textcolor(int newcolor)
- задает цвет символов, не
затрагивая установленный цвет фона. Цвет может быть или числом,
или формироваться из символических констант, значения
которых определяет перечисляемый тип COLORS.
void textbackground(int newcolor)
- задает цвет фона
символов, не затрагивая установленный цвет символа. Цвет может
быть или числом, или формироваться из символических
констант
void gotoxy(int х, int у)
- устанавливает курсор в строку
у и столбец х в текущем активном окне экрана. Верхний левый
угол окна имеет координаты (1,1). При попытке позиционировать
курсор за границы окна он останавливается на границе окна.
Особенностью функции является то, что координаты х и у
являются относительными, приведенными к левому верхнему
углу. Например, если текущее окно было описано функцией
window(1,8,80,25), обращение gotoxy(5,5); установит курсор
в пятый относительный столбец окна (совпадает с абсолютным
столбцом 4, отсчитываемым от О) в пятой относительной строке
(так как верхняя строка окна задана равной 5, то абсолютная
строка будет равна 5+8-1, если отсчет строк ведется от О)
int wherex(void),
int wherey(void)
- сообщают столбец и
строку текущей позиции курсора; возвращают целое число в
диапазоне
void clreol(void)
- стирает в текстовом окне строку, на которую
установлен курсор, начиная с текущей позиции курсора и
до конца строки (до правой вертикальной границы окна).
void clrscr(void)
- очищает все текстовое окно. Цвет «заливки»
окна при очистке будет соответствовать значению, установленному
символической переменной attribute в описании
окна (структурная переменная по шаблону text_info).
void delline(void)
- стирает в текстовом окне всю строку
текста, на которую установлен курсор.
void insline(void)
- вставляет пустую строку в текущей
позиции курсора со сдвигом всех остальных строк окна на одну
строку вниз. При этом самая нижняя строка текста окна теряется.
int cprintf(const char *format, ...)
- выполняет вывод
информации с преобразованием по заданной форматной строке,
на которую указывает format. Является аналогом функции
стандартной библиотеки printf(), но выполняет вывод в пределах
заданного окна. В отличие от printf() функция cprintf()
иначе реагирует на специальный символ '\п' - курсор переводится
на новую строку, но не возвращается к левой границе окна.
Поэтому для перевода курсора на начало новой строки текстового
окна следует вывести последовательность символов CR-
LF (OxOd,OxOa). Остальные специальные символы воздействуют
на курсор так же, как и в случае функций стандартного ввода-
вывода. Функция возвращает число выведенных байтов, а не
число обработанных полей, как это делает функция printf().
int cputs(const char *str)
- выводит строку символов в
текстовое окно, начиная с текущей позиции курсора. На начало
выводимой ASCIIZ-строки указывает str. Является аналогом
функции стандартной библиотеки puts(), выполняет вывод в
пределах заданного окна и при выводе не добавляет специальный
символ '\п'. Реакция cputs() на специальный символ '\п'
аналогична реакции cprintf(): курсор переводится на новую
строку, но не возвращается к левой границе окна. Поэтому для
перевода курсора на начало новой строки текстового окна следует
вывести последовательность символов CR-LF (OxOd,OxOa).
Остальные специальные символы воздействуют на курсор так
же, как и в случае функций стандартного ввода/вывода. Функция
возвращает ASCII-код последнего выведенного на экран
символа. В отличие от puts() в функции отсутствует возврат
символа EOF (вывод на экран происходит в любом случае).
int movetext(int left, int top, int right, int bottom, int destleft, int desttop)
- переносит окно, заданное координатами
левого верхнего (left, top) и правого нижнего (right, bottom)
углов, в другое место на экране, заданное координатами левого
верхнего угла нового положения окна. Размеры окна по горизонтали
и вертикали сохраняются. Все координаты задаются относительно
координат верхнего левого угла экрана (1,1). Функция
возвращает ненулевое значение, если перенос заданного
окна выполнен. В противном случае возвращается О. Функция
корректно выполняет перекрывающиеся переносы, т.е. переносы,
в которых прямоугольная область-источник и область, в которую
окно переносится, частично покрывают друг друга.
int putch(int ch)
- выводит символ в текущей позиции
текстового окна экрана. Как и для функций cprintf(), cputs(),
специальный символ '\п' вызывает только переход курсора на
новую строку текстового окна без возврата к его левой вертикальной
границе. Остальные специальные символы воздействуют
на курсор так же, как и для функций стандартного ввода-
вывода.
int puttext(int left, int top, int right, int bottom, void source)
- выводит на экран текстовое окно, заданное координатами
левого верхнего (left, top) и правого нижнего (right,
bottom) углов. Символы и атрибуты располагаются в буфере,
адрес начала которого задаёт указатель source (функция «открывает»
или «восстанавливает» текстовое окно экрана). Обычно
используется вместе с функцией gettext(), выполняющей
обратную операцию - запись в source символов/атрибутов,
полностью описывающих все знакоместа текстового окна. Функция
проверяет по заданным координатам окна, можно ли разместить
окно на экране для текущего режима видеоадаптера и
корректны ли эти координаты. В случае, когда окно успешно
выведено, возвращается ненулевое значение.
int gettext(int left, int top, int right, int bottom, void destin)
- записывает в буфер destin символы и атрибуты текстового
окна, заданного строкой и столбцом левого верхнего
(left, top) и правого нижнего (right, bottom) углов. Первые два
слова буфера занимают ширина и длина скопированного окна.
Работает только в текстовых режимах видеоадаптера. Координаты
задаются относительно верхнего левого угла экрана (1,1). В
случае успеха возвращает ненулевое число.
=== Файловый ввод/вывод
Прототипы функций ввода-вывода и используемые для этого
типы данных описаны в стандартном заголовочном файле
«stdio.h».
Для файлового ввода/вывода в Си предусмотрены две основные
группы функций:
• функции низкоуровневого ввода/вывода, использующие
для доступа к файлам целочисленные файловые дескрипторы;
• функции более высокого уровня, осуществляющие буферизованный
ввод/вывод с применением потоков.
Поток в Си - это объект, служащий для доступа к файлам
как к упорядоченной последовательности символов.
Поток представляется структурой типа FILE, с которой ассоциирован
некоторый открытый файл. При необходимости несколько
потоков могут ссылаться на один и тот же файл.
+5
View File
@@ -0,0 +1,5 @@
MDVIEW Sample Document
This is a sample Markdown file for testing the Sprinter *mdview text
viewer*. Phase 3 adds inline emphasis: bold, italic and underscore
runs render with distinct background colours.
+9 -9
View File
@@ -1758,7 +1758,7 @@ static void clamp_top(void)
} }
/* Прокрутка вверх на n строк с частичной перерисовкой при n == 1. */ /* Прокрутка вверх на n строк с частичной перерисовкой при n == 1. */
static void scroll_up(uint16_t n) static void md_scroll_up(uint16_t n)
{ {
uint16_t new_top_line = (top_line >= n) ? (uint16_t)(top_line - n) : 0; uint16_t new_top_line = (top_line >= n) ? (uint16_t)(top_line - n) : 0;
if( new_top_line != top_line) { if( new_top_line != top_line) {
@@ -1773,7 +1773,7 @@ static void scroll_up(uint16_t n)
} }
/* Прокрутка вниз на n строк с частичной перерисовкой при n == 1. */ /* Прокрутка вниз на n строк с частичной перерисовкой при n == 1. */
static void scroll_down(uint16_t n) static void md_scroll_down(uint16_t n)
{ {
uint16_t new_top_line = (top_line + n < n_lines - VIEW_H) ? uint16_t new_top_line = (top_line + n < n_lines - VIEW_H) ?
(uint16_t)(top_line + n) : n_lines - VIEW_H; (uint16_t)(top_line + n) : n_lines - VIEW_H;
@@ -1791,7 +1791,7 @@ static void scroll_down(uint16_t n)
/* Горизонтальный сдвиг (только если в окне есть nowrap-строки). /* Горизонтальный сдвиг (только если в окне есть nowrap-строки).
* Максимум сдвига ограничен самой широкой nowrap-строкой на экране. */ * Максимум сдвига ограничен самой широкой nowrap-строкой на экране. */
static void scroll_h(int8_t delta) static void md_scroll_horizon(int8_t delta)
{ {
uint16_t maxw = 0; uint16_t maxw = 0;
for (uint8_t i = 0; i < VIEW_H; i++) { for (uint8_t i = 0; i < VIEW_H; i++) {
@@ -1999,12 +1999,12 @@ int main(int argc, char **argv)
switch (scan) { switch (scan) {
case KEY_F1: show_help(); break; case KEY_F1: show_help(); break;
case KEY_F10: goto exit_loop; case KEY_F10: goto exit_loop;
case KEY_UP: scroll_up(1); break; case KEY_UP: md_scroll_up(1); break;
case KEY_DOWN: scroll_down(1); break; case KEY_DOWN: md_scroll_down(1); break;
case KEY_LEFT: scroll_h(-(int8_t)HPAN_STEP); break; case KEY_LEFT: md_scroll_horizon(-(int8_t)HPAN_STEP); break;
case KEY_RIGHT: scroll_h(+(int8_t)HPAN_STEP); break; case KEY_RIGHT: md_scroll_horizon(+(int8_t)HPAN_STEP); break;
case KEY_PGUP: scroll_up(VIEW_H); break; case KEY_PGUP: md_scroll_up(VIEW_H); break;
case KEY_PGDN: scroll_down(VIEW_H); break; case KEY_PGDN: md_scroll_down(VIEW_H); break;
case KEY_HOME: top_line = 0; case KEY_HOME: top_line = 0;
viewport_x = 0; viewport_x = 0;
render_viewport(); break; render_viewport(); break;
+79
View File
@@ -0,0 +1,79 @@
# MDView - CHANGELOG
”®à¬ â: ¢¥àᨨ ¨ ¤ âë ¯® git-¨áâ®à¨¨ ¯à®¥ªâ . ” ©« á®åà ­ñ­ ¢ CP866 -
¥£® 㤮¡­® ç¨â âì á ¬¨¬ MDView (§ ®¤­® íâ® â¥áâ  ¢â®¤¥â¥ªâ  ª®¤¨à®¢ª¨).
## v1.0 (b3) - 2026-07-05
- ”®­®¢ ï ¯®¤£®â®¢ª  ¢â®à®£® ­ ¡®à  ª®¤¨à®¢ª¨: ¯®ª  ¯®«ì§®¢ â¥«ì ç¨â ¥â
¤®ªã¬¥­â, ¯ ã§ë ¬¥¦¤ã ª« ¢¨è ¬¨ âà âïâáï ­  ¨­¤¥ªá æ¨î ¢â®à®£® ­ ¡®à 
(UTF-8 ¯à¨ 8-¡¨â­®¬ ¯¥à¢¨ç­®¬ ¨ ­ ®¡®à®â). Š ¯¥à¢®¬ã F8 ­ ¡®à ®¡ëç­®
㦥 £®â®¢ - ¯¥à¥ª«î祭¨¥ ¬£­®¢¥­­®. F8 ¤® £®â®¢­®á⨠­¥ áâந⠭ ¡®à
§ ­®¢®,   ¤®ªàã稢 ¥â ­ ç â®¥ ä®­®¬ (¦¤ âì ®áâ ñâáï ¬¥­ìè¥).
- ”®­ ¯®«­®áâìî ­¥¢¨¤¨¬: íªà ­ ­¥ âண ¥âáï, ¯®á«¥ ª ¦¤®© ª« ¢¨è¨
¢ë¤¥à¦¨¢ ¥âáï 宫¤ ã­ - ¯à¨ áªà®««¥ á  ¢â®¯®¢â®à®¬ ä®­ ­¥ ¢ª«¨­¨¢ ¥âáï.
- ˆ­¤¥ªá â®à ¯¥à¥¢¥¤ñ­ ­  १àã¥¬ë¥ è £¨ (index_begin/index_step);
¯®¡®ç­® § ªàë⠪ࠩ: ®¡àë¢ UTF-ª®­¢¥àâ æ¨¨ ­  ᢥà夫¨­­®¬ (>4 Š)
 ¡§ æ¥ ¡®«ìè¥ ­¥ ®¡à¥§ ¥â ®áâ â®ª ¤®ªã¬¥­â .
- Š®¤¨à®¢ª  ¢ áâ âãá-¡ à¥ ¯®ª §ë¢ ¥âáï áà §ã: ¤¥â¥ªâ¨à®¢ ­­ ï - ¢ ¬®¬¥­â
­ ç «  ¯¥à¢®© ¨­¤¥ªá æ¨¨, ­®¢ ï ¯® F8 - ¢ ¬®¬¥­â ¯¥à¥ª«î祭¨ï, ­¥
¤®¦¨¤ ïáì ª®­æ  ᡮન ­ ¡®à .
## v1.0 (b1) - 2026-07-05
- ®¢ë© HEX-०¨¬ (F4): ¤ ¬¯ Žˆƒˆ€‹œŽƒŽ ä ©«  ¢ ä®à¬ â¥
"0x012340 : 16 ¡ ©â hex : 16 ¯¥ç â­ëå ᨬ¢®«®¢".
¥ç â­ ï ª®«®­ª  ãç¨â뢠¥â ⥪ãéãî ª®¤¨à®¢ªã; ¤«ï UTF-8 £«¨ä
áâ ¢¨âáï ­  ¯®§¨æ¨¨ «¨¤-¡ ©â , continuation-¡ ©âë - â®çª®©.
‘¬¥­  ª®¤¨à®¢ª¨ (F8) ¢ HEX ¬¥­ï¥â ⮫쪮 ¯¥ç â­ãî ª®«®­ªã.
- பàã⪠ HEX ­  ®¤­ã áâபã -  ¯¯ à â­ë© áªà®«« á ¯®¤à¨á®¢ª®©
®¤­®£® à鸞 (ª ª ¢ MD/RAW).
- ‘¯à ¢ª : ¤®¡ ¢«¥­  ª« ¢¨è  F4, ¢¥àá¨ï v1.0 (b1).
## v1.0 (a4) - 2026-07-04
- …¤¨­ ï ¯®§¨æ¨ï ¯à¨ ¯¥à¥ª«î祭¨ïå ¢¨¤®¢ ¨ ª®¤¨à®¢®ª: F2 (MD/RAW),
F8 (8-¡¨â­ë¥ / UTF-8) á®åà ­ïîâ ⥪ã饥 ¬¥áâ® ¢ ¤®ªã¬¥­â¥.
Œ¥¦¤ã ­ ¡®à ¬¨ à §­®£® à §¬¥à  ¯®§¨æ¨ï ¯¥à¥­®á¨âáï ¯à®¯®à樮­ «ì­®
¡ ©â ¬ á â®ç­®áâìî ¤® áâப¨.
- ˆá¯à ¢«¥­®: F8 ¢ RAW-०¨¬¥ à¨á®¢ « MD-¢¨¤ ¯®¢¥àå RAW.
- Ž¯â¨¬¨§ æ¨ï à §¬¥à : exe 28215 -> 25100 ¡ ©â (-11%):
 £à¥áᨢ­ ï  ««®ª æ¨ï ॣ¨áâ஢ SDCC (--max-allocs), § ­ã«¥­¨¥
_DATA ¢ crt0 (á­ïâë ¢á¥ "= 0" ã áâ â¨ª®¢), ¤¥¤ã¯«¨ª æ¨ï ¯ àá¥à ,
᫨ﭨ¥ ¤¢ãå ᪠­¥à®¢ inline-à §¬¥âª¨ ¢ ®¤¨­ scan_stream.
## v1.0 (a3) - 2026-06-30
- ‹¨¬¨â ä ©«  256 Š: ¡®«ì訩 ä ©« ®¡à¥§ ¥âáï á ¯à¥¤ã¯à¥¦¤¥­¨¥¬,
¯à®ç¨â ­­ ï ç áâì ¤®áâ㯭 .
- Ž¡àë¢ ¯® ¨áç¥à¯ ­¨î ¨­¤¥ªá /ªíè  § ¢¥àè ¥âáï áâப®©-á®®¡é¥­¨¥¬
á ¯à¨ç¨­®© (¢¬¥áâ® â¨å®© ¯®â¥à¨ áâப).
- ®«¨à®¢ª  áâ âãá-¡ à  ¨ á¯à ¢ª¨; ¯à®æ¥­â ¯à®ªàã⪨ ­  16-¡¨â­®©
 à¨ä¬¥â¨ª¥ (¡¥§ 32-¡¨â­®£® ¤¥«¥­¨ï).
- ’¥áâ®¢ë¥ ä ©«ë £à ­¨ç­ëå á«ãç ¥¢ (testfiles/, £¥­¥à â®à).
## v1.0 (a2) - 2026-06-26
- Œ®­®«¨â à á饯«ñ­ ­  ¬®¤ã«¨: ï¤à® + index/md/raw/enc/table/status/help
(à §¬¥à-­¥©âà «ì­®, ç¨â ¥¬®áâì).
- ˆá¯à ¢«¥­ ¤ã¡«¨ª â áâப¨ ¢ RAW ¯à¨ áªà®««¥ ã ª®­æ  ä ©« .
## v1.0 (a1) - 2026-06-25
- ®¤¤¥à¦ª  ª®¤¨à®¢®ª CP866/CP1251/KOI8-R/UTF-8 (F8):
 ¢â®®¯à¥¤¥«¥­¨¥ ¯à¨ ®âªàë⨨, ¬£­®¢¥­­ë© ६ ¯ 8-¡¨â­ëå,
UTF-8 ¤¥ª®¤¨àã¥âáï ¢ ®â¤¥«ì­ë© ­ ¡®à (¨­ªà¥¬¥­â «ì­®).
- RAW-¯à®á¬®âà ¨á室­¨ª  (F2): ¯¥à¥­®á ªà â­® 80 ¨«¨ ¯ ­®à ¬  (F3).
- Ž¯â¨¬¨§ æ¨ï à §¬¥à  ª®¤  (-1.9 Š).
## v1.0 (a0) - 2026-06-23..24
- ¥à¢ë© ¢ë¯ãáª: render-cache  àå¨â¥ªâãà  - ®¤¨­ ¯à®å®¤ ¯ àá¥à ,
£®â®¢ë¥ áâப¨ (ᨬ¢®«+ âਡãâ) ¢ EMM, ¬£­®¢¥­­ ï ¯à®ªàã⪠.
- Markdown: § £®«®¢ª¨, ᯨ᪨, æ¨â âë, à §¤¥«¨â¥«¨, fenced-ª®¤,
â ¡«¨æë á à ¬ª®© ¨  ¢â®-è¨à¨­®© ª®«®­®ª, inline-à §¬¥âª 
(¦¨à­ë©/ªãàᨢ/¯®¤çñભãâë©/§ çñભãâë©/ª®¤, íªà ­¨à®¢ ­¨¥).
- ணà¥áᨢ­ ï § £à㧪 : ¯¥à¢ë© íªà ­ áà §ã, ¨­¤¥ªá æ¨ï ¢ ä®­¥,
­ ¢¨£ æ¨ï ¯® £®â®¢®© ç áâ¨, Esc/F10 ¯à¥à뢠¥â.
- ƒ®à¨§®­â «ì­ë© áªà®«« ª®¤®¢ëå ¡«®ª®¢ ¨ â ¡«¨æ.
- “᪮७¨¥ ¯à¥¯à®æ¥áᨭ£  (22á -> 10á ­  ¡®«ì讬 ä ©«¥).
+840
View File
@@ -0,0 +1,840 @@
# 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.len``uint8_t`. Касается всех строк, но заметнее всего на
**горизонтально скроллируемых** строках (блоки кода и строки таблиц, флаг
`IF_HSCROLL`), которые в MD-режиме можно листать вправо.
**Что происходит с более длинной строкой:** при индексации `gc_put()` молча
отбрасывает каждую ячейку после 255-й (`if (g_ncells < MAX_CACHE_LINE_LEN)`).
В кэш попадают только первые 255 ячеек, остаток **теряется** — до него нельзя
доскроллить и **нет маркера обрезки** на 255-й позиции (маркер `>` означает лишь
«есть ещё в пределах кэша»). Переполнения буфера нет — `gc_put` проверяет границу.
Важно: «255 ячеек» — это **отрендеренная ширина**, не байты исходника. Табы в
коде разворачиваются в пробелы (до `TAB_STOP`), а ячейки таблицы добиваются
пробелами до ширины колонки + рамки `│` — поэтому кап достигается раньше, чем
255 «полезных» символов.
> **RAW-режим (`F2`) этого лимита не имеет** — он рисует прямо из файла
> побайтово, длинные строки видны целиком (через wrap `F3` или гориз. скролл).
**Идея снятия лимита** (оценка, не реализовано) — расширить длину до `uint16_t`:
- `cache_rec_t.len` `uint8_t→uint16_t` — структура остаётся **ровно 8 байт**
(len съедает один pad-байт), адресация `idx<<3` не меняется. Бесплатно.
- `g_ncells` / `g_ncells_at_space``uint16_t` — главная цена по **коду/скорости**:
16-битная арифметика на Z80 в горячем `gc_put` (вызов на каждую ячейку) и в
scan-циклах. Ориентир: **+0.2…0.4 КБ кода** + замедление индексации.
- Буфер `g_cells[MAX_CACHE_LINE_LEN*2]` в near-RAM (W2) — главная цена по
**памяти**: `2 × кап` байт. Сейчас 510 Б; кап 512 → +0.5 КБ, 1024 → +1.5 КБ.
EMM-кэш контента (1 МБ) длинные строки тянет легко — узкое место именно near.
- `viewport_x` (и копия в `docset_t`) + потолок `max_vx` (сейчас 248) → `uint16_t`,
иначе хранить >255 можно, а доскроллить нельзя. Плюс `widths[]`/`ccx` в таблицах/коде.
Реалистичный компромисс — кап 512–1023: хватит почти всем листингам/таблицам,
цена ~+0.3 КБ кода и +0.5…1.5 КБ near-RAM. **Дешёвая полумера без `uint16_t`**
ставить честный маркер обрезки на 255-й позиции, чтобы потеря была видна.
## TODO
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 × количество строк/столбцов экономии. Применимо ко всем композитным примитивам.
+24 -16
View File
@@ -8,37 +8,45 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..) PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := mdview2 EXAMPLE := mdview2
EXTRA_SRCS := mdview2_raw.c mdview2_hex.c mdview2_help.c mdview2_table.c mdview2_enc.c mdview2_md.c mdview2_status.c mdview2_index.c
MEMORY := small MEMORY := small
# --max-allocs 100000: агрессивнее аллокация регистров SDCC — минус ~850 байт
# кода на всём приложении (замер 2026-07-04) ценой более долгой компиляции.
EXTRA_FLAGS := --max-allocs 100000
include $(PROJ_ROOT)/app.mk include $(PROJ_ROOT)/app.mk
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Образ дискеты: только mdview2.exe + README.MD (перекодированный # Образ дискеты: mdview2.exe + документация. Каждый документ — в СВОЕЙ
# из UTF-8 в CP866 — рабочую кодировку Sprinter). # кодировке, чтобы дискета заодно покрывала все пути автодетекта:
# # README.MD — документация, ОРИГИНАЛЬНЫЙ UTF-8 (как в репозитории);
# README.MD хранится в репозитории в UTF-8; iconv -c конвертирует # CHANGES.MD — история версий, CP866 (хранится так, кладётся как есть;
# его в CP866, отбрасывая символы без аналога в целевой кодировке. # имя укорочено: "CHANGELOG" не влезает в FAT 8.3);
# Результат кладётся в .disk_tmp/README.MD, чтобы make_disk.py # DEMO.MD — демонстрация разметки, конвертируется UTF-8 → CP1251.
# использовал правильное имя файла на диске.
# #
# iconv -c возвращает ненулевой код, если хоть один символ отброшен # iconv -c возвращает ненулевой код, если хоть один символ отброшен
# (даже с -c) — это ОЖИДАЕМО при потере символов без аналога в CP866, # (даже с -c) — это ОЖИДАЕМО при потере символов без аналога в целевой
# не ошибка конвертации; вывод при этом всё равно корректно записан. # кодировке, не ошибка конвертации; вывод при этом корректно записан.
# Поэтому код возврата iconv игнорируется (|| true). # Поэтому код возврата iconv игнорируется (|| true).
#
# Тестовые файлы лимита 256 КБ лежат в testfiles/ (генератор gen_testfiles.py);
# на диск НЕ кладутся — при необходимости добавить вручную в список ниже.
# ------------------------------------------------------------------ # ------------------------------------------------------------------
DISK_TMP := .disk_tmp DISK_TMP := .disk_tmp
README_DISK := $(DISK_TMP)/README.MD DEMO_DISK := $(DISK_TMP)/DEMO.MD
CHLOG_DISK := $(DISK_TMP)/CHANGES.MD
$(DISK_TMP): $(DISK_TMP):
mkdir -p $@ mkdir -p $@
$(README_DISK): README.MD | $(DISK_TMP) $(CHLOG_DISK): CHANGELOG.md | $(DISK_TMP)
iconv -c -f UTF-8 -t CP866 README.MD > $@ || true cp CHANGELOG.md $@
# UTF8TEST.MD кладётся на диск КАК ЕСТЬ (в UTF-8, без перекодировки) — $(DEMO_DISK): DEMO.MD | $(DISK_TMP)
# это тестовый вход для проверки UTF-8 рендеринга (Фаза 2 кодировок). iconv -c -f UTF-8 -t CP1251 DEMO.MD > $@ || true
floppy: $(EXAMPLE).exe $(README_DISK)
python3 $(MAKE_DISK) $(FLOPPY_IMG) $(EXAMPLE).exe $(README_DISK) UTF8TEST.MD floppy: $(EXAMPLE).exe $(DEMO_DISK) $(CHLOG_DISK)
python3 $(MAKE_DISK) $(FLOPPY_IMG) $(EXAMPLE).exe README.md $(DEMO_DISK) $(CHLOG_DISK)
@echo @echo
@echo "Floppy ready: $(FLOPPY_IMG)" @echo "Floppy ready: $(FLOPPY_IMG)"
@echo "Run: cd $(MAME_DIR) && ./run_mame.sh" @echo "Run: cd $(MAME_DIR) && ./run_mame.sh"
+153
View File
@@ -0,0 +1,153 @@
# MDView v1.0 (b3) — просмотрщик Markdown для Sprinter
**MDView** — программа просмотра документов Markdown для компьютера
**Sprinter-2000** (Z80, ОС ESTEX). Файл загружается в расширенную память (EMM)
и один раз «прогоняется» через парсер: готовые к показу строки (пары
символ+атрибут) складываются в **рендер-кэш** в EMM, после чего прокрутка в
любую сторону — это просто копирование готовых строк на экран, без повторного
парсинга. Даже на файлах в сотни килобайт листание остаётся мгновенным.
Текстовый режим 80×32, цветное оформление элементов разметки, три режима
просмотра (MD / RAW / HEX) и четыре кодировки с автоопределением.
---
## Возможности
* **Markdown-рендеринг** с цветовым оформлением:
* заголовки `#``######` (H5/H6 отображаются как H4);
* **жирный** (`**текст**`), *курсив* (`*текст*`), подчёркнутый (`_текст_`),
~~зачёркнутый~~ (`~~текст~~`), `встроенный код` (`` `текст` ``);
* экранирование `\*`, `\_`, `` \` `` и любой ASCII-пунктуации;
* чекбоксы `[x]` / `[ ]` в списках;
* ненумерованные (`-`, `*`, `+`) и нумерованные (`1.`, `1)`) списки
с базовой вложенностью по отступам;
* цитаты `>` (склейка многострочных, маркер │ на переносах);
* fenced-блоки кода ` ``` ` (без переносов, горизонтальный скролл);
* таблицы `| … | … |` — рисуются псевдографической рамкой, ширины колонок
вычисляются по содержимому (до 16 колонок);
* горизонтальные разделители `---` / `***` / `___`;
* жёсткие переносы (два пробела или `\` в конце строки);
* мягкая склейка абзацев с переносом по словам под ширину экрана.
* **Кодировки: CP866, CP1251, KOI8-R, UTF-8.**
* автоопределение при открытии (BOM → UTF-8; валидность multibyte-структуры;
частотный анализ ходовых русских букв для 8-битных);
* переключение по кругу клавишей **F8** в любой момент;
* 8-битные кодировки отличаются только перекодировкой глифов на отрисовке —
переключение мгновенно;
* UTF-8 декодируется в CP866 в **отдельный набор** (файл + индекс + кэш);
второй набор готовится **в фоне**, пока вы читаете документ, — обычно
к первому нажатию F8 он уже построен и переключение мгновенно,
с сохранением позиции. Если фон не успел, F8 докручивает начатую
сборку (со спиннером), а не начинает её заново.
* **Три режима просмотра:**
* **MD** — форматированный Markdown (по умолчанию);
* **RAW** (**F2**) — исходный текст без разметки: перенос строк кратно 80
(**F3** — режим панорамы с горизонтальным скроллом);
* **HEX** (**F4**) — дамп *оригинального* файла:
`0x012340 │ 16 байт hex │ 16 печатных символов`. Печатная колонка
интерпретируется текущей кодировкой; для UTF-8 глиф ставится на позиции
лид-байта, continuation-байты показываются точкой.
* **Единая позиция** при любых переключениях: MD ↔ RAW ↔ HEX и смена
кодировки сохраняют текущее место в документе (между наборами разного
размера — пропорционально, с точностью до строки).
* **Прогрессивная загрузка**: первый экран показывается сразу, индексация
продолжается в фоне; по готовой части документа уже можно листать,
**Esc**/**F10** прерывают загрузку.
* **Фоновая работа незаметна**: второй набор кодировки строится только в
паузах между клавишами (после нажатия выдерживается пауза), поэтому
скролл — в том числе с автоповтором — не теряет плавности.
---
## Запуск
```
MDVIEW2.EXE <файл.md>
```
Без аргумента открывается `README.MD` из текущего каталога.
## Клавиши
| Клавиша | Действие |
|--------------|-------------------------------------------------------------|
| ↑ / ↓ | прокрутка на одну строку |
| PgUp / PgDn | прокрутка на экран (30 строк) |
| Home / End | в начало / в конец документа |
| ← / → | горизонтальный сдвиг: код/таблицы в MD, панорама в RAW |
| F1 | справка |
| F2 | RAW-режим ↔ MD |
| F3 | в RAW: перенос строк ↔ панорама |
| F4 | HEX-режим ↔ прежний вид |
| F8 | кодировка: CP866 → CP1251 → KOI8-R → UTF-8 → … |
| Esc / F10 | выход (во время загрузки — прервать её) |
Статус-бар (верхняя строка): имя файла, кодировка, диапазон видимых строк
и процент прокрутки. Нижняя строка — меню доступных F-клавиш.
---
## Ограничения
| Параметр | Значение |
|---------------------------------|-------------------------------------------|
| Размер файла | до 256 КБ (больший — обрезается с предупреждением) |
| Логических строк (после переносов) | до 18 432 |
| Длина строки в рендер-кэше | 255 ячеек (RAW/HEX ограничения не имеют) |
| Колонок в таблице | до 16 |
| Шаг табуляции | 4 |
При исчерпании любого лимита документ завершается строкой-сообщением
с указанием причины обрыва; всё, что вошло, доступно для просмотра.
## Требования
* Sprinter-2000 с ОС ESTEX;
* расширенная память (EMM): в худшем случае (файл 256 КБ + оба набора
кодировок) — до ~150 страниц по 16 КБ (~2,4 МБ). Для типичных файлов
в десятки килобайт достаточно нескольких десятков страниц.
Код, данные, стек и куча программы занимают окна W1+W2 (32 КБ, режим
памяти `small`); окно W3 используется только для доступа к EMM-страницам.
---
## Сборка
Требуется тулчейн этого репозитория (обёртка `sprinter-cc` над SDCC 4.5).
Из каталога `examples/mdview2`:
```
make # собрать mdview2.exe
make floppy # собрать и упаковать дискету для MAME (mc.img)
make run # floppy + запуск MAME
```
На дискету кладутся: `MDVIEW2.EXE` и три документа, каждый в своей кодировке
(заодно покрывают все пути автодетекта): `README.MD` — этот файл, как есть
(UTF-8); `DEMO.MD` — демонстрация всех элементов разметки (CP1251);
`CHANGES.MD` — история версий (CP866).
## Структура исходников
| Файл | Назначение |
|------------------|--------------------------------------------------------------|
| `mdview2.c` | ядро: EMM-аллокации, рендер-кэш, наборы кодировок, загрузка файла, главный цикл |
| `mdview2_index.c`| парсер/индексатор Markdown — единственный проход по файлу |
| `mdview2_md.c` | MD-вид: отрисовка из кэша, прокрутка |
| `mdview2_raw.c` | RAW-вид (F2/F3) |
| `mdview2_hex.c` | HEX-вид (F4) |
| `mdview2_enc.c` | кодировки: детект, ремап-таблицы, конвертер UTF-8 → CP866 |
| `mdview2_table.c`| отрисовка таблиц |
| `mdview2_status.c`| статус-бар, меню, спиннер |
| `mdview2_help.c` | справка (F1) |
| `mdview2_conf.h` | конфигурация: `WITH_RAW` / `WITH_HEX` (модули отключаемы) |
| `mdview2.h` | общие константы, атрибуты, межмодульный API |
Подробности архитектуры — в `docs/mdview2-plan.md`.
## Лицензия и авторы
© 2026 Петров А.Г. Часть проекта Sprinter C Compiler
(см. LICENSE в корне репозитория).
+634 -2060
View File
File diff suppressed because it is too large Load Diff
+304
View File
@@ -0,0 +1,304 @@
/*
* mdview2.h общие определения и интерфейс между модулями mdview2.
*
* Монолит mdview2.c расщеплён на ядро + модули: index (парсер), md/raw (виды),
* enc (кодировки), table, help. Здесь разделяемые константы экрана/атрибутов,
* словари флагов/стилей, тип cache_rec_t, extern-объявления разделяемого
* состояния ядра и API каждого модуля (сгруппированы по секциям ниже).
*/
#ifndef MDVIEW2_H
#define MDVIEW2_H
#include <stdint.h>
#include <palette.h> /* COLOR(), COLOR_* */
#include "mdview2_conf.h" /* WITH_RAW / WITH_HEX — выбор опциональных модулей */
/* ---- Геометрия экрана -------------------------------------------- */
#define SCREEN_W 80
#define SCREEN_H 32
#define VIEW_TOP_ROW 1
#define VIEW_H 30 /* видимая область: строки 1..30 включительно */
#define MENU_ROW 31
#define TAB_STOP 4
#define HPAN_STEP 8u /* шаг горизонтального сдвига (←/→) */
/* ---- Геометрия статус-бара (строка 0): фиксированные поля и разделители ---- */
#define SPINNER_COL 8 /* слот спиннера загрузки */
#define DIV1_X 45 /* разделитель │ перед числами диапазона строк */
#define DIV2_X 71 /* разделитель │ перед процентом */
/* Числа/метка пишутся с DIVn_X+2; область между разделителями — [DIV1_X+1 .. DIV2_X-1]. */
/* ---- Параметры файла и памяти ------------------------------------ */
#define PAGE_BITS 14u
#define PAGE_SIZE (1u << PAGE_BITS) /* размер EMM-страницы: 16 КБ */
#define PAGE_MASK ((uint16_t)(PAGE_SIZE - 1u))
#define MAX_PAGES 16 /* 16 страниц × 16 КБ = 256 КБ */
#define MAX_FILE ((uint32_t)MAX_PAGES * PAGE_SIZE)
#define FILE_BUF ((char *)0xC000) /* окно W3, куда мапится текущая EMM-страница */
/* ---- Палитра атрибутов ------------------------------------------- */
#define ATTR_RESET COLOR(COLOR_LIGHTGRAY, COLOR_BLACK)
#define ATTR_TEXT COLOR(COLOR_LIGHTGRAY, COLOR_BLUE)
#define ATTR_TEXT_TITLE1 COLOR(COLOR_YELLOW, COLOR_BLUE)
#define ATTR_TEXT_TITLE2 COLOR(COLOR_LIGHTBLUE, COLOR_BLUE)
#define ATTR_TEXT_TITLE3 COLOR(COLOR_LIGHTGREEN, COLOR_BLUE)
#define ATTR_TEXT_TITLE4 COLOR(COLOR_LIGHTGREEN, COLOR_BLUE)
#define ATTR_TEXT_BOLD COLOR(COLOR_LIGHTRED, COLOR_BLUE)
#define ATTR_TEXT_ITALIC COLOR(COLOR_LIGHTGREEN, COLOR_BLUE)
#define ATTR_TEXT_UNDERSORE COLOR(COLOR_LIGHTMAGENTA, COLOR_BLUE)
#define ATTR_TEXT_CODE COLOR(COLOR_WHITE, COLOR_BLUE)
#define ATTR_TEXT_STRIKE COLOR(COLOR_DARKGRAY, COLOR_BLUE)
#define ATTR_LIST_MARKER COLOR(COLOR_LIGHTCYAN, COLOR_BLUE)
#define ATTR_QUOTE_MARKER COLOR(COLOR_CYAN, COLOR_BLUE)
#define ATTR_HR COLOR(COLOR_CYAN, COLOR_BLUE)
#define ATTR_BOX COLOR(COLOR_CYAN, COLOR_BLUE)
#define ATTR_TRUNC COLOR(COLOR_YELLOW, COLOR_BLUE)
#define ATTR_BAR COLOR(COLOR_BLACK, COLOR_LIGHTCYAN)
#define ATTR_BAR_SPINNER COLOR(COLOR_WHITE, COLOR_LIGHTCYAN)
#define ATTR_MENU_T COLOR(COLOR_BLACK, COLOR_LIGHTCYAN)
#define ATTR_MENU_K COLOR(COLOR_YELLOW, COLOR_BLACK)
#define ATTR_WARN COLOR(COLOR_YELLOW, COLOR_RED) /* строка-обрыв (исчерпан кэш/лимит) */
/* ---- Атрибуты диалога справки ------------------------------------ */
#define ATTR_HELP_BG COLOR(COLOR_LIGHTGRAY, COLOR_BLACK)
#define ATTR_HELP_BDR COLOR(COLOR_WHITE, COLOR_BLACK)
#define ATTR_HELP_TIT COLOR(COLOR_YELLOW, COLOR_BLACK)
#define ATTR_HELP_HDR COLOR(COLOR_WHITE, COLOR_BLACK)
/* ---- Режимы просмотра -------------------------------------------- */
#define VIEW_MD 0 /* markdown с форматированием (по умолчанию) */
#define VIEW_RAW_WRAP 1 /* RAW: длинные строки переносятся кратно 80 */
#define VIEW_RAW_HSCROLL 2 /* RAW: длинные строки в одну, гориз. скролл */
#define VIEW_HEX 3 /* HEX-дамп оригинального файла */
#define VIEW_IS_RAW(v) ((v) == VIEW_RAW_WRAP || (v) == VIEW_RAW_HSCROLL)
/* ---- Кодировки --------------------------------------------------- */
/* 8-битные (CP866/CP1251/KOI8R) различаются только ремапом глифов [128-255]
* на отрисовке; UTF-8 отдельный декодированный набор. */
#define ENC_CP866 0
#define ENC_CP1251 1
#define ENC_KOI8R 2
#define ENC_UTF8 3
#define ENC_UNSUPPORTED -1 /* UTF16 / UTF32 */
#define CONV_MARGIN 4096u /* на сколько байт держать UTF-конвертацию впереди индексатора */
/* ---- Флаги сегмента индекса (IF_*) — общие для индексатора/кэша/вью ---- */
#define IF_CONT 0x01u /* сегмент является продолжением перенесённой строки */
#define IF_NOWRAP 0x02u /* строка не переносится (кодовый блок / HR / таблица) */
#define IF_BLANK 0x04u /* визуально пустая строка */
#define IF_CODE 0x08u /* тело fenced code-блока (verbatim-режим) */
#define IF_HSCROLL 0x10u /* блок горизонтально скроллируется ЦЕЛИКОМ (код/таблица),
* включая строки короче 80 двигаются как единый блок.
* НЕ ставится на HR и границы fence (им нечего прятать). */
#define IF_TRUNC_MSG 0x20u /* строка-обрыв: рисуется ВЖИВУЮ фикс. текстом (без контент-кэша);
* cache_rec.reserved несёт код причины (TRUNC_*). */
/* ---- Причина обрыва индексации (cache_rec.reserved при IF_TRUNC_MSG) ---- */
#define TRUNC_CONTENT 1 /* исчерпан контент-кэш (MAX_CACHE_CONTENT_PAGES) */
#define TRUNC_LINES 2 /* исчерпана ёмкость индекса (max_lines) */
#define TRUNC_FILE 3 /* файл больше MAX_FILE — прочитаны первые 256 КБ */
/* ---- Словарь сегментов/стилей (общий для индексатора и таблиц) ---- */
/* Тип сегмента-продолжения, передаваемый в emit_seg()/inline_scan(). */
#define CK_PLAIN 0
#define CK_QUOTE 1
#define CK_LIST 2
#define CK_OTHER 3
/* Начальный inline-стиль сегмента (INIT_STYLE_*). */
#define INIT_STYLE_PLAIN 0x0
#define INIT_STYLE_BOLD 0x1
#define INIT_STYLE_ITALIC 0x2
#define INIT_STYLE_UNDER 0x3
#define INIT_STYLE_CODE 0x4
#define INIT_STYLE_STRIKE 0x5
/* ---- Таблицы: CP866 light box-drawing (общий для индексатора и модуля) ---- */
#define TBL_MAX_COLS 16
#define TBL_ATTR ATTR_BOX
#define TBL_H 0xC4 /* ─ */
#define TBL_V 0xB3 /* │ */
#define TBL_TL 0xDA /* ┌ */
#define TBL_TM 0xC2 /* ┬ */
#define TBL_TR 0xBF /* ┐ */
#define TBL_ML 0xC3 /* ├ */
#define TBL_MM 0xC5 /* ┼ */
#define TBL_MR 0xB4 /* ┤ */
#define TBL_BL 0xC0 /* └ */
#define TBL_BM 0xC1 /* ┴ */
#define TBL_BR 0xD9 /* ┘ */
/* ================================================================== *
* Разделяемые символы mdview2.c, используемые модулем RAW.
* ================================================================== */
extern uint8_t g_view; /* активный режим просмотра (VIEW_*) */
extern uint32_t file_size; /* размер активного буфера документа */
extern const uint8_t *g_remap; /* таблица ремапа [128-255] (0 = нет) */
extern uint8_t g_scratch_phys; /* scratch EMM-страница для win_rest */
char fb(uint32_t p); /* байт активного буфера (W3-маппинг) */
void win_rest(uint8_t row, uint8_t col, uint8_t h, uint8_t w, uint8_t page, uint16_t off);
void fill_row(uint8_t y, uint8_t attr);
uint8_t pct16(uint16_t num, uint16_t den); /* num*100/den (0..100), 16-бит, без __divulong */
uint32_t seg_off(uint16_t idx); /* offset исходника для логической строки idx */
uint16_t line_at_off(uint32_t off); /* обратно: последняя строка с seg_off <= off */
/* ================================================================== *
* API модуля RAW (mdview2_raw.c).
* ================================================================== */
void raw_seed_from(uint16_t md_top_line); /* вход в RAW: позиция по top_line */
uint32_t raw_pos(void); /* текущий байт-offset верха экрана RAW */
void raw_reanchor(uint32_t off); /* поставить RAW на строку с байтом off */
void raw_draw(void); /* перерисовать область документа в RAW */
uint8_t raw_pct(void); /* % прокрутки по байтам (для render_raw_status_numbers) */
uint8_t raw_key(uint8_t scan); /* навигация RAW; 1 = обработано */
void raw_screen_init(void); /* очистка экрана с нужным аттрибутом */
void raw_renorm(void); /* выровнять позицию при смене под-режима (F3) */
/* ================================================================== *
* API модуля HEX (mdview2_hex.c) дамп ОРИГИНАЛЬНОГО файла (F4).
* Позиция hex_pos/hex_reanchor в байтах ОРИГИНАЛА (orig_file_*);
* конвертацию в/из активного буфера делает ядро (map_off).
* ================================================================== */
uint32_t hex_pos(void); /* offset верхнего ряда (кратен 16) */
void hex_reanchor(uint32_t orig_off); /* поставить на ряд с байтом orig_off */
void hex_draw(void); /* перерисовать область документа */
uint8_t hex_key(uint8_t scan); /* навигация HEX; 1 = обработано */
void hex_screen_init(void); /* очистка экрана */
/* ================================================================== *
* Запись директории рендер-кэша (одна на видимую строку, РОВНО 8 байт
* cache_dir_get/put адресуют сдвигом idx<<3). Контент len пар (char,attr).
* ================================================================== */
typedef struct cache_rec_s {
uint8_t page; /* EMM-страница рендер-кэша (физический номер) */
uint16_t off; /* смещение в странице, 0..16383 (байты, не ячейки) */
uint8_t len; /* длина контента в ЯЧЕЙКАХ (char,attr пар), 0..255; 0 = пустая/HR */
uint8_t flags; /* копия IF_NOWRAP/IF_CODE/IF_BLANK на момент рендера */
uint8_t reserved; /* не используется */
uint8_t pad[2]; /* явный резерв, добивка до 8 байт */
} cache_rec_t;
void cache_dir_get(uint16_t idx, cache_rec_t *r); /* читатель директории кэша (ядро) */
void put_str_attr(uint8_t x, uint8_t y, const char *s, uint8_t attr); /* печать строки с атрибутом */
/* ================================================================== *
* Состояние навигации/представления (определено в mdview2.c).
* ================================================================== */
extern uint16_t n_lines; /* всего строк в индексе текущего набора */
extern uint16_t top_line; /* верхняя видимая строка */
extern uint8_t viewport_x; /* горизонтальный сдвиг (nowrap-строки) */
extern char filename[]; /* имя файла для статус-бара */
extern uint8_t g_loading; /* 1 во время index_lines() */
extern uint8_t g_ready; /* 1 когда первичный документ построен (F2 RAW) */
extern uint8_t g_f8_enabled; /* можно ли сейчас переключать кодировку */
/* ================================================================== *
* Статус-бар (строка 0), меню (строка 31), спиннер mdview2_status.c.
* Атомарные части: prerender (фикс. хром, один раз) / encoding (поле кодировки)
* / numbers (диапазон строк + %) / menu / spinner обновляются по отдельности.
* ================================================================== */
void prerender_status(void); /* фикс. часть: фон, разделители, MDVIEW, имя файла (1 раз) */
void status_encoding(void); /* только поле кодировки (col 37) */
void render_md_status_numbers(void); /* числа MD: диапазон строк + % */
void render_raw_status(void); /* RAW mode: метка режима */
void render_hex_status(void); /* HEX mode: метка режима */
void render_full_status(void); /* encoding + numbers (MD) */
void render_menu(void); /* строка меню (по смене режима/готовности) */
uint16_t drawable_lines(void); /* число строк, готовых к показу */
void spinner_tick(void); /* кадр спиннера загрузки */
void spinner_show(uint8_t on); /* вкл/выкл спиннер */
void render_percent_progress(uint8_t pct); /* показать процентный прогресс просмотра */
/* ================================================================== *
* Отрисовка области MD-документа и прокрутка (mdview2_md.c).
* ================================================================== */
void draw_viewport_from_cache(void); /* перерисовать область документа из кэша */
void clamp_top(void); /* привести top_line к диапазону */
void md_scroll_up(uint16_t n);
void md_scroll_down(uint16_t n);
void md_scroll_horizon(int8_t delta); /* горизонтальный сдвиг nowrap-блоков */
uint8_t md_key(uint8_t scan); /* навигация MD (после загрузки); 1 = обработано */
/* ================================================================== *
* API модуля справки (mdview2_help.c).
* ================================================================== */
void show_help(void); /* модальный диалог F1 */
/* ================================================================== *
* Парсер/индексатор (mdview2_index.c) и разделяемое с ним состояние.
* Хранилище индекса (index_blk/pages/phys, n_lines/max_lines) в ядре
* (аллокация/doc-slot), индексатор пишет в него; cache_*-писатели и
* progress_tick ядро экспортирует индексатору (spinner_tick из status).
* ================================================================== */
#define INDEX_RECS_PER_PAGE 2048u /* 16384 / 8; запись всегда в одной странице */
#define MAX_CACHE_LINE_LEN 255u /* кап длины контента строки в ячейках (буфер g_cells) */
extern uint16_t max_lines; /* ёмкость индекса: index_pages * 2048 */
extern uint8_t index_blk, index_pages; /* дескриптор EMM-блока индекса */
extern uint8_t index_phys[]; /* физ. страницы индекса */
extern uint8_t index_truncated; /* индекс упёрся в ёмкость */
extern uint8_t g_abort; /* F10/Esc во время загрузки → прервать индексацию */
extern uint8_t g_file_clamped; /* файл был обрезан до MAX_FILE при загрузке (>256 КБ) */
uint8_t cache_reserve(uint16_t nbytes, uint8_t *out_page, uint16_t *out_off);
void cache_commit(uint8_t page, uint16_t off, const void *buf, uint16_t len);
void cache_dir_put(uint16_t idx, const cache_rec_t *r);
void progress_tick(void); /* кооперативный шаг loading-loop (рисует/листает) */
/* Резюмируемая индексация (для фоновой сборки второго набора): begin сбрасывает
* проход, step выполняет до budget итераций (блоков исходника) и возвращает 1,
* когда индекс финализирован. Межшаговое состояние статики модуля index;
* оно не входит в docset_t (см. bg_step ядра). */
void index_begin(void);
uint8_t index_step(uint8_t budget);
extern uint8_t g_bg_building; /* 1 = фоновый шаг индексации: не трогать экран (спиннер) */
/* ================================================================== *
* Буфер ячеек рендера (mdview2.c) общий с модулем таблиц.
* Таблицы эмитят свои строки теми же примитивами, что и индексатор.
* ================================================================== */
extern uint8_t g_ncells; /* ячеек в текущем сегменте */
void gc_put(char ch, uint8_t attr); /* добавить ячейку в g_cells */
void gc_fill(char ch, uint8_t attr, uint8_t n); /* n одинаковых ячеек */
uint8_t inline_scan(uint32_t q, uint32_t q_end, uint8_t col,
uint8_t ckind, uint8_t line_style, uint8_t base_attr,
uint8_t nowrap);
/* ================================================================== *
* API модуля таблиц (mdview2_table.c) вызывается из index_lines().
* ================================================================== */
uint32_t row_end(uint32_t p); /* offset завершающего '\n' (или file_size) */
uint32_t table_first_cell(uint32_t row_start); /* контент первой ячейки строки */
uint8_t table_next_cell(uint32_t *pp, uint32_t lineend, uint32_t *cs, uint32_t *ce);
uint8_t table_is_sep_row(uint32_t row_start, uint32_t lineend);
void table_border(const uint8_t *widths, uint8_t ncols, char left, char mid, char right);
void table_data_row(uint32_t row_start, uint32_t lineend, const uint8_t *widths, uint8_t ncols);
/* ================================================================== *
* Кодировки и UTF-8 конвертация (mdview2_enc.c).
* Состояние кодека владеется модулем; build_doc()/index_lines() ядра
* оркестрируют его (привязка UTF-буфера к живому file_*, сборка наборов).
* ================================================================== */
extern uint8_t g_encoding; /* активная кодировка (ENC_*) */
extern uint8_t g_utf_building; /* 1 пока UTF-конвертация не дошла до конца исходника */
extern uint8_t utf_avail; /* 1 = страницы UTF-8 выделены */
extern uint8_t utf_blk, utf_pages; /* дескриптор EMM-буфера UTF-набора */
extern uint8_t utf_phys[]; /* физ. страницы UTF-буфера */
extern uint32_t utf_size; /* размер сконвертированного UTF-потока */
/* Ядро-owned состояние, читаемое кодеком (определено в mdview2.c). */
extern uint8_t cur_page; /* текущая страница файла в W3 (инвалидация маппинга) */
extern uint8_t orig_file_phys[]; /* физ. страницы снимка исходного файла */
extern uint8_t orig_file_pages;
extern uint32_t orig_file_size;
void set_encoding(uint8_t enc); /* активная кодировка + таблица ремапа */
const char *enc_name(uint8_t enc); /* короткое имя для статус-бара */
uint8_t detect_encoding(void); /* автоопределение по сэмплу байт */
void win_rest_remap(uint8_t row, uint8_t w, uint8_t page, uint16_t off); /* вывод среза с ремапом */
void utf_convert_more(uint32_t target); /* инкрементальная конвертация до target байт */
uint8_t utf_alloc(void); /* выделить EMM-страницы под UTF-набор; 0 = нет EMM */
uint8_t utf_cp_glyph(uint32_t cp); /* кодпойнт → один CP866-глиф ('?' без аналога) */
#endif /* MDVIEW2_H */
+16
View File
@@ -0,0 +1,16 @@
/*
* mdview2_conf.h конфигурация сборки mdview2: какие опциональные модули
* просмотра включать. 1 = включить (код входит в бинарь), 0 = исключить
* полностью (нулевой расход кода/памяти).
*
* Модули обёрнуты в #if WITH_xxx целиком, поэтому файлы можно держать в
* сборке всегда (при 0 они компилируются в пустой объектник) менять
* нужно только эти define.
*/
#ifndef MDVIEW2_CONF_H
#define MDVIEW2_CONF_H
#define WITH_RAW 1 /* RAW-просмотр исходника (F2): mdview2_raw.c */
#define WITH_HEX 1 /* HEX-дамп оригинального файла (F4): mdview2_hex.c */
#endif /* MDVIEW2_CONF_H */
+319
View File
@@ -0,0 +1,319 @@
/*
* mdview2_enc.c кодировки и конвертация UTF-8 CP866.
*
* Две связанные подсистемы:
* 1. 8-битные кодировки (CP866/CP1251/KOI8-R) различаются только ремапом
* старших байтов [128-255] на ОТРИСОВКЕ (g_remap, win_rest_remap).
* Автоопределение detect_encoding по частоте ходовых букв.
* 2. UTF-8 отдельный набор: исходник инкрементально декодируется в
* utf_phys[] как чистый CP866 (utf_convert_more), после чего весь
* конвейер индексации/кэша работает по нему без ремапа.
*
* Владеет состоянием кодека (conv-буфер, позиция конвертации, UTF-страницы);
* описание разделяемых символов и API в mdview2.h. Оркестрацией (привязка
* UTF-буфера к живому file_*, сборка наборов) занимается build_doc() ядра.
*/
#include <stdint.h>
#include <conio.h> /* COLOR()/COLOR_* для ATTR_* в attr_is_content */
#include <sprinter.h> /* sprinter_page_w3 */
#include <sprinter_mem.h> /* bank_read/bank_write/mem_alloc_pages/mem_get_page */
#include "mdview2.h"
/* ---- Состояние кодировки/UTF (владелец — этот модуль) ------------- */
uint8_t g_encoding = ENC_CP866; /* активная кодировка */
uint8_t utf_blk;
uint8_t utf_pages;
uint8_t utf_phys[MAX_PAGES];
uint32_t utf_size;
uint8_t utf_avail; /* 1 = страницы UTF-8 выделены */
uint8_t g_utf_building; /* 1 пока конвертация не дошла до конца исходника */
/* Приватное состояние конвертера (наружу не торчит — сброс через
* utf_conv_reset(), чтение оригинала через cv_read). */
static uint32_t utf_src; /* позиция чтения ОРИГИНАЛА (utf-8) */
static uint8_t cv_page = 0xFF; /* orig-страница в W3 для cv_read (своя от cur_page) */
/* Таблицы ремапа СТАРШИХ байтов [128-255] в CP866. Индексируются (ch-0x80):
* младшие 128 (ASCII) identity, в ремапе не участвуют (win_rest_remap
* трогает только ch>=0x80), поэтому в таблицах их нет. Кириллица + ходовая
* пунктуация; неизвестное '?' (0x3F). Сгенерированы Python codecs. */
static const uint8_t cp1251_to_866[128] = {
/* 80 */ 0x3F, 0x3F, 0x27, 0x3F, 0x22, 0x2E, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3C, 0x3F, 0x3F, 0x3F, 0x3F,
/* 90 */ 0x3F, 0x27, 0x27, 0x22, 0x22, 0x07, 0x2D, 0x2D, 0x3F, 0x3F, 0x3F, 0x3E, 0x3F, 0x3F, 0x3F, 0x3F,
/* A0 */ 0xFF, 0xF6, 0xF7, 0x3F, 0xFD, 0x3F, 0x3F, 0x3F, 0xF0, 0x63, 0xF2, 0x3C, 0x3F, 0x3F, 0x72, 0xF4,
/* B0 */ 0xF8, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0xFA, 0xF1, 0xFC, 0xF3, 0x3E, 0x3F, 0x3F, 0x3F, 0xF5,
/* C0 */ 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F,
/* D0 */ 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F,
/* E0 */ 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF,
/* F0 */ 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF,
};
static const uint8_t koi8r_to_866[128] = {
/* 80 */ 0xC4, 0xB3, 0xDA, 0xBF, 0xC0, 0xD9, 0xC3, 0xB4, 0xC2, 0xC1, 0xC5, 0xDF, 0xDC, 0xDB, 0xDD, 0xDE,
/* 90 */ 0xB0, 0xB1, 0xB2, 0x3F, 0xFE, 0xF9, 0xFB, 0x3F, 0x3F, 0x3F, 0xFF, 0x3F, 0xF8, 0x3F, 0xFA, 0x3F,
/* A0 */ 0xCD, 0xBA, 0xD5, 0xF1, 0xD6, 0xC9, 0xB8, 0xB7, 0xBB, 0xD4, 0xD3, 0xC8, 0xBE, 0xBD, 0xBC, 0xC6,
/* B0 */ 0xC7, 0xCC, 0xB5, 0xF0, 0xB6, 0xB9, 0xD1, 0xD2, 0xCB, 0xCF, 0xD0, 0xCA, 0xD8, 0xD7, 0xCE, 0x63,
/* C0 */ 0xEE, 0xA0, 0xA1, 0xE6, 0xA4, 0xA5, 0xE4, 0xA3, 0xE5, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE,
/* D0 */ 0xAF, 0xEF, 0xE0, 0xE1, 0xE2, 0xE3, 0xA6, 0xA2, 0xEC, 0xEB, 0xA7, 0xE8, 0xED, 0xE9, 0xE7, 0xEA,
/* E0 */ 0x9E, 0x80, 0x81, 0x96, 0x84, 0x85, 0x94, 0x83, 0x95, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E,
/* F0 */ 0x8F, 0x9F, 0x90, 0x91, 0x92, 0x93, 0x86, 0x82, 0x9C, 0x9B, 0x87, 0x98, 0x9D, 0x99, 0x97, 0x9A,
};
/* Устанавливает активную кодировку и таблицу ремапа (для 8-битных). */
void set_encoding(uint8_t enc)
{
g_encoding = enc;
g_remap = (enc == ENC_CP1251) ? cp1251_to_866 :
(enc == ENC_KOI8R) ? koi8r_to_866 : (const uint8_t *)0;
}
/* Самые ходовые строчные русские буквы (о е а и н т с р в л) — их байты
* различают 8-битные кодировки по частоте. */
// static const uint8_t common866 [10] = {0xAE,0xA5,0xA0,0xA8,0xAD,0xE2,0xE1,0xE0,0xA2,0xAB};
// static const uint8_t common1251[10] = {0xEE,0xE5,0xE0,0xE8,0xED,0xF2,0xF1,0xF0,0xE2,0xEB};
// static const uint8_t commonkoi [10] = {0xCF,0xC5,0xC1,0xC9,0xCE,0xD4,0xD3,0xD2,0xD7,0xCC};
// static uint8_t in_set10(const uint8_t *s, uint8_t b)
// {
// for (uint8_t i = 0; i < 10; i++) if (s[i] == b) return 1;
// return 0;
// }
/* Проверяем только пять самых популярных символов (о е а и н) */
static const uint8_t common866 [5] = {0xAE,0xA5,0xA0,0xA8,0xAD};
static const uint8_t common1251[5] = {0xEE,0xE5,0xE0,0xE8,0xED};
static const uint8_t commonkoi [5] = {0xCF,0xC5,0xC1,0xC9,0xCE};
static uint8_t in_set10(const uint8_t *s, uint8_t b)
{
for (uint8_t i = 0; i < 5; i++) if (s[i] == b) return 1;
return 0;
}
/* Автоопределение кодировки дешёвым сканом байтов (до построения индекса).
* BOM UTF8; иначе валидность UTF-8 (структура multibyte); иначе 8-бит по
* частоте ходовых букв; фолбэк CP866. */
uint8_t detect_encoding(void)
{
if (file_size >= 3 && (uint8_t)fb(0) == 0xEF && (uint8_t)fb(1) == 0xBB && (uint8_t)fb(2) == 0xBF)
return ENC_UTF8;
uint32_t n = file_size;
if (n > 1024u) n = 1024u; /* сэмпл: первый 1 КБ — детекции хватает */
uint8_t utf_ok = 1, has_mb = 0, has_high = 0, cont = 0;
uint16_t s866 = 0, s1251 = 0, skoi = 0;
for (uint32_t p = 0; p < n; p++) {
uint8_t b = (uint8_t)fb(p);
if (b < 0x80) { if (cont) { utf_ok = 0; cont = 0; } continue; }
has_high = 1;
if (in_set10(common866, b)) s866++;
if (in_set10(common1251, b)) s1251++;
if (in_set10(commonkoi, b)) skoi++;
if (cont) {
if ((b & 0xC0) == 0x80) cont--;
else { utf_ok = 0; cont = 0; }
}
else if (b >= 0xC2 && b <= 0xDF) { cont = 1; has_mb = 1; }
else if (b >= 0xE0 && b <= 0xEF) { cont = 2; has_mb = 1; }
else if (b >= 0xF0 && b <= 0xF4) { cont = 3; has_mb = 1; }
else utf_ok = 0; /* битый лид/одиночный континюэйшн */
}
/* Незавершённая multibyte-последовательность на КОНЦЕ — нарушение только
* если это настоящий EOF; на границе сэмпла (n<file_size) это просто
* обрезка, не считаем за ошибку. */
if (cont && n == file_size) utf_ok = 0;
if (!has_high) return ENC_CP866; /* чистый ASCII */
if (utf_ok && has_mb) return ENC_UTF8;
if (s1251 >= s866 && s1251 >= skoi) return ENC_CP1251;
if (skoi >= s866) return ENC_KOI8R;
return ENC_CP866;
}
const char *enc_name(uint8_t enc)
{
return (enc == ENC_CP1251) ? "CP1251" :
(enc == ENC_KOI8R) ? "KOI8-R" :
(enc == ENC_UTF8) ? "UTF-8 " : "CP866 ";
}
/* Контентный глиф (ремапим при смене кодировки) vs структурный (рамка/HR/
* маркеры уже CP866, не трогаем). Различаем по attr. */
static uint8_t attr_is_content(uint8_t a)
{
return (uint8_t)(a != ATTR_BOX && a != ATTR_HR &&
a != ATTR_LIST_MARKER && a != ATTR_QUOTE_MARKER);
}
/* Вывод среза строки кэша с ремапом контентных глифов [128-255] через
* g_remap (CP1251/KOI8). Без активной таблицы прямой win_rest. */
void win_rest_remap(uint8_t row, uint8_t w, uint8_t page, uint16_t off)
{
if (!g_remap || w == 0) {
win_rest(row, 0, 1, w, page, off);
return;
}
uint8_t buf[SCREEN_W * 2];
bank_read(page, off, buf, (uint16_t)w * 2u);
for (uint8_t i = 0; i < w; i++) {
uint8_t ch = buf[(uint16_t)i * 2u];
if (ch >= 0x80 && attr_is_content(buf[(uint16_t)i * 2u + 1u]))
buf[(uint16_t)i * 2u] = g_remap[ch - 0x80]; /* таблицы хранят только старшие 128 */
}
bank_write(g_scratch_phys, 0, buf, (uint16_t)w * 2u);
win_rest(row, 0, 1, w, g_scratch_phys, 0);
}
/* ========================= UTF-8 → CP866 (Фаза 2) ======================== */
/* Кириллица U+0400..U+045F → CP866. Русский набор + Ё/ё и часть украинских
* (Є є Ї ї Ў ў), которые есть в CP866; пропуски '?' (0x3F). */
static const uint8_t utf_cyr_to_866[96] = {
/* 0400 */ 0x3F, 0xF0, 0x3F, 0x3F, 0xF2, 0x3F, 0x3F, 0xF4, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0xF6, 0x3F,
/* 0410 */ 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F,
/* 0420 */ 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F,
/* 0430 */ 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF,
/* 0440 */ 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF,
/* 0450 */ 0x3F, 0xF1, 0x3F, 0x3F, 0xF3, 0x3F, 0x3F, 0xF5, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0xF7, 0x3F,
};
/* Поток вывода конвертера: пишем в utf_phys чанками по странице, буфер в
* near-памяти, флаш через один bank_write (не побайтовый swap W3). */
static uint8_t conv_buf[256];
static uint16_t conv_n; /* байт в буфере, ещё не сброшено */
static uint8_t conv_page; /* индекс текущей страницы в utf_phys */
static uint16_t conv_off; /* уже сброшенное смещение в этой странице */
static void conv_flush(void)
{
if (conv_n) {
bank_write(utf_phys[conv_page], conv_off, conv_buf, conv_n);
conv_off = (uint16_t)(conv_off + conv_n);
conv_n = 0;
}
}
/* Добавляет один CP866-байт в выходной поток. Запись никогда не пересекает
* границу страницы (256 делит 16384 нацело). */
static void conv_put(uint8_t b)
{
if ((uint16_t)(conv_off + conv_n) == PAGE_SIZE) { /* страница заполнена */
conv_flush();
conv_page++;
conv_off = 0;
}
conv_buf[conv_n++] = b;
if (conv_n == (uint16_t)sizeof(conv_buf)) conv_flush();
}
/* Подстановки одиночных не-кириллических символов UTF-8 → один CP866-байт.
* Таблица вместо switch (экономия кода); линейный поиск дёшев вызывается
* лишь при конвертации на редких символьных кодпойнтах. Чтобы добавить новый
* символ достаточно дописать одну строку {кодпойнт, байт-CP866}.
* (Символы с НЕ-1:1 заменой, напр. -> "...", обрабатываются отдельно ниже.) */
typedef struct { uint16_t utf8; uint8_t cp866; } utf_sym_t;
static const utf_sym_t utf_sym[] = {
{0x00A0, 0x20}, {0x00AB, 0x3C}, {0x00BB, 0x3E}, /* nbsp « » → space < > */
{0x00B0, 0xF8}, {0x00B7, 0xFA}, /* ° · */
{0x2013, 0x2D}, {0x2014, 0x2D}, /* — → '-' */
{0x2018, 0x27}, {0x2019, 0x27}, /* → '\'' */
{0x201C, 0x22}, {0x201D, 0x22}, /* “ ” → '"' */
{0x2022, 0xF9}, {0x2116, 0xFC}, /* • → ∙ № */
{0x2190, 0x1B}, {0x2191, 0x18}, {0x2192, 0x1A}, {0x2193, 0x19}, /* ← ↑ → ↓ */
{0x2500, 0xC4}, {0x2502, 0xB3}, {0x250C, 0xDA}, {0x2510, 0xBF}, /* ─ │ ┌ ┐ */
{0x2514, 0xC0}, {0x2518, 0xD9}, {0x251C, 0xC3}, {0x2524, 0xB4}, /* └ ┘ ├ ┤ */
{0x252C, 0xC2}, {0x2534, 0xC1}, {0x253C, 0xC5}, /* ┬ ┴ ┼ */
{0x2580, 0xDF}, {0x2584, 0xDC}, {0x2588, 0xDB}, /* ▀ ▄ █ */
{0x2591, 0xB0}, {0x2592, 0xB1}, {0x2593, 0xB2}, {0x25A0, 0xFE}, /* ░ ▒ ▓ ■ */
{0x2713, 0xFB}, {0x2714, 0xFB}, /* ✓ ✔ → галка */
{0x2715, 0x78}, {0x2717, 0x78}, {0x2718, 0x78}, /* ✕ ✗ ✘ → 'x' */
};
#define UTF_SYM_N (uint8_t)(sizeof(utf_sym) / sizeof(utf_sym[0]))
/* Кодпойнт → ОДИН CP866-глиф: кириллица по таблице, ходовые символы по
* utf_sym[], прочее '?'. Общая для конвертера и HEX-printable (F4). */
uint8_t utf_cp_glyph(uint32_t cp)
{
if (cp < 0x80) return (uint8_t)cp;
if (cp >= 0x0400 && cp <= 0x045F) return utf_cyr_to_866[cp - 0x0400];
for (uint8_t i = 0; i < UTF_SYM_N; i++)
if (utf_sym[i].utf8 == (uint16_t)cp) return utf_sym[i].cp866;
return '?';
}
/* Кодпойнт → CP866-глиф(ы) в поток конвертера. Спецслучаи с НЕ-1:1 заменой
* ( "...", BOM ничего) здесь; остальное через utf_cp_glyph(). */
static void conv_emit_cp(uint32_t cp)
{
if (cp == 0x2026) { conv_put('.'); conv_put('.'); conv_put('.'); return; } /* … → "..." */
if (cp == 0xFEFF) return; /* BOM/ZWNBSP — выкинуть */
conv_put(utf_cp_glyph(cp));
}
/* Последовательное чтение байта ОРИГИНАЛА (utf-8) конвертером. Оригинал лежит
* в orig_file_phys[]; мапим его страницу в W3 напрямую, со своей кэш-переменной
* cv_page (отдельной от cur_page индексатора, т.к. оба используют W3 и
* чередуются). На границе с fb()-чтением utf-буфера кэши взаимно сбрасываются. */
static uint8_t cv_read(uint32_t s)
{
uint8_t pg = (uint8_t)(s >> PAGE_BITS);
if (pg != cv_page) { sprinter_page_w3(orig_file_phys[pg]); cv_page = pg; }
return *((volatile uint8_t *)(0xC000u + (uint16_t)(s & PAGE_MASK)));
}
/* Возобновляемая конвертация: дописывает utf_phys из оригинала, пока
* сконвертированных (flushed) байт меньше target и не достигнут конец
* исходника. По выходу file_size = доступная (сконвертированная) часть; при
* достижении конца фиксирует utf_size и снимает g_utf_building. */
void utf_convert_more(uint32_t target)
{
cv_page = 0xFF; /* W3 был на utf (fb индексатора) — пере-смаппим orig */
uint32_t n = orig_file_size;
while (utf_src < n &&
(uint32_t)((uint32_t)conv_page * PAGE_SIZE + conv_off) < target) {
uint8_t b = (uint8_t)cv_read(utf_src++);
if (b < 0x80) {
conv_emit_cp(b);
continue;
}
uint32_t cp;
uint8_t need;
if ((b & 0xE0) == 0xC0) { cp = (uint32_t)(b & 0x1F); need = 1; }
else if ((b & 0xF0) == 0xE0) { cp = (uint32_t)(b & 0x0F); need = 2; }
else if ((b & 0xF8) == 0xF0) { cp = (uint32_t)(b & 0x07); need = 3; }
else { conv_put('?'); continue; }
uint8_t ok = 1;
for (uint8_t k = 0; k < need; k++) {
if (utf_src >= n) { ok = 0; break; }
uint8_t cb = (uint8_t)cv_read(utf_src);
if ((cb & 0xC0) != 0x80) { ok = 0; break; }
cp = (cp << 6) | (uint32_t)(cb & 0x3F);
utf_src++;
}
if (!ok || need == 3) { conv_put('?'); continue; }
conv_emit_cp(cp);
}
conv_flush();
file_size = (uint32_t)conv_page * PAGE_SIZE + conv_off; /* доступно индексатору */
if (utf_src >= n) { utf_size = file_size; g_utf_building = 0; }
cur_page = 0xFF; /* W3 был на orig — fb индексатора пере-смаппит utf */
}
/* Выделяет страницы под UTF-8 набор (конвертированный ≤ оригинала по размеру)
* и сбрасывает приватное состояние конвертера в начало (conv-буфер, utf_src,
* g_utf_building). Сама конвертация инкрементальная, через utf_convert_more().
* Возврат: 1 успех, 0 нет EMM. */
uint8_t utf_alloc(void)
{
utf_pages = orig_file_pages;
utf_blk = mem_alloc_pages(utf_pages);
if (utf_blk == 0) return 0;
for (uint8_t i = 0; i < utf_pages; i++)
utf_phys[i] = mem_get_page(utf_blk, i);
conv_n = 0; conv_page = 0; conv_off = 0; /* converter reset (вызывается один раз) */
utf_src = 0;
g_utf_building = 1;
return 1;
}
+106
View File
@@ -0,0 +1,106 @@
/*
* mdview2_help.c диалог справки (F1).
*
* Модальное окно поверх документа: рамка с заголовком, список горячих
* клавиш и поддерживаемых markdown-элементов. Блокирует до нажатия любой
* клавиши, после чего восстанавливает статус-бар, область документа и меню.
*
* Зависимости от ядра (mdview2.c): render_full_status / draw_viewport_from_cache
* / render_menu для восстановления экрана. Текст справки в CP866.
*/
#include <stdint.h>
#include <string.h>
#include <conio.h>
#include <bios/text.h>
#include "mdview2.h"
/* Геометрия диалога справки (в символьных координатах 80×32). */
#define HELP_X 8u /* левая граница рамки */
#define HELP_Y 3u /* верхняя граница рамки */
#define HELP_W 64u /* ширина рамки (включая │) */
#define HELP_H 26u /* высота рамки (включая ─) */
/* Заполняет одну внутреннюю строку диалога (r=0 — первая строка за рамкой).
* Строка s в кодировке CP866; остаток до края дополняется пробелами. */
static void help_line(uint8_t r, const char *s, uint8_t attr)
{
uint8_t x = HELP_X + 1u;
uint8_t y = HELP_Y + 1u + r;
uint8_t len = (uint8_t)strlen(s);
if (len > HELP_W - 2u)
len = HELP_W - 2u;
bios_set_place(y, x);
if (len > 0)
bios_writeattr(s, len, attr);
/* place уже продвинут bios_writeattr на len колонок (verified). */
if (len < HELP_W - 2u) {
bios_fillcharattr(' ', ATTR_HELP_BG, (uint8_t)(HELP_W - 2u - len));
}
}
void show_help(void)
{
/* " Помощь " в CP866 (8 байт) */
static const char title[] = " \x8F\xAE\xAC\xAE\xE9\xEC ";
uint8_t tlen = 8u;
uint8_t lft = (uint8_t)((HELP_W - 2u - tlen) / 2u); /* = 27 */
uint8_t rgt = (uint8_t)(HELP_W - 2u - tlen - lft);
/* Верхняя граница рамки с заголовком по центру.
* place продвигается каждым BIOS-вызовом (verified), поэтому
* достаточно одной установки места на всю строку рамки. */
wrchar(HELP_X, HELP_Y, 0xDA, ATTR_HELP_BDR); /* ┌ */
bios_set_place(HELP_Y, (uint8_t)(HELP_X + 1u));
if (lft > 0) bios_fillcharattr(0xC4, ATTR_HELP_BDR, lft);
if (tlen > 0) bios_writeattr(title, tlen, ATTR_HELP_TIT);
if (rgt > 0) bios_fillcharattr(0xC4, ATTR_HELP_BDR, rgt);
wrchar(HELP_X + HELP_W - 1u, HELP_Y, 0xBF, ATTR_HELP_BDR); /* ┐ */
/* Боковые границы (левый и правый │ для каждой строки тела) */
for (uint8_t r = 1u; r < HELP_H - 1u; r++) {
wrchar(HELP_X, HELP_Y + r, 0xB3, ATTR_HELP_BDR); /* │ */
wrchar(HELP_X + HELP_W - 1u, HELP_Y + r, 0xB3, ATTR_HELP_BDR); /* │ */
}
/* Нижняя граница рамки */
wrchar(HELP_X, HELP_Y + HELP_H - 1u, 0xC0, ATTR_HELP_BDR); /* └ */
bios_set_place((uint8_t)(HELP_Y + HELP_H - 1u), (uint8_t)(HELP_X + 1u));
bios_fillcharattr(0xC4, ATTR_HELP_BDR, (uint8_t)(HELP_W - 2u));
wrchar(HELP_X + HELP_W - 1u, HELP_Y + HELP_H - 1u, 0xD9, ATTR_HELP_BDR); /* ┘ */
/* Содержимое (20 внутренних строк) */
uint8_t r = 0;
help_line(r++, "", ATTR_HELP_BG);
help_line(r++, " MDView v1.0 (b3) -- Markdown Viewer for Sprinter", ATTR_HELP_HDR);
help_line(r++, " (c) 2026 \x8F\xA5\xE2\xE0\xAE\xA2 \x80\x2E\x83\x2E",
ATTR_HELP_BG);
help_line(r++, "", ATTR_HELP_BG);
help_line(r++, " Navigation:", ATTR_HELP_HDR);
help_line(r++, " \x18 \x19 Scroll one line up / down", ATTR_HELP_BG);
help_line(r++, " PgUp PgDn Scroll one page up / down", ATTR_HELP_BG);
help_line(r++, " Home End Jump to beginning / end of document", ATTR_HELP_BG);
help_line(r++, " \x1B \x1A Horizontal pan (code blocks/tables/unwrap)", ATTR_HELP_BG);
help_line(r++, " Esc F10 Exit", ATTR_HELP_BG);
help_line(r++, "", ATTR_HELP_BG);
help_line(r++, " Markdown elements:", ATTR_HELP_HDR);
help_line(r++, " # ## ### Headings H1-H6, **bold**, *italic*,", ATTR_HELP_BG);
help_line(r++, " `code`, ``` code ```, ~~strike~~, > quote", ATTR_HELP_BG);
help_line(r++, " - * + 1. 2. Ordered list, |----|----| Tables", ATTR_HELP_BG);
help_line(r++, "", ATTR_HELP_BG);
help_line(r++, " Encoding and View modes:", ATTR_HELP_HDR);
help_line(r++, " F2 RAW Mode", ATTR_HELP_BG);
help_line(r++, " F3 Wrap/Unwrap Mode for RAW View", ATTR_HELP_BG);
help_line(r++, " F4 HEX Mode (dump of original file)", ATTR_HELP_BG);
help_line(r++, " F8 Cycle CP866 / CP1251 / KOI8-R / UTF-8", ATTR_HELP_BG);
help_line(r++, " Auto-detected on open; F8 to override", ATTR_HELP_BG);
help_line(r++, "", ATTR_HELP_BG);
help_line(r++, " File size: up to 256 KB (EMM). Lines: up to 18432.", ATTR_HELP_BG);
(void)getkey();
render_full_status();
draw_viewport_from_cache();
render_menu();
}
+209
View File
@@ -0,0 +1,209 @@
/*
* mdview2_hex.c HEX-дамп ОРИГИНАЛЬНОГО файла (F4).
*
* Показывает исходные байты файла (orig_file_phys), а не активный буфер:
* hex-дамп декодированного UTF-набора был бы искажением. Printable-колонка
* интерпретируется ТЕКУЩЕЙ кодировкой: CP866 как есть, CP1251/KOI8 через
* g_remap, UTF-8 глиф на позиции лид-байта (continuation-байты '.').
*
* Формат ряда (79 колонок, один атрибут ATTR_TEXT как RAW):
* 0x012340 XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX 16 символов
*
* Ряд выровнен на 16 байт и никогда не пересекает границу EMM-страницы
* (16 делит 16384 нацело) один bank_read на ряд, fb()/W3-маппинг не нужны.
* Позиция (hex_top) живёт в байтах ОРИГИНАЛА; конвертацию в/из байт-позиции
* активного буфера делает ядро (view_pos/view_reanchor через map_off).
*/
#include <stdint.h>
#include <string.h>
#include <conio.h>
#include <bios/text.h>
#include <sprinter_mem.h> /* bank_read (чтение оригинала по страницам EMM) */
#include "mdview2.h"
#if WITH_HEX /* весь модуль опционален (mdview2_conf.h); при 0 пустой объектник */
#define HEX_BPR 16u /* байт на ряд */
#define HEX_SPAN ((uint32_t)(VIEW_H - 1) * HEX_BPR)
/* Геометрия ряда (колонки). */
#define HX_OFF 1u /* "0x012340" */
#define HX_SEP1 10u /* │ */
#define HX_DUMP 12u /* пары hex-цифр */
#define HX_SEP2 61u /* │ */
#define HX_PRINT 63u /* printable */
static uint32_t hex_top; /* offset верхнего ряда (кратен 16) */
uint32_t hex_pos(void) { return hex_top; }
/* Максимальный hex_top: последний ряд файла на нижней строке экрана. */
static uint32_t hex_max_top(void)
{
uint32_t last = (orig_file_size == 0) ? 0
: ((orig_file_size - 1u) & ~(uint32_t)(HEX_BPR - 1u));
return (last > HEX_SPAN) ? (last - HEX_SPAN) : 0;
}
void hex_reanchor(uint32_t orig_off)
{
uint32_t mt = hex_max_top();
orig_off &= ~(uint32_t)(HEX_BPR - 1u);
hex_top = (orig_off > mt) ? mt : orig_off;
}
void hex_screen_init(void)
{
bios_clearwin(VIEW_TOP_ROW, 0, VIEW_H, SCREEN_W, ATTR_TEXT);
}
/* ---- отрисовка ----------------------------------------------------- */
static uint8_t hexdig(uint8_t v)
{
v &= 15u;
return (uint8_t)((v < 10u) ? ('0' + v) : ('A' - 10 + v));
}
/* Один байт оригинала (для UTF-lookahead за границей ряда — редкий путь). */
static uint8_t fbo(uint32_t off)
{
uint8_t b;
bank_read(orig_file_phys[off >> PAGE_BITS], (uint16_t)(off & PAGE_MASK), &b, 1);
return b;
}
/* Printable-глиф для n байт ряда b[] (off — offset b[0] в файле) → out[]. */
static void hex_print_cells(const uint8_t *b, uint32_t off, uint8_t n, uint8_t *out)
{
for (uint8_t i = 0; i < n; i++) {
uint8_t c = b[i];
uint8_t g;
if (c < 0x20u) {
g = '.';
} else if (c < 0x80u || g_encoding != ENC_UTF8) {
g = (uint8_t)((c >= 0x80u && g_remap) ? g_remap[c - 0x80u] : c);
} else if ((c & 0xC0u) == 0x80u) {
g = '.'; /* continuation-байт */
} else {
/* Лид-байт: собрать кодпойнт (хвост может выйти за ряд → fbo).
* 4-байтовые (за пределами CP866) и битые последовательности '.'. */
uint32_t cp;
uint8_t need, ok = 1;
if ((c & 0xE0u) == 0xC0u) { cp = (uint32_t)(c & 0x1Fu); need = 1; }
else if ((c & 0xF0u) == 0xE0u) { cp = (uint32_t)(c & 0x0Fu); need = 2; }
else { out[i] = '.'; continue; }
for (uint8_t k = 1; k <= need; k++) {
uint32_t p = off + i + k;
if (p >= orig_file_size) { ok = 0; break; }
uint8_t cb = (uint8_t)((i + k < n) ? b[i + k] : fbo(p));
if ((cb & 0xC0u) != 0x80u) { ok = 0; break; }
cp = (cp << 6) | (uint32_t)(cb & 0x3Fu);
}
g = ok ? utf_cp_glyph(cp) : '.';
if (g < 0x20u) g = '.'; /* overlong мог дать управляющий */
}
out[i] = g;
}
}
static uint8_t hex_line[SCREEN_W]; /* собранный ряд (только символы, единый attr) */
static void hex_draw_row(uint32_t off, uint8_t row)
{
uint8_t buf[HEX_BPR];
uint8_t n = 0;
memset(hex_line, ' ', SCREEN_W);
if (off < orig_file_size) {
uint32_t rem = orig_file_size - off;
n = (rem >= HEX_BPR) ? (uint8_t)HEX_BPR : (uint8_t)rem;
bank_read(orig_file_phys[off >> PAGE_BITS], (uint16_t)(off & PAGE_MASK), buf, n);
}
if (n) { /* ряды за EOF — пустые */
hex_line[HX_OFF] = '0';
hex_line[HX_OFF + 1] = 'x';
hex_line[HX_OFF + 2] = hexdig((uint8_t)(off >> 20));
hex_line[HX_OFF + 3] = hexdig((uint8_t)(off >> 16));
hex_line[HX_OFF + 4] = hexdig((uint8_t)(off >> 12));
hex_line[HX_OFF + 5] = hexdig((uint8_t)(off >> 8));
hex_line[HX_OFF + 6] = hexdig((uint8_t)(off >> 4));
hex_line[HX_OFF + 7] = hexdig((uint8_t)off);
hex_line[HX_SEP1] = TBL_V;
for (uint8_t i = 0; i < n; i++) {
uint8_t x = (uint8_t)(HX_DUMP + i * 3u + ((i >= 8u) ? 1u : 0u));
hex_line[x] = hexdig((uint8_t)(buf[i] >> 4));
hex_line[x + 1] = hexdig(buf[i]);
}
hex_line[HX_SEP2] = TBL_V;
hex_print_cells(buf, off, n, hex_line + HX_PRINT);
}
bios_set_place(row, 0);
bios_write_until((const char *)hex_line, SCREEN_W, 0);
}
void hex_draw(void)
{
uint32_t off = hex_top;
for (uint8_t r = 0; r < VIEW_H; r++) {
hex_draw_row(off, (uint8_t)(VIEW_TOP_ROW + r));
off += HEX_BPR;
}
}
/* ---- статус-бар: % по байтам (как calc_raw_pct) --------------------- */
static uint8_t calc_hex_pct(void)
{
return pct16((uint16_t)(hex_top >> 8), (uint16_t)(orig_file_size >> 8));
}
/* ---- навигация (вызывается из главного цикла при g_view == VIEW_HEX) */
uint8_t hex_key(uint8_t scan)
{
uint32_t mt = hex_max_top();
switch (scan) {
/* Одна строка — аппаратный скролл окна + подрисовка одного ряда
* (как в MD/RAW), без перерисовки всего экрана. */
case KEY_DOWN:
if (hex_top < mt) {
hex_top += HEX_BPR;
scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 1, 0); /* содержимое вверх */
hex_draw_row(hex_top + HEX_SPAN, (uint8_t)(VIEW_TOP_ROW + VIEW_H - 1));
}
break;
case KEY_UP:
if (hex_top) {
hex_top -= HEX_BPR;
scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 2, 0); /* содержимое вниз */
hex_draw_row(hex_top, VIEW_TOP_ROW);
}
break;
case KEY_PGDN: {
uint32_t nt = hex_top + (uint32_t)VIEW_H * HEX_BPR;
if (nt > mt) nt = mt;
if (nt != hex_top) { hex_top = nt; hex_draw(); }
break;
}
case KEY_PGUP: {
uint32_t d = (uint32_t)VIEW_H * HEX_BPR;
uint32_t nt = (hex_top >= d) ? hex_top - d : 0;
if (nt != hex_top) { hex_top = nt; hex_draw(); }
break;
}
case KEY_HOME:
if (hex_top) { hex_top = 0; hex_draw(); }
break;
case KEY_END:
if (hex_top != mt) { hex_top = mt; hex_draw(); }
break;
default:
return 0; /* не наша клавиша */
}
render_percent_progress(calc_hex_pct());
return 1;
}
#endif /* WITH_HEX */
File diff suppressed because it is too large Load Diff
+227
View File
@@ -0,0 +1,227 @@
/*
* mdview2_md.c представление MD-документа (peer к mdview2_raw.c): отрисовка
* области из рендер-кэша + прокрутка (верт./гориз.) + навигация md_key().
* Статус-бар, меню и спиннер в mdview2_status.c (общие для MD/RAW).
*
* Работает поверх готового рендер-кэша (cache_dir_get cache_rec_t) и
* состояния навигации (top_line/viewport_x/n_lines, см. mdview2.h). Никакого
* парсинга/обращения к исходному файлу: всё рисуется из (char,attr)-ячеек кэша
* через win_rest_remap (ремап 8-битных кодировок в mdview2_enc.c).
*
* Пара видов: mdview2_md.c (MD, из кэша) mdview2_raw.c (RAW, из fb()).
*/
#include <stdint.h>
#include <conio.h> /* wrchar/scroll/COLOR */
#include <bios/text.h> /* bios_set_place/bios_fillcharattr */
#include "mdview2.h"
/* Фаза 4-5 — cache-only draw path: рисует одну строку ИЗ КЭША, без
* единого обращения к fb()/исходному файлу. Горизонтальный скролл для
* nowrap-строк (Фаза 5) это просто смещение начала среза на vx*2
* байт ВНУТРИ ТОГО ЖЕ кэш-буфера (cache_reserve гарантирует, что вся
* строка лежит в одной странице, так что off+vx*2 не пересекает
* границу страницы); не nowrap-строки игнорируют viewport_x, как и
* раньше в живом render_line() (там это называлось effective_vx). */
static void draw_line_from_cache(uint16_t line_idx, uint8_t row)
{
cache_rec_t rec;
cache_dir_get(line_idx, &rec);
/* Строка-обрыв (исчерпан контент-кэш / лимит строк): фикс. текст вживую,
* без обращения к контент-кэшу (см. index_lines финализацию). */
if (rec.flags & IF_TRUNC_MSG) {
const char *msg;
switch (rec.reserved) {
case TRUNC_LINES: msg = "*** Line limit reached - document truncated ***"; break;
case TRUNC_FILE: msg = "*** File too large - truncated at 256 KB ***"; break;
default: msg = "*** Content cache exhausted - document truncated ***"; break;
}
bios_set_place(row, 0);
bios_fillcharattr(' ', ATTR_WARN, SCREEN_W);
put_str_attr(2, row, msg, ATTR_WARN);
return;
}
bios_set_place(row, 0);
bios_fillcharattr(' ', ATTR_TEXT, SCREEN_W);
/* Горизонтальный сдвиг применяется ко ВСЕМ строкам hscroll-блока (код,
* таблицы) по ТИПУ, а не по длине: блок двигается целиком, включая
* строки короче 80 (короткая строка, ушедшая за левый край, просто
* показывает маркер '<' на пустом месте). HR и границы fence не имеют
* IF_HSCROLL и не сдвигаются. */
uint8_t vx = (rec.flags & IF_HSCROLL) ? viewport_x : 0;
if (rec.len > vx) {
uint16_t remaining = (uint16_t)(rec.len - vx);
uint8_t w = (remaining > SCREEN_W) ? SCREEN_W : (uint8_t)remaining;
uint16_t off = (uint16_t)(rec.off + (uint16_t)vx * 2u);
win_rest_remap(row, w, rec.page, off);
if (remaining > SCREEN_W) {
wrchar(SCREEN_W - 1, row, '>', ATTR_TRUNC);
}
}
if (vx > 0) {
wrchar(0, row, '<', ATTR_TRUNC);
}
}
/* Перерисовывает все VIEW_H строк окна из кэша — основной путь
* перерисовки видимой области после загрузки файла (Фаза 3-4). */
void draw_viewport_from_cache(void)
{
for (uint8_t i = 0; i < VIEW_H; i++) {
draw_line_from_cache((uint16_t)(top_line + i), (uint8_t)(VIEW_TOP_ROW + i));
}
}
/* ==================================================================
* Прокрутка
* ================================================================== */
/* Ограничивает top_line допустимым диапазоном с учётом высоты окна. */
void clamp_top(void)
{
if (n_lines <= VIEW_H) {
top_line = 0;
} else if (top_line > n_lines - VIEW_H) {
top_line = (uint16_t)(n_lines - VIEW_H);
}
}
/* Прокрутка вверх на n строк с частичной перерисовкой при n == 1. */
void md_scroll_up(uint16_t n)
{
uint16_t new_top_line = (top_line >= n) ? (uint16_t)(top_line - n) : 0;
if( new_top_line != top_line) {
top_line = new_top_line;
if (n == 1) {
scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 2, 0);
draw_line_from_cache((uint16_t)(top_line), (uint8_t)(VIEW_TOP_ROW));
} else {
draw_viewport_from_cache();
}
}
}
/* Прокрутка вниз на n строк с частичной перерисовкой при n == 1. */
void md_scroll_down(uint16_t n)
{
uint16_t new_top_line = (top_line + n < n_lines - VIEW_H) ?
(uint16_t)(top_line + n) : n_lines - VIEW_H;
if( new_top_line != top_line) {
top_line = new_top_line;
if (n == 1) {
scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 1, 0);
draw_line_from_cache((uint16_t)(top_line + VIEW_H - 1), (uint8_t)(VIEW_TOP_ROW + VIEW_H - 1));
} else {
clamp_top();
draw_viewport_from_cache();
}
}
}
/* Горизонтальный сдвиг (только если в окне есть nowrap-строки).
* Максимум сдвига ограничен самой широкой nowrap-строкой на экране.
* Фаза 5: ширина берётся прямо из директории кэша (rec.len уже
* посчитанная реальная длина в ячейках после рендера), без единого
* обращения к fb()/seg_off()/исходному файлу. */
void md_scroll_horizon(int8_t delta)
{
uint16_t maxw = 0;
for (uint8_t i = 0; i < VIEW_H; i++) {
uint16_t li = (uint16_t)(top_line + i);
if (li >= n_lines) break;
cache_rec_t rec;
cache_dir_get(li, &rec);
if (!(rec.flags & IF_HSCROLL)) continue; /* только код/таблицы */
if (rec.len > maxw) maxw = rec.len; /* самая широкая строка блока */
}
if (maxw == 0) return; /* нет строк шире экрана */
/* Максимальный сдвиг = ширина за пределами экрана, в границах uint8. */
uint16_t over = (maxw > SCREEN_W) ? (uint16_t)(maxw - SCREEN_W) : 0;
if (over > 248u) over = 248u;
uint8_t max_vx = (uint8_t)over;
int16_t nx = (int16_t)viewport_x + delta;
if (nx < 0) nx = 0;
if (nx > (int16_t)max_vx) nx = max_vx;
/* ПОРЯДОК ВАЖЕН: записываем viewport_x ДО сравнения, а сравниваем уже
* сохранённую копию old_vx с new_vx. Иначе SDCC генерирует для
* `if (new_vx != viewport_x) viewport_x = new_vx;` такой код:
* ld a, new_vx ; sub a,(viewport_x) ; jr Z,.. ; ld (viewport_x),a
* т.е. для записи переиспользует регистр A, испорченный вычитанием в
* сравнении, и кладёт в viewport_x не new_vx, а (new_vx - old_vx).
* (asm-дамп 2026-06-24 подтвердил; даёт 10-8=2, 10-2=8 ровно
* наблюдавшийся "прыжок" скролла.) Записав viewport_x первой, мы убираем
* портящий sub с пути записи; сравнение ниже только решает, перерисовывать
* ли экран. */
uint8_t new_vx = (uint8_t)nx;
uint8_t old_vx = viewport_x;
viewport_x = new_vx;
if (new_vx != old_vx)
draw_viewport_from_cache();
}
/* Процент прокрутки (0..100) относительно доступного диапазона. */
static uint8_t calc_md_pct(void)
{
uint16_t total = drawable_lines();
if (total <= VIEW_H) return 100;
return pct16(top_line, (uint16_t)(total - VIEW_H));
}
/* ---- навигация MD после загрузки (peer к raw_key) ---------------- */
/* Обрабатывает навигационную клавишу в MD-режиме (вызывается из главного
* цикла). Сама перерисовывает область/статус. Возврат: 1 обработано,
* 0 не наша клавиша (F1/F8/F10 разбирает main). */
uint8_t md_key(uint8_t scan)
{
switch (scan) {
case KEY_UP:
md_scroll_up(1);
break;
case KEY_DOWN:
md_scroll_down(1);
break;
case KEY_LEFT:
md_scroll_horizon(-(int8_t)HPAN_STEP);
break;
case KEY_RIGHT:
md_scroll_horizon(+(int8_t)HPAN_STEP);
break;
case KEY_PGUP:
md_scroll_up(VIEW_H);
break;
case KEY_PGDN:
md_scroll_down(VIEW_H);
break;
case KEY_HOME:
if(top_line != 0 || viewport_x != 0 ) {
top_line = 0;
viewport_x = 0;
draw_viewport_from_cache();
}
break;
case KEY_END:
uint16_t new_top_line = (n_lines > VIEW_H) ? (uint16_t)(n_lines - VIEW_H) : 0;
if(top_line != new_top_line || viewport_x != 0 ) {
top_line = new_top_line;
viewport_x = 0;
draw_viewport_from_cache();
}
break;
default:
return 0; /* не наша клавиша */
}
render_percent_progress(calc_md_pct());
render_md_status_numbers();
return 1;
}
+308
View File
@@ -0,0 +1,308 @@
/*
* mdview2_raw.c RAW-просмотр исходного текста (без markdown-форматирования).
*
* Работает по АКТИВНОМУ буферу документа через fb() (8-бит = исходные байты,
* UTF-8 = декодированный в CP866 буфер), ремап 0x80 (CP1251/KOI8) на
* отрисовке. Индекс/кэш markdown не используются: позиция байт-offset,
* 1 байт = 1 ячейка, \t показываем пробелом. Два режима:
* VIEW_RAW_WRAP длинные строки переносятся кратно 80 (рвём слова);
* VIEW_RAW_HSCROLL строка в один ряд, горизонтальный скролл.
*
* Память: 0 доп. EMM; near-буфер строки + переиспользование g_scratch_phys.
*/
#include <stdint.h>
#include <stdio.h>
#include <conio.h>
#include <bios/text.h>
#include "mdview2.h"
#if WITH_RAW /* весь модуль — опционален (mdview_conf.h); при 0 пустой объектник */
static uint32_t raw_top; /* байт-offset верхней строки экрана */
static uint16_t raw_hpan; /* гориз. сдвиг (только HSCROLL) */
/* ---- примитивы навигации по физическим строкам / переносам -------- */
static uint32_t raw_line_start(uint32_t off)
{
while (off && fb(off - 1) != '\n') off--;
return off;
}
/* WRAP: начало следующего ряда от off (\n завершает строку; иначе +80). */
static uint32_t raw_next_wrap(uint32_t off)
{
uint32_t e = off, lim = off + SCREEN_W;
while (e < file_size) {
if (fb(e) == '\n') return e + 1;
e++;
if (e >= lim) { /* прошли 80 без \n */
if (e < file_size && fb(e) == '\n') return e + 1; /* ровно 80 + \n */
return e; /* длинная строка → продолжение */
}
}
return e; /* EOF */
}
static uint32_t raw_prev_wrap(uint32_t off)
{
if (off == 0) return 0;
uint32_t pls = raw_line_start(off - 1); /* off-1 — это \n пред. строки */
while (pls + SCREEN_W < (off - 1)) pls += SCREEN_W;
return pls;
}
/* HSCROLL: начало следующей/предыдущей физической строки. */
static uint32_t raw_next_line(uint32_t off)
{
while (off < file_size && fb(off) != '\n') off++;
return (off < file_size) ? off + 1 : off;
}
static uint32_t raw_prev_line(uint32_t off)
{
if (off == 0) return 0;
return raw_line_start(off - 1);
}
/* ---- отрисовка ряда (общий near-буфер → g_scratch_phys → win_rest) - */
static uint8_t raw_buf[SCREEN_W + 1]; /* только символы (атрибут единый, задаётся при выводе) */
/* В RAW атрибут единый (ATTR_TEXT): буфер — только 80 символов, без пар
* (char,attr). Кладём один байт-глиф (\tпробел, ремап 0x80). */
static void raw_cell(uint8_t i, uint8_t ch)
{
if (ch >= 0x80 && g_remap) ch = g_remap[ch - 0x80];
raw_buf[i] = ch;
}
/* Вывод готового 80-символьного буфера в строку экрана одним BIOS-вызовом
* (char-буфер + единый attr без WINREST/scratch-страницы). */
static void raw_flush(uint8_t row)
{
bios_set_place(row, 0);
// bios_writeattr((const char *)raw_buf, SCREEN_W, ATTR_TEXT);
// bios_writeattr_until((const char *)raw_buf, SCREEN_W, ATTR_TEXT, 0);
bios_write_until((const char *)raw_buf, SCREEN_W, 0);
}
static uint32_t raw_draw_wrap_row(uint32_t off, uint8_t row)
{
uint8_t i = 0;
uint32_t e = off, lim = off + SCREEN_W;
while (i < SCREEN_W && e < file_size) {
char c = fb(e);
if (c == '\n') {
raw_cell(i, 0);
break;
}
raw_cell(i, (uint8_t)(c == '\t' ? ' ' : c));
i++; e++;
}
raw_flush(row);
return raw_next_wrap(off);
}
static uint32_t raw_draw_hscroll_row(uint32_t off, uint8_t row)
{
uint32_t e = off;
while (e < file_size && fb(e) != '\n') e++; /* e = конец строки */
uint16_t len = e - off;
uint8_t i = 0;
uint32_t p = off + raw_hpan;
while (i < SCREEN_W && p < e) {
char c = fb(p);
raw_cell(i, (uint8_t)(c == '\t' ? ' ' : c));
i++; p++;
}
// raw_buf[i] = 0;
if (raw_hpan > 0)
raw_buf[0] = '<';
if (len > (uint16_t)raw_hpan + SCREEN_W)
raw_buf[SCREEN_W - 1] = '>';
if (i < SCREEN_W)
raw_buf[i] = 0;
raw_flush(row);
return (e < file_size) ? e + 1 : e;
}
/* Рисует один ряд от off (по режиму) и возвращает offset следующего ряда. */
static uint32_t raw_draw_one(uint32_t off, uint8_t row)
{
if (off >= file_size) { fill_row(row, ATTR_TEXT); return off; }
return (g_view == VIEW_RAW_WRAP) ? raw_draw_wrap_row(off, row)
: raw_draw_hscroll_row(off, row);
}
void raw_draw(void)
{
uint32_t off = raw_top;
for (uint8_t r = 0; r < VIEW_H; r++)
off = raw_draw_one(off, (uint8_t)(VIEW_TOP_ROW + r));
}
/* Прокрутка на одну строку: аппаратный scroll окна документа + отрисовка
* ОДНОЙ новой строки (как в MD-режиме). Смещение нижнего ряда вычисляется
* проходом VIEW_H рядов от raw_top без отдельного инкрементального счётчика,
* который рассинхронизировался, когда контент кончается в середине экрана
* (короче окна после wrapunwrap у конца файла). */
static uint32_t raw_next(uint32_t off)
{
return (g_view == VIEW_RAW_WRAP) ? raw_next_wrap(off) : raw_next_line(off);
}
static uint32_t raw_prev(uint32_t off)
{
return (g_view == VIEW_RAW_WRAP) ? raw_prev_wrap(off) : raw_prev_line(off);
}
/* ---- статус-бар: % по байтам (отрисовка — render_raw_status_numbers
* в mdview2_status.c, читает позицию через этот raw_pct) ---- */
/* Процент по байтам без 32-битного деления: масштабируем offset'ы (>>8 →
* 1024), дальше 16-битный pct16 (без __divulong). raw_top приватен модулю,
* поэтому % считаем здесь и отдаём готовым числом в статус-модуль. */
uint8_t calc_raw_pct(void)
{
uint16_t a = (uint16_t)(raw_top >> 8);
uint16_t b = (uint16_t)(file_size >> 8);
return pct16(a, b); /* den==0 (файл < 1 КБ) → pct16 вернёт 0 */
}
/* ---- посев позиции / пересев при смене активного буфера ----------- */
uint32_t raw_pos(void)
{
return raw_top;
}
/* Поставить RAW на начало строки, содержащей байт off (позиционирование
* при F2 MDRAW и F8-переносе позиции между наборами). */
void raw_reanchor(uint32_t off)
{
if (off > file_size) off = file_size;
raw_top = raw_line_start(off);
raw_hpan = 0;
}
void raw_seed_from(uint16_t md_top_line)
{
raw_reanchor(seg_off(md_top_line));
}
/* Сброс в начало (клавиша Home). */
static void raw_home(void)
{
raw_top = 0;
raw_hpan = 0;
}
void raw_end(void)
{
raw_top = file_size; /* отмотать VIEW_H рядов назад */
for (uint8_t k = 0; k < VIEW_H; k++)
raw_top = raw_prev(raw_top);
raw_hpan = 0;
}
void raw_screen_init(void) {
bios_clearwin(1, 0, 30, 80, ATTR_TEXT);
}
/* Нормализация позиции при смене под-режима RAW (F3 Wrap/Unwrap): выровнять
* raw_top на начало физической строки и снять горизонтальный сдвиг. */
void raw_renorm(void)
{
raw_top = raw_line_start(raw_top);
raw_hpan = 0;
}
void raw_scroll_down(uint8_t delta) {
uint32_t off = raw_top;
for (uint8_t r = 0; r < VIEW_H; r++) {
if (off >= file_size)
return; /* экран не заполнен контентом */
off = raw_next(off);
}
if (off >= file_size)
return; /* контент ровно по экран, ниже пусто */
if(delta ==1) {
scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 1, 0); /* содержимое вверх */
off = raw_draw_one(off, (uint8_t)(VIEW_TOP_ROW + VIEW_H - 1)); /* новый нижний ряд */
raw_top = raw_next(raw_top);
} else {
for (uint8_t k = 0; k < delta; k++) {
uint32_t n2 = raw_next(raw_top);
if (n2 >= file_size)
break;
raw_top = n2;
}
raw_draw(); /* PgUp/PgDn/Home/End — полный экран */
}
}
void raw_scroll_up(uint8_t delta) {
if (raw_top == 0)
return;
if (delta == 1) {
raw_top = raw_prev(raw_top);
scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 2, 0); /* содержимое вниз */
(void)raw_draw_one(raw_top, VIEW_TOP_ROW);
} else {
for (uint8_t k = 0; k < delta; k++)
raw_top = raw_prev(raw_top);
raw_draw(); /* PgUp/PgDn/Home/End — полный экран */
}
}
/* ---- навигация (вызывается из главного цикла при g_view != VIEW_MD) - */
uint8_t raw_key(uint8_t scan)
{
switch (scan) {
case KEY_DOWN:
raw_scroll_down(1);
break;
case KEY_UP:
raw_scroll_up(1);
break;
case KEY_PGDN:
raw_scroll_down(VIEW_H);
break;
case KEY_PGUP:
raw_scroll_up(VIEW_H);
break;
case KEY_HOME:
raw_home();
raw_draw();
break;
case KEY_END:
raw_end();
raw_draw();
break;
case KEY_LEFT:
if (g_view == VIEW_RAW_HSCROLL && raw_hpan) {
raw_hpan = (raw_hpan >= HPAN_STEP) ? (uint16_t)(raw_hpan - HPAN_STEP) : 0;
raw_draw();
}
break;
case KEY_RIGHT:
if (g_view == VIEW_RAW_HSCROLL && raw_hpan < 248u) {
raw_hpan = (uint16_t)(raw_hpan + HPAN_STEP);
raw_draw();
}
break;
default:
return 0; /* не наша клавиша */
}
// render_raw_status_numbers();
render_percent_progress(calc_raw_pct());
return 1;
}
#endif /* WITH_RAW */
+192
View File
@@ -0,0 +1,192 @@
/*
* mdview2_status.c статус-бар (строка 0), нижнее меню (строка 31) и спиннер.
*
* Хром бара АТОМИЗИРОВАН обновляется по частям, без перерисовки всего бара:
* prerender_status() фон + разделители + "MDVIEW" + имя файла (1 раз);
* status_encoding() только поле кодировки (col 37), по F8;
* render_md_status_numbers() числа MD: диапазон строк + % (при скролле);
* render_raw_status_numbers() числа RAW: метка режима + % по байтам;
* render_full_status() encoding + numbers (MD), при смене набора/режима;
* render_menu() строка меню, по смене режима/готовности;
* spinner_tick/show индикатор занятости во время загрузки.
* Оба *_status_numbers() самоочищаются по геометрии разделителей (DIV1_X/DIV2_X
* в mdview2.h) фикс. хром не перерисовывается. RAW-позицию даёт raw_pct()
* из mdview2_raw.c (raw_top там приватен).
*
* Раскладка строки 0:
* 0 1..6 8 10.. 37..43 45 46..70 71 72..79
* . MDVIEW spinner filename encod. L a-b / total pct%
*/
#include <stdint.h>
#include <stdio.h> /* dec8/dec16, cputs */
#include <conio.h> /* textattr/gotoxy/wrchar/COLOR */
#include <limits.h>
#include <bios/text.h> /* bios_fillcharattr/bios_write_until */
#include "mdview2.h"
/* ---- Спиннер (слот SPINNER_COL в баре) --------------------------- */
static const char spinner_chars[4] = { '|', '/', '-', '\\' };
static uint8_t spinner_phase = 0;
static uint8_t spinner_active = 0;
/* Продвигает спиннер на один кадр (если включён). */
void spinner_tick(void)
{
if (!spinner_active)
return;
wrchar(SPINNER_COL, 0, spinner_chars[spinner_phase & 3], ATTR_BAR_SPINNER);
spinner_phase++;
}
/* Вкл/выкл спиннер; при выключении гасит его позицию. */
void spinner_show(uint8_t on)
{
spinner_active = on;
if (!on) wrchar(SPINNER_COL, 0, ' ', ATTR_BAR);
}
/* ---- Вычисления для чисел статус-бара ----------------------------- */
/* Число строк, готовых к показу. Во время загрузки последняя эмитированная
* строка ещё в g_cells (не зафлашена), поэтому доступны [0..n_lines-2]. */
uint16_t drawable_lines(void)
{
if (g_loading)
return (n_lines >= 1) ? (uint16_t)(n_lines - 1) : 0;
return n_lines;
}
/* ---- Атомарные части статус-бара ---------------------------------- */
/* Фиксированный хром бара — рисуется ОДИН раз при старте: фон, два
* разделителя , метка "MDVIEW" и имя файла (они не меняются за сессию). */
void prerender_status(void)
{
fill_row(0, ATTR_BAR);
wrchar(DIV1_X, 0, 0xB3, ATTR_BAR); /* │ */
wrchar(DIV2_X, 0, 0xB3, ATTR_BAR); /* │ */
put_str_attr(1, 0, "MDVIEW", ATTR_BAR);
put_str_attr(10, 0, filename, ATTR_BAR);
}
/* Поле текущей кодировки (col 37, 7 ячеек). Меняется только по F8. */
void status_encoding(void)
{
textattr(ATTR_BAR);
gotoxy(DIV1_X - 10, 0);
bios_write_until(enc_name(g_encoding), 8, 0);
}
static uint16_t local_total = UINT_MAX;
static uint16_t local_last = UINT_MAX;
static uint8_t local_loading = UCHAR_MAX;
static uint8_t local_pct = UCHAR_MAX;
/* Числовая часть (MD): "L a-b / total" между разделителями + "pct%" справа.
* Перезаписывает ТОЛЬКО области между/после разделителей, сами и фикс.
* хром не трогает. */
void render_md_status_numbers(void)
{
uint16_t total = drawable_lines();
uint16_t last = top_line + VIEW_H;
if (last > total)
last = total;
if(local_total != total || local_last != last || local_loading != g_loading) {
local_total = total; local_last = last; local_loading = g_loading;
textattr(ATTR_BAR);
gotoxy(DIV1_X + 2, 0);
bios_fillcharattr(' ', ATTR_BAR, DIV2_X - DIV1_X - 2); /* очистить [DIV1_X+1 .. DIV2_X-1] */
gotoxy(DIV1_X + 2, 0);
cputs("L ");
dec16(top_line + 1);
cputs("-");
dec16(last);
cputs(" / ");
dec16(total);
if (g_loading) cputs("..."); /* ещё грузится */
}
}
/* Полное обновление переменной части (MD): кодировка + числа. */
void render_full_status(void)
{
local_loading = UCHAR_MAX;
status_encoding();
render_md_status_numbers();
}
void render_percent_progress(uint8_t pct) {
if (local_pct != pct) {
local_pct = pct;
gotoxy(DIV2_X + 2, 0);
bios_fillcharattr(' ', ATTR_BAR, SCREEN_W - DIV2_X - 2); /* очистить [DIV2_X+1 .. конец] */
gotoxy(DIV2_X + 2, 0);
dec8(pct);
cputs("%");
}
}
#if WITH_RAW
/* RAW-вариант числовой части бара (peer к render_md_status_numbers): метка
* режима между разделителями + процент справа. Сам очищает свои области (та же
* геометрия DIVn_X), атомарен не требует общего fill всего бара. Позиция
* берётся готовым числом из raw_pct() (raw_top приватен mdview2_raw.c). */
void render_raw_status(void)
{
// uint8_t pct = raw_pct();
textattr(ATTR_BAR);
gotoxy(DIV1_X + 1, 0);
bios_writeattr_until(g_view == VIEW_RAW_WRAP ? " RAW wrap" : " RAW pan",
DIV2_X - DIV1_X - 1, ATTR_BAR, 0); /* метка + пад до DIV2_X-1 */
}
#endif /* WITH_RAW */
#if WITH_HEX
/* HEX-вариант числовой части бара (peer к render_raw_status). */
void render_hex_status(void)
{
textattr(ATTR_BAR);
gotoxy(DIV1_X + 1, 0);
bios_writeattr_until(" HEX", DIV2_X - DIV1_X - 1, ATTR_BAR, 0);
}
#endif /* WITH_HEX */
/* ---- Нижнее меню (строка 31) -------------------------------------- */
void render_menu(void)
{
/* Блок i = колонка i*8 (8 шириной): 2 поз. НОМЕР клавиши без 'F' (стиль
* ATTR_MENU_K, ведущий «чёрный» пробел кроме F10) + 6 поз. ТЕКСТ-функция
* (ATTR_MENU_T) сразу за номером. Номера рисуем для ВСЕХ 10 клавиш; текст
* только у задействованных и доступных сейчас. */
char num[3];
fill_row(MENU_ROW, ATTR_MENU_T);
num[2] = 0;
for (uint8_t i = 0; i < 9; i++) { /* F1..F9: ' 1'..' 9' */
num[0] = ' '; num[1] = (char)('1' + i);
put_str_attr((uint8_t)(i * 8), MENU_ROW, num, ATTR_MENU_K);
}
put_str_attr(9 * 8, MENU_ROW, "10", ATTR_MENU_K); /* F10 */
put_str_attr(0 * 8 + 2, MENU_ROW, "Help", ATTR_MENU_T);
#if WITH_RAW
if (g_ready) /* RAW доступен после постройки документа */
put_str_attr(1 * 8 + 2, MENU_ROW, (g_view == VIEW_MD) ? "RAW" : "MD", ATTR_MENU_T);
if (VIEW_IS_RAW(g_view)) /* Wrap/Unwrap — только в RAW */
put_str_attr(2 * 8 + 2, MENU_ROW,
(g_view == VIEW_RAW_WRAP) ? "UnWrap" : "Wrap", ATTR_MENU_T);
#endif
#if WITH_HEX
if (g_ready) /* HEX доступен после постройки документа */
put_str_attr(3 * 8 + 2, MENU_ROW, (g_view == VIEW_HEX) ? "Back" : "Hex", ATTR_MENU_T);
#endif
if (g_f8_enabled) /* смена кодировки сейчас возможна */
put_str_attr(7 * 8 + 2, MENU_ROW, "CodePg", ATTR_MENU_T);
put_str_attr(9 * 8 + 2, MENU_ROW, "Exit", ATTR_MENU_T);
}
+91
View File
@@ -0,0 +1,91 @@
/*
* mdview2_table.c выровненная отрисовка markdown-таблиц.
*
* Разбор |-разделённых ячеек и эмиссия рамок/строк в общий буфер ячеек
* рендера (gc_put/gc_fill, см. mdview2.h). Вызывается из index_lines()
* ядра при обнаружении таблицы. Геометрия рамок (TBL_n), словарь стилей
* (CK_n, INIT_STYLE_n) и cell-buffer API объявлены в mdview2.h.
*/
#include <stdint.h>
#include <conio.h> /* COLOR() / COLOR_* для атрибутов TBL_ATTR/ATTR_TEXT */
#include "mdview2.h"
uint32_t row_end(uint32_t p)
{
while (p < file_size && fb(p) != '\n') p++;
return p;
}
/* Начало контента первой ячейки строки (после ведущих пробелов и '|'). */
uint32_t table_first_cell(uint32_t row_start)
{
uint32_t p = row_start;
while (p < file_size && fb(p) == ' ') p++;
if (p < file_size && fb(p) == '|') p++;
return p;
}
/* Следующая ячейка: [*cs,*ce) — обрезанный диапазон; *pp продвигается за '|'.
* 0 ячеек больше нет (включая хвостовую пустоту после последнего '|'). */
uint8_t table_next_cell(uint32_t *pp, uint32_t lineend, uint32_t *cs, uint32_t *ce)
{
uint32_t p = *pp;
if (p >= lineend) return 0;
uint32_t a = p;
while (p < lineend && fb(p) != '|') p++;
uint32_t b = p;
uint8_t had_pipe = (uint8_t)(p < lineend && fb(p) == '|');
if (had_pipe) p++;
*pp = p;
while (a < b && fb(a) == ' ') a++;
while (b > a && fb(b - 1) == ' ') b--;
if (!had_pipe && a == b) return 0;
*cs = a; *ce = b;
return 1;
}
/* Строка-разделитель (|---|:-:|): каждая ячейка непустая и только '-'/':'. */
uint8_t table_is_sep_row(uint32_t row_start, uint32_t lineend)
{
uint32_t p = table_first_cell(row_start);
uint32_t cs, ce;
uint8_t n = 0;
while (table_next_cell(&p, lineend, &cs, &ce)) {
if (cs >= ce) return 0;
for (uint32_t q = cs; q < ce; q++) { char c = fb(q); if (c != '-' && c != ':') return 0; }
n++;
}
return (uint8_t)(n > 0);
}
/* Горизонтальная рамка в g_cells: left + (H×(w+2) + mid|right) по колонкам. */
void table_border(const uint8_t *widths, uint8_t ncols, char left, char mid, char right)
{
gc_put(left, TBL_ATTR);
for (uint8_t c = 0; c < ncols; c++) {
gc_fill(TBL_H, TBL_ATTR, (uint8_t)(widths[c] + 2));
gc_put((c == (uint8_t)(ncols - 1)) ? right : mid, TBL_ATTR);
}
}
/* Строка данных в g_cells: │ <ячейка, добитая до widths[c]> │ … */
void table_data_row(uint32_t row_start, uint32_t lineend, const uint8_t *widths, uint8_t ncols)
{
uint32_t p = table_first_cell(row_start);
uint32_t cs, ce;
gc_put(TBL_V, TBL_ATTR);
for (uint8_t c = 0; c < ncols; c++) {
uint8_t got = table_next_cell(&p, lineend, &cs, &ce);
gc_put(' ', TBL_ATTR);
uint8_t rendered = 0;
if (got && ce > cs) {
uint8_t before = g_ncells;
(void)inline_scan(cs, ce, 0, CK_OTHER, INIT_STYLE_PLAIN, ATTR_TEXT, 1);
rendered = (uint8_t)(g_ncells - before);
}
if (rendered < widths[c]) gc_fill(' ', ATTR_TEXT, (uint8_t)(widths[c] - rendered));
gc_put(' ', TBL_ATTR);
gc_put(TBL_V, TBL_ATTR);
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""Генератор тестовых файлов mdview2 для проверки лимита 256 КБ и путей обрыва.
Результат кладётся рядом со скриптом (examples/mdview2/testfiles/):
BIG.MD ~216 КБ обычной прозы (CP866): успешный рендер большого файла.
TABLES.MD таблично-тяжёлый (ASCII): пробивает кап контент-кэша (40 стр./набор),
т.к. неровные таблицы (короткие ячейки + один широкий столбец)
раздувают число (символ,атрибут)-ячеек кратно относительно источника.
LINES.MD ~22000 коротких строк (ASCII): пробивает лимит строк (18432).
Запуск: python3 gen_testfiles.py
"""
import os
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
def write(name, data):
path = os.path.join(HERE, name)
mode = 'wb' if isinstance(data, (bytes, bytearray)) else 'w'
with open(path, mode) as f:
f.write(data)
print(f"{name}: {os.path.getsize(path)} bytes")
# --- BIG.MD / HUGE.MD: склейка README + READMEBG, перекодировано в CP866 -----
# BIG (×2 ≈170 КБ) — успешный рендер большого файла (<256 КБ).
# HUGE (×4 ≈340 КБ) — БОЛЬШЕ 256 КБ: проверка клампа (читаются первые 256 КБ +
# строка-предупреждение TRUNC_FILE).
prose = bytearray()
for fn in ('README.MD', 'READMEBG.MD'):
with open(os.path.join(ROOT, fn), encoding='utf-8') as f:
prose += f.read().encode('cp866', errors='replace')
write('BIG.MD', bytes(prose * 2))
write('HUGE.MD', bytes(prose * 4))
# --- TABLES.MD: неровные таблицы -> исчерпание контент-кэша ------------------
# Один широкий столбец (72 симв.) на таблицу задаёт ширину; остальные ячейки
# короткие, но при рендере добиваются до этой ширины -> ~80 ячеек/строка из
# ~8 байт источника. ~300 таблиц × 30 строк дают ~840K ячеек (1.68 МБ контента)
# при ~108 КБ источника -> кап 640 КБ/набор пробивается, строк (~11K) < 18432,
# значит первой срабатывает причина TRUNC_CONTENT.
NT, ROWS = 300, 30
wide = 'x' * 72
out = ['# Tables test - content cache exhaustion trigger', '']
for t in range(NT):
out.append(f'## Table {t + 1}')
out.append('| Column A | B |')
out.append('|---|---|')
out.append(f'| {wide} | y |')
for r in range(ROWS):
out.append(f'| a{r} | b |')
out.append('')
write('TABLES.MD', '\n'.join(out) + '\n')
# --- LINES.MD: масса коротких строк -> исчерпание лимита строк ---------------
# ВАЖНО: вьювер склеивает подряд идущие непустые строки в один параграф
# (markdown soft-wrap), поэтому простые строки потекли бы в абзац. Нужен
# контент БЕЗ склейки -> кладём всё в fenced code block (verbatim, 1:1
# строка-источник = экранная строка). 22000 строк > 18432 -> TRUNC_LINES;
# контент мал (~9 ячеек/строка), кап контента не задевается.
N = 25000 # ~250 КБ -> 16 страниц -> max_lines = 18432; строк больше -> обрыв на 18432
out = ['# Short lines test - line limit trigger', '', '```']
out += [f'row {i:05d}' for i in range(1, N + 1)]
out.append('```')
write('LINES.MD', '\n'.join(out) + '\n')
-106
View File
@@ -1,106 +0,0 @@
# Build lib/libsprinter.lib — the Sprinter target libc archive.
#
# Includes all libc/*.c modules plus the runtime helpers that get
# auto-pulled by SDCC's codegen (heap for malloc, bank trampolines for
# __banked). The crt0 family is NOT in the lib — they are always
# explicitly linked, never DCE-eligible.
#
# Each .c file becomes its own .rel inside the archive. The linker
# pulls only those .rel files whose exported symbols are referenced,
# giving free dead-code elimination at module granularity.
PROJ_ROOT := $(abspath $(CURDIR)/..)
SDCC_BIN := $(PROJ_ROOT)/third_party/sdcc/bin
SDCC := $(SDCC_BIN)/sdcc
SDAR := $(SDCC_BIN)/sdar
SDASZ80 := $(SDCC_BIN)/sdasz80
INC := -I$(PROJ_ROOT)/libc/include
CC_FLAGS := -mz80 --no-std-crt0 --std-c99 --opt-code-size $(INC)
BUILD := $(PROJ_ROOT)/lib/build
# All libc C modules.
LIBC_C := \
libc/sys/atexit.c \
libc/conio/conio.c \
libc/conio/cprintf.c \
libc/conio/text_palette.c \
libc/bios/text.c \
libc/io/dir.c \
libc/video/videomode_raw.c \
libc/video/palette.c \
libc/errno/_errno_set.c \
libc/env/env.c \
libc/errno/errno.c \
libc/io/fsdir.c \
libc/io/lseek.c \
libc/mouse/mouse.c \
libc/io/open.c \
libc/io/read.c \
libc/time/sleep.c \
libc/time/time.c \
libc/time/posix_time.c \
libc/io/unlink.c \
libc/io/stat.c \
libc/mem/bank_io_w3.c \
libc/mem/bank_io_w1.c \
libc/mem/mem_estex.c \
libc/mem/mem_bios.c \
libc/gfx/gfx_core.c \
libc/gfx/gfx_palette.c \
libc/gfx/gfx_raw_common.c \
libc/gfx/gfx_raw_256.c \
libc/gfx/gfx_raw_16.c \
libc/gfx/gfx_256.c \
libc/gfx/gfx_16.c \
libc/gfx/gfx_font.c \
libc/gfx/gfx_text_256.c \
libc/gfx/gfx_text_16.c \
libc/stdio/getchar.c \
libc/stdio/putchar.c \
libc/stdio/puts.c \
libc/file/file.c \
libc/stdio/hex_print.c \
libc/stdio/dec_print.c \
libc/string/strlwr.c \
libc/string/strupr.c
# Runtime modules to bundle (pulled by symbol references from libc-using code).
# NOTE: runtime/bank.s is NOT bundled — its trampoline depends on the banking
# window (W1 for BIG, W3 for HUGE) which is decided per-build by sprinter-cc.
# That tool assembles bank.s on each banked build with the right BANK_W1 flag.
RUNTIME_S := runtime/heap.s
LIBC_RELS := $(patsubst libc/%.c,$(BUILD)/%.rel,$(LIBC_C))
RUNTIME_RELS := $(patsubst runtime/%.s,$(BUILD)/%.rel,$(RUNTIME_S))
ALL_RELS := $(LIBC_RELS) $(RUNTIME_RELS)
LIB := sprinter.lib
all: $(LIB)
# Pattern rule for C modules — preserves the libc/io|mem|stdio path
# so .rel members keep their natural names inside the archive.
$(BUILD)/%.rel: $(PROJ_ROOT)/libc/%.c
@mkdir -p $(dir $@)
$(SDCC) $(CC_FLAGS) -c -o $@ $<
# Runtime .s → .rel
$(BUILD)/%.rel: $(PROJ_ROOT)/runtime/%.s
@mkdir -p $(dir $@)
$(SDASZ80) -o $@ $<
# Archive — sdar with `rcs` = replace/create/symtab.
$(LIB): $(ALL_RELS)
rm -f $@
$(SDAR) -rcs $@ $(ALL_RELS)
@echo
@echo " Built $@ with $(words $(ALL_RELS)) modules:"
@$(SDAR) -t $@ | sed 's/^/ /'
clean:
rm -rf $(BUILD) $(LIB)
.PHONY: all clean
+79
View File
@@ -0,0 +1,79 @@
# Build lib/bgi256.lib — Sprinter BGI graphics driver (320×256×256).
#
# libbgi/ — самостоятельная графическая библиотека, отделённая от libc.
# Две driver-библиотеки собираются из одной исходной базы:
# bgi256.lib = common/*.rel + bgi256/*.rel (эта — Фаза 1)
# bgi16.lib = common/*.rel + bgi16/*.rel (Фаза 2; пока не собирается)
# common/ — mode-agnostic: .rel компилируется ОДИН раз и кладётся в ОБА
# архива (один исходник для функций, общих 256- и 16-цветной версиям).
# bgi256/ — mode-specific leaf'ы (реальные реализации, без обёрток).
#
# Каждый .c — отдельный .rel в архиве (1 публичная функция = 1 модуль);
# линкер тянет только нужные .rel → DCE на уровне файлов. См. план
# «Рефакторинг GFX → BGI» и memory/bgi_two_lib_design.
#
# make build lib/bgi256.lib
# make clean remove build/ + lib/bgi256.lib
#
# (Фаза 2 добавит bgi16.lib = common + bgi16; common .rel переиспользуются.)
PROJ_ROOT := $(abspath $(CURDIR)/..)
SDCC_BIN := $(PROJ_ROOT)/third_party/sdcc/bin
SDCC := $(SDCC_BIN)/sdcc
SDAR := $(SDCC_BIN)/sdar
# -I libbgi/include (gfx.h, graphics.h) + -I libc/include (palette.h,
# sprinter_exit.h — публичные заголовки libc; libbgi всегда линкуется с
# libc). SDCC stdint и пр. — из собственного include-пути SDCC.
INC := -I$(CURDIR)/include -I$(PROJ_ROOT)/libc/include
CC_FLAGS := -mz80 --no-std-crt0 --std-c99 --opt-code-size $(INC)
BUILD := $(CURDIR)/build
COMMON_C := $(wildcard $(CURDIR)/common/*.c)
BGI256_C := $(wildcard $(CURDIR)/bgi256/*.c)
# BGI16_C := $(wildcard $(CURDIR)/bgi16/*.c) # Фаза 2
COMMON_RELS := $(patsubst $(CURDIR)/common/%.c,$(BUILD)/common/%.rel,$(COMMON_C))
BGI256_RELS := $(patsubst $(CURDIR)/bgi256/%.c,$(BUILD)/bgi256/%.rel,$(BGI256_C))
KNOWN_RELS := $(COMMON_RELS) $(BGI256_RELS)
LIB_DIR := $(PROJ_ROOT)/lib
LIB256 := $(LIB_DIR)/bgi256.lib
# Штамп списка модулей — иначе удаление/переименование исходника не
# триггерит перелинковку и stale .rel остаётся в архиве. На exFAT
# грубая mtime — сравниваем по списку, не по времени.
MODLIST := $(BUILD)/.modules
ifneq ($(shell cat $(MODLIST) 2>/dev/null),$(KNOWN_RELS))
_ := $(shell mkdir -p $(BUILD); echo "$(KNOWN_RELS)" > $(MODLIST); rm -f $(LIB256))
endif
all: $(LIB256)
# Pattern rule для common/ и bgi256/ (один уровень под build/).
$(BUILD)/common/%.rel: $(CURDIR)/common/%.c
@mkdir -p $(dir $@)
$(SDCC) $(CC_FLAGS) -c -o $@ $<
$(BUILD)/bgi256/%.rel: $(CURDIR)/bgi256/%.c
@mkdir -p $(dir $@)
$(SDCC) $(CC_FLAGS) -c -o $@ $<
# Archive — sdar rcs = replace/create/symtab. Перед упаковкой удаляем
# stale .rel (wildcard не знает об удалённых исходниках).
$(LIB256): $(COMMON_RELS) $(BGI256_RELS) $(MODLIST)
@find $(BUILD) -name '*.rel' -print | sort > $(BUILD)/.have
@printf '%s\n' $(KNOWN_RELS) | sort > $(BUILD)/.want
@comm -23 $(BUILD)/.have $(BUILD)/.want | while read f; do \
echo " stale: $$f (исходник удалён) — rm"; \
rm -f "$$f" "$${f%.rel}.asm" "$${f%.rel}.lst" "$${f%.rel}.sym"; \
done
rm -f $@
$(SDAR) -rcs $@ $(COMMON_RELS) $(BGI256_RELS)
@echo " Built $@ with $(words $(COMMON_RELS)) common + $(words $(BGI256_RELS)) bgi256 modules."
clean:
rm -rf $(BUILD) $(LIB256)
.PHONY: all clean
+197
View File
@@ -0,0 +1,197 @@
/*
* _bgi.h внутренности графической подсистемы BGI (НЕ публичный заголовок).
*
* Слияние старых libc/bgi/_bgi.h и libc/gfx/_gfx.h. Графика вынесена из
* libc в самостоятельную библиотеку libbgi/ (см. docs + memory
* bgi_two_lib_design). Из libbgi собираются две driver-библиотеки
* (каждый .c отдельный .rel):
* bgi256.lib common/ + bgi256/ (320×256×256, mode 0x81)
* bgi16.lib common/ + bgi16/ (640×256×16, mode 0x82)
* common/ mode-agnostic: .rel компилируется ОДИН раз и кладётся в
* ОБА архива (один исходник для функций, общих 256- и 16-цветной
* версиям). bgi256/ и bgi16/ mode-specific leaf'ы (реальные
* реализации, БЕЗ обёрток-делегатов). Режим выбирается линковкой
* (--gfx 256 / --gfx 16), одновременно один.
*
* Публичные leaf-символы (_bgi_plot_raw, _bgi_hspan_raw, _bgi_read_raw,
* _bgi_vspan_raw, _bgi_clear_raw, _bgi_mode_set) одинаково объявлены в
* обоих driver-каталогах, поэтому common-математика не знает режима
* нужный .rel тянет линкер из выбранной библиотеки.
*/
#ifndef _BGI_INTERNAL_H
#define _BGI_INTERNAL_H
#include <stdint.h>
#include <graphics.h>
#include <gfx.h> /* GFX_MODE_*, размеры — для внутренних таблиц */
/* ---- Состояние BGI (_bgi_state.c) -------------------------------- */
extern uint8_t _bgi_fg; /* текущий цвет рисования */
extern uint8_t _bgi_bg; /* фоновый цвет */
extern int _bgi_cx; /* текущая позиция X (CP) */
extern int _bgi_cy; /* текущая позиция Y */
extern int _bgi_maxx; /* правая граница (319 / 639) */
extern int _bgi_maxy; /* нижняя граница (255) */
extern int _bgi_maxcolor; /* макс. индекс цвета (255 / 15) */
extern uint8_t _bgi_prevmode; /* видеорежим до initgraph */
extern int _bgi_result; /* код graphresult */
/* ---- Общее gfx-состояние (common/_gfx_state.c) ------------------ *
* Пишут gfx_set_visible_page / gfx_set_draw_page /
* gfx_set_bank; читают все mode-специфичные примитивы. */
extern uint8_t _gfx_visible_page;
extern uint8_t _gfx_draw_page;
/* Байт страницы W3 (0x50..0x5F) — какой видеобанк мапится при записи.
* См. memory/sprinter_vram_transparency.md (0x50 normal, 0x54 temp,
* 0x58 transparent, 0x5C оба). */
extern uint8_t _gfx_bank;
/* CPU-адрес колонки 0 текущей draw-страницы: 0xC000 (page 0) или
* 0xC140 (page 1); каждый примитив использует его вместо константы. */
extern uint16_t _gfx_addr_base;
/* ---- Низкоуровневый видеорежим (libc/video) ---------------------- */
uint8_t _videomode_raw_get(void);
int _videomode_raw_set(uint8_t mode);
/* ---- W3-скобки (common/_bgi_begin.c, _bgi_end.c) ----------------- *
* begin: DI + сохранить текущий W3 и замапить _gfx_bank;
* end: восстановить W3 + EI. Не реентерабельно (сейв в статике). */
extern uint8_t _gfx_saved_w3;
void _bgi_begin(void);
void _bgi_end(void);
/* ---- LEAF-операции драйвера (bgi256/ или bgi16/) ----------------- *
* Каждая self-contained: raw-варианты НЕ ставят свою W3-скобку
* (вызывающий обязан обернуть в _bgi_begin/_bgi_end); self-bracket
* (_bgi_read/_bgi_hspan/_bgi_clearall) ставят скобку сами. */
uint8_t _bgi_mode_set(void); /* войти в режим + палитра + границы */
void _bgi_plot_raw (int x, int y, uint8_t color); /* точка, без скобки */
uint8_t _bgi_read_raw (int x, int y); /* чтение, без скобки */
void _bgi_hspan_raw(int x, int y, int len, uint8_t color); /* H-отрезок, без скобки */
void _bgi_vspan_raw(int x, int y, int len, uint8_t color); /* V-отрезок, без скобки */
void _bgi_clear_raw(uint8_t color); /* заливка экрана, без скобки */
/* Self-bracket версии (begin + raw + end) — для разовых вызовов из
* математики, которой не нужен пакетный режим (floodfill, cleardevice).
* _bgi_read возвращает цвет пикселя как unsigned (в SDCC __sdcccall(1)
* 8-битный возврат идёт в A, расширяется до int). */
unsigned _bgi_read (int x, int y);
void _bgi_hspan (int x, int y, int len, uint8_t color);
void _bgi_clearall(uint8_t color);
/* ---- Скретч акселератора 256 (bgi256/_gfx_acc256.c) -------------- *
* Общий для hspan/vspan/clear; однопоточно, IRQ выключены между
* _bgi_begin/_bgi_end. */
extern uint8_t _gfx_acc_color;
extern uint8_t _gfx_acc_y;
extern uint8_t _gfx_acc_len; /* 0 = 256 — конвенция акселератора */
extern uint16_t _gfx_acc_addr;
/* Горизонтальный Fill-burst: _gfx_acc_len байт с _gfx_acc_addr на
* строке _gfx_acc_y (W3 замаплен, DI активен). */
void _gfx_hfill256_chunk(void);
/* ---- Скретч 16-цветных примитивов (bgi16/_gfx_g16_state.c) ------- */
extern uint8_t _gfx_g16_y;
extern uint8_t _gfx_g16_byte; /* nibble | (nibble<<4) */
extern uint8_t _gfx_g16_nibble; /* цвет в нужной половине байта */
extern uint8_t _gfx_g16_mask; /* маска, сохраняющая ДРУГУЮ половину */
extern uint8_t _gfx_g16_len; /* размер accel-блока (0 = 256) */
extern uint16_t _gfx_g16_addr;
/* Горизонтальный Fill-burst байтами _gfx_g16_byte (bgi16/_gfx_hfill16.c). */
void _gfx_hfill16_chunk(void);
/* RMW одного нибла по (_gfx_g16_addr, _gfx_g16_y) (bgi16/_gfx_rmw16.c). */
void _gfx_rmw16_pixel(void);
/* ---- Шрифт (common/_gfx_font_state.c, _gfx_font_ensure.c) -------- *
* Формат: 256 глифов × 8 рядов, INTERLEAVED offset = row*256 + char;
* биты MSB-first. См. memory/sprinter_font_format. */
#define _GFX_FONT_BYTES 2048
extern uint8_t _gfx_font_buf[_GFX_FONT_BYTES];
extern const uint8_t *_gfx_font_ptr;
extern uint8_t _gfx_font_loaded;
/* Ленивая инициализация: грузит системный шрифт при первом использовании. */
void _gfx_font_ensure(void);
/* 8 байт строк глифа c (грузит шрифт при первом использовании) —
* mode-agnostic, просто читает _gfx_font_ptr. common/_bgi_font_rows.c. */
void _bgi_font_rows(unsigned char c, uint8_t *rows8);
/* ---- Тригонометрия для дуг (common/_bgi_trig.c) ------------------ *
* Угол в градусах; результат sin/cos × 128 (Q7 чтобы радиус×значение
* влезало в int без 32-бит арифметики). */
int _bgi_sin128(int deg);
int _bgi_cos128(int deg);
/* Общий рисователь дуги/контура эллипса (common/_bgi_arc_draw.c):
* полилиния по точкам от stangle до endangle с полуосями xr,yr вокруг
* (cx,cy). */
void _bgi_ellipse_arc(int cx, int cy, int stangle, int endangle,
int xr, int yr, uint8_t color);
/* Целочисленный sqrt (0..~32767) для fillellipse (common/_bgi_isqrt.c). */
int _bgi_isqrt(int n);
/* ---- Состояние линий (common/_bgi_line_state.c) ------------------ */
extern uint8_t _bgi_line_style; /* SOLID_LINE..USERBIT_LINE */
extern uint8_t _bgi_line_thick; /* 1 (NORM) или 3 (THICK) */
extern uint16_t _bgi_line_pattern; /* маска для USERBIT_LINE */
/* Отрезок текущим стилем/толщиной линии (common/_bgi_styled_line.c). */
void _bgi_styled_line(int x0, int y0, int x1, int y1, uint8_t color);
/* Отрезок (x0,y0)->(x1,y1) БЕЗ стиля: ортогонал → hspan/vspan,
* диагональ Брезенхэм over _bgi_plot_raw, своя W3-скобка
* (common/_bgi_lineseg.c). Реальная логика, не обёртка. */
void _bgi_lineseg(int x0, int y0, int x1, int y1, uint8_t color);
/* ---- Состояние заливки (common/_bgi_fill_state.c) ---------------- */
extern uint8_t _bgi_fill_pattern; /* *_FILL */
extern uint8_t _bgi_fill_color;
/* Залить строку [x0..x1] на y ТЕКУЩИМ стилем заливки (паттерн+цвет),
* с клипом по экрану. Вызывать между _bgi_begin/_bgi_end. */
void _bgi_fill_span(int x0, int x1, int y);
/* ---- Стиль текста (common/_bgi_text_state.c) --------------------- */
extern uint8_t _bgi_text_dir; /* HORIZ_DIR / VERT_DIR */
extern uint8_t _bgi_text_size; /* масштаб 1..10 */
/* Нарисовать строку s в (x,y) текущим стилем текста (масштаб/направление,
* цвет _bgi_fg, прозрачный фон) common/_bgi_draw_scaled.c. */
void _bgi_draw_scaled(int x, int y, const char *s);
/* ---- Скан-таблицы min/max X по строкам для fillpoly -------------- *
* (common/_bgi_poly_state.c). GFX_HEIGHT высота экрана (256 в обоих
* режимах), см. <gfx.h>. */
extern int _bgi_span_min[GFX_HEIGHT];
extern int _bgi_span_max[GFX_HEIGHT];
/* Прогнать ребро (Брезенхэм) обновляя _bgi_span_min/max
* (common/_bgi_poly_edge.c). */
void _bgi_poly_edge(int x0, int y0, int x1, int y1);
/* ---- Cross-library: внутренние символы libc/cbl ------------------ *
* libbgi ВСЕГДА линкуется вместе с libc (sprinter.lib), поэтому
* зависимость от внутренних символов libc допустима. gfx_wait_vsync
* держит bit7 порта 0x004E через рефкаунт _cbl_port_ref/unref, чтобы
* бит 5 порта 0xFE (позиция луча) был доступен.
* ВНИМАНИЕ: это ВНУТРЕННИЕ символы libc/cbl при изменении сигнатур
* СВЕРЯТЬ с libc/cbl/_cbl.h и синхронизировать. (По тому же паттерну,
* каким здесь живут _videomode_raw_* из libc/video.) */
extern void _cbl_port_ref(void);
extern void _cbl_port_unref(void);
#endif
+20
View File
@@ -0,0 +1,20 @@
/*
* _bgi_clear_raw залить всё поле 640×256 цветом color, mode 0x82.
* Построчный hfill: 256 строк × 2 burst'а (256 + 64 байта). W3-naive.
*/
#include "../_bgi.h"
void _bgi_clear_raw(uint8_t color)
{
_gfx_g16_byte = (uint8_t)((color & 0x0F) | ((color & 0x0F) << 4));
for (int y = 0; y < GFX_HEIGHT_16; y++) {
_gfx_g16_y = (uint8_t)y;
_gfx_g16_addr = _gfx_addr_base;
_gfx_g16_len = 0;
_gfx_hfill16_chunk();
_gfx_g16_addr = (uint16_t)(_gfx_addr_base + 0x100);
_gfx_g16_len = 64;
_gfx_hfill16_chunk();
}
}
+50
View File
@@ -0,0 +1,50 @@
/*
* _bgi_hspan_raw горизонтальная линия len пикселей от (x,y),
* mode 0x82: невыровненные крайние ниблы через RMW, середина
* Fill-burst'ами акселератора по целым байтам. W3-naive.
*/
#include "../_bgi.h"
void _bgi_hspan_raw(int x, int y, int len, uint8_t color)
{
if ((unsigned)y >= GFX_HEIGHT_16) return;
if (x < 0) { len += x; x = 0; }
if (x >= GFX_WIDTH_16) return;
if (x + len > GFX_WIDTH_16) len = GFX_WIDTH_16 - x;
if (len <= 0) return;
_gfx_g16_y = (uint8_t)y;
uint8_t cnib = color & 0x0F;
_gfx_g16_byte = (uint8_t)(cnib | (cnib << 4));
/* Ведущий невыровненный пиксель: x нечётный — ПРАВАЯ половина
* крайнего левого байта. */
if (x & 1) {
_gfx_g16_addr = (uint16_t)(_gfx_addr_base + ((unsigned)x >> 1));
_gfx_g16_nibble = cnib;
_gfx_g16_mask = 0xF0;
_gfx_rmw16_pixel();
x++;
len--;
if (len <= 0) return;
}
/* x чётный; len/2 целых байтов через accel hfill. */
int full = len >> 1;
_gfx_g16_addr = (uint16_t)(_gfx_addr_base + ((unsigned)x >> 1));
while (full > 0) {
int chunk = full > 256 ? 256 : full;
_gfx_g16_len = (chunk == 256) ? 0 : (uint8_t)chunk;
_gfx_hfill16_chunk();
full -= chunk;
_gfx_g16_addr += chunk;
}
/* Хвостовой пиксель нечётной длины: ЛЕВАЯ половина след. байта. */
if (len & 1) {
_gfx_g16_nibble = (uint8_t)(cnib << 4);
_gfx_g16_mask = 0x0F;
_gfx_rmw16_pixel();
}
}
+22
View File
@@ -0,0 +1,22 @@
/*
* _bgi_plot_raw пиксель (x,y) цветом 0..15, mode 0x82.
* Байт по адресу _gfx_addr_base + x/2 держит два пикселя: high nibble =
* ЛЕВЫЙ (чётный x), low = ПРАВЫЙ (нечётный). W3-naive.
*/
#include "../_bgi.h"
void _bgi_plot_raw(int x, int y, uint8_t color)
{
if ((unsigned)x >= GFX_WIDTH_16 || (unsigned)y >= GFX_HEIGHT_16) return;
_gfx_g16_y = (uint8_t)y;
_gfx_g16_addr = (uint16_t)(_gfx_addr_base + ((unsigned)x >> 1));
if (x & 1) {
_gfx_g16_nibble = (uint8_t)(color & 0x0F);
_gfx_g16_mask = 0xF0;
} else {
_gfx_g16_nibble = (uint8_t)((color & 0x0F) << 4);
_gfx_g16_mask = 0x0F;
}
_gfx_rmw16_pixel();
}
+30
View File
@@ -0,0 +1,30 @@
/*
* _bgi_vspan_raw вертикальная линия len пикселей от (x,y),
* mode 0x82. Вертикального accel-пути нет: байт покрывает два
* горизонтальных пикселя, Fill затёр бы соседний нибл поэтому
* RMW на каждую строку. W3-naive.
*/
#include "../_bgi.h"
void _bgi_vspan_raw(int x, int y, int len, uint8_t color)
{
if ((unsigned)x >= GFX_WIDTH_16) return;
if (y < 0) { len += y; y = 0; }
if (y >= GFX_HEIGHT_16) return;
if (y + len > GFX_HEIGHT_16) len = GFX_HEIGHT_16 - y;
if (len <= 0) return;
_gfx_g16_addr = (uint16_t)(_gfx_addr_base + ((unsigned)x >> 1));
if (x & 1) {
_gfx_g16_nibble = (uint8_t)(color & 0x0F);
_gfx_g16_mask = 0xF0;
} else {
_gfx_g16_nibble = (uint8_t)((color & 0x0F) << 4);
_gfx_g16_mask = 0x0F;
}
for (int i = 0; i < len; i++) {
_gfx_g16_y = (uint8_t)(y + i);
_gfx_rmw16_pixel();
}
}
+14
View File
@@ -0,0 +1,14 @@
/*
* _gfx_g16_state скретч 16-цветных raw-примитивов (только данные).
* Общий для putpixel/hline/vline/clear; однопоточно, IRQ выключены
* между W3-begin/end.
*/
#include "../_bgi.h"
uint8_t _gfx_g16_y;
uint8_t _gfx_g16_byte; /* nibble | (nibble<<4) — цвет в обе половины */
uint8_t _gfx_g16_nibble; /* цвет в нужной половине (low или high) */
uint8_t _gfx_g16_mask; /* маска, сохраняющая ДРУГУЮ половину */
uint8_t _gfx_g16_len; /* размер accel-блока (0 = 256) */
uint16_t _gfx_g16_addr;
+38
View File
@@ -0,0 +1,38 @@
/*
* _gfx_hfill16_chunk горизонтальный Fill-burst акселератора для
* mode 0x82: _gfx_g16_len байт значения _gfx_g16_byte с _gfx_g16_addr
* на строке _gfx_g16_y. Один burst красит до 256 байт = 512 пикселей.
* Pre: W3 замаплен, DI активен.
*
* SMC только у байта длины. Цвет предзагружается в C и уходит через
* `ld a, c` (0x79) вставить ещё один `ld a,#n` между LD C,C и
* стреляющим LD (HL),A нельзя: FSM акселератора перечитает immediate
* как новый размер блока.
*/
#include "../_bgi.h"
void _gfx_hfill16_chunk(void) __naked
{
__asm
ld a, (__gfx_g16_len)
ld (_hfill16_len_imm), a
ld a, (__gfx_g16_byte)
ld c, a
ld a, (__gfx_g16_y)
out (#0x89), a
ld hl, (__gfx_g16_addr)
ld d, d ; 0x52 режим размера блока
ld a, #0 ; 0x3E nn длина (патчится)
_hfill16_len_imm = . - 1
ld c, c ; 0x49 горизонтальный Fill
ld a, c ; 0x79 A = байт цвета
ld (hl), a ; выстрел
ld b, b ; 0x40 выключить
ret
__endasm;
}
+28
View File
@@ -0,0 +1,28 @@
/*
* _gfx_rmw16_pixel RMW одного нибла по (_gfx_g16_addr, _gfx_g16_y):
* прочитать байт, стереть целевой нибл маской _gfx_g16_mask, вписать
* _gfx_g16_nibble. ~10 инструкций на пиксель медленно, но байт
* покрывает два горизонтальных пикселя, иначе никак.
* Pre: W3 замаплен, DI активен.
*/
#include "../_bgi.h"
void _gfx_rmw16_pixel(void) __naked
{
__asm
ld a, (__gfx_g16_y)
out (#0x89), a
ld hl, (__gfx_g16_addr)
ld a, (__gfx_g16_mask)
ld b, a ; B = маска сохранения
ld a, (__gfx_g16_nibble)
ld c, a ; C = новый нибл (в нужной половине)
ld a, (hl)
and a, b ; стереть целевой нибл
or a, c ; вписать новый
ld (hl), a
ret
__endasm;
}
+21
View File
@@ -0,0 +1,21 @@
/*
* _bgi_clear_raw залить всё поле 320×256 цветом color, mode 0x81.
* Построчный hfill: 256 строк × 2 burst'а (256 + 64 байта) = 512.
* W3-naive: вызывающий обрамляет begin/end.
*/
#include "../_bgi.h"
void _bgi_clear_raw(uint8_t color)
{
_gfx_acc_color = color;
for (int y = 0; y < GFX_HEIGHT; y++) {
_gfx_acc_y = (uint8_t)y;
_gfx_acc_addr = _gfx_addr_base; /* burst 256 байт */
_gfx_acc_len = 0;
_gfx_hfill256_chunk();
_gfx_acc_addr = (uint16_t)(_gfx_addr_base + 256); /* burst 64 байта */
_gfx_acc_len = 64;
_gfx_hfill256_chunk();
}
}
+28
View File
@@ -0,0 +1,28 @@
/*
* _bgi_hspan_raw горизонтальная линия len пикселей от (x,y),
* mode 0x81, через Fill-burst'ы акселератора (до 256 байт за выстрел).
* W3-naive: вызывающий обрамляет begin/end.
*/
#include "../_bgi.h"
void _bgi_hspan_raw(int x, int y, int len, uint8_t color)
{
if ((unsigned)y >= GFX_HEIGHT) return;
if (x < 0) { len += x; x = 0; }
if (x >= GFX_WIDTH) return;
if (x + len > GFX_WIDTH) len = GFX_WIDTH - x;
if (len <= 0) return;
_gfx_acc_color = color;
_gfx_acc_y = (uint8_t)y;
_gfx_acc_addr = (uint16_t)(_gfx_addr_base + (unsigned)x);
while (len > 0) {
int chunk = len > 256 ? 256 : len;
_gfx_acc_len = (chunk == 256) ? 0 : (uint8_t)chunk;
_gfx_hfill256_chunk();
len -= chunk;
_gfx_acc_addr += chunk;
}
}
+44
View File
@@ -0,0 +1,44 @@
/*
* _bgi_mode_set (drv256) вход в графический режим 320×256×256.
*
* Загружает EGA-совместимые цвета 0..15 в палитру 0, чтобы setcolor(RED)
* и прочие BGI-константы давали ожидаемый цвет. Заполняет границы
* экрана в состоянии BGI. Возвращает предыдущий видеорежим.
*/
#include "../_bgi.h"
#include <gfx.h>
/* 16 EGA-цветов, формат записи палитры (B, G, R, pad). */
static const uint8_t _bgi_ega_pal[16 * 4] = {
0, 0, 0, 0, /* 0 BLACK */
168, 0, 0, 0, /* 1 BLUE */
0, 168, 0, 0, /* 2 GREEN */
168, 168, 0, 0, /* 3 CYAN */
0, 0, 168, 0, /* 4 RED */
168, 0, 168, 0, /* 5 MAGENTA */
0, 84, 168, 0, /* 6 BROWN */
168, 168, 168, 0, /* 7 LIGHTGRAY */
84, 84, 84, 0, /* 8 DARKGRAY */
255, 84, 84, 0, /* 9 LIGHTBLUE */
84, 255, 84, 0, /* 10 LIGHTGREEN */
255, 255, 84, 0, /* 11 LIGHTCYAN */
84, 84, 255, 0, /* 12 LIGHTRED */
255, 84, 255, 0, /* 13 LIGHTMAGENTA */
84, 255, 255, 0, /* 14 YELLOW */
255, 255, 255, 0, /* 15 WHITE */
};
uint8_t _bgi_mode_set(void)
{
uint8_t prev = _videomode_raw_get();
_videomode_raw_set(GFX_MODE_320x256x256);
_gfx_bank = 0x50;
gfx_set_visible_page(0);
gfx_set_draw_page(0);
gfx_pal_load(0, 0, 16, _bgi_ega_pal);
_bgi_maxx = GFX_WIDTH - 1;
_bgi_maxy = GFX_HEIGHT - 1;
_bgi_maxcolor = 255;
return prev;
}
+30
View File
@@ -0,0 +1,30 @@
/*
* _bgi_plot_raw пиксель (x,y) цветом color, mode 0x81.
*
* "Raw" = W3-naive: вызывающий уже сделал _gfx_w3_video_begin (W3 на
* видеобанке, DI). Адресация: CPU _gfx_addr_base + x, Port_Y (0x89) = y.
*
* Скретч отдельный от _gfx_acc_* чтобы putpixel внутри цикла
* Брезенхэма не топтал внешнее состояние акселератора.
*/
#include "../_bgi.h"
static uint8_t _gfx_pp_y;
static uint16_t _gfx_pp_addr;
static uint8_t _gfx_pp_color;
void _bgi_plot_raw(int x, int y, uint8_t color)
{
if ((unsigned)x >= GFX_WIDTH || (unsigned)y >= GFX_HEIGHT) return;
_gfx_pp_y = (uint8_t)y;
_gfx_pp_addr = (uint16_t)(_gfx_addr_base + (unsigned)x);
_gfx_pp_color = color;
__asm
ld a, (__gfx_pp_y)
out (#0x89), a
ld hl, (__gfx_pp_addr)
ld a, (__gfx_pp_color)
ld (hl), a
__endasm;
}
+27
View File
@@ -0,0 +1,27 @@
/*
* _bgi_read_raw прочитать пиксель (x,y) БЕЗ своей W3-скобки
* (вызывающий уже сделал _gfx_w3_video_begin). Для блиттинга image:
* getimage/putimage читают тысячи пикселей в одной скобке.
* За пределами экрана возвращает 0.
*/
#include "../_bgi.h"
static uint8_t _gfx_gpr_y;
static uint16_t _gfx_gpr_addr;
static uint8_t _gfx_gpr_color;
uint8_t _bgi_read_raw(int x, int y)
{
if ((unsigned)x >= GFX_WIDTH || (unsigned)y >= GFX_HEIGHT) return 0;
_gfx_gpr_y = (uint8_t)y;
_gfx_gpr_addr = (uint16_t)(_gfx_addr_base + (unsigned)x);
__asm
ld a, (__gfx_gpr_y)
out (#0x89), a
ld hl, (__gfx_gpr_addr)
ld a, (hl)
ld (__gfx_gpr_color), a
__endasm;
return _gfx_gpr_color;
}
+50
View File
@@ -0,0 +1,50 @@
/*
* _bgi_vspan_raw вертикальная линия len пикселей от (x,y),
* mode 0x81. Вертикальный Fill акселератора сам инкрементирует Port_Y,
* так что вся колонка (высота 256) один burst. W3-naive.
*/
#include "../_bgi.h"
/* Вертикальный Fill: _gfx_acc_len пикселей в колонке _gfx_acc_addr,
* верхняя строка _gfx_acc_y. Про SMC и правила FSM акселератора
* см. _gfx_hfill256.c. */
static void vfill_chunk(void) __naked
{
__asm
ld a, (__gfx_acc_len)
ld (_vfill256_len_imm), a
ld a, (__gfx_acc_y)
out (#0x89), a ; стартовый Y
ld a, (__gfx_acc_color)
ld c, a
ld hl, (__gfx_acc_addr)
ld d, d ; 0x52 режим размера блока
ld a, #0 ; immediate-длина (патчится)
_vfill256_len_imm = . - 1
ld e, e ; 0x5B вертикальный Fill
ld a, c ; A = цвет
ld (hl), a ; выстрел
ld b, b ; 0x40 выключить
ret
__endasm;
}
void _bgi_vspan_raw(int x, int y, int len, uint8_t color)
{
if ((unsigned)x >= GFX_WIDTH) return;
if (y < 0) { len += y; y = 0; }
if (y >= GFX_HEIGHT) return;
if (y + len > GFX_HEIGHT) len = GFX_HEIGHT - y;
if (len <= 0) return;
/* GFX_HEIGHT = 256, полная колонка — один accel-burst. */
_gfx_acc_color = color;
_gfx_acc_y = (uint8_t)y;
_gfx_acc_addr = (uint16_t)(_gfx_addr_base + (unsigned)x);
_gfx_acc_len = (len == 256) ? 0 : (uint8_t)len;
vfill_chunk();
}
+12
View File
@@ -0,0 +1,12 @@
/*
* _gfx_acc256 скретч акселератора для 256-цветных raw-примитивов
* (модуль только с данными). Общий для hline/vline/clear; однопоточно
* GFX работает с выключенными прерываниями между W3-begin/end.
*/
#include "../_bgi.h"
uint8_t _gfx_acc_color;
uint8_t _gfx_acc_y;
uint8_t _gfx_acc_len; /* 0 = 256 — конвенция акселератора */
uint16_t _gfx_acc_addr;
+45
View File
@@ -0,0 +1,45 @@
/*
* _gfx_hfill256_chunk горизонтальный Fill-burst акселератора:
* _gfx_acc_len байт цвета _gfx_acc_color с адреса _gfx_acc_addr на
* строке _gfx_acc_y. Pre: W3 замаплен на видеобанк, DI активен.
*
* Опкоды акселератора (docs/converted/accel_r.txt):
* LD D,D (0x52) режим «задать размер блока»; СЛЕДУЮЩИЙ байт длина,
* ОБЯЗАН быть immediate-операндом LD A,n (0x3E)
* LD C,C (0x49) горизонтальный Fill (LD (HL),A заливает n байт)
* LD B,B (0x40) выключить акселератор
*
* Байт длины патчится SMC HOME после загрузки DSS это RAM, патчить
* собственный .EXE безопасно. Вставлять другой `ld a,#n` между LD C,C
* и стреляющим LD (HL),A нельзя FSM акселератора перечитает immediate
* как новый размер блока.
*/
#include "../_bgi.h"
void _gfx_hfill256_chunk(void) __naked
{
__asm
;; Пропатчить immediate LD A,#n байтом длины.
ld a, (__gfx_acc_len)
ld (_hfill256_len_imm), a
ld a, (__gfx_acc_y)
out (#0x89), a ; Port_Y = y
;; Цвет в C и адрес в HL готовим ДО взведения акселератора.
ld a, (__gfx_acc_color)
ld c, a
ld hl, (__gfx_acc_addr)
;; --- ПОСЛЕДОВАТЕЛЬНОСТЬ АКСЕЛЕРАТОРА ---
ld d, d ; 0x52 режим размера блока
ld a, #0 ; 0x3E nn размер (nn патчится выше)
_hfill256_len_imm = . - 1
ld c, c ; 0x49 горизонтальный Fill
ld a, c ; 0x79 A = цвет (НЕ ещё один ld a,#n)
ld (hl), a ; выстрел; заливает _gfx_acc_len байт
ld b, b ; 0x40 выключить
ret
__endasm;
}
+33
View File
@@ -0,0 +1,33 @@
/*
* getpixel (bgi256) цвет пикселя (x,y), mode 0x81. Полностью inline
* (bounds + W3-скобка + чтение пикселя), без вызовов _bgi_read/raw.
* Возвращает 0 вне экрана.
*/
#include "../_bgi.h"
static uint8_t _gp_y;
static uint16_t _gp_addr;
static uint8_t _gp_color;
unsigned getpixel(int x, int y)
{
if ((unsigned)x >= GFX_WIDTH || (unsigned)y >= GFX_HEIGHT) return 0;
_gp_y = (uint8_t)y;
_gp_addr = (uint16_t)(_gfx_addr_base + (unsigned)x);
__asm
di
in a, (#0xE2)
ld (__gfx_saved_w3), a
ld a, (__gfx_bank)
out (#0xE2), a
ld a, (_gp_y)
out (#0x89), a
ld hl, (_gp_addr)
ld a, (hl)
ld (_gp_color), a
ld a, (__gfx_saved_w3)
out (#0xE2), a
ei
__endasm;
return _gp_color;
}
+39
View File
@@ -0,0 +1,39 @@
/*
* putpixel (bgi256) точка (x,y) цветом color, mode 0x81.
* ПОЛНОСТЬЬЮ inline: bounds + W3-скобка + запись пикселя в теле, БЕЗ
* вызовов _bgi_plot / gfx_putpixel256 / _bgi_plot_raw (устранены
* прокладки старой цепочки putpixel_bgi_plotgfx_putpixel256raw).
* Адресация: CPU _gfx_addr_base + x, Port_Y (0x89) = y; W3 мапится на
* _gfx_bank, сейв восстанавливается. Bounds compile-time GFX_WIDTH/
* HEIGHT (меньше/быстрее, runtime _bgi_maxx/_bgi_maxy).
*/
#include "../_bgi.h"
/* File-static скретч (как в _bgi_plot_raw, но свой — putpixel не должен
* топтать скретч акселератора/plot_raw, если звать вперемешку). */
static uint8_t _pp_y;
static uint16_t _pp_addr;
static uint8_t _pp_color;
void putpixel(int x, int y, int color)
{
if ((unsigned)x >= GFX_WIDTH || (unsigned)y >= GFX_HEIGHT) return;
_pp_y = (uint8_t)y;
_pp_addr = (uint16_t)(_gfx_addr_base + (unsigned)x);
_pp_color = (uint8_t)color;
__asm
di
in a, (#0xE2)
ld (__gfx_saved_w3), a
ld a, (__gfx_bank)
out (#0xE2), a
ld a, (_pp_y)
out (#0x89), a
ld hl, (_pp_addr)
ld a, (_pp_color)
ld (hl), a
ld a, (__gfx_saved_w3)
out (#0xE2), a
ei
__endasm;
}
+33
View File
@@ -0,0 +1,33 @@
/*
* _bgi_ellipse_arc контур дуги/эллипса полилинией.
*
* Точки параметризованы углом (BGI: 0°=восток, против часовой, ось Y
* вниз): px = cx + xr*cos(a), py = cy - yr*sin(a). Соседние выборки
* соединяются отрезками контур сплошной при любом радиусе.
*
* Тригонометрия в Q7 (×128): xr*cos128 255×128 < 32767, помещается в
* int БЕЗ 32-битной арифметики (её SDCC/Z80 собирает криво: раньше
* эллипс через (long)>>8 рисовался прямоугольником).
*/
#include "../_bgi.h"
void _bgi_ellipse_arc(int cx, int cy, int stangle, int endangle,
int xr, int yr, uint8_t color)
{
int a, x, y, px, py, first;
if (endangle < stangle) endangle += 360; /* дуга через 0° */
px = 0;
py = 0;
first = 1;
for (a = stangle; a <= endangle; a++) {
x = cx + ((xr * _bgi_cos128(a)) >> 7);
y = cy - ((yr * _bgi_sin128(a)) >> 7);
if (!first) _bgi_lineseg(px, py, x, y, color);
px = x;
py = y;
first = 0;
}
}
+24
View File
@@ -0,0 +1,24 @@
/*
* _bgi_begin открыть W3-сессию для пакетного рисования: DI, сохранить
* текущую страницу W3 и замапить _gfx_bank (видеобанк 0x50..0x5F).
* Композитные примитивы обрамляют одной парой _bgi_begin/_bgi_end весь
* внутренний цикл W3-танец оплачивается один раз на операцию, а не
* на пиксель. НЕ реентерабельно (сейв в _gfx_saved_w3): между begin и
* end IRQ выключены.
*
* Real код (поглощён из старого libc/gfx/_gfx_w3_video_begin.c) не
* обёртка. Mode-agnostic: использует только _gfx_bank/_gfx_saved_w3.
*/
#include "../_bgi.h"
void _bgi_begin(void) __naked
{
__asm
di
in a, (#0xE2)
ld (__gfx_saved_w3), a
ld a, (__gfx_bank)
out (#0xE2), a
ret
__endasm;
}
+13
View File
@@ -0,0 +1,13 @@
/*
* _bgi_clearall залить весь экран цветом, self-bracket (своя W3-скобка).
* cleardevice зовёт это фоновым цветом. Реальная композиция
* _bgi_begin + _bgi_clear_raw + _bgi_end.
*/
#include "../_bgi.h"
void _bgi_clearall(uint8_t color)
{
_bgi_begin();
_bgi_clear_raw(color);
_bgi_end();
}
+48
View File
@@ -0,0 +1,48 @@
/*
* _bgi_draw_scaled строка растровым шрифтом 8×8 с целочисленным
* масштабом и направлением, цветом _bgi_fg, прозрачным фоном.
*
* HORIZ_DIR: пиксель глифа (c,r) экран (x+c*size, y+r*size).
* VERT_DIR (поворот 90° против часовой): (c,r) (x+r*size, y-c*size).
* Каждый «пиксель» глифа блок size×size из raw-плотов, всё в одной
* W3-скобке.
*/
#include "../_bgi.h"
void _bgi_draw_scaled(int x, int y, const char *s)
{
uint8_t rows[8];
uint8_t b, fg, size, dir;
int r, c, bx, by, px, py;
fg = _bgi_fg;
size = _bgi_text_size ? _bgi_text_size : 1;
dir = _bgi_text_dir;
_bgi_begin();
while (*s) {
_bgi_font_rows((unsigned char)*s, rows);
for (r = 0; r < 8; r++) {
b = rows[r];
for (c = 0; c < 8; c++) {
if (!((b >> (7 - c)) & 1)) continue;
for (by = 0; by < size; by++) {
for (bx = 0; bx < size; bx++) {
if (dir == VERT_DIR) {
px = x + r * size + by;
py = y - c * size - bx;
} else {
px = x + c * size + bx;
py = y + r * size + by;
}
_bgi_plot_raw(px, py, fg);
}
}
}
}
if (dir == VERT_DIR) y -= 8 * size;
else x += 8 * size;
s++;
}
_bgi_end();
}
+15
View File
@@ -0,0 +1,15 @@
/*
* _bgi_end закрыть W3-сессию: восстановить сохранённую страницу W3 и
* EI. Real код (поглощён из старого libc/gfx/_gfx_w3_video_end.c).
*/
#include "../_bgi.h"
void _bgi_end(void) __naked
{
__asm
ld a, (__gfx_saved_w3)
out (#0xE2), a
ei
ret
__endasm;
}
+47
View File
@@ -0,0 +1,47 @@
/*
* _bgi_fill_span залить строку [x0..x1] на y текущим стилем заливки.
*
* SOLID быстрый raw-hline; EMPTY hline фоновым цветом; паттерн
* поточечно по 8×8-битовой маске (строка паттерна = pat[y&7], бит x&7).
* Клип по экрану внутри. ВЫЗЫВАТЬ между _bgi_begin/_bgi_end.
*/
#include "../_bgi.h"
/* Стандартные 8×8-паттерны Borland, индексы LINE_FILL..CLOSE_DOT_FILL. */
static const uint8_t _bgi_pat[10][8] = {
{ 0xFF,0xFF,0x00,0x00,0xFF,0xFF,0x00,0x00 }, /* LINE_FILL */
{ 0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80 }, /* LTSLASH_FILL */
{ 0xE0,0xC1,0x83,0x07,0x0E,0x1C,0x38,0x70 }, /* SLASH_FILL */
{ 0xF0,0x78,0x3C,0x1E,0x0F,0x87,0xC3,0xE1 }, /* BKSLASH_FILL */
{ 0x80,0x40,0x20,0x10,0x08,0x04,0x02,0x01 }, /* LTBKSLASH_FILL */
{ 0xFF,0x88,0x88,0x88,0xFF,0x88,0x88,0x88 }, /* HATCH_FILL */
{ 0x81,0x42,0x24,0x18,0x18,0x24,0x42,0x81 }, /* XHATCH_FILL */
{ 0xCC,0x33,0xCC,0x33,0xCC,0x33,0xCC,0x33 }, /* INTERLEAVE_FILL */
{ 0x80,0x00,0x08,0x00,0x80,0x00,0x08,0x00 }, /* WIDE_DOT_FILL */
{ 0x88,0x00,0x22,0x00,0x88,0x00,0x22,0x00 }, /* CLOSE_DOT_FILL */
};
void _bgi_fill_span(int x0, int x1, int y)
{
int x;
uint8_t p, row;
if (x1 < x0) { x = x0; x0 = x1; x1 = x; }
if (y < 0 || y > _bgi_maxy) return;
if (x0 < 0) x0 = 0;
if (x1 > _bgi_maxx) x1 = _bgi_maxx;
if (x1 < x0) return;
p = _bgi_fill_pattern;
if (p == EMPTY_FILL) { _bgi_hspan_raw(x0, y, x1 - x0 + 1, _bgi_bg); return; }
if (p == SOLID_FILL || p == USER_FILL || p > CLOSE_DOT_FILL) {
_bgi_hspan_raw(x0, y, x1 - x0 + 1, _bgi_fill_color);
return;
}
/* Паттерн LINE_FILL..CLOSE_DOT_FILL. */
row = _bgi_pat[p - LINE_FILL][y & 7];
for (x = x0; x <= x1; x++)
if ((row >> (x & 7)) & 1)
_bgi_plot_raw(x, y, _bgi_fill_color);
}
+10
View File
@@ -0,0 +1,10 @@
/*
* _bgi_fill_state текущий стиль заливки BGI (паттерн + цвет).
* Значения по умолчанию выставляет initgraph (SOLID_FILL/WHITE).
* Без инициализации (crt0 зануляет _DATA).
*/
#include "../_bgi.h"
uint8_t _bgi_fill_pattern;
uint8_t _bgi_fill_color;
+20
View File
@@ -0,0 +1,20 @@
/*
* _bgi_font_rows 8 байт строк глифа символа c (mode-agnostic: просто
* читает шрифт через _gfx_font_ptr). Шрифт системный, interleaved:
* offset = row*256 + char (MSB-first), см. memory/sprinter_font_format.
* Грузится лениво (_gfx_font_ensure) при первом использовании.
*
* Раньше жил в drv256/ по недосмотру перенесён в common/, т.к. логика
* не зависит от видеорежима (один и тот же .rel в bgi256.lib и bgi16.lib).
*/
#include "../_bgi.h"
void _bgi_font_rows(unsigned char c, uint8_t *rows8)
{
const uint8_t *f;
int r;
_gfx_font_ensure();
f = _gfx_font_ptr;
for (r = 0; r < 8; r++)
rows8[r] = f[r * 256 + c];
}
+13
View File
@@ -0,0 +1,13 @@
/*
* _bgi_hspan горизонтальный отрезок len пикселей от (x,y), self-bracket
* (своя W3-скобка). Для floodfill (заливка строки вне пакетной сессии).
* Реальная композиция _bgi_begin + _bgi_hspan_raw + _bgi_end.
*/
#include "../_bgi.h"
void _bgi_hspan(int x, int y, int len, uint8_t color)
{
_bgi_begin();
_bgi_hspan_raw(x, y, len, color);
_bgi_end();
}
+15
View File
@@ -0,0 +1,15 @@
/*
* _bgi_isqrt целочисленный floor(sqrt(n)) для n в 0..~32767.
* Линейный поиск (n180² 180 шагов) для fillellipse достаточно,
* без float и 32-бит.
*/
#include "../_bgi.h"
int _bgi_isqrt(int n)
{
int x = 0;
if (n < 0) return 0;
while ((x + 1) * (x + 1) <= n) x++;
return x;
}
+9
View File
@@ -0,0 +1,9 @@
/*
* _bgi_line_state текущий стиль/толщина/маска линий BGI.
* initgraph ставит SOLID_LINE / NORM / 0xFFFF. Без инициализации.
*/
#include "../_bgi.h"
uint8_t _bgi_line_style;
uint8_t _bgi_line_thick;
uint16_t _bgi_line_pattern;
+49
View File
@@ -0,0 +1,49 @@
/*
* _bgi_lineseg отрезок (x0,y0)->(x1,y1) БЕЗ стиля/толщины (SOLID+NORM).
* Real логика (поглощена из старого libc/gfx/gfx_line256.c), не обёртка.
*
* Ортогональные отрезки уходят на driver-leaf'ы _bgi_hspan_raw /
* _bgi_vspan_raw (акселератор); наклонные Брезенхэм поточечно через
* _bgi_plot_raw. Каждый путь ставит свою W3-скобку (_bgi_begin/_bgi_end)
* вызывать _bgi_lineseg ВНЕ чужой скобки (W3-сейв не реентерабелен).
* Вызывают: _bgi_styled_line (SOLID+NORM), _bgi_arc_draw, fillpoly,
* sector все вне скобки.
*/
#include "../_bgi.h"
void _bgi_lineseg(int x0, int y0, int x1, int y1, uint8_t color)
{
/* Горизонталь — один H-burst. */
if (y0 == y1) {
int x = x0 <= x1 ? x0 : x1;
int w = (x0 <= x1 ? x1 - x0 : x0 - x1) + 1;
_bgi_begin();
_bgi_hspan_raw(x, y0, w, color);
_bgi_end();
return;
}
/* Вертикаль — один V-burst (256) или RMW-цикл (16). */
if (x0 == x1) {
int y = y0 <= y1 ? y0 : y1;
int h = (y0 <= y1 ? y1 - y0 : y0 - y1) + 1;
_bgi_begin();
_bgi_vspan_raw(x0, y, h, color);
_bgi_end();
return;
}
/* Наклонный — Брезенхэм в одной W3-скобке. */
int dx = x1 - x0; int sx = dx < 0 ? -1 : 1; if (dx < 0) dx = -dx;
int dy = y1 - y0; int sy = dy < 0 ? -1 : 1; if (dy < 0) dy = -dy;
int err = (dx > dy ? dx : -dy) / 2;
int x = x0, y = y0;
_bgi_begin();
for (;;) {
_bgi_plot_raw(x, y, color);
if (x == x1 && y == y1) break;
int e2 = err;
if (e2 > -dx) { err -= dy; x += sx; }
if (e2 < dy) { err += dx; y += sy; }
}
_bgi_end();
}
+30
View File
@@ -0,0 +1,30 @@
/*
* _bgi_poly_edge прогнать ребро (x0,y0)->(x1,y1) по Брезенхэму,
* обновляя _bgi_span_min/_bgi_span_max для каждой затронутой строки.
* Только целочисленные сложения (без 32-бит). Строки вне экрана
* пропускаются. Заливка потом идёт от min до max по строке.
*/
#include "../_bgi.h"
void _bgi_poly_edge(int x0, int y0, int x1, int y1)
{
int dx, dy, sx, sy, err, e2;
dx = x1 - x0; if (dx < 0) dx = -dx;
dy = y1 - y0; if (dy < 0) dy = -dy;
sx = (x0 < x1) ? 1 : -1;
sy = (y0 < y1) ? 1 : -1;
err = dx - dy;
for (;;) {
if (y0 >= 0 && y0 < GFX_HEIGHT) {
if (x0 < _bgi_span_min[y0]) _bgi_span_min[y0] = x0;
if (x0 > _bgi_span_max[y0]) _bgi_span_max[y0] = x0;
}
if (x0 == x1 && y0 == y1) break;
e2 = 2 * err;
if (e2 > -dy) { err -= dy; x0 += sx; }
if (e2 < dx) { err += dx; y0 += sy; }
}
}
+11
View File
@@ -0,0 +1,11 @@
/*
* _bgi_poly_state скан-таблицы min/max X по строкам для fillpoly.
* По строке экрана хранит крайние левый/правый X контура заливка
* идёт от min до max (выпуклая оболочка строки). ~1 КБ в _DATA,
* однопоточно (рисование не реентерабельно).
*/
#include "../_bgi.h"
int _bgi_span_min[GFX_HEIGHT];
int _bgi_span_max[GFX_HEIGHT];
+16
View File
@@ -0,0 +1,16 @@
/*
* _bgi_read цвет пикселя (x,y), self-bracket (своя W3-скобка).
* Для разовых чтений из математики, которой не нужен пакетный режим
* (floodfill). getpixel тоже зовёт это. Реальная композиция
* _bgi_begin + _bgi_read_raw + _bgi_end не обёртка над обёрткой.
*/
#include "../_bgi.h"
unsigned _bgi_read(int x, int y)
{
unsigned c;
_bgi_begin();
c = _bgi_read_raw(x, y);
_bgi_end();
return c;
}
+19
View File
@@ -0,0 +1,19 @@
/*
* _bgi_state.c общее состояние BGI-слоя.
*
* File-scope переменные НЕ инициализируются (crt0 зануляет _DATA;
* см. memory/sdcc_static_storage_gotcha). Осмысленные значения
* выставляет initgraph().
*/
#include "../_bgi.h"
uint8_t _bgi_fg;
uint8_t _bgi_bg;
int _bgi_cx;
int _bgi_cy;
int _bgi_maxx;
int _bgi_maxy;
int _bgi_maxcolor;
uint8_t _bgi_prevmode;
int _bgi_result;
+58
View File
@@ -0,0 +1,58 @@
/*
* _bgi_styled_line отрезок текущим стилем и толщиной линии.
*
* SOLID+NORM быстрый _bgi_lineseg (accel). Иначе Брезенхэм с
* 16-битной маской (бит на шаг; когда бит 0 пропуск) и, для THICK,
* дублированием перпендикулярно основной оси (±1). Всё raw в одной
* W3-скобке.
*/
#include "../_bgi.h"
/* Маски стандартных стилей (SOLID/DOTTED/CENTER/DASHED). */
static const uint16_t _bgi_line_pat[4] = { 0xFFFF, 0xCCCC, 0xFF18, 0xF8F8 };
void _bgi_styled_line(int x0, int y0, int x1, int y1, uint8_t color)
{
uint16_t pat;
uint8_t bit, style, thick;
int dx, dy, sx, sy, err, e2, major_x;
style = _bgi_line_style;
thick = _bgi_line_thick;
if (style == SOLID_LINE && thick <= 1) {
_bgi_lineseg(x0, y0, x1, y1, color);
return;
}
pat = (style == USERBIT_LINE) ? _bgi_line_pattern
: _bgi_line_pat[style & 3];
dx = x1 - x0; if (dx < 0) dx = -dx;
dy = y1 - y0; if (dy < 0) dy = -dy;
sx = (x0 < x1) ? 1 : -1;
sy = (y0 < y1) ? 1 : -1;
err = dx - dy;
major_x = (dx >= dy);
bit = 0;
_bgi_begin();
for (;;) {
if ((pat >> (bit & 15)) & 1) {
_bgi_plot_raw(x0, y0, color);
if (thick > 1) {
if (major_x) {
_bgi_plot_raw(x0, y0 - 1, color);
_bgi_plot_raw(x0, y0 + 1, color);
} else {
_bgi_plot_raw(x0 - 1, y0, color);
_bgi_plot_raw(x0 + 1, y0, color);
}
}
}
bit++;
if (x0 == x1 && y0 == y1) break;
e2 = 2 * err;
if (e2 > -dy) { err -= dy; x0 += sx; }
if (e2 < dx) { err += dx; y0 += sy; }
}
_bgi_end();
}
+8
View File
@@ -0,0 +1,8 @@
/*
* _bgi_text_state направление и масштаб текста BGI.
* initgraph ставит HORIZ_DIR / size 1. Без инициализации.
*/
#include "../_bgi.h"
uint8_t _bgi_text_dir;
uint8_t _bgi_text_size;
+43
View File
@@ -0,0 +1,43 @@
/*
* _bgi_trig целочисленная тригонометрия для дуг/эллипсов BGI.
*
* Угол в градусах (Turbo-C); sin/cos масштабированы на 128 (Q7):
* _bgi_sin128(90) = 128. Масштаб 128 (а не 256) выбран так, чтобы
* произведение радиус×значение помещалось в 16-бит int для любого
* экранного радиуса (255×128 = 32640 < 32767) и НЕ приходилось
* трогать 32-битную арифметику, которую SDCC/Z80 собирает криво
* (см. memory/avoid_32bit_arith_z80; ловили эллипс-в-прямоугольник).
*/
#include "../_bgi.h"
/* sin(0°..90°) × 128. */
static const int _bgi_sintab[91] = {
0, 2, 4, 7, 9, 11, 13, 16, 18, 20,
22, 24, 27, 29, 31, 33, 35, 37, 40, 42,
44, 46, 48, 50, 52, 54, 56, 58, 60, 62,
64, 66, 68, 70, 72, 73, 75, 77, 79, 81,
82, 84, 86, 87, 89, 91, 92, 94, 95, 97,
98, 99, 101, 102, 104, 105, 106, 107, 109, 110,
111, 112, 113, 114, 115, 116, 117, 118, 119, 119,
120, 121, 122, 122, 123, 124, 124, 125, 125, 126,
126, 126, 127, 127, 127, 128, 128, 128, 128, 128,
128,
};
/* sin(угол°) × 128, угол приводится к 0..359. */
int _bgi_sin128(int deg)
{
deg %= 360;
if (deg < 0) deg += 360;
if (deg <= 90) return _bgi_sintab[deg];
if (deg <= 180) return _bgi_sintab[180 - deg];
if (deg <= 270) return -_bgi_sintab[deg - 180];
return -_bgi_sintab[360 - deg];
}
/* cos(угол°) × 128 = sin(угол+90). */
int _bgi_cos128(int deg)
{
return _bgi_sin128(deg + 90);
}
+12
View File
@@ -0,0 +1,12 @@
/*
* _gfx_font_ensure ленивая инициализация шрифта: текстовые рендеры
* зовут её при каждом входе, системный шрифт грузится при первом
* использовании (чисто графическая программа BIOS-вызов не платит).
*/
#include "../_bgi.h"
void _gfx_font_ensure(void)
{
if (!_gfx_font_loaded) gfx_load_default_font();
}
+16
View File
@@ -0,0 +1,16 @@
/*
* _gfx_font_state буфер шрифта, указатель и флаг загрузки (данные).
*
* Формат (ZX-Spectrum-совместимый): 256 глифов × 8 рядов × 1 байт = 2 КБ,
* INTERLEAVED offset = row*256 + char (ряд 0 всех глифов занимает
* 0x000..0x0FF и т.д.), биты MSB-first (бит 7 = левый пиксель).
*
* _gfx_font_ptr всегда указывает на валидные данные после
* _gfx_font_ensure(); программы могут подменить шрифт gfx_set_font().
*/
#include "../_bgi.h"
uint8_t _gfx_font_buf[_GFX_FONT_BYTES];
const uint8_t *_gfx_font_ptr = _gfx_font_buf;
uint8_t _gfx_font_loaded;
+20
View File
@@ -0,0 +1,20 @@
/*
* _gfx_state общее состояние графики (модуль только с данными).
*
* Пишут gfx_set_visible_page / gfx_set_draw_page /
* gfx_set_bank; читают все mode-специфичные примитивы.
*/
#include "../_bgi.h"
uint8_t _gfx_visible_page;
uint8_t _gfx_draw_page;
/* См. memory/sprinter_vram_transparency.md про биты 0x50..0x5F
* (0x50 normal, 0x54 temp, 0x58 transparent, 0x5C оба). */
uint8_t _gfx_bank = 0x50;
/* Каждая VRAM-строка — 1024 байта: page 0 занимает байты 0..319
* (CPU 0xC000+), page 1 320..639 (CPU 0xC140+), остаток дескрипторы
* режима/палитра, их не трогаем. */
uint16_t _gfx_addr_base = 0xC000;
+10
View File
@@ -0,0 +1,10 @@
/*
* _gfx_w3_state сохранённый байт W3 между begin и end (данные).
*
* НЕ реентерабельно безопасно только потому, что GFX держит
* прерывания выключенными между _gfx_w3_video_begin и _end.
*/
#include "../_bgi.h"
uint8_t _gfx_saved_w3;
+7
View File
@@ -0,0 +1,7 @@
/* arc — дуга окружности радиуса r от stangle до endangle (градусы). */
#include "../_bgi.h"
void arc(int x, int y, int stangle, int endangle, int radius)
{
_bgi_ellipse_arc(x, y, stangle, endangle, radius, radius, _bgi_fg);
}
+18
View File
@@ -0,0 +1,18 @@
/*
* bar залитый прямоугольник (left,top)-(right,bottom) включительно
* ТЕКУЩИМ стилем заливки (setfillstyle), без рамки как в Turbo-C.
*/
#include "../_bgi.h"
void bar(int left, int top, int right, int bottom)
{
int y, t;
if (right < left) { t = left; left = right; right = t; }
if (bottom < top) { t = top; top = bottom; bottom = t; }
_bgi_begin();
for (y = top; y <= bottom; y++)
_bgi_fill_span(left, right, y);
_bgi_end();
}
+27
View File
@@ -0,0 +1,27 @@
/*
* bar3d трёхмерный брусок: передняя грань залита текущим стилем
* заливки, рёбра текущим цветом рисования. depth глубина (вправо-
* вверх), topflag != 0 рисует верхнюю грань.
*/
#include "../_bgi.h"
void bar3d(int left, int top, int right, int bottom, int depth, int topflag)
{
int t;
if (right < left) { t = left; left = right; right = t; }
if (bottom < top) { t = top; top = bottom; bottom = t; }
bar(left, top, right, bottom); /* передняя грань — заливка */
rectangle(left, top, right, bottom); /* рамка переда — тек. цвет */
if (depth != 0) {
line(right, top, right + depth, top - depth);
line(right, bottom, right + depth, bottom - depth);
line(right + depth, top - depth, right + depth, bottom - depth);
if (topflag) {
line(left, top, left + depth, top - depth);
line(left + depth, top - depth, right + depth, top - depth);
}
}
}
+40
View File
@@ -0,0 +1,40 @@
/*
* circle окружность (midpoint) с центром (cx,cy) радиуса r,
* текущим цветом. Целочисленный алгоритм, 8 октантов за шаг.
*
* Вся окружность рисуется в ОДНОЙ W3-скобке (_bgi_begin/_bgi_end) +
* raw-плот иначе скобка на каждый пиксель делает большие окружности
* мучительно медленными (тысячи DI/EI).
*/
#include "../_bgi.h"
void circle(int cx, int cy, int r)
{
int x, y, d;
uint8_t c = _bgi_fg;
if (r < 0) return;
x = 0;
y = r;
d = 1 - r;
_bgi_begin();
while (x <= y) {
_bgi_plot_raw(cx + x, cy + y, c);
_bgi_plot_raw(cx - x, cy + y, c);
_bgi_plot_raw(cx + x, cy - y, c);
_bgi_plot_raw(cx - x, cy - y, c);
_bgi_plot_raw(cx + y, cy + x, c);
_bgi_plot_raw(cx - y, cy + x, c);
_bgi_plot_raw(cx + y, cy - x, c);
_bgi_plot_raw(cx - y, cy - x, c);
if (d < 0) {
d += 2 * x + 3;
} else {
d += 2 * (x - y) + 5;
y--;
}
x++;
}
_bgi_end();
}
+9
View File
@@ -0,0 +1,9 @@
/* cleardevice — очистить экран фоновым цветом, вернуть CP в (0,0). */
#include "../_bgi.h"
void cleardevice(void)
{
_bgi_clearall(_bgi_bg);
_bgi_cx = 0;
_bgi_cy = 0;
}
+8
View File
@@ -0,0 +1,8 @@
/* closegraph — вернуть текстовый режим, действовавший до initgraph.
* _bgi_mode_done удалён _videomode_raw_set (mode-agnostic) */
#include "../_bgi.h"
void closegraph(void)
{
_videomode_raw_set(_bgi_prevmode);
}
+17
View File
@@ -0,0 +1,17 @@
/*
* drawpoly ломаная по numpoints точкам (массив x0,y0,x1,y1,),
* текущим цветом. Как в Turbo-C, контур НЕ замыкается автоматически:
* чтобы получить замкнутый полигон, повторите первую точку последней.
*/
#include "../_bgi.h"
void drawpoly(int numpoints, const int *polypoints)
{
int i;
if (numpoints < 2) return;
for (i = 0; i < numpoints - 1; i++) {
_bgi_styled_line(polypoints[2 * i], polypoints[2 * i + 1],
polypoints[2 * i + 2], polypoints[2 * i + 3],
_bgi_fg);
}
}
+11
View File
@@ -0,0 +1,11 @@
/*
* ellipse контур эллипса/эллиптической дуги с полуосями xradius,
* yradius от stangle до endangle. Полный эллипс: stangle=0,endangle=360.
*/
#include "../_bgi.h"
void ellipse(int x, int y, int stangle, int endangle,
int xradius, int yradius)
{
_bgi_ellipse_arc(x, y, stangle, endangle, xradius, yradius, _bgi_fg);
}
+31
View File
@@ -0,0 +1,31 @@
/*
* fillellipse залитый эллипс с полуосями rx,ry, центр (cx,cy).
* Нутро текущим стилем заливки, контур текущим цветом.
*
* По строкам: полуширина w = rx*sqrt(ry²-dy²)/ry (целочисленно через
* _bgi_isqrt). ry²,dy² ~180² < 32767, rx*s rx*ry < 32767 без
* 32-бит (см. memory/avoid_32bit_arith_z80).
*/
#include "../_bgi.h"
void fillellipse(int cx, int cy, int rx, int ry)
{
int dy, s, w;
if (rx < 0 || ry < 0) return;
_bgi_begin();
if (ry == 0) {
_bgi_fill_span(cx - rx, cx + rx, cy);
} else {
for (dy = -ry; dy <= ry; dy++) {
s = _bgi_isqrt(ry * ry - dy * dy); /* 0..ry */
w = (rx * s) / ry;
_bgi_fill_span(cx - w, cx + w, cy + dy);
}
}
_bgi_end();
/* Контур текущим цветом. */
ellipse(cx, cy, 0, 360, rx, ry);
}
+54
View File
@@ -0,0 +1,54 @@
/*
* fillpoly залитый многоугольник по numpoints точкам {x0,y0,}.
* Нутро текущим стилем заливки, контур текущим цветом. Полигон
* авто-замыкается (последняя точка соединяется с первой).
*
* Заливка: для каждой строки берём крайние X контура (min/max) и
* заполняем между ними точно для выпуклых, для вогнутых = выпуклая
* оболочка по строкам (упрощение). Крайние X даёт брезенхэмовский
* проход рёбер (_bgi_poly_edge) без 32-бит арифметики.
*/
#include "../_bgi.h"
void fillpoly(int numpoints, const int *polypoints)
{
int i, j, y, ymin, ymax, xa, ya, xb, yb;
if (numpoints < 2) return;
ymin = 32767;
ymax = -32768;
for (i = 0; i < numpoints; i++) {
y = polypoints[2 * i + 1];
if (y < ymin) ymin = y;
if (y > ymax) ymax = y;
}
if (ymin < 0) ymin = 0;
if (ymax > GFX_HEIGHT - 1) ymax = GFX_HEIGHT - 1;
if (ymax < ymin) return;
for (y = ymin; y <= ymax; y++) {
_bgi_span_min[y] = 32767;
_bgi_span_max[y] = -32768;
}
for (i = 0; i < numpoints; i++) {
j = (i + 1) % numpoints;
_bgi_poly_edge(polypoints[2 * i], polypoints[2 * i + 1],
polypoints[2 * j], polypoints[2 * j + 1]);
}
_bgi_begin();
for (y = ymin; y <= ymax; y++)
if (_bgi_span_max[y] >= _bgi_span_min[y])
_bgi_fill_span(_bgi_span_min[y], _bgi_span_max[y], y);
_bgi_end();
/* Контур текущим цветом (замкнутый). */
for (i = 0; i < numpoints; i++) {
j = (i + 1) % numpoints;
xa = polypoints[2 * i]; ya = polypoints[2 * i + 1];
xb = polypoints[2 * j]; yb = polypoints[2 * j + 1];
_bgi_lineseg(xa, ya, xb, yb, _bgi_fg);
}
}
+70
View File
@@ -0,0 +1,70 @@
/*
* floodfill заливка связной области с (x,y) до границы цвета border.
*
* Скан-строчный (span) flood: заполняем горизонтальный отрезок текущим
* цветом заливки, затем ищем новые «затравки» в строках сверху/снизу.
* Стоп-условие пикселя: вне экрана / цвет == border / цвет уже == цвет
* заливки (иначе не сойдётся). Паттерн в floodfill не применяется
* заливаем сплошным _bgi_fill_color (упрощение).
*
* Чтение/запись self-bracket (_bgi_read/_bgi_hspan): чуть медленнее,
* зато не держим DI на всю (возможно большую) заливку.
*
* Стек затравок ограничен FF_STACK; при переполнении лишние затравки
* отбрасываются (заливка может остаться неполной на очень «рваных»
* фигурах) компромисс по памяти.
*/
#include "../_bgi.h"
#define FF_STACK 300
static int ff_sx[FF_STACK];
static int ff_sy[FF_STACK];
static int ff_sp;
/* 1 = пиксель нельзя заливать (стоп): вне экрана / border / уже залит. */
static int ff_stop(int x, int y, int border)
{
int c;
if (x < 0 || x > _bgi_maxx || y < 0 || y > _bgi_maxy) return 1;
c = _bgi_read(x, y);
return (c == border || c == _bgi_fill_color);
}
static void ff_push(int x, int y)
{
if (ff_sp < FF_STACK) { ff_sx[ff_sp] = x; ff_sy[ff_sp] = y; ff_sp++; }
}
void floodfill(int x, int y, int border)
{
int xl, xr, xi, yy, d;
uint8_t fc = _bgi_fill_color;
if (ff_stop(x, y, border)) return;
ff_sp = 0;
ff_push(x, y);
while (ff_sp > 0) {
ff_sp--;
x = ff_sx[ff_sp];
y = ff_sy[ff_sp];
if (ff_stop(x, y, border)) continue;
for (xl = x; !ff_stop(xl - 1, y, border); ) xl--;
for (xr = x; !ff_stop(xr + 1, y, border); ) xr++;
_bgi_hspan(xl, y, xr - xl + 1, fc);
for (d = -1; d <= 1; d += 2) {
yy = y + d;
xi = xl;
while (xi <= xr) {
while (xi <= xr && ff_stop(xi, yy, border)) xi++;
if (xi > xr) break;
while (xi <= xr && !ff_stop(xi, yy, border)) xi++;
ff_push(xi - 1, yy); /* правый край найденного пробега */
}
}
}
}
+7
View File
@@ -0,0 +1,7 @@
/* getbkcolor — текущий фоновый цвет. */
#include "../_bgi.h"
int getbkcolor(void)
{
return _bgi_bg;
}
+7
View File
@@ -0,0 +1,7 @@
/* getcolor — текущий цвет рисования. */
#include "../_bgi.h"
int getcolor(void)
{
return _bgi_fg;
}
+9
View File
@@ -0,0 +1,9 @@
/* getfillsettings — прочитать текущий паттерн и цвет заливки. */
#include "../_bgi.h"
void getfillsettings(struct fillsettingstype *fillinfo)
{
if (!fillinfo) return;
fillinfo->pattern = _bgi_fill_pattern;
fillinfo->color = _bgi_fill_color;
}
+27
View File
@@ -0,0 +1,27 @@
/*
* getimage сохранить прямоугольник экрана в буфер bitmap.
* Формат: uint16 width, uint16 height, затем w*h байт (по строкам).
* Чтение raw в одной W3-скобке.
*/
#include "../_bgi.h"
void getimage(int left, int top, int right, int bottom, void *bitmap)
{
uint8_t *p = (uint8_t *)bitmap;
int w = right - left + 1;
int h = bottom - top + 1;
int x, y;
if (w <= 0 || h <= 0) return;
*p++ = (uint8_t)w;
*p++ = (uint8_t)(w >> 8);
*p++ = (uint8_t)h;
*p++ = (uint8_t)(h >> 8);
_bgi_begin();
for (y = 0; y < h; y++)
for (x = 0; x < w; x++)
*p++ = (uint8_t)_bgi_read_raw(left + x, top + y);
_bgi_end();
}
+10
View File
@@ -0,0 +1,10 @@
/* getlinesettings — прочитать текущий стиль/маску/толщину линий. */
#include "../_bgi.h"
void getlinesettings(struct linesettingstype *lineinfo)
{
if (!lineinfo) return;
lineinfo->linestyle = _bgi_line_style;
lineinfo->upattern = _bgi_line_pattern;
lineinfo->thickness = _bgi_line_thick;
}
+7
View File
@@ -0,0 +1,7 @@
/* getmaxcolor — максимальный индекс цвета (255 в 256-цветном режиме). */
#include "../_bgi.h"
int getmaxcolor(void)
{
return _bgi_maxcolor;
}

Some files were not shown because too many files have changed in this diff Show More