63 Commits

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 11:00:49 +03:00
snark13 a6e0aacc80 mdview2: план поддержки кодировок CP1251/KOI8-R/UTF-8 (ревизия 2)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 22:39:49 +03:00
snark13 13ef8d2fa5 mdview2: выровненная отрисовка таблиц с рамкой
Таблица обрабатывается как блок в два прохода:
- проход 1: границы блока + ОТРЕНДЕРЕННЫЕ ширины колонок (мерим тем же
  inline_scan, что и при отрисовке — невидимые маркеры стиля **/код не
  раздувают столбцы);
- проход 2: верхняя рамка ┌┬┐ → строки данных │ ячейка<pad> │ → разделитель
  заголовка ├┼┤ (из строки |---|) → нижняя рамка └┴┘.

Колонки выровнены по содержимому, рамка CP866 box-drawing. Широкая таблица
остаётся nowrap+hscroll. Ячейки разбиваются cell-итератором (без массивов
на стеке); пустой g_cells под измерение освобождается ручным флашем
pending-сегмента (флаг g_skip_flush в emit_seg, чтобы не флашить повторно).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 22:23:57 +03:00
snark13 68201dca44 mdview2: 4 улучшения рендеринга markdown
1. Экранирование пунктуации: \* \_ \` \[ … → литерал, маркером НЕ считается
   (CommonMark ASCII-punctuation). Напр. "**...FILE\***" → болд "...FILE*".
   Не действует внутри инлайн-кода.
2. [x] / [ ] — ровно один символ в квадратных скобках → болд (нестандартно,
   для читабельного отображения task-list checkbox-ов). Не внутри кода.
3. Соседние пункты списка: если следующий имеет МЕНЬШИЙ отступ (dedent),
   пустую строку между ними больше не подавляем — пункты визуально разделены.
4. Абзац с ведущим отступом: все его перенесённые строки получают такой же
   отступ (continuation-префикс из col пробелов, как у списка/цитаты).

Реализация — в обоих inline-сканерах (inline_scan/scan_join_stream) + ветках
index_lines; новый helper is_escapable() и leading_spaces().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 21:38:54 +03:00
snark13 50596e6a2a mdview2: прогрессивная загрузка (первый экран сразу, скролл во время индексации)
Препроцессинг 52КБ занимает ~10с; теперь UI не ждёт его завершения.
index_lines() кооперативно вызывает progress_tick() (без прерываний —
IM2 отложен):
- первый экран рисуется, как только готово ≥ VIEW_H строк (~0.3с);
- статус-бар показывает растущее число обработанных строк с многоточием
  ("L 1-30 / 247...");
- ↑↓ PgUp PgDn Home End (=последняя готовая страница) и F1 работают по
  УЖЕ готовым строкам (drawable_lines = [0..n_lines-2], т.к. последний
  сегмент ещё в g_cells до flush);
- Esc/F10 во время загрузки — корректный выход (прерывание индексации
  флагом g_abort + штатный unload_file/pal_reset/clrscr).

После каждого тика форсируем ре-маппинг W3 (cur_page=0xFF), т.к.
отрисовка/WINREST/BIOS могли сбить страницу, на которую опирается fb().

Временно: debug-индикатор времени обработки "t=Ns" в зоне имени файла
(по просьбе — следить за временем при дальнейших оптимизациях).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 16:46:57 +03:00
snark13 de11882d16 mdview2: снять рудиментные merge-гварды (11→10с)
После полной миграции на merge флаги g_cur_merged/g_next_merged всегда
равны 1 (все типы блоков строит форвард-сканер). Убраны сами флаги и ~30
гвардов `if (g_cur_merged)` в горячем посимвольном цикле — это и небольшое
ускорение (ветка на символ), и чистка.

Проверено замером: инлайн gc_put (макрос/inline) в этот регистро-нагруженный
цикл, наоборот, ЗАМЕДЛЯЕТ (~11→13с) из-за роста спиллов — оставлен функцией.
Вывод: per-char микрооптимизации здесь исчерпаны (asm-fb не помог, инлайн
навредил, снятие гвардов дало ~1с).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 16:06:48 +03:00
snark13 92c0a9a4a9 mdview2: merge препроцессинга в один проход (22→11с)
Раньше препроцессинг сканировал файл ДВАЖДЫ: index_lines считал точки
переноса (forward-сканеры scan_join_stream/inline_scan), а отдельный
render_line_to_cache заново сканировал каждый сегмент для сборки
(char,attr)-ячеек. Профилирование показало, что всё время — в этих двух
посимвольных проходах (bank I/O и cont-walk ≈ 0).

Теперь forward-сканер собирает ячейки в g_cells ПО ХОДУ единственного
прохода; emit_seg флашит ячейки предыдущего сегмента в кэш (lag-1),
последний — после цикла. Перенос строки усекает буфер до снимка на
последнем пробеле (g_ncells_at_space), continuation-сегменты получают
префикс (отступ списка / маркер цитаты 0xB3 / title только в 1-й строке
заголовка). Прямые типы (код verbatim, HR, таблица через nowrap-inline,
blank/fence) строят ячейки на месте.

Миграция шла по типам блоков с dual-verify (старый render строил эталон,
merge сверял ячейки) — найдены и согласованы расхождения forward-сканера
со старым рендером: backtick внутри эмфазиса = литерал; одиночный маркер
закрывает ЛЮБОЙ активный эмфазис; soft-join пропускает ведущие пробелы
строки-продолжения; хвостовые пробелы нерелевантны. Все типы дали 0
расхождений, после чего render_line_to_cache / cc_put / cc_fill /
handle_inline_marker и verify-каркас удалены (−185 строк).

Итог на 52КБ README: 22→11с (2×). Совокупно с прошлым коммитом 24→11с.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 15:41:29 +03:00
snark13 2f4eaf5b25 mdview2: ускорение препроцессинга — asm fb() + инлайн рендер-цикла
Профилирование (52КБ README, раздельный замер): препроцессинг 24с, из них
bank I/O ~0с, cont-walk ~1с — всё время в посимвольных скан-циклах
(индексация 8с + рендер 13с, два прохода по файлу).

- fb() переписана на ассемблере (была закомментированная заготовка): убран
  вызов функции и 32-битная арифметика на каждый байт. Раскладка __sdcccall(1)
  для uint32 аргумента (p=HLDE) и возврат char в A сверены через sdcc -S;
  координация маппинга с bank_read держится на том, что _io_page_w3 — порт
  (__sfr 0xE2), пишем OUT — bank_read читает IN.
- render_line_to_cache: горячий путь обычного текста больше не вызывает
  handle_inline_marker (6 аргументов) на каждый символ — только на маркерах
  ` * _ ~; cc_put заинлайнен. Поведение идентично.

Итого 24→18с. Индекс-скан (8с) лёгкого инлайна не имеет (нет per-char
вызовов). Следующий шаг — merge двух проходов в один.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 11:23:21 +03:00
snark13 138e783e72 mdview2: горизонтальный скролл по типу блока + фиксы fence/HR/SDCC
Горизонтальный скролл (Фаза 5, финал):
- Скроллим по ТИПУ, а не по длине: новый флаг IF_HSCROLL ставится только
  на код и таблицы; HR и границы fence (IF_NOWRAP без IF_HSCROLL) не
  двигаются. Блок едет целиком, включая строки короче 80.
- Обход бага кодогенерации SDCC z80: `if (n!=g) g=n;` пишет (n-g) вместо n
  (SUB сравнения затирает A, store переиспользует испорченный A). Лечится
  записью viewport_x ДО сравнения. Минимальный репродьюсер и оба описания
  для трекера — в docs/bugs/sdcc-z80-cmp-store-a/ (воспроизводится на чистом
  sdcc 4.5, в т.ч. с --no-peep → это кодогенератор, не peephole).

Рендеринг:
- Отступленный fence (```c внутри списка) теперь распознаётся: is_fence_raw
  пропускает ведущие пробелы/табы; то же в рендере прячет строку-границу.
- Строки-разделители (HR, ровно 80) больше не участвуют в скролле.

Чистка: удалён мёртвый код (is_fence_delim, get_init_style[_raw], is_cont,
seg_flags). Makefile (mdview/mdview2): iconv UTF-8→CP866 завершается ненулевым
кодом при отбрасывании символов — игнорируем (|| true).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 10:11:27 +03:00
snark13 eab5a2d6ac mdview2: add render-cache markdown viewer (Phases 0-4)
New examples/mdview2 — render-cache version of mdview: each logical
line is rendered into an EMM (char,attr) buffer once during file load
(interleaved with index_lines()), then scrolling draws straight from
the cache via ESTEX WINREST, with no re-parsing or fb() access in the
steady state. Horizontal scroll still uses the old live-render path
(Phase 5, not yet migrated).

Format and budget were derisked empirically first (tests/winrest):
confirmed ESTEX WINCOPY/WINREST buffer layout and measured EMM free
space, both folded into the implementation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 22:41:32 +03:00
snark13 1b39a60ff0 tests: add hello2, simple, banktest smoke tests
hello2 — stdio vs conio output API comparison (fast/no-attr vs
slower/attributed), also exercises textcolor/textbackground.
simple — packed 2-bit style array bit-twiddling smoke test.
banktest — __banked function calls across two explicit banks in huge
memory mode.

hello2.c updated to call the renamed gettextmode/settextmode (see the
conio rename in 5e5e70d).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 17:12:19 +03:00
snark13 8cfdcf307a libc: add mem_bios/mem_estex EMM page-allocation wrappers
mem_alloc_pages/mem_free_block/mem_get_page/mem_info, implemented twice:
mem_estex.c via ESTEX $3C-$3E (INFOMEM/GETMEM/FREEMEM), mem_bios.c via
the BIOS $C0/$C2-$C4/$C6 EMM_FN* calls. lib/Makefile already listed both
under LIBC_C.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 17:09:14 +03:00
snark13 49a87b4d02 libc: add Sprinter palette API (graphics + text planes)
palette.h/video/palette.c wrap BIOS $A4 PIC_SET_PAL/PIC_GET_PAL and $A6
SET_PAL_INIT for the 8 palette pages (0-3 graphics, 4-7 text planes).
gfx_palette.c and conio/text_palette.c are thin per-domain wrappers;
text_palette.c maps the text "plane" 0..3 (paper/ink/blink-paper/
blink-ink) onto BIOS pages 4..7. Covered by tests/text_palette.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 17:07:55 +03:00
snark13 5e5e70d0c8 conio: switch cursor get/set to BIOS calls, rename text-mode accessors
_get_cursor/_set_cursor now use BIOS GetCursor/SetCursor (RST 8, 08Eh/084h)
instead of ESTEX CURSOR/LOCATE; cursor position packed into a two_bytes
union. get_videotextmode/set_videotextmode renamed to gettextmode/settextmode.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 17:07:16 +03:00
snark13 016fedd94c mdview: speed up rendering via bios/text.h, fix wrap-boundary style bug
Replace per-character wrchar() loops in render_line/fill_row/help screens
with batched BIOS calls (bios_writeattr/bios_fillcharattr), and cache the
per-line index record (idx_get) instead of refetching it ~13 times per
rendered line — each refetch cost two W3 bank switches via bank_read().

Also fixes a pre-existing indexer bug: when word-wrap pushes a token that
starts with an emphasis/code marker (e.g. `_text_`) onto a new line, the
marker got re-scanned a second time during the wrap continuation, flipping
line_style back off and corrupting the rendered attribute of the next
word. Fixed by snapshotting line_style at the last seen space and rolling
back to that snapshot (both the saved continuation style and the live
scan state) when a wrap is taken.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 17:06:35 +03:00
snark13 c9d5ea35a4 libc: add bios/text.h BIOS text-output API
Direct __naked z80 wrappers over BIOS LP_PRINT_*/LP_SET_PLACE/LP_GET_PLACE
(RST 8, opcodes 081h-08Eh) for fast text rendering — a single BIOS call
replaces a wrchar() loop for constant-attribute/constant-character runs.
Covered by tests/bios_text.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 17:06:13 +03:00
snark13 bd116d7361 ChangeLog:
- update.
2026-06-10 10:41:03 +03:00
snark13 858e5755ad ChangeLog:
- big commit.
2026-06-10 10:35:48 +03:00
snark13 f87b52bb7f ChangeLog:
- memory optimized version.
2026-06-08 15:34:36 +03:00
snark13 07c4004bd9 ChangeLog:
- commit current version MDView.c
2026-06-08 11:22:15 +03:00
snark13 6992c1436e docs(mdview): добавить описание работы и тестовые паттерны
Co-Authored-By: Oz <oz-agent@warp.dev>
2026-06-07 22:39:14 +03:00
snark13 07c398a560 ChangeLog:
- Russian commentaries.
2026-06-07 22:01:13 +03:00
snark13 4bed9d3f3f fix(mdview): корректный multiline quote join в render_line
- исправлен rewind до первого non-cont сегмента для continuation
- для quote-потока newline обрабатывается как soft join с пропуском сырого ' > ' маркера
- восстановлен quote-префикс на continuation строках

Co-Authored-By: Oz <oz-agent@warp.dev>
2026-06-07 20:26:19 +03:00
snark13 463a058f56 mdview: multiline quote paragraphs with quote-aware joining
- Quote blocks now join consecutive quote lines into one paragraph with
  soft breaks and wrap continuation under quote prefix.
- Empty quote lines ('>' / '>   ') act as quote-paragraph separators.
- Nested quote starts ('> > ...') are not merged into the parent text and
  stay separate rows.
- Keeps existing inline emphasis handling inside quote content.

Co-Authored-By: Oz <oz-agent@warp.dev>
2026-06-07 19:24:59 +03:00
snark13 8abc1d6c16 mdview: drop printf from status rendering
Replace status-line printf formatting with compact manual decimal rendering
(u16/u8 right-aligned helpers + wrchar/put_str_attr). This removes runtime
printf usage from mdview.c and keeps fixed status columns without stale digits.

Co-Authored-By: Oz <oz-agent@warp.dev>
2026-06-07 19:19:13 +03:00
snark13 982af12710 mdview: multiline list items with lazy continuation and blank-line grouping
- List items (UL/OL) can now span multiple source lines: non-marker
  lines are joined into the current item as lazy continuation.
- Continuation-line leading indentation is trimmed before joining so
  wrapped item text is separated by a single space.
- A single blank line between adjacent list markers is suppressed
  (same visual list), while 2+ blank lines still produce a separator.

Co-Authored-By: Oz <oz-agent@warp.dev>
2026-06-07 19:13:02 +03:00
snark13 394ee3a2cd mdview: blank-row block separation; tables nowrap; fix continuation marker
- Paragraph scanning no longer swallows the blank line separating it from
  the next block; runs of blank lines collapse to one row. Restores blank
  separation between paragraphs, headers and horizontal rules.
- Detect table rows (first non-space char '|') as nowrap segments; they are
  no longer merged into surrounding text or each other.
- Continuation (wrapped) rows render content as plain text and are no longer
  re-classified, so a wrapped word starting with '-'/'#'/'>' is not mis-drawn
  as a list/heading/quote marker.

Co-Authored-By: Oz <oz-agent@warp.dev>
2026-06-07 12:50:30 +03:00
snark13 47c9cd326a mdview: wide-break '\\', strikethrough, horizontal scroll bounds + '<'
- A trailing backslash before a newline now forces an in-paragraph line
  break (like two trailing spaces); render consumes the marker (non-code).
- Add ~~strikethrough~~ inline style (INIT_STYLE_STRIKE / EM_STRIKE),
  parsed in inline_scan, the paragraph merger and render, mirroring **.
- Horizontal pan is bounded by the widest nowrap segment on screen, and a
  '<' indicator marks hidden content off the left edge.

Co-Authored-By: Oz <oz-agent@warp.dev>
2026-06-07 11:55:19 +03:00
snark13 ca5f30b332 mdview: move segment index to EMM bank; inline hot byte reads
- Index is now an 8-byte record per visible segment in a dedicated EMM
  block (idx_get/idx_put), freeing ~11 KB of near RAM and lifting the
  old 2048-line cap (dynamic max_lines = index_pages * 2048).
- The per-byte scan keeps the previous segment offset in a near var
  (cur_seg_off) and mirrors the last record (cur_rec), so it never reads
  the index back from the bank.
- fb()/map_page() are inlined now that there is code headroom, removing
  per-byte call + 32-bit argument marshalling overhead.

Co-Authored-By: Oz <oz-agent@warp.dev>
2026-06-07 11:49:36 +03:00
snark13 0ad0559fc8 mdview: faster file-byte access (fb byte-decode + page cache)
Decode page/offset from the 32-bit offset's bytes to avoid SDCC z80
32-bit shift/mask helpers on the hot path; map_page() uses a cached
file_phys[] table to skip mem_get_page() on every W3 swap.
Behaviour-preserving.

Co-Authored-By: Oz <oz-agent@warp.dev>
2026-06-07 01:00:49 +03:00
snark13 035d93ab51 mdview: fix fenced code block emphasis leak and carry emphasis across wrap lines
- Add fenced code block tracking to the wrap-pass in index_lines().
  line_style is reset to PLAIN at every ``` delimiter, and all segs
  inside a fenced block get init_style=PLAIN. This prevents emphasis
  markers (e.g. _ in __var) inside code blocks from leaking into later
  normal text.
- Also carry init_style across wrap continuation segs so that a long
  bold/italic line that is wrapped continues with the correct style on
  the next segment.
- The fence bitmap pass now only updates in_code[], since init_style is
  already set correctly by the wrap pass.
2026-06-06 12:01:15 +03:00
381 changed files with 63928 additions and 6074 deletions
+3
View File
@@ -24,6 +24,9 @@ examples/*/*.cdb
examples/*/*.mem examples/*/*.mem
examples/*/*.rst examples/*/*.rst
# Temporary build directory for floppy disk image preparation
examples/*/.disk_tmp/
tests/*/*.exe tests/*/*.exe
tests/*/*.asm tests/*/*.asm
tests/*/*.lst tests/*/*.lst
+68
View File
@@ -0,0 +1,68 @@
# Sprinter C-Compiler — правила проекта
Target-слой SDCC 4.5 (z80) для компьютера Sprinter Sp2000: crt0,
линковка, libc, mkexe. Общение и комментарии — на русском.
## Сборка и проверка
```
make # tools + lib + все тесты (43) + examples
make -C lib # только libc → lib/sprinter.lib
make floppy # упаковать все .exe в mame/v306/IMG/mc.img
make size-check # размерный регресс: _CODE vs docs/size_baseline.tsv
make size-baseline # принять текущие размеры эталоном
```
Одиночный тест: `cd tests/<имя> && make run` (пакует ТОЛЬКО этот exe
+ EXTRA_DATA на дискету и запускает MAME). Тесты в MAME гоняет
пользователь — готовь дискету и проси прогнать.
После правок libc: пересборка от чистого листа (`make -C lib 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.
- Имя файла = имя функции. lib/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 в lib/build/, дамп, репро) — см.
memory/defer_unexplained_quirks.
## Структура
- `libc/<area>/*.c` — модули libc; `libc/include/` — публичные заголовки
- `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 несовместим — только как образец)
+17 -5
View File
@@ -13,12 +13,15 @@
# 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 dec_test gets stest2 winrest \
bios_text text_palette \
gfx_demo gfx_d16 gfx_text gfx_mous gfx_dbuf
# 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
@@ -33,7 +36,8 @@ DATA_FILES := \
tests/seek/big.txt \ tests/seek/big.txt \
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
@@ -66,6 +70,14 @@ 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 lib clean
+1 -2
View File
@@ -52,7 +52,7 @@ pages by program size — small programs get only one page. Pick a memory mode
on what your program needs: on what your program needs:
| Mode | Code lives in | Banking | Use when | Note | | Mode | Code lives in | Banking | Use when | Note |
|---|---|---|---| |---|---|---|---|---|
| `tiny` (default) | W2 (0x8100+) | no | code+data < 14 KB | | | `tiny` (default) | W2 (0x8100+) | no | code+data < 14 KB | |
| `small` | W1-W2 (0x4100+) | no | code+data < 30 KB | | | `small` | W1-W2 (0x4100+) | no | code+data < 30 KB | |
| `big` | W2 + W1 banking | yes (W1) | tiny + extra code modules | | | `big` | W2 + W1 banking | yes (W1) | tiny + extra code modules | |
@@ -217,7 +217,6 @@ This repository contains:
* **Sprinter / Peters Plus** — Иван Мак, Дмитрий Паринов and the original team * **Sprinter / Peters Plus** — Иван Мак, Дмитрий Паринов and the original team
* **SDCC** — for the underlying Z80 compiler * **SDCC** — for the underlying Z80 compiler
* **z88dk +pps** — Дмитрий M. for paving the way with the first Sprinter target
* **MAME** — for the Sprinter Sp2000 emulation * **MAME** — for the Sprinter Sp2000 emulation
--- ---
+6 -1
View File
@@ -51,7 +51,12 @@ 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):
+6
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
@@ -82,6 +85,7 @@ 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
# ------- Parse args ---------------------------------------------------------- # ------- Parse args ----------------------------------------------------------
usage() { usage() {
@@ -105,6 +109,7 @@ 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;;
--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;;
@@ -272,6 +277,7 @@ run "$SDASZ80" -o "$HEAP_TOP_REL" "$HEAP_TOP_SRC"
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" "${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"
+150 -433
View File
@@ -1,436 +1,153 @@
# 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) - [ ] **П6/железо**: MAME-смоук всех тестов после libc-сплита (conio,
- [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` ptime, stattest, mouse, gfx_demo/gfx_d16/gfx_text/gfx_mous —
- [x] `libc/mem/bank_io.c` — HOME-резидентные `bank_read`/`bank_write`/`bank_load_byte`/`bank_store_byte` со свопом W3 внутри трогался asm акселератора); затем прогон на реальном Sprinter
- [x] `examples/malloc_test/` — проверка SDCC's malloc (~210 64-байтных allocations через всю heap) (mdview2 + FILE* v2 + fdmax — подтвердить лимит 8 манипуляторов
- [x] `examples/mem_test/` — проверка page allocator: 3 страницы, разные паттерны через bank_write, верификация через bank_read и зависание DSS на 9-м OPEN)
- [ ] Мигрировать оставшиеся examples на sprinter-cc вместо ручных
## Этап 6 — argv parsing + sprinter-cc wrapper ✅ ГОТОВО Makefile (косметика)
- [ ] check_banks.py: разбивка size = code + const + bss per bank
- [x] crt0 парсит ESTEX command-line из IX-prefix (inline asm в `runtime/crt0.s`)
- [x] Strip leading CP/M-style space (DSS quirk) ## Auto-banking (memory/banking_roadmap.md)
- [x] Передача `argc`/`argv` в main() через HL/DE (SDCC __sdcccall(1) ABI)
- [x] argv[0] = basename .EXE через ESTEX APPINFO ($47 subfn 2) Phase 1 — file-level bin-packing (`toolchain/auto_bank.py`) — когда
- [x] `runtime/crt0_minimal.s` — opt-out для очень маленьких программ проект перерастёт ~30 KB кода: парсинг размеров из .rel/.map,
- [x] `runtime/crt0_banked.s` — теперь тоже парсит argv (parse_argv + get_progname скопированы из crt0.s; будет factored в argv.s когда возьмёмся за libsprinter.lib) first-fit-decreasing, уважение `#pragma codeseg BANKn`, перелинковка,
- [x] Bash-обёртка `bin/sprinter-cc`: `sprinter-cc -o foo.exe foo.c` одной строкой печать плана. Phase 2-5 (rebalance, banks.toml, function-level) —
- [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) ✅ ГОТОВО ## ОБЯЗАТЕЛЬНОЕ ДЛЯ 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. ### Turbo-C-style graphics API (BGI-like)
- [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 горизонтальных пикселя). Расширить `gfx_*` до уровня Turbo-C `<graphics.h>`: initgraph/
- [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. closegraph/cleardevice-алиасы, current point (moveto/lineto/linerel),
setcolor/setbkcolor, getpixel, circle/arc/ellipse/pieslice, drawpoly/
См. memory/sprinter_graphics.md, sprinter_accelerator.md, sprinter_graphics_16.md, sprinter_font_format.md. fillpoly, floodfill, bar/bar3d, outtext[xy]/settextstyle/textwidth,
imagesize/getimage/putimage (COPY/XOR/AND/OR/NOT_PUT), setviewport/
Открытые мелочи (не блокируют): клиппинг, setactivepage/setvisualpage (2 страницы есть), setlinestyle.
- [ ] Шрифт-quad для 640: per-cell палитра (mode 0x82 разрешает 1 из 4 палитр per 16×8 cell) — через прямой доступ к area-описания экрана 0x0300..0x039F Acceptance: типичная BGI-программа переносится без существенных
правок. Референс: Turbo C 2.x BGIDEMO.
## Auto-banking (см. `memory/banking_roadmap.md` для деталей)
### IM2 Interrupt Handlers
Phase 1 — file-level bin-packing — реализовывать когда проект перерастёт ~30 KB кода.
User-ISR через IM 2 — timer ticks, музыка (AY/COVOX), real-time
- [ ] `toolchain/auto_bank.py`: игры, async input. Решение: отдельный memory mode `--memory im2`.
- Парсит размеры из `.rel`-файлов (или из .map после dry-run link'а) Полный research/design: docs/im2_isr_design.md (vector 0xFF общий,
- First-fit-decreasing bin-packing disambiguation по портам 0x19/0xFE; таблица/ISR/стек в W2; chain к
- Уважает `#pragma codeseg BANKn` как manual override DSS-хендлеру обязателен).
- Перелинковывает с новыми `-Wl-b_BANKn=...` параметрами
- Печатает план распределения ### Прочее v2
Phase 2-5: incremental rebalance, declarative `banks.toml`, function-level, call-graph-aware. Только если/когда понадобится. - [ ] **Audio API** — AY-3-8910 + COVOX (требует IM2)
- [ ] **ISA-8 slot support** — ZX-Bus карты (требует IM2)
## Bank-local static data (mutable data в том же банке что и код) — ✅ ГОТОВО
## GFX: расширения по accelerator_doc.txt
- [x] Пример `examples/bank_local_data/` — функция в BANK1 со своим writable BSS array + const table + malloc-тест
- [x] `mkexe -p 0` для нулевого padding банков (BSS-storage обнуляется при загрузке) Quick wins:
- [x] Канонический рецепт: `--codeseg BANK1 --constseg BANK1 --dataseg BANK1` для bank1.c + `-Wl-b_BANK1=0x1C000` для линковки. **`--dataseg BANK1` РАБОТАЕТ** — раньше казалось обратное из-за trampoline bug который маскировал результат. - [ ] block-size через `LD A,(nn)` вместо SMC (док разрешает LD A,(HL/BC/DE))
- [x] **Критичный фикс trampoline'a в runtime/bank.s** — старый `pop af; out (n), a` клобберил A → все banked-функции возвращающие uint8_t тихо возвращали мусор. Новый `pop bc; out (c), b` сохраняет A. - [ ] кэширование block-size между burst'ами (accel помнит размер)
- [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) - [ ] `gfx_blit` / `gfx_blit_transparent` — block copy (LD L,L / LD A,A),
прозрачность через bank 0x58 («FF is transparent»)
Зачем: для модулей с большим private state (level loader, audio engine, scene data). Экономит W2 heap для динамики, а статика остаётся в бэке. - [ ] `gfx_xor_rect` / `gfx_or_rect` / `gfx_and_rect` / `gfx_invert_rect`
- [ ] шрифты ≠ 8×8: gfx_set_font_data(ptr,w,h,advance), proportional,
## Подсказки из solid-c (нативный Sprinter C — `third_party/solid-c/`) 8×16/16×16, отдельный font_id API; font-quad для 640×256
(per-cell палитра через дескрипторы 0x0300..0x039F)
После анализа solid-c'овской libc (см. `memory/solid_c_findings.md`) выявлены готовые паттерны для следующих недостающих функций. Приоритет от **высокого** к низкому:
Оптимизации (не сейчас):
### High-priority gaps (легко портировать, большая польза) - [ ] gfx_line через accel для пологих диагоналей (runs ≥ 4-5 px)
- [x] **`errno` + `strerror`/`perror`** — табличка 32 ошибок (libc/io/errno.c) - [ ] композитные примитивы с одним W3-swap на операцию
- [x] **Расширенный `open()`** для O_CREAT/O_TRUNC/O_APPEND/O_EXCL state machine
- [x] **`atexit`** — 8-callback LIFO + `exit()` + `_exit()` (libc/io/atexit.c) ## Прочий backlog
- [x] **`setjmp`/`longjmp`** — 6-байт jmp_buf={sp,ix,pc} (libc/io/setjmp.c)
- [x] **`sleep(seconds)`** — 50Hz halt-loop (libc/io/sleep.c) - [ ] factoring parse_argv из crt0/crt0_banked в общий argv.s
- [x] **ESTEX ENV API** ($46, getenv/putenv) — libc/io/env.c. Учли doc-bug: реально A=0 это NOT FOUND - [ ] `restore SP on EXIT` (паттерн z88dk +pps) — проверить нужность
- [ ] CI: MAME с -aviwrite для screenshot-сравнения без человека
### Medium-priority (нужно для shell-like утилит) - [ ] linker duplicate-symbol warnings: сейчас фильтруются в
- [ ] **Mouse driver**`rst $30h`, 17 функций. **Сначала тест что работает в MAME**. sprinter-cc (наши overrides _puts/___sdcc_heap/_asctime/…);
- [x] **`ffirst`/`fnext` + ffblk_t struct** для directory listing — реализовано, demo: ls.exe радикально — --nostdlib с ручным списком модулей z80.lib
- [x] **`getdatetime`/`setdatetime`** через ESTEX $21/$22 — libc/io/time.c, demo: time_dir_test - [ ] ZX Spectrum-совместимый target; ZX-Bus драйверы; PGO-tools
- [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
- [ ] **Port_Y banking trick** (адреса 0xC000+0x400*N → строки
### Low-priority — ✅ FILE* stack ГОТОВО Y..Y+15): в MAME 0.283 НЕ работает (2026-06-01). На железе:
dual-write тест → если работает, кэшировать Port_Y в putpixel
- [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`. (~8× меньше OUT для Брезенхэма); если нет — вычистить из доков.
- [ ] fprintf / fscanf — нужна printf-через-callback machinery. Пока пользователь может `sprintf(buf, ...) + fputs(buf, fp)`. - [ ] fdmax: лимит манипуляторов и зависание 9-го OPEN — MAME vs железо.
- [ ] Опциональный buffered mode (setvbuf, line/block buffering) — если когда-то понадобится.
## Known quirks (зафиксированы, обходы в libc)
### POSIX time API — ✅ ГОТОВО
- [x] `libc/io/posix_time.c` — time/localtime/gmtime/mktime/asctime/ctime поверх getdatetime. SDCC's time.rel избегаем (нельзя override _RtcRead). См. `examples/ptime`. - ESTEX $46 ENV: A=0 это NOT FOUND (док врёт) — memory/sprinter_platform
- ESTEX WRITE $14: на успехе DE=0, не счётчик; успех = CF=0 & A=0 —
### sys/stat — ✅ ГОТОВО memory/estex_write_de_quirk
- [x] `libc/io/stat.c` — POSIX stat/fstat. Гибрид open+fstat для файлов, ffirst+iter для папок (включая "."/".."). См. `examples/stattest` и `memory/estex_ffirst_dotdot.md`. - DSS: 8 манипуляторов, 9-й OPEN вешает систему; _fd_guard в libc —
memory/dss_fd_limit
### assert — ✅ ГОТОВО (используем SDCC's __assert через fallback include path) - SDCC z80 `if(n!=g)g=n;` пишет (n-g) — memory/sdcc_z80_cmp_store_a_bug
## 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.) ## Этап 10 — libc: сплит + FILE v2 + Solid-C (2026-07-05/06) ✅
- `<ctype.h>` (toupper/tolower)
- `<math.h>` (sinf/cosf/sqrtf/etc.) Полный план/итоги: docs/libc-roadmap.md. Кратко:
- вся libc разложена «1 функция = 1 модуль» (~250 модулей, wildcard-
Линкер автоматически тянет из z80.lib когда нужно. **НЕ переписывать**. сборка, DCE на уровне файлов): gfx_text 4.4 КБ, timedir/ls/stattest
−3 КБ и т.д.; правила asm-связок: docs/libc-split-asm-cases.md
Наши Sprinter-specific обязательные модули остаются: atexit, env, errno, setjmp, putchar/puts/getchar, conio, fsdir, time, mouse, open/read/lseek/close. - **FILE* v2 (B+)**: ленивый буфер 512 на чтение/запись с
автопереключением, таблица OPEN_MAX=8, flush-on-exit, ungetc,
## Build-system: libsprinter.lib + sprinter-cc — ✅ ГОТОВО fprintf/vfprintf, fdopen/freopen/fclosall/fgetpos/fsetpos; горячие
пути fgetc/fputc/fgets на asm (fgets 100 КБ: 144с unbuffered-оценка
- [x] `lib/Makefile` — собирает каждый libc/*.c в `.rel`, архивирует через sdar в `lib/sprinter.lib` → ~1 с). Дизайн: docs/file-buffering-design.md
- [x] Включает runtime/bank.s и runtime/heap.s (auto-pulled при __banked/malloc) - **scanf/fscanf/sscanf** — своё C-ядро (в SDCC z80 нет)
- [x] `bin/sprinter-cc` — bash-wrapper: `sprinter-cc -o foo.exe foo.c` одной строкой - **Solid-C совместимость закрыта**: <dos.h> (даты/диски/absread),
- [x] Поддержка опций `--crt0=default|minimal|banked`, `--bank N=FILE.c`, `-I`, `-L`/`-E`/`-S`, `-Wl`, `--mkexe` errno-алиасы, <sprinter_solid.h> — docs/solid_c_compatibility.md
- [x] `examples/hello_sccc/` — демо: `hello.c` собирается за один shell-вызов, размер совпадает с ручным Makefile (925 байт) - гигиена: stale .rel чистка, все 43 теста в make all, размерный
- [x] Split `putchar.c``putchar.c` + `puts.c` для per-function granularity (puts override SDCC's z80.lib version) регресс (make size-check), контракт заголовков docs/libc-headers.md
- [x] Включён в `make all` (зависимость `lib` перед `examples`) - справочник API: docs/libc-reference.md
Возможные улучшения (опционально): ## Этап 9 — memory modes (tiny/small/big/huge/manual) ✅ 2026-05-30
- [ ] Мигрировать остальные examples на sprinter-cc вместо ручных Makefile (косметика)
- [ ] Дальнейшая декомпозиция libc/*.c per-function (но текущая granularity уже даёт нужный размер — линкер пакетует .rel целиком, и для большинства файлов это одна функция) `--memory MODE` в sprinter-cc; crt0-семейство (crt0/minimal/small/
banked); small: ESTEX GETMEM+SETWIN2 до gsinit, auto-detect W2 по
## Этап 9 — memory modes для sprinter-cc порту 0xC2; big/huge: параметризация crt0_banked/bank.s через
BANK_W1; --debug, --stack-size. Детали: memory/memory_modes_
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). implemented, memory/sprinter_memory_modes.
- [x] **`tiny`**: всё (CODE+DATA+стек) в W2. Default. Verified hello/argv/conio/malloc/file/etc. Дизайн-решения: одна sprinter.lib на все режимы (DCE per-member);
- [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. gfx.lib отдельно не нужен; libc_banked + sprinter_home.lib — идея
- [x] **`--memory-manual SPEC`**: парсит `CODE=W1|W2,DATA=W1|W2|SAME,BANKED=W1|W3`. Реализовано 2026-05-30. на потом (триггер: HOME забит user-кодом).
- [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. ## Этап 8 — графика ✅
- [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. 320×256×256 + 640×256×16, акселератор (Fill h/v, SMC block-size),
- [x] **`--debug` флаг**: prepend `DEBUG_RT = 1` в crt0 + `-DDEBUG_RT` в sdcc. Открывает symbol `_w2_self_allocated` (uint8_t) — runtime diagnostic кто аллоцировал W2. Реализовано 2026-05-30. Брезенхэм, bitmap font (WIN_GET_ZG, interleaved), gfx_text.
memory/sprinter_graphics*, sprinter_accelerator, sprinter_font_format.
### Дизайн-решения по libc и crt0
## Bank-local data ✅
**Одна `sprinter.lib`** работает для всех memory mode — `.rel`-члены relocatable, SDLD делает dead-code elimination per-member (без графики не подтягивает `gfx_core.rel` и т.д.). Verified hello vs malloc_test через map-файлы.
--codeseg/--constseg/--dataseg BANKn + mkexe -p 0; фикс трамплина
**`gfx.lib` отдельно — НЕ нужен**: dead-code elimination уже работает. (pop bc/out (c),b — сохраняет A); malloc из банка прозрачен (heap в
W2). memory/bank_local_data_pattern.
**`libc_banked` (libc в bank вместо HOME)** — идея на потом, когда HOME (16 KB) забит user-кодом + libc в `huge` mode. Реализуется через `--codeseg BANK0` при компиляции libc; trade-off: trampoline ~30 циклов на каждый libc-вызов. Триггер: реальная программа упрётся в HOME budget.
## Этапы 5-7 и ранняя libc ✅
**HW-зависимые модули — `sprinter_home.lib` отдельно.** Часть libc физически не может быть забанкована в W3, потому что она РАБОТАЕТ с W3:
- `gfx_*` — пишет в видеопамять `0xC000+` после swap W3 на video page - malloc/free (SDCC + runtime/heap.s в W2), page allocator
- `bank_io` (mem_alloc_pages/bank_read/bank_write) — swap'ит W3 через `OUT (0xE2)` (mem_alloc_pages, ESTEX $3C-$3E + BIOS $C4), bank_read/bank_write
- Будущие ISR — прерывание может прийти когда W3 на чём угодно - crt0 argv-парсинг (IX-prefix, CP/M-space quirk, APPINFO basename),
sprinter-cc wrapper со всеми опциями
В huge mode эти модули ДОЛЖНЫ остаться в HOME (W1). Когда будем делать `libc_banked`, **одновременно** выделяем `sprinter_home.lib` (HOME-only) из `sprinter.lib` (bankable). Финальная схема: - errno+strerror/perror, open state-machine, atexit, setjmp/longjmp,
``` sleep, ENV API ($46), ffirst/fnext, getdatetime/setdatetime,
sprinter_home.lib HOME-only: gfx, bank_io, ISR shims chdir/getcwd/mkdir/rmdir, conio (полный), mouse (RST 30h, 14 ф-й),
sprinter.lib bankable: printf, malloc, string, conio, stdio, env, ... POSIX time API, sys/stat, assert
sprinter_banked.lib тот же sprinter.lib но --codeseg BANK0 (для huge) - text I/O split (stdio fast / conio attr) — memory/text_output_api_split
``` - SDCC stdlib НЕ переписываем — memory/sdcc_stdlib_works
Триггер: реализация `--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:` строки) из вывода `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 × количество строк/столбцов экономии. Применимо ко всем композитным примитивам.
+127
View File
@@ -0,0 +1,127 @@
# [z80] `global = local` after `if (local != global)` stores `local - global` (A clobbered by comparison)
## Summary
On the z80 backend, the sequence
```c
if (n != g) { g = n; }
```
where `n` is in register `A` and `g` is a global, miscompiles. The compiler
evaluates the `!=` comparison with `SUB A,(HL)`, which destroys `A`, and then
emits the assignment `g = n` by storing `A` — which now holds `n - g`, not `n`.
As a result the global ends up holding `(unsigned char)(n - old_g)` instead of `n`.
It should either use `CP (HL)` (which leaves `A` intact) for the comparison, or
reload `n` before the store.
## Version
SDCC 4.5.0 #15242 (Mac OS X x86_64). Default options; also reproduces with
`--opt-code-speed` and with `--no-peep` (so this is a code-generator bug, not a
peephole-optimizer bug).
## Minimal reproducer
```c
unsigned char vx;
void update(unsigned char n)
{
if (n != vx) {
vx = n;
}
}
```
Build:
```
sdcc -mz80 -S repro.c
```
## Generated assembly (wrong)
```asm
_update::
;repro.c: if (n != vx) {
ld hl, #_vx
sub a, (hl) ; A (= n) is destroyed: A = n - vx
ret Z
;repro.c: vx = n;
ld (_vx+0), a ; stores (n - vx) instead of n
;repro.c: }
ret
```
With `--no-peep` the same defect is present (only the branch shape differs):
```asm
_update::
ld iy, #_vx
sub a, 0 (iy) ; A (= n) destroyed
jp NZ, 00112$
jp 00103$
00112$:
ld (_vx+0), a ; stores (n - vx)
00103$:
ret
```
## Why it happens
`n` arrives in `A` (sdcccall). The code generator picks `SUB A,(HL)` to evaluate
the relational `n != vx`. `SUB` overwrites `A` with the difference. The generator
then treats the still-live value `n` as if it were still in `A` and emits a bare
store `LD (_vx),A` for the assignment, without reloading `n` first. Because the
defect survives `--no-peep`, it is in code generation (register/lifetime tracking
across the comparison), not in the peephole optimizer.
The correct lowering for the comparison is `CP (HL)`, which sets the flags exactly
like `SUB` but preserves `A`, so the subsequent store would be correct with no
extra instructions.
## Variants that also reproduce
- `if (n == vx) return; vx = n;` (early-return form)
- `n` coming from a function call result instead of a parameter
- both `-mz80` default and `--opt-code-speed`
## Workaround
Store into the global *before* the comparison, so the destructive `SUB` is no
longer on the store path; compare a saved copy instead:
```c
void update(unsigned char n)
{
unsigned char old = vx;
vx = n; /* store first, A still holds n */
if (n != old) {
/* side effect */
}
}
```
generates the correct:
```asm
_update::
ld (_vx+0), a
ret
```
## Files in this directory
- `repro.c` — minimal reproducer
- `repro.asm` — generated output, default options (defect visible)
- `repro.nopeep.asm` — generated output with `--no-peep` (defect still present)
- `workaround.c` / `workaround.asm` — store-before-compare workaround (correct)
## Tracker search
A search of the SDCC bug tracker did not turn up an exact duplicate. The closest
version-matching report, #3834 "[Z80][SDCC 4.5] Compiler bug", is a *different*
defect (`genPointerSet`, swapped push/pop order), not this comparison-clobbers-A
case.
+127
View File
@@ -0,0 +1,127 @@
# [z80] `global = local` после `if (local != global)` пишет `local - global` (A затирается сравнением)
## Кратко
На бэкенде z80 последовательность
```c
if (n != g) { g = n; }
```
где `n` лежит в регистре `A`, а `g` — глобальная переменная, компилируется
неверно. Сравнение `!=` вычисляется через `SUB A,(HL)`, который **разрушает `A`**,
после чего присваивание `g = n` выполняется записью `A`а там уже `n - g`, а не
`n`. В итоге в глобальную переменную попадает `(unsigned char)(n - старое_g)`
вместо `n`.
Корректно было бы использовать для сравнения `CP (HL)` (он не меняет `A`) либо
перезагрузить `n` перед записью.
## Версия
SDCC 4.5.0 #15242 (Mac OS X x86_64). Опции по умолчанию; также воспроизводится с
`--opt-code-speed` и с `--no-peep` (то есть это баг кодогенератора, а не
peephole-оптимизатора).
## Минимальный пример
```c
unsigned char vx;
void update(unsigned char n)
{
if (n != vx) {
vx = n;
}
}
```
Сборка:
```
sdcc -mz80 -S repro.c
```
## Сгенерированный ассемблер (неверный)
```asm
_update::
;repro.c: if (n != vx) {
ld hl, #_vx
sub a, (hl) ; A (= n) разрушается: A = n - vx
ret Z
;repro.c: vx = n;
ld (_vx+0), a ; пишет (n - vx) вместо n
;repro.c: }
ret
```
С `--no-peep` дефект тот же (отличается лишь форма ветвления):
```asm
_update::
ld iy, #_vx
sub a, 0 (iy) ; A (= n) разрушен
jp NZ, 00112$
jp 00103$
00112$:
ld (_vx+0), a ; пишет (n - vx)
00103$:
ret
```
## Почему так происходит
`n` приходит в `A` (sdcccall). Кодогенератор выбирает `SUB A,(HL)` для вычисления
отношения `n != vx`. `SUB` перезаписывает `A` разностью. Далее генератор считает,
что ещё «живое» значение `n` по-прежнему в `A`, и для присваивания выдаёт голую
запись `LD (_vx),A`, не перезагрузив `n`. Поскольку дефект сохраняется при
`--no-peep`, он находится в кодогенерации (учёт регистров/времён жизни через
сравнение), а не в peephole-оптимизаторе.
Правильное преобразование сравнения — `CP (HL)`: он выставляет флаги ровно как
`SUB`, но сохраняет `A`, поэтому последующая запись была бы корректной без единой
лишней инструкции.
## Варианты, которые тоже воспроизводят
- `if (n == vx) return; vx = n;` (форма с ранним выходом)
- `n` как результат вызова функции, а не как параметр
- и `-mz80` по умолчанию, и `--opt-code-speed`
## Обходной путь (workaround)
Записывать в глобальную переменную **до** сравнения, чтобы разрушающий `SUB` не
оказался на пути записи; сравнивать сохранённую копию:
```c
void update(unsigned char n)
{
unsigned char old = vx;
vx = n; /* запись первой, A ещё держит n */
if (n != old) {
/* побочный эффект */
}
}
```
даёт корректное:
```asm
_update::
ld (_vx+0), a
ret
```
## Файлы в этом каталоге
- `repro.c` — минимальный пример
- `repro.asm` — вывод с опциями по умолчанию (дефект виден)
- `repro.nopeep.asm` — вывод с `--no-peep` (дефект сохраняется)
- `workaround.c` / `workaround.asm` — обход «запись до сравнения» (корректно)
## Поиск по трекеру
Поиск по баг-трекеру SDCC точного дубликата не нашёл. Ближайший по версии отчёт
#3834 «[Z80][SDCC 4.5] Compiler bug» — это **другой** дефект (`genPointerSet`,
переставленный порядок push/pop), а не данный случай «сравнение затирает A».
+61
View File
@@ -0,0 +1,61 @@
;--------------------------------------------------------
; File Created by SDCC : free open source ISO C Compiler
; Version 4.5.0 #15242 (Mac OS X x86_64)
;--------------------------------------------------------
.module repro
.optsdcc -mz80 sdcccall(1)
;--------------------------------------------------------
; Public variables in this module
;--------------------------------------------------------
.globl _update
.globl _vx
;--------------------------------------------------------
; special function registers
;--------------------------------------------------------
;--------------------------------------------------------
; ram data
;--------------------------------------------------------
.area _DATA
_vx::
.ds 1
;--------------------------------------------------------
; ram data
;--------------------------------------------------------
.area _INITIALIZED
;--------------------------------------------------------
; absolute external ram data
;--------------------------------------------------------
.area _DABS (ABS)
;--------------------------------------------------------
; global & static initialisations
;--------------------------------------------------------
.area _HOME
.area _GSINIT
.area _GSFINAL
.area _GSINIT
;--------------------------------------------------------
; Home
;--------------------------------------------------------
.area _HOME
.area _HOME
;--------------------------------------------------------
; code
;--------------------------------------------------------
.area _CODE
;repro.c:20: void update(unsigned char n)
; ---------------------------------
; Function update
; ---------------------------------
_update::
;repro.c:22: if (n != vx) {
ld hl, #_vx
sub a, (hl)
ret Z
;repro.c:23: vx = n;
ld (_vx+0), a
;repro.c:25: }
ret
.area _CODE
.area _INITIALIZER
.area _CABS (ABS)
+25
View File
@@ -0,0 +1,25 @@
/*
* Minimal reproducer — SDCC z80 codegen bug.
*
* "global = local" right after "if (local != global)" stores a WRONG value:
* the SUB used to evaluate the comparison clobbers register A (which still
* holds `n`), and the store reuses that clobbered A.
*
* Build (stock SDCC, no extra flags, no custom toolchain):
* sdcc -mz80 -S repro.c
*
* Tested with: SDCC 4.5.0 #15242 (Mac OS X x86_64), default options.
* Also reproduces with --opt-code-speed and with --no-peep
* (so it is a code-generator bug, not a peephole-optimizer bug).
*
* Expected semantics: after update(n), vx == n whenever n != vx.
* Actual result: vx == (unsigned char)(n - old_vx).
*/
unsigned char vx;
void update(unsigned char n)
{
if (n != vx) {
vx = n;
}
}
@@ -0,0 +1,64 @@
;--------------------------------------------------------
; File Created by SDCC : free open source ISO C Compiler
; Version 4.5.0 #15242 (Mac OS X x86_64)
;--------------------------------------------------------
.module repro
.optsdcc -mz80 sdcccall(1)
;--------------------------------------------------------
; Public variables in this module
;--------------------------------------------------------
.globl _update
.globl _vx
;--------------------------------------------------------
; special function registers
;--------------------------------------------------------
;--------------------------------------------------------
; ram data
;--------------------------------------------------------
.area _DATA
_vx::
.ds 1
;--------------------------------------------------------
; ram data
;--------------------------------------------------------
.area _INITIALIZED
;--------------------------------------------------------
; absolute external ram data
;--------------------------------------------------------
.area _DABS (ABS)
;--------------------------------------------------------
; global & static initialisations
;--------------------------------------------------------
.area _HOME
.area _GSINIT
.area _GSFINAL
.area _GSINIT
;--------------------------------------------------------
; Home
;--------------------------------------------------------
.area _HOME
.area _HOME
;--------------------------------------------------------
; code
;--------------------------------------------------------
.area _CODE
;repro.c:20: void update(unsigned char n)
; ---------------------------------
; Function update
; ---------------------------------
_update::
;repro.c:22: if (n != vx) {
ld iy, #_vx
sub a, 0 (iy)
jp NZ, 00112$
jp 00103$
00112$:
;repro.c:23: vx = n;
ld (_vx+0), a
00103$:
;repro.c:25: }
ret
.area _CODE
.area _INITIALIZER
.area _CABS (ABS)
@@ -0,0 +1,57 @@
;--------------------------------------------------------
; File Created by SDCC : free open source ISO C Compiler
; Version 4.5.0 #15242 (Mac OS X x86_64)
;--------------------------------------------------------
.module workaround
.optsdcc -mz80 sdcccall(1)
;--------------------------------------------------------
; Public variables in this module
;--------------------------------------------------------
.globl _update
.globl _vx
;--------------------------------------------------------
; special function registers
;--------------------------------------------------------
;--------------------------------------------------------
; ram data
;--------------------------------------------------------
.area _DATA
_vx::
.ds 1
;--------------------------------------------------------
; ram data
;--------------------------------------------------------
.area _INITIALIZED
;--------------------------------------------------------
; absolute external ram data
;--------------------------------------------------------
.area _DABS (ABS)
;--------------------------------------------------------
; global & static initialisations
;--------------------------------------------------------
.area _HOME
.area _GSINIT
.area _GSFINAL
.area _GSINIT
;--------------------------------------------------------
; Home
;--------------------------------------------------------
.area _HOME
.area _HOME
;--------------------------------------------------------
; code
;--------------------------------------------------------
.area _CODE
;workaround.c:4: void update(unsigned char n)
; ---------------------------------
; Function update
; ---------------------------------
_update::
ld (_vx+0), a
;workaround.c:8: if (n != old) {
;workaround.c:11: }
ret
.area _CODE
.area _INITIALIZER
.area _CABS (ABS)
@@ -0,0 +1,11 @@
/* Workaround: store BEFORE the comparison so the SUB that evaluates the
* comparison no longer sits on the store path. Generates correct code. */
unsigned char vx;
void update(unsigned char n)
{
unsigned char old = vx;
vx = n; /* store first, with A still holding n */
if (n != old) {
/* redraw / side effect */
}
}
+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*).
+46
View File
@@ -0,0 +1,46 @@
# Заголовки 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, gfx.h, mouse.h, palette.h,
sprinter*.h, unistd.h, bios/*.
## Правила
- новый стандартный заголовок — сначала пробовать паттерн 1
(include_next); паттерн 2 — только если надо переопределить
реализацию (как puts/putchar на ESTEX);
- в заголовках паттерна 2 — комментарий, какие декларации обслуживают
z80.lib;
- internal-заголовки libc (`_conio.h`, `_file.h`, `_gfx.h`, …) живут
РЯДОМ с исходниками в libc/<area>/, не в libc/include.
+194
View File
@@ -0,0 +1,194 @@
# 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)` | 50 halt/с по кадровому IRQ |
**Лимит: 8 одновременных манипуляторов**; 9-й OPEN вешает DSS —
libc отказывает сама (EMFILE, предохранитель _fd_guard).
## <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> — графика (0x81: 320×256×256; 0x82: 640×256×16)
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()`.
Примитивы (суффикс _256 / _16): `clear putpixel hline vline line
rect fill_rect` — через акселератор (hline/vline burst до 256 байт;
в 0x82 vline через RMW). Координаты int, клиппинг по краям.
Текст: `gfx_putchar256/16, gfx_text256/16` (8×8, fg/bg; в 0x82 x —
чётный), `gfx_load_default_font`, `gfx_set_font(ptr)` (interleaved
font[row*256+char]).
Палитра: `gfx_pal_load/set/get/get_color/reset` (страницы 0..3).
## <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] lib/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. Верификация после сплита
- [ ] `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) ведут на метки внутри своей же функции — сплиту не мешают.
+188
View File
@@ -0,0 +1,188 @@
# MDView: описание работы и тестовые паттерны
## Назначение
`mdview` — консольный просмотрщик Markdown для текстового режима 80x32.
Программа загружает `.MD`-файл, индексирует его на экранные сегменты и рендерит содержимое с учётом переносов, стилей и специальных блоков.
## Как работает программа
### 1) Загрузка файла
- На старте вызывается `load_file(path)`.
- Если путь не передан, используется `SAMPLE.MD`.
- При ошибках загрузки программа показывает причину и завершает работу после нажатия клавиши.
### 2) Индексация (`index_lines`)
- Файл разбирается в массив сегментов для быстрого рендера.
- Поддерживаются ключевые типы строк:
- заголовки `#..####`,
- маркированные/нумерованные списки,
- цитаты `>`,
- горизонтальные разделители,
- fenced code-блоки,
- таблицы (nowrap-режим),
- обычные параграфы.
- Для list/quote/plain используется общая логика сканирования с режимами склейки строк.
### 3) Рендер (`render_line`, `render_viewport`)
- Для каждой видимой строки рисуется префикс (маркер списка, цитаты и т.д.) и текст.
- Поддерживаются inline-стили:
- `**жирный**`,
- `*курсив*`,
- `_подчёркнутый_`,
- `~~зачёркнутый~~`,
- `` `code` ``.
- Табы расширяются до фиксированного шага.
- Для длинных nowrap-строк применяется горизонтальный сдвиг и индикаторы `<`/`>`.
### 4) Навигация
- `Up/Down` — прокрутка на 1 строку.
- `PgUp/PgDn` — прокрутка на экран.
- `Home/End` — начало/конец документа.
- `Left/Right` — горизонтальная прокрутка nowrap-контента.
- `Esc` или `F10` — выход.
## Сборка и запуск
```bash
make -C examples/mdview
```
Запуск на целевой системе:
```bash
mdview.exe <путь_к_файлу.md>
```
Если путь не указан, открывается `SAMPLE.MD`.
## Тестовые паттерны
Ниже набор паттернов для ручной регрессии.
### P01 — Базовый smoke-тест открытия
Вход:
- валидный markdown-файл среднего размера.
Ожидается:
- файл открывается без ошибок;
- статус-бар и меню отображаются корректно;
- прокрутка работает.
### P02 — Ошибка открытия файла
Вход:
- несуществующий путь к файлу.
Ожидается:
- сообщение `mdview: cannot load file`;
- корректный код причины (`open failed` и т.п.);
- ожидание клавиши перед выходом.
### P03 — Пустой файл
Вход:
- пустой `.md`.
Ожидается:
- сообщение `mdview: empty file`;
- корректное завершение после нажатия клавиши.
### P04 — Обычный параграф и переносы
Шаблон:
```md
Это длинный абзац для проверки переноса строк в обычном тексте.
Вторая строка должна мягко склеиться с первой.
```
Ожидается:
- строки склеиваются как единый параграф;
- переносы происходят по ширине экрана без потери символов.
### P05 — Hard break в параграфе
Шаблон:
```md
Первая строка с двумя пробелами в конце.
Вторая строка после hard break.
```
Ожидается:
- между строками сохраняется принудительный разрыв;
- inline-стиль не ломается.
### P06 — Многострочный список с lazy continuation
Шаблон:
```md
- Пункт списка, который продолжается
на следующей строке с отступом.
```
Ожидается:
- continuation-строка склеивается с пунктом через один пробел;
- лишние ведущие отступы continuation не попадают в итоговый текст;
- переносы не ломают маркер списка.
### P07 — Многострочная цитата
Шаблон:
```md
> Первая строка цитаты
> Вторая строка цитаты
> Третья строка цитаты
```
Ожидается:
- для продолжений корректно повторяется quote-префикс;
- сырой символ `>` из исходника не «просачивается» в середину текста.
### P08 — Пустая quoted-строка как разделитель
Шаблон:
```md
> Первый параграф цитаты
>
> Второй параграф цитаты
```
Ожидается:
- пустая quoted-строка разделяет два параграфа;
- рендер не объединяет их в один поток.
### P09 — Таблица в nowrap-режиме
Шаблон:
```md
| col1 | col2 | col3 |
|------|------|------|
| очень длинное значение | очень длинное значение | очень длинное значение |
```
Ожидается:
- строка таблицы не переносится по словам;
- работает горизонтальный сдвиг `Left/Right`;
- индикаторы `<`/`>` показывают скрытый контент.
### P10 — Fenced code-block
Шаблон:
~~~md
```text
**это не жирный**
_это не подчёркнутый_
```
~~~
Ожидается:
- внутри code-блока inline-разметка не применяется;
- текст отображается как литерал.
### P11 — Inline-стили на обычном тексте
Шаблон:
```md
Текст с **жирным**, *курсивом*, _подчёркнутым_, ~~зачёркнутым~~ и `code`.
```
Ожидается:
- все перечисленные стили рендерятся корректно;
- литеральные случаи вроде `2 * 3` не превращаются в стиль.
### P12 — Статус-бар и границы прокрутки
Вход:
- документ на несколько экранов.
Ожидается:
- корректные `L x-y / total` и `%`;
- `Home/End` приводят к ожидаемым позициям;
- при выходе за границы документа top-line корректно clamp-ится.
## Рекомендация по регрессии
После любых изменений в логике индексации/рендера прогонять минимум:
- `P04`, `P05`, `P06`, `P07`, `P09`, `P10`, `P11`, `P12`.
+44
View File
@@ -0,0 +1,44 @@
# Эталон размеров _CODE (байт); обновление: python3 toolchain/size_check.py --update
argv 3431
assrtest 3847
attrprob 4091
banked 1056
bankedbg 1067
banklocl 4832
banktest 3767
bios_text 4470
cat 915
conio 4634
conio2 3946
dec_test 860
errno 6216
fbench 9040
fdmax 6337
filetest 11624
gets 509
gfx_d16 3319
gfx_dbuf 2396
gfx_demo 3219
gfx_mous 5542
gfx_text 2560
hello 4176
hello2 4251
ls 4919
malloc 4462
mdview 19122
mdview2 27029
mem_test 4558
mouse 4394
openenv 6127
ptime 5896
rt_test 4103
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 bios_text 4470
10 cat 915
11 conio 4634
12 conio2 3946
13 dec_test 860
14 errno 6216
15 fbench 9040
16 fdmax 6337
17 filetest 11624
18 gets 509
19 gfx_d16 3319
20 gfx_dbuf 2396
21 gfx_demo 3219
22 gfx_mous 5542
23 gfx_text 2560
24 hello 4176
25 hello2 4251
26 ls 4919
27 malloc 4462
28 mdview 19122
29 mdview2 27029
30 mem_test 4558
31 mouse 4394
32 openenv 6127
33 ptime 5896
34 rt_test 4103
35 seek 4177
36 simple 963
37 solidt 12381
38 stattest 7700
39 stdlib 6643
40 stest2 3644
41 strtest 1340
42 text_palette 5039
43 timedir 5471
44 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, с которой ассоциирован
некоторый открытый файл. При необходимости несколько
потоков могут ссылаться на один и тот же файл.
+36 -2
View File
@@ -1,10 +1,44 @@
# Build mdview.exe — Markdown viewer for Sprinter. # Build mdview.exe — Markdown viewer for Sprinter.
# #
# small memory mode: code in W1, data/stack/heap in W2 (32 KB total). # small memory mode: code in W1, data/stack/heap in W2 (32 KB total).
# W3 stays free for the file buffer (EMM-mapped) and Phase 3+ render cache. # W3 stays free for the file buffer (EMM-mapped).
PROJ_ROOT := $(abspath $(CURDIR)/../..) PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := mdview EXAMPLE := mdview
MEMORY := small MEMORY := small
EXTRA_DATA := SAMPLE.MD PLAN_866.md
include $(PROJ_ROOT)/app.mk include $(PROJ_ROOT)/app.mk
# ------------------------------------------------------------------
# Образ дискеты: только mdview.exe + README.MD (перекодированный
# из UTF-8 в CP866 — рабочую кодировку Sprinter).
#
# README.MD хранится в репозитории в UTF-8; iconv -c конвертирует
# его в CP866, отбрасывая символы без аналога в целевой кодировке.
# Результат кладётся в .disk_tmp/README.MD, чтобы make_disk.py
# использовал правильное имя файла на диске.
#
# iconv -c возвращает ненулевой код, если хоть один символ отброшен
# (даже с -c) — это ОЖИДАЕМО при потере символов без аналога в CP866,
# не ошибка конвертации; вывод при этом всё равно корректно записан.
# Поэтому код возврата iconv игнорируется (|| true).
# ------------------------------------------------------------------
DISK_TMP := .disk_tmp
README_DISK := $(DISK_TMP)/README.MD
$(DISK_TMP):
mkdir -p $@
$(README_DISK): README.MD | $(DISK_TMP)
iconv -c -f UTF-8 -t CP866 README.MD > $@ || true
floppy: $(EXAMPLE).exe $(README_DISK)
python3 $(MAKE_DISK) $(FLOPPY_IMG) $(EXAMPLE).exe $(README_DISK)
@echo
@echo "Floppy ready: $(FLOPPY_IMG)"
@echo "Run: cd $(MAME_DIR) && ./run_mame.sh"
clean:
rm -rf .sprinter-cc-* $(EXAMPLE).exe $(DISK_TMP)
.PHONY: all clean floppy run
-570
View File
@@ -1,570 +0,0 @@
# План: текстовый Markdown viewer для Sprinter (`examples/mdview`)
## Context
Тестовая крупная задача — проверить нашу libc на нетривиальном interactive-приложении (полноэкранный UI, файловый I/O, парсер). Параллельно даст хороший showcase платформы и поможет вытащить недоделки в conio/io. Конечная цель: viewer для `.md` файлов с подсветкой синтаксиса, навигацией по тексту и постраничным скроллингом.
**Ограничения v1 (зафиксированы пользователем):**
- POSIX file API (open/read/lseek/close); FILE pointer / fread / fgets — не использовать
- Подсветка через цвет: **размер заголовка** → цвет шрифта; **bold/italic** → цвет фона (моноширинный фонт без жирного/курсивного начертания)
**Расширения, реализованные после плана:**
- Файлы до 128 KB (18 EMM-страниц, lazy map в W3 через `fb()/map_page()`) — изначально было в v2
- Анимированный spinner и предварительная отрисовка UI при старте — UX
- Inline emphasis: `_`/`*`/`**` подчиняются XOR flanking-правилу (whitespace ровно с одной стороны), `COLOR_YELLOW` и `2 * 3` остаются литералами
---
## Текущий статус (2026-06-05)
| Phase | Статус | Комментарий |
|---|---|---|
| 1 Plain text + nav | ✓ | загрузка, индексация, status/menu, ↑↓/PgUp/PgDn/Home/End/F1/F10/Esc |
| 2 Headers + HR | ✓ | H1..H4, `---`/`***`/`___` (с ≥3 marker'ов) |
| 3 Inline emphasis | ✓ | `**` / `*` / `_` / `` ` ``; XOR flanking (см. выше) |
| 4 Lists / quote / fenced code | ✓ | `- / * / +`, `N. / N)`, `> `, ``` ``` ```; light nested lists |
| 4-tables | ✗ | таблицы отложены вместе с Phase 6 |
| 5 Wrap / Unwrap (F2) | ✓ | wrap-by-default; soft wrap; F2 переключает; hpan ←/→ в truncate-режиме |
| 6 Полный layout таблиц | ✗ | deferred |
| 7 Links + search | ✗ | deferred |
| 8 F8 Raw toggle | ✗ | deferred |
| Cache рендеренных строк | ✗ | не нужно по скорости |
**UX-поправки (отдельно от phase-плана, 2026-06-05):**
- UI (menu + title bar) отрисовывается ДО `load_file`/`index_lines` — пользователь сразу видит интерфейс, а не чёрный экран
- Title bar: `MDVIEW <spinner> <filename>` (3 пробела между MDVIEW и filename, slot спиннера — col 8)
- Spinner крутится во время `load_file` (по странице) и `index_lines` (раз в 32 логических строки), включая `toggle_wrap`
---
## Архитектура
### Раскладка экрана (80×32, текст mode 0x03)
```
Row 0: ┃ MDVIEW │ mdview.md │ L 1-30 / 142 │ 21% ┃ ← status bar (BG=blue, FG=white)
▲ ▲
│ └── filename @ col 10
└────────── spinner slot @ col 8 (anim. while busy)
Row 1: ┃ ┃
... ┃ document viewport (30 rows) ┃
Row 30: ┃ ┃
Row 31: ┃ F1 Help F2 Wrap F10 Exit ┃ ← menu bar (BG=blue, FG=cyan)
```
- Viewport = 30 строк × 80 столбцов.
- Status / menu рендерятся через `wrchar()` (без авто-скролла), viewport — через `LOCATE` + посимвольный `wrchar()` (тоже без авто-скролла, ставит и char и attr за один call).
### Память
Memory mode: **`small`** — DSS отводит под наш образ **два банка (W1 + W2)** = 32 KB суммарно (CODE в W1, DATA + STACK + HEAP в W2). Этого должно хватить чтобы НЕ заводить `__banked` функции. W3 остаётся полностью свободным для маппинга больших буферов:
```
W0 (0x0000-0x3FFF): ESTEX (system, untouchable)
W1 (0x4000-0x7FFF): CODE (small mode page 1)
W2 (0x8000-0xBFFF): DATA + STACK + HEAP (small mode page 2)
W3 (0xC000-0xFFFF): paged window — лениво переключается между EMM-страницами
файла (до 8 страниц = 128 KB). `cur_page` кэширует
текущую mapping, `fb(p)` маппит нужную страницу при
первом обращении.
```
Почему small + W3:
- 32 KB на код+данные с большим запасом → нет банкинга
- W3 — стандартный paged window, под него у нас уже есть `bank_io_w3` API
- Файлы до 128 KB поддерживаются нативно: `mem_alloc_pages(pages_needed)` под весь файл; `map_page()` через `sprinter_page_w3()`; `fb(p)` — единая точка доступа из индексатора и рендера.
Статики (в W2):
- `line_offset[MAX_LINES]` — uint16_t смещение каждой строки в файловой странице (4 KB на 2048 строк)
- `cache_tag[CACHE_N]` — uint16_t тег слота (200 байт на 100 слотов, появляется в Phase 3)
- `filename[64]`, `top_line`, `total_lines`, `file_size`, `file_blk`, `cache_blk` — единицы байт
**FILE_BUF**: `((char*)0xC000)` — фиксированная адресация в W3 после маппинга нужной страницы.
### Поток данных
```
main → open() → read() chunks 1KB → write to W1 (mapped EMM page) → close()
→ index_lines() (одно сканирование, заполняет line_offset[])
→ render_viewport() + main loop { getkey(); handle(); render_status(); render_viewport() }
```
**Тонкость с `read()`**: ESTEX READ записывает по dst-указателю в адресном пространстве вызывающего. Поскольку мы маппим EMM-страницу в W3 (0xC000) ДО вызова read(), указатель 0xC000+offset валиден. Если выяснится что BIOS трогает W3 во время read (графический видеобуфер по умолчанию в W3 при графических режимах, но в текстовом — должен быть свободен) — fallback: читать в 1 KB буфер в W2 и копировать в W3 через `bank_write_w3()`.
---
## Реализация — поэтапная
### Phase 1 — Plain text viewer (MVP) ✓
**Что работает:**
- Загрузка файла (`open/read/close`) в W1-страницу
- Индексация строк (LF / CRLF разделители)
- Status bar: имя файла, L N-M / Total, процент скроллинга
- Menu bar: `F1 Help F10 Exit`
- Навигация: ↑/↓ (1 строка), PgUp/PgDn (30 строк), Home/End (начало/конец), Esc/F10 (выход), F1 (help screen)
- Обрезка строк длиннее 80 символов (без word-wrap)
- Цвета: текст белый на чёрном; status/menu — белый на синем
**Критичные файлы:**
- `examples/mdview/mdview.c` — main, key loop, rendering, indexing (one-file MVP)
- `examples/mdview/Makefile`
- `examples/mdview/SAMPLE.MD` — тестовый markdown файл
### Phase 2 — Headers и горизонтальная линия ✓
**MD фичи:**
- `# H1` → ярко-жёлтый (COLOR_YELLOW = 14) на чёрном
- `## H2` → ярко-голубой (COLOR_LBLUE = 11)
- `### H3` → ярко-зелёный (COLOR_LGREEN = 10)
- `#### H4+` → серый (COLOR_GREY = 8)
- `---` / `***` на отдельной строке → линия 0xC4 (горизонтальная рамка ASCII) во всю ширину
### Phase 3 — Inline emphasis ✓
**Парсер inline (per-line, runs в одну строку):**
- `**bold**` → ATTR_TEXT_BOLD
- `*italic*` → ATTR_TEXT_ITALIC
- `_underscore_` → ATTR_TEXT_UNDERSORE
- `` `code` `` → ATTR_TEXT_CODE
- Маркеры `**`/`*`/`_`/`` ` `` НЕ рендерятся (съедаются)
State machine: один активный стиль одновременно (без вложенности); конфликтующий маркер при чужом активном стиле всё равно консьюмится (zero-width) для синхронизации ширины с index_lines. Состояние сбрасывается на каждой строке.
**Flanking-правило (CommonMark intraword, реализовано после изначального плана):**
- `*` / `**` / `_` считаются markdown-маркером только если whitespace/EOL
ровно с ОДНОЙ стороны (XOR).
- Случаи "оба whitespace" (`2 * 3`, `2 ** 3`) → литералы (арифметика).
- Случаи "ни одного whitespace" (`COLOR_YELLOW`, `FILE*/fread`, `foo*bar*baz`)
→ литералы (intraword).
- Backtick (`` ` ``) flanking НЕ требует — `` `code` `` работает без пробелов.
- Правило применено симметрично в 4 местах (`index_lines`, cont-render,
основной inline-парсер, truncation peek), иначе wrap-индексатор и
рендер разъедутся по ширине.
> **Кэш отформатированных строк** — отложен в самый конец, см. "Phase ∞: оптимизации".
> Скорости текущего наивного рендера хватает на 80×30 = 2400 wrchar / кадр; PgUp/PgDn визуально мгновенен.
### Phase 4 — Block elements ✓ (без таблиц)
- Маркированные списки: `- foo`, `* foo`, `+ foo` → префикс `` (0x07) + пробел; цвет маркера ярче основного
- Нумерованные списки: `1. foo`, `2. foo` → как есть (число оставляем)
- Blockquote: строки с `> ` → префикс `` (0xB3) серого цвета, остальной текст слегка приглушённый
- Fenced code blocks: `` ``` `` открывает/закрывает блок; все строки между — bg=серый, моноширинно (без inline-парсинга)
- Indented code blocks (4+ пробелов): аналогично fenced, но без явного маркера
**Light nested lists (v1 — реализовано):**
- `classify_line()` пропускает leading spaces перед ulist/olist/quote маркером,
возвращает `content_off` после маркера → `content_off - p_start` = indent + marker
бит в visible col.
- `render_line()` рисует leading-spaces в `ATTR_TEXT`, потом маркер на сдвинутой
позиции (col = indent). Marker всё ещё фиксирован при горизонтальном pan'е.
- HR / header / fence delim остаются строго col-0 (CommonMark разрешает до 3
ведущих пробелов для них — упростили).
- Tab-indent → не распознаётся как nesting (только spaces).
**Phase 4-full — полная поддержка вложенности (deferred):**
- **Tab-indent**: считать tab = 4 пробела для определения уровня.
- **Quote nesting** (`> > foo`): каждый `>` подряд = +1 уровень, каждый рисуется
отдельным `` в `ATTR_QUOTE_MARKER` (визуальная "лестница" слева).
- **Hanging indent в wrap-continuation**: когда `- some very long bullet text
that wraps...` — continuation seg должен начинаться от content-col (после
маркера), а не от col 0. Сейчас continuation идёт от col 0 (v1 simplification).
Требует хранить `marker_width` per логическая строка (8 бит) или re-classify
first seg при рендере continuation.
- **Lazy continuation**: строки без маркера, но с правильным indent под
предыдущим bullet'ом, должны считаться продолжением того bullet'а
(визуально — общий attr).
- **Strict CommonMark indent rules**: вложенный пункт должен быть на indent
≥ content_col родителя, иначе считается breakout. Нужен мини-stack
активных списков при индексации.
### Phase 5 — Wrap / Unwrap длинных строк ✓
Дефолт: **wrap on**. F2 переключает; в меню-баре подпись отражает действие
("Unwrap" когда wrap включён, "Wrap" когда выключен). Во время реиндексации
крутится спиннер на title bar.
**v1 — реализовано:**
- Один массив `line_offset[2048]` хранит ВИДИМЫЕ сегменты (а не логические
строки); биты 0..13 — байтовое смещение, бит 15 — CONT-флаг continuation.
- Wrap-режим: soft wrap на последнем пробеле ≤ 80; hard fallback если
пробела нет.
- Маркеры эмфазиса (`**`/`*`/`_`/`` ` ``) и header-префиксы (`#`/`##`/...) не
учитываются в visible-col при поиске точки переноса.
- "Специальные" логические строки не wrap'аются вообще (одна seg-запись на
логическую строку): fence delim, table row (header/separator/body), HR.
- F2 toggle сохраняет визуальную позицию через `top_offset` в FILE_BUF.
- Bitmaps (`in_code`/`in_table`/`is_tab_hdr`) перестраиваются вместе с
сегментами, индексируются seg-индексом, биты ставятся только на первом
seg'е логической строки.
- Continuation-сегменты рендерятся в стиле "v1: плоско" — plain text,
никаких markdown-классификаций; padding до конца строки `ATTR_TEXT`.
**v1.5 — отложено для полной картины wrap:**
- **Hanging indent**: continuation от ulist/olist/quote должен выравниваться
под content, а не от col 0. Требует хранить marker_width per логическая
строка ИЛИ re-classify первого seg'а при рендере continuation.
- **Наследование base_attr**: continuation от header'а должен сохранять
цвет; continuation от code body — фон ATTR_TEXT_CODE. Требует хранить
1 байт `base_attr` per seg ИЛИ lookup первого seg'а.
- **Inline emphasis через границу**: эмфазис, открытый в первом seg'е и не
закрытый, должен продолжаться во втором. Требует хранить emph state per
seg (3 бита).
- Compact way: добавить параллельный массив `seg_meta[MAX_SEGS]` по 1 байту
— пакует marker_width (4 бита) + emph_state (3 бита) + base_attr_idx
(4 бита из таблицы → нужен 2-байтовый seg_meta).
- **Hpan для длинных строк**: если wrap выключен, добавить ←/→ для
горизонтального скролла >80 cols. Общий механизм с tables (Phase 6).
**РЕАЛИЗОВАНО** (light) — `viewport_x` + полный re-render на каждое ←/→.
- **Ускорение hpan через ESTEX WINCOPY/WINREST** (deferred): сейчас pan
делает полный `render_viewport()` = 30 строк × 80 wrchar. Можно
скопировать существующее содержимое viewport'а на N cols влево/вправо
через win-copy, потом рендерить только узкую полосу справа/слева
(HPAN_STEP cols × 30 rows ≈ 240 wrchar вместо 2400). ESTEX SCROLL
горизонталь не поддерживает — нужна именно WINCOPY-операция или
rdchar/wrchar loop. Активировать когда ощутится тормоз; сейчас на
типовом markdown'е не заметно.
**v2 — отдельная фича, мимо wrap:**
- Toggle подсветки целиком (F3?)
- Search по тексту (Ctrl+F / F4)
- Links `[text](url)` → синий подчёркнутый text, url прячется
- Images `![alt](path)` → `[IMG: alt]`
### Phase 7 — Links и поиск (post-v1)
- `[text](url)` → отрисовать только `text` с ярко-синим FG (визуально подчёркнутое)
- `![alt](path)` → `[IMG: alt]` в скобках
- Search по тексту (F3 / Ctrl+F): инкрементальный, подсветка совпадений
### Phase 8 — F8 Raw / Render toggle
Переключатель режима отображения: при включённом Raw показывается исходный
текст файла как есть — все markdown-маркеры (`#`, `**`, `_`, `` ` ``, `|`,
`>`, `-`, etc.) рендерятся литералами с `ATTR_TEXT`, без классификации.
Полезно когда:
- нужно увидеть точную разметку (отлаживание .md, скриншоты, копирование)
- markdown-классификатор ошибся и хочется увидеть оригинал
- хочется быстро сравнить "до/после" рендера
**Поведение:**
- F8 переключает `render_mode` (1=render, 0=raw); меню показывает обратное
действие ("Raw" когда сейчас render, "Render" когда сейчас raw) — той же
логикой что F2/Wrap/Unwrap.
- В Raw режиме: `render_line()` идёт по короткому пути — никакого
`classify_line`, `is_fence_delim`, `is_code_body`, inline-эмфазиса; просто
байтовый дамп FILE_BUF от seg-offset до next-seg/EOL с tab-expansion и
ATTR_TEXT.
- Раздельно от F2: оба режима независимы (можно Raw+Wrap, Raw+Truncate,
Render+Wrap, Render+Truncate). Wrap-логика в `index_lines` работает в
обоих случаях одинаково (опирается на визуальные колонки независимо от
раскраски).
- Статус-бар: добавить индикатор `[R]` / `[V]` (Raw / View) или текстом
`RAW` рядом с именем файла.
**Минимальная реализация:**
- Один новый static `uint8_t render_mode = 1;`
- В `render_line()`: на самом верху `if (!render_mode) { … raw render … return; }`
- В `render_menu()`: добавить ярлык F8 рядом с F2.
- В главном цикле: `case KEY_F8: toggle_render(); break;`
- `toggle_render()` отличается от `toggle_wrap()` тем, что НЕ перестраивает
`line_offset[]` (wrap-сегментация не меняется), только перерендерит экран.
---
## API-новинки в libc (минимальные)
### `getkey()` — extended key reader (в libc)
Текущий `getch()` теряет scan code расширенных клавиш (возвращает только E=ASCII). Добавляем **сразу в `libc/conio/conio.c`** новую функцию рядом с `getch()`:
```c
// Returns scan in high byte, ASCII in low byte.
// Extended keys (arrows, F-keys, PgUp/PgDn, Home/End): ASCII=0, scan code в high byte.
// Plain keys: ASCII в low byte; high byte содержит positional scan (бит 7 = Ctrl/Alt/Shift modifier).
uint16_t getkey(void) __naked {
__asm
ld c, #0x30 ; ESTEX WAITKEY
rst #0x10 ; A=ASCII, D=scan, E=ASCII
ld e, a ; ensure E=ASCII even if E clobbered
ret ; SDCC __sdcccall(1): возврат uint16_t в DE (D=scan, E=ASCII)
__endasm;
}
```
Дополнительно — **в `libc/include/conio.h`** прописать прототип и константы scan-кодов:
```c
uint16_t getkey(void);
/* Scan codes for getkey() high byte when ASCII=0 (extended keys). */
#define KEY_F1 0x0E
#define KEY_F2 0x0F
#define KEY_F3 0x10
#define KEY_F4 0x11
#define KEY_F5 0x12
#define KEY_F6 0x13
#define KEY_F7 0x14
#define KEY_F8 0x15
#define KEY_F9 0x16
#define KEY_F10 0x17
#define KEY_F11 0x18
#define KEY_F12 0x19
#define KEY_END 0x24
#define KEY_DOWN 0x25
#define KEY_PGDN 0x26
#define KEY_LEFT 0x27
#define KEY_RIGHT 0x29
#define KEY_HOME 0x2A
#define KEY_UP 0x2B
#define KEY_PGUP 0x2C
#define KEY_INS 0x23
#define KEY_DEL 0x22
```
**Скан-коды (из docs/converted/ProgrammerManual.txt:2143-2323):**
| Клавиша | scan | Клавиша | scan |
|---|---|---|---|
| F1 | 0x0E | Up | 0x2B |
| F10 | 0x17 | Down | 0x25 |
| F11 | 0x18 | Left | 0x27 |
| F12 | 0x19 | Right | 0x29 |
| | | PgUp | 0x2C |
| | | PgDn | 0x26 |
| | | Home | 0x2A |
| | | End | 0x24 |
### Что **переиспользуем** из существующей libc
- `open/read/lseek/close` — `libc/io/{open,read,lseek}.c` (POSIX wrappers)
- `mem_alloc_pages/mem_free_block/mem_get_page` — `libc/mem/mem_alloc.c`
- `sprinter_page_w3()` — inline `__sfr` write в `libc/include/sprinter.h:113`
- `bank_read_w3/bank_write_w3` — `libc/mem/bank_io_w3.c` (для fallback или v2 multi-page)
- `wrchar(x, y, ch, attr)` — `libc/conio/conio.c:476` (без auto-scroll, идеально для viewport)
- `clrscr_attr(attr)` — `libc/conio/conio.c:395`
- `gotoxy/wherex/wherey` — `libc/conio/conio.c:412-462` (если нужно)
- `kbhit()` — `libc/conio/conio.c:22` (для non-blocking опроса, опционально)
- `dec16/dec8` — `libc/stdio/dec_print.c` (для status bar: текущая строка / total / %)
- `COLOR(fg, bg)` макрос — `libc/include/conio.h:152`
- Цветовые константы `COLOR_*` — `libc/include/conio.h:145`
- `strlen/memcpy/memset` — z80.lib (НЕ переписывать)
---
## Структура исходников
```
examples/mdview/
├── Makefile # стандартный pattern (см. examples/cat/Makefile)
├── mdview.c # Phase 1: всё в одном файле (main, keys, indexing, render)
├── SAMPLE.MD # тестовый markdown
└── README.md # описание и controls
```
После Phase 3 раскидать по модулям (если суммарный размер > ~6KB):
```
mdview.c — main loop, status/menu bars, key dispatch
mdrender.c — line rendering with MD inline parser
mdindex.c — file load + line indexing
```
### Сборка
```makefile
PROJ ?= ../..
SPRINTER_CC := $(PROJ)/bin/sprinter-cc
mdview.exe: mdview.c
$(SPRINTER_CC) --memory small -o $@ mdview.c
```
`--memory small`: код+данные в W2 (DSS даёт нужное число страниц); файл — отдельная EMM-страница в W3.
---
## Структура mdview.c (Phase 1, эскиз)
```c
#include <stdint.h>
#include <stdio.h>
#include <conio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sprinter.h>
#include <sprinter_mem.h>
#define VIEW_TOP 1
#define VIEW_BOT 30 // inclusive
#define VIEW_H 30
#define SCREEN_W 80
#define MAX_LINES 2048
#define FILE_BUF ((char*)0xC000) /* W3 — EMM page mapped here */
#define TAB_STOP 4
#define ATTR_TEXT COLOR(COLOR_WHITE, COLOR_BLACK)
#define ATTR_BAR COLOR(COLOR_WHITE, COLOR_BLUE)
#define ATTR_MENU_K COLOR(COLOR_YELLOW, COLOR_BLUE)
#define ATTR_MENU_T COLOR(COLOR_WHITE, COLOR_BLUE)
static uint16_t line_off[MAX_LINES];
static uint16_t n_lines;
static uint16_t top_line;
static uint16_t file_size;
static uint8_t file_blk;
static char filename[64];
static int load_file(const char *path); // open, alloc EMM page, map W3, read, close
static void index_lines(void); // scan FILE_BUF, fill line_off[]
static void render_status(void); // row 0
static void render_menu(void); // row 31
static void render_line(uint16_t idx, uint8_t row); // one line @ row
static void render_viewport(void); // VIEW_H lines starting from top_line
static void scroll_up(uint16_t n);
static void scroll_down(uint16_t n);
static void help_screen(void); // F1
int main(int argc, char **argv) {
if (argc < 2) { puts("Usage: mdview <file.md>"); return 1; }
if (load_file(argv[1]) < 0) { puts("load error"); return 1; }
index_lines();
clrscr_attr(ATTR_TEXT);
render_menu();
render_status();
render_viewport();
for (;;) {
uint16_t k = getkey();
uint8_t ascii = k & 0xFF;
uint8_t scan = (k >> 8) & 0x7F; // strip mod bit
if (ascii) {
if (ascii == 0x1B) break; // Esc → exit
continue;
}
switch (scan) {
case KEY_F10: goto exit;
case KEY_F1: help_screen(); break;
case KEY_UP: scroll_up(1); break;
case KEY_DOWN: scroll_down(1); break;
case KEY_PGUP: scroll_up(VIEW_H); break;
case KEY_PGDN: scroll_down(VIEW_H); break;
case KEY_HOME: top_line = 0;
render_viewport(); break;
case KEY_END: /* clamp to last viewport */ break;
}
render_status();
}
exit:
mem_free_block(file_blk);
clrscr_attr(ATTR_TEXT);
return 0;
}
```
---
## Verification
### Phase 1
1. Подготовить `SAMPLE.MD` ~5 KB (заголовки, абзацы, списки) — рендериться будет plain.
2. `cd examples/mdview && make`
3. `python make_disk.py mdview.exe SAMPLE.MD → mc.img && ./run_mame.sh`
4. Проверить:
- Status bar показывает `SAMPLE.MD L 1-30 / N X%`
- Menu bar внизу
- ↑/↓: 1 строка
- PgUp/PgDn: 30 строк, корректное clamp на границах
- Home: top_line=0
- End: top_line = total_lines - VIEW_H
- F1: показывает help, любая клавиша возвращает
- F10 / Esc: выход, экран очищен
5. Edge cases: пустой файл, файл из одной строки, файл с очень длинной строкой (>80), CRLF и LF mixed.
### Phase 2-4
Расширять `SAMPLE.MD` с фичами по мере добавления, визуально верифицировать в MAME. Скриншот-сравнение опционально.
### Регрессии
- Никаких изменений в libc на Phase 1 кроме (потенциально) добавления `getkey()` в `libc/conio/conio.c` — если так, прогнать `examples/conio2` и `examples/filetest` чтобы убедиться что ничего не сломалось.
---
## Решения по неоднозначностям
1. **Word-wrap vs truncate**: v1 = truncate (просто). v2 — F2 toggle wrap.
2. **Горизонтальный скроллинг**: v1 — нет; v2 — `←/→` сдвиг viewport по столбцам.
3. **Tab handling**: преобразование при рендере, **tabstop = 4** (стандарт MD). Оригинал в W3 не трогаем.
4. **UTF-8**: рендерим байты как есть. Если файл в CP866 — отрисуется кириллицей через системный фонт. UTF-8 — пока не поддерживаем (визуально будет каша на не-ASCII символах; детект и предупреждение — в v2).
---
### Phase 6 — Полный layout таблиц (deferred)
Сейчас (Phase 4-tables) таблицы рисуются "декоративно" — пайпы и тире
заменяются на box-drawing chars, но ширины колонок берутся как есть из
исходника. Цель Phase 6 — пересчитать таблицу в нормализованный вид:
- **Pre-scan таблицы**: пройти все строки одного table-блока, найти
максимальную ширину каждой колонки (с учётом съеденных inline-маркеров —
визуальный размер, не байтовый).
- **Re-emit в буфере**: при загрузке файла (или при первой встрече таблицы)
переписать строки в FILE_BUF так, чтобы все ячейки одной колонки имели
одинаковую ширину; добавить top/bottom рамки (`┌─┬─┐` / `└─┴─┘`) как
синтетические строки. Это позволит сохранить 1:1 соответствие "логическая
строка → одна viewport row" без специальной логики при рендере.
- **Память**: re-emit может УВЕЛИЧИТЬ файл за счёт padding и доп.рамок.
Если буфер близок к 16KB — отрезать таблицу и пометить её overflow'ом.
- **Горизонтальный скроллинг**: если итоговая ширина таблицы (или любой
строки) > SCREEN_W = 80 — добавить ←/→ для horizontal pan. Это будет
общий механизм для длинных строк (см. также wrap mode), не только таблиц.
- **Выравнивание из separator-row**: `:-` → left, `-:` → right, `:-:` →
center; учитывать при padding'е содержимого ячейки.
- **Шаги реализации**:
1. Walking pass по фенсам/таблицам прямо в `index_lines()` — собрать
extents всех таблиц.
2. Для каждой таблицы — определить ширины колонок.
3. Решение: rewrite-in-buffer (проще для рендера, но мутирует исходник)
vs render-time layout (cleaner, но требует отдельной структуры
описания layout'а на каждую таблицу).
4. Hpan: общий `viewport_x_offset` для всего экрана, или отдельный
"широкий режим" только внутри таблиц.
> Не блокирующая фича. Запускать когда станет понятен типовой источник
> markdown-файлов (узкие читалки → достаточно текущего декоратора;
> широкие README с большими таблицами → нужен полный layout).
---
### Phase ∞ — Кэш рендеренных строк (low priority)
Отложено: текущая скорость более чем достаточна. Активировать если появится
сценарий, где видна задержка PgUp/PgDn (например, при тяжёлом inline-парсере
v2 с UTF-8 / linkifier / таблицами).
**Кэш отформатированных строк** (W3, отдельная EMM-страница):
```
Cache layout (16 KB EMM page, всего 16000 байт используется):
slot 0: 80 chars + 80 attrs = 160 bytes @ offset 0
...
slot 99: 80 chars + 80 attrs = 160 bytes @ offset 15840
Cache tags (W2 static): uint16_t cache_tag[100] = 200 bytes
cache_tag[i] = line_id, или 0xFFFF = invalid
```
Стратегия — **direct map (no LRU)**: `slot = line_id % 100`. Коллизия → вытеснение.
**Batched viewport render**: 2 page-swap'а на ВЕСЬ viewport (cache → file → cache),
не 60 как при наивной реализации.
При сборке: `cache_blk = mem_alloc_pages(1)` после `file_blk`; `mem_free_block` на exit.
---
## Что отложено в v2
- ~~Файлы >16 KB~~ — **сделано в v1.5** (до 128 KB через 18 EMM-страниц + lazy map в W3).
- ~~Word wrap~~ — **сделано** (Phase 5, F2 toggle).
- Search (Find / Find next) — F3 / F4.
- F8 Raw / Render toggle — спецификация в Phase 8.
- Links `[text](url)` + image alt — Phase 7.
- Tables — Phase 6 (полный layout).
- Toggle highlight on/off — частный случай F8 Raw.
-570
View File
@@ -1,570 +0,0 @@
# « ­: ⥪áâ®¢ë© Markdown viewer ¤«ï Sprinter (`examples/mdview`)
## Context
’¥á⮢ ï ªà㯭 ï § ¤ ç  - ¯à®¢¥à¨âì ­ èã libc ­  ­¥âਢ¨ «ì­®¬ interactive-¯à¨«®¦¥­¨¨ (¯®«­®íªà ­­ë© UI, ä ©«®¢ë© I/O, ¯ àá¥à).  à ««¥«ì­® ¤ áâ å®à®è¨© showcase ¯« âä®à¬ë ¨ ¯®¬®¦¥â ¢ëâ é¨âì ­¥¤®¤¥«ª¨ ¢ conio/io. Š®­¥ç­ ï 楫ì: viewer ¤«ï `.md` ä ©«®¢ á ¯®¤á¢¥âª®© ᨭ⠪á¨á , ­ ¢¨£ æ¨¥© ¯® ⥪áâã ¨ ¯®áâà ­¨ç­ë¬ áªà®««¨­£®¬.
**Ž£à ­¨ç¥­¨ï v1 (§ ä¨ªá¨à®¢ ­ë ¯®«ì§®¢ â¥«¥¬):**
- POSIX file API (open/read/lseek/close); FILE pointer / fread / fgets - ­¥ ¨á¯®«ì§®¢ âì
- ®¤á¢¥âª  ç¥à¥§ 梥â: **à §¬¥à § £®«®¢ª ** -> 梥â èà¨äâ ; **bold/italic** -> 梥â ä®­  (¬®­®è¨à¨­­ë© ä®­â ¡¥§ ¦¨à­®£®/ªãàᨢ­®£® ­ ç¥àâ ­¨ï)
** áè¨à¥­¨ï, ॠ«¨§®¢ ­­ë¥ ¯®á«¥ ¯« ­ :**
- ” ©«ë ¤® 128 KB (1-8 EMM-áâà ­¨æ, lazy map ¢ W3 ç¥à¥§ `fb()/map_page()`) - ¨§­ ç «ì­® ¡ë«® ¢ v2
- €­¨¬¨à®¢ ­­ë© spinner ¨ ¯à¥¤¢ à¨â¥«ì­ ï ®âà¨á®¢ª  UI ¯à¨ áâ à⥠- UX
- Inline emphasis: `_`/`*`/`**` ¯®¤ç¨­ïîâáï XOR flanking-¯à ¢¨«ã (whitespace ஢­® á ®¤­®© áâ®à®­ë), `COLOR_YELLOW` ¨ `2 * 3` ®áâ îâáï «¨â¥à « ¬¨
---
## ’¥ªã騩 áâ âãá (2026-06-05)
| Phase | ‘â âãá | Š®¬¬¥­â à¨© |
|---|---|---|
| 1 Plain text + nav | | § £à㧪 , ¨­¤¥ªá æ¨ï, status/menu, ^V/PgUp/PgDn/Home/End/F1/F10/Esc |
| 2 Headers + HR | | H1..H4, `---`/`***`/`___` (á >=3 marker'®¢) |
| 3 Inline emphasis | | `**` / `*` / `_` / `` ` ``; XOR flanking (á¬. ¢ëè¥) |
| 4 Lists / quote / fenced code | | `- / * / +`, `N. / N)`, `> `, ``` ``` ```; light nested lists |
| 4-tables | | â ¡«¨æë ®â«®¦¥­ë ¢¬¥á⥠á Phase 6 |
| 5 Wrap / Unwrap (F2) | | wrap-by-default; soft wrap; F2 ¯¥à¥ª«îç ¥â; hpan <-/-> ¢ truncate-०¨¬¥ |
| 6 ®«­ë© layout â ¡«¨æ | | deferred |
| 7 Links + search | | deferred |
| 8 F8 Raw toggle | | deferred |
| Cache ७¤¥à¥­­ëå áâப | | ­¥ ­ã¦­® ¯® ᪮à®á⨠|
**UX-¯®¯à ¢ª¨ (®â¤¥«ì­® ®â phase-¯« ­ , 2026-06-05):**
- UI (menu + title bar) ®âà¨á®¢ë¢ ¥âáï „Ž `load_file`/`index_lines` - ¯®«ì§®¢ â¥«ì áà §ã ¢¨¤¨â ¨­â¥à䥩á,   ­¥ çñà­ë© íªà ­
- Title bar: `MDVIEW <spinner> <filename>` (3 ¯à®¡¥«  ¬¥¦¤ã MDVIEW ¨ filename, slot ᯨ­­¥à  - col 8)
- Spinner ªàãâ¨âáï ¢® ¢à¥¬ï `load_file` (¯® áâà ­¨æ¥) ¨ `index_lines` (à § ¢ 32 «®£¨ç¥áª¨å áâப¨), ¢ª«îç ï `toggle_wrap`
---
## €àå¨â¥ªâãà 
###  áª« ¤ª  íªà ­  (80x32, ⥪áâ mode 0x03)
```
Row 0: MDVIEW ³ mdview.md ³ L 1-30 / 142 ³ 21% <- status bar (BG=blue, FG=white)
³ ÀÄÄ filename @ col 10
ÀÄÄÄÄÄÄÄÄÄÄ spinner slot @ col 8 (anim. while busy)
Row 1:
... document viewport (30 rows)
Row 30:
Row 31: F1 Help F2 Wrap F10 Exit <- menu bar (BG=blue, FG=cyan)
```
- Viewport = 30 áâப x 80 á⮫¡æ®¢.
- Status / menu ७¤¥àïâáï ç¥à¥§ `wrchar()` (¡¥§  ¢â®-áªà®«« ), viewport - ç¥à¥§ `LOCATE` + ¯®á¨¬¢®«ì­ë© `wrchar()` (⮦¥ ¡¥§  ¢â®-áªà®«« , áâ ¢¨â ¨ char ¨ attr §  ®¤¨­ call).
###  ¬ïâì
Memory mode: **`small`** - DSS ®â¢®¤¨â ¯®¤ ­ è ®¡à § **¤¢  ¡ ­ª  (W1 + W2)** = 32 KB á㬬 à­® (CODE ¢ W1, DATA + STACK + HEAP ¢ W2). ⮣® ¤®«¦­® 墠â¨âì çâ®¡ë … § ¢®¤¨âì `__banked` ä㭪樨. W3 ®áâ ñâáï ¯®«­®áâìî ᢮¡®¤­ë¬ ¤«ï ¬ ¯¯¨­£  ¡®«ìè¨å ¡ãä¥à®¢:
```
W0 (0x0000-0x3FFF): ESTEX (system, untouchable)
W1 (0x4000-0x7FFF): CODE (small mode page 1)
W2 (0x8000-0xBFFF): DATA + STACK + HEAP (small mode page 2)
W3 (0xC000-0xFFFF): paged window - «¥­¨¢® ¯¥à¥ª«îç ¥âáï ¬¥¦¤ã EMM-áâà ­¨æ ¬¨
ä ©«  (¤® 8 áâà ­¨æ = 128 KB). `cur_page` ªíè¨àã¥â
⥪ãéãî mapping, `fb(p)` ¬ ¯¯¨â ­ã¦­ãî áâà ­¨æã ¯à¨
¯¥à¢®¬ ®¡à é¥­¨¨.
```
®ç¥¬ã small + W3:
- 32 KB ­  ª®¤+¤ ­­ë¥ á ¡®«ì訬 § ¯ á®¬ -> ­¥â ¡ ­ª¨­£ 
- W3 - áâ ­¤ àâ­ë© paged window, ¯®¤ ­¥£® ã ­ á 㦥 ¥áâì `bank_io_w3` API
- ” ©«ë ¤® 128 KB ¯®¤¤¥à¦¨¢ îâáï ­ â¨¢­®: `mem_alloc_pages(pages_needed)` ¯®¤ ¢¥áì ä ©«; `map_page()` ç¥à¥§ `sprinter_page_w3()`; `fb(p)` - ¥¤¨­ ï â®çª  ¤®áâ㯠 ¨§ ¨­¤¥ªá â®à  ¨ ७¤¥à .
‘â â¨ª¨ (¢ W2):
- `line_offset[MAX_LINES]` - uint16_t ᬥ饭¨¥ ª ¦¤®© áâப¨ ¢ ä ©«®¢®© áâà ­¨æ¥ (4 KB ­  2048 áâப)
- `cache_tag[CACHE_N]` - uint16_t ⥣ á«®â  (200 ¡ ©â ­  100 ᫮⮢, ¯®ï¢«ï¥âáï ¢ Phase 3)
- `filename[64]`, `top_line`, `total_lines`, `file_size`, `file_blk`, `cache_blk` - ¥¤¨­¨æë ¡ ©â
**FILE_BUF**: `((char*)0xC000)` - 䨪á¨à®¢ ­­ ï  ¤à¥á æ¨ï ¢ W3 ¯®á«¥ ¬ ¯¯¨­£  ­ã¦­®© áâà ­¨æë.
### ®â®ª ¤ ­­ëå
```
main -> open() -> read() chunks 1KB -> write to W1 (mapped EMM page) -> close()
-> index_lines() (®¤­® ᪠­¨à®¢ ­¨¥, § ¯®«­ï¥â line_offset[])
-> render_viewport() + main loop { getkey(); handle(); render_status(); render_viewport() }
```
**’®­ª®áâì á `read()`**: ESTEX READ § ¯¨á뢠¥â ¯® dst-㪠§ â¥«î ¢  ¤à¥á­®¬ ¯à®áâà ­á⢥ ¢ë§ë¢ î饣®. ®áª®«ìªã ¬ë ¬ ¯¯¨¬ EMM-áâà ­¨æã ¢ W3 (0xC000) „Ž ¢ë§®¢  read(), 㪠§ â¥«ì 0xC000+offset ¢ «¨¤¥­. …᫨ ¢ëïá­¨âáï çâ® BIOS âண ¥â W3 ¢® ¢à¥¬ï read (£à ä¨ç¥áª¨© ¢¨¤¥®¡ãä¥à ¯® 㬮«ç ­¨î ¢ W3 ¯à¨ £à ä¨ç¥áª¨å ०¨¬ å, ­® ¢ ⥪á⮢®¬ - ¤®«¦¥­ ¡ëâì ᢮¡®¤¥­) - fallback: ç¨â âì ¢ 1 KB ¡ãä¥à ¢ W2 ¨ ª®¯¨à®¢ âì ¢ W3 ç¥à¥§ `bank_write_w3()`.
---
## ¥ «¨§ æ¨ï - ¯®íâ ¯­ ï
### Phase 1 - Plain text viewer (MVP)
**—â® à ¡®â ¥â:**
- ‡ £à㧪  ä ©«  (`open/read/close`) ¢ W1-áâà ­¨æã
- ˆ­¤¥ªá æ¨ï áâப (LF / CRLF à §¤¥«¨â¥«¨)
- Status bar: ¨¬ï ä ©« , L N-M / Total, ¯à®æ¥­â áªà®««¨­£ 
- Menu bar: `F1 Help F10 Exit`
-  ¢¨£ æ¨ï: ^/V (1 áâப ), PgUp/PgDn (30 áâப), Home/End (­ ç «®/ª®­¥æ), Esc/F10 (¢ë室), F1 (help screen)
- Ž¡à¥§ª  áâப ¤«¨­­¥¥ 80 ᨬ¢®«®¢ (¡¥§ word-wrap)
- –¢¥â : ⥪áâ ¡¥«ë© ­  çñà­®¬; status/menu - ¡¥«ë© ­  ᨭ¥¬
**Šà¨â¨ç­ë¥ ä ©«ë:**
- `examples/mdview/mdview.c` - main, key loop, rendering, indexing (one-file MVP)
- `examples/mdview/Makefile`
- `examples/mdview/SAMPLE.MD` - â¥áâ®¢ë© markdown ä ©«
### Phase 2 - Headers ¨ £®à¨§®­â «ì­ ï «¨­¨ï
**MD ä¨ç¨:**
- `# H1` -> ïમ-¦ñ«âë© (COLOR_YELLOW = 14) ­  çñà­®¬
- `## H2` -> ïમ-£®«ã¡®© (COLOR_LBLUE = 11)
- `### H3` -> ïમ-§¥«ñ­ë© (COLOR_LGREEN = 10)
- `#### H4+` -> á¥àë© (COLOR_GREY = 8)
- `---` / `***` ­  ®â¤¥«ì­®© áâப¥ -> «¨­¨ï 0xC4 (£®à¨§®­â «ì­ ï à ¬ª  ASCII) ¢® ¢áî è¨à¨­ã
### Phase 3 - Inline emphasis
** àá¥à inline (per-line, runs ¢ ®¤­ã áâபã):**
- `**bold**` -> ATTR_TEXT_BOLD
- `*italic*` -> ATTR_TEXT_ITALIC
- `_underscore_` -> ATTR_TEXT_UNDERSORE
- `` `code` `` -> ATTR_TEXT_CODE
- Œ àª¥àë `**`/`*`/`_`/`` ` `` … ७¤¥àïâáï (áꥤ îâáï)
State machine: ®¤¨­  ªâ¨¢­ë© áâ¨«ì ®¤­®¢à¥¬¥­­® (¡¥§ ¢«®¦¥­­®áâ¨); ª®­ä«¨ªâãî騩 ¬ àª¥à ¯à¨ ç㦮¬  ªâ¨¢­®¬ á⨫¥ ¢áñ à ¢­® ª®­áìâáï (zero-width) ¤«ï ᨭåà®­¨§ æ¨¨ è¨à¨­ë á index_lines. ‘®áâ®ï­¨¥ á¡à á뢠¥âáï ­  ª ¦¤®© áâப¥.
**Flanking-¯à ¢¨«® (CommonMark intraword, ॠ«¨§®¢ ­® ¯®á«¥ ¨§­ ç «ì­®£® ¯« ­ ):**
- `*` / `**` / `_` áç¨â îâáï markdown-¬ àª¥à®¬ ⮫쪮 ¥á«¨ whitespace/EOL
஢­® á Ž„މ áâ®à®­ë (XOR).
- ‘«ãç ¨ "®¡  whitespace" (`2 * 3`, `2 ** 3`) -> «¨â¥à «ë ( à¨ä¬¥â¨ª ).
- ‘«ãç ¨ "­¨ ®¤­®£® whitespace" (`COLOR_YELLOW`, `FILE*/fread`, `foo*bar*baz`)
-> «¨â¥à «ë (intraword).
- Backtick (`` ` ``) flanking … âॡã¥â - `` `code` `` à ¡®â ¥â ¡¥§ ¯à®¡¥«®¢.
- à ¢¨«® ¯à¨¬¥­¥­® ᨬ¬¥âà¨ç­® ¢ 4 ¬¥áâ å (`index_lines`, cont-render,
®á­®¢­®© inline-¯ àá¥à, truncation peek), ¨­ ç¥ wrap-¨­¤¥ªá â®à ¨
७¤¥à ࠧꥤãâáï ¯® è¨à¨­¥.
> **Šíè ®âä®à¬ â¨à®¢ ­­ëå áâப** - ®â«®¦¥­ ¢ á ¬ë© ª®­¥æ, á¬. "Phase : ®¯â¨¬¨§ æ¨¨".
> ‘ª®à®á⨠⥪ã饣® ­ ¨¢­®£® ७¤¥à  墠⠥⠭  80x30 = 2400 wrchar / ª ¤à; PgUp/PgDn ¢¨§ã «ì­® ¬£­®¢¥­¥­.
### Phase 4 - Block elements (¡¥§ â ¡«¨æ)
- Œ àª¨à®¢ ­­ë¥ ᯨ᪨: `- foo`, `* foo`, `+ foo` -> ¯à¥ä¨ªá `o` (0x07) + ¯à®¡¥«; 梥⠬ àª¥à  ïàç¥ ®á­®¢­®£®
- 㬥஢ ­­ë¥ ᯨ᪨: `1. foo`, `2. foo` -> ª ª ¥áâì (ç¨á«® ®áâ ¢«ï¥¬)
- Blockquote: áâப¨ á `> ` -> ¯à¥ä¨ªá `³` (0xB3) á¥à®£® 梥â , ®áâ «ì­®© ⥪áâ á«¥£ª  ¯à¨£«ãèñ­­ë©
- Fenced code blocks: `` ``` `` ®âªà뢠¥â/§ ªà뢠¥â ¡«®ª; ¢á¥ áâப¨ ¬¥¦¤ã - bg=á¥àë©, ¬®­®è¨à¨­­® (¡¥§ inline-¯ àᨭ£ )
- Indented code blocks (4+ ¯à®¡¥«®¢):  ­ «®£¨ç­® fenced, ­® ¡¥§ ®£® ¬ àª¥à 
**Light nested lists (v1 - ॠ«¨§®¢ ­®):**
- `classify_line()` ¯à®¯ã᪠¥â leading spaces ¯¥à¥¤ ulist/olist/quote ¬ àª¥à®¬,
¢®§¢à é ¥â `content_off` ¯®á«¥ ¬ àª¥à  -> `content_off - p_start` = indent + marker
¡¨â ¢ visible col.
- `render_line()` à¨áã¥â leading-spaces ¢ `ATTR_TEXT`, ¯®â®¬ ¬ àª¥à ­  ᤢ¨­ã⮩
¯®§¨æ¨¨ (col = indent). Marker ¢áñ ¥éñ 䨪á¨à®¢ ­ ¯à¨ £®à¨§®­â «ì­®¬ pan'¥.
- HR / header / fence delim ®áâ îâáï áâண® col-0 (CommonMark à §à¥è ¥â ¤® 3
¢¥¤ãé¨å ¯à®¡¥«®¢ ¤«ï ­¨å - ã¯à®á⨫¨).
- Tab-indent -> ­¥ à á¯®§­ ñâáï ª ª nesting (⮫쪮 spaces).
**Phase 4-full - ¯®«­ ï ¯®¤¤¥à¦ª  ¢«®¦¥­­®á⨠(deferred):**
- **Tab-indent**: áç¨â âì tab = 4 ¯à®¡¥«  ¤«ï ®¯à¥¤¥«¥­¨ï ã஢­ï.
- **Quote nesting** (`> > foo`): ª ¦¤ë© `>` ¯®¤àï¤ = +1 ã஢¥­ì, ª ¦¤ë© à¨áã¥âáï
®â¤¥«ì­ë¬ `³` ¢ `ATTR_QUOTE_MARKER` (¢¨§ã «ì­ ï "«¥áâ­¨æ " á«¥¢ ).
- **Hanging indent ¢ wrap-continuation**: ª®£¤  `- some very long bullet text
that wraps...` - continuation seg ¤®«¦¥­ ­ ç¨­ âìáï ®â content-col (¯®á«¥
¬ àª¥à ),   ­¥ ®â col 0. ‘¥©ç á continuation ¨¤ñâ ®â col 0 (v1 simplification).
’ॡã¥â åà ­¨âì `marker_width` per «®£¨ç¥áª ï áâப  (8 ¡¨â) ¨«¨ re-classify
first seg ¯à¨ ७¤¥à¥ continuation.
- **Lazy continuation**: áâப¨ ¡¥§ ¬ àª¥à , ­® á ¯à ¢¨«ì­ë¬ indent ¯®¤
¯à¥¤ë¤ã騬 bullet'®¬, ¤®«¦­ë áç¨â âìáï ¯à®¤®«¦¥­¨¥¬ ⮣® bullet' 
(¢¨§ã «ì­® - ®¡é¨© attr).
- **Strict CommonMark indent rules**: ¢«®¦¥­­ë© ¯ã­ªâ ¤®«¦¥­ ¡ëâì ­  indent
>= content_col த¨â¥«ï, ¨­ ç¥ áç¨â ¥âáï breakout. 㦥­ ¬¨­¨-stack
 ªâ¨¢­ëå ᯨ᪮¢ ¯à¨ ¨­¤¥ªá æ¨¨.
### Phase 5 - Wrap / Unwrap ¤«¨­­ëå áâப
„¥ä®«â: **wrap on**. F2 ¯¥à¥ª«îç ¥â; ¢ ¬¥­î-¡ à¥ ¯®¤¯¨áì ®âà ¦ ¥â ¤¥©á⢨¥
("Unwrap" ª®£¤  wrap ¢ª«îçñ­, "Wrap" ª®£¤  ¢ëª«î祭). ‚® ¢à¥¬ï २­¤¥ªá æ¨¨
ªàãâ¨âáï ᯨ­­¥à ­  title bar.
**v1 - ॠ«¨§®¢ ­®:**
- ޤ¨­ ¬ áᨢ `line_offset[2048]` åà ­¨â ‚ˆ„ˆŒ›… ᥣ¬¥­âë (  ­¥ «®£¨ç¥áª¨¥
áâப¨); ¡¨âë 0..13 - ¡ ©â®¢®¥ ᬥ饭¨¥, ¡¨â 15 - CONT-ä« £ continuation.
- Wrap-०¨¬: soft wrap ­  ¯®á«¥¤­¥¬ ¯à®¡¥«¥ <= 80; hard fallback ¥á«¨
¯à®¡¥«  ­¥â.
- Œ àª¥àë í¬ä §¨á  (`**`/`*`/`_`/`` ` ``) ¨ header-¯à¥ä¨ªáë (`#`/`##`/...) ­¥
ãç¨â뢠îâáï ¢ visible-col ¯à¨ ¯®¨áª¥ â®çª¨ ¯¥à¥­®á .
- "‘¯¥æ¨ «ì­ë¥" «®£¨ç¥áª¨¥ áâப¨ ­¥ wrap' îâáï ¢®®¡é¥ (®¤­  seg-§ ¯¨áì ­ 
«®£¨ç¥áªãî áâபã): fence delim, table row (header/separator/body), HR.
- F2 toggle á®åà ­ï¥â ¢¨§ã «ì­ãî ¯®§¨æ¨î ç¥à¥§ `top_offset` ¢ FILE_BUF.
- Bitmaps (`in_code`/`in_table`/`is_tab_hdr`) ¯¥à¥áâà ¨¢ îâáï ¢¬¥á⥠á
ᥣ¬¥­â ¬¨, ¨­¤¥ªá¨àãîâáï seg-¨­¤¥ªá®¬, ¡¨âë áâ ¢ïâáï ⮫쪮 ­  ¯¥à¢®¬
seg'¥ «®£¨ç¥áª®© áâப¨.
- Continuation-ᥣ¬¥­âë ७¤¥àïâáï ¢ á⨫¥ "v1: ¯«®áª®" - plain text,
­¨ª ª¨å markdown-ª« áá¨ä¨ª æ¨©; padding ¤® ª®­æ  áâப¨ `ATTR_TEXT`.
**v1.5 - ®â«®¦¥­® ¤«ï ¯®«­®© ª à⨭ë wrap:**
- **Hanging indent**: continuation ®â ulist/olist/quote ¤®«¦¥­ ¢ëà ¢­¨¢ âìáï
¯®¤ content,   ­¥ ®â col 0. ’ॡã¥â åà ­¨âì marker_width per «®£¨ç¥áª ï
áâப  ˆ‹ˆ re-classify ¯¥à¢®£® seg'  ¯à¨ ७¤¥à¥ continuation.
- ** á«¥¤®¢ ­¨¥ base_attr**: continuation ®â header'  ¤®«¦¥­ á®åà ­ïâì
梥â; continuation ®â code body - ä®­ ATTR_TEXT_CODE. ’ॡã¥â åà ­¨âì
1 ¡ ©â `base_attr` per seg ˆ‹ˆ lookup ¯¥à¢®£® seg' .
- **Inline emphasis ç¥à¥§ £à ­¨æã**: í¬ä §¨á, ®âªàëâë© ¢ ¯¥à¢®¬ seg'¥ ¨ ­¥
§ ªàëâë©, ¤®«¦¥­ ¯à®¤®«¦ âìáï ¢® ¢â®à®¬. ’ॡã¥â åà ­¨âì emph state per
seg (3 ¡¨â ).
- Compact way: ¤®¡ ¢¨âì ¯ à ««¥«ì­ë© ¬ áᨢ `seg_meta[MAX_SEGS]` ¯® 1 ¡ ©âã
- ¯ ªã¥â marker_width (4 ¡¨â ) + emph_state (3 ¡¨â ) + base_attr_idx
(4 ¡¨â  ¨§ â ¡«¨æë -> ­ã¦¥­ 2-¡ ©â®¢ë© seg_meta).
- **Hpan ¤«ï ¤«¨­­ëå áâப**: ¥á«¨ wrap ¢ëª«î祭, ¤®¡ ¢¨âì <-/-> ¤«ï
£®à¨§®­â «ì­®£® áªà®««  >80 cols. ޡ鍩 ¬¥å ­¨§¬ á tables (Phase 6).
**…€‹ˆ‡Ž‚€Ž** (light) - `viewport_x` + ¯®«­ë© re-render ­  ª ¦¤®¥ <-/->.
- **“᪮७¨¥ hpan ç¥à¥§ ESTEX WINCOPY/WINREST** (deferred): ᥩç á pan
¤¥« ¥â ¯®«­ë© `render_viewport()` = 30 áâப x 80 wrchar. Œ®¦­®
᪮¯¨à®¢ âì áãé¥áâ¢ãî饥 ᮤ¥à¦¨¬®¥ viewport'  ­  N cols ¢«¥¢®/¢¯à ¢®
ç¥à¥§ win-copy, ¯®â®¬ ७¤¥à¨âì ⮫쪮 㧪ãî ¯®«®áã á¯à ¢ /á«¥¢ 
(HPAN_STEP cols x 30 rows 240 wrchar ¢¬¥áâ® 2400). ESTEX SCROLL
£®à¨§®­â «ì ­¥ ¯®¤¤¥à¦¨¢ ¥â - ­ã¦­  ¨¬¥­­® WINCOPY-®¯¥à æ¨ï ¨«¨
rdchar/wrchar loop. €ªâ¨¢¨à®¢ âì ª®£¤  ®éãâ¨âáï â®à¬®§; ᥩç á ­ 
⨯®¢®¬ markdown'¥ ­¥ § ¬¥â­®.
**v2 - ®â¤¥«ì­ ï ä¨ç , ¬¨¬® wrap:**
- Toggle ¯®¤á¢¥âª¨ 楫¨ª®¬ (F3?)
- Search ¯® ⥪áâã (Ctrl+F / F4)
- Links `[text](url)` -> ᨭ¨© ¯®¤çñભãâë© text, url ¯àïç¥âáï
- Images `![alt](path)` -> `[IMG: alt]`
### Phase 7 - Links ¨ ¯®¨áª (post-v1)
- `[text](url)` -> ®âà¨á®¢ âì ⮫쪮 `text` á ïમ-ᨭ¨¬ FG (¢¨§ã «ì­® ¯®¤çñભã⮥)
- `![alt](path)` -> `[IMG: alt]` ¢ ᪮¡ª å
- Search ¯® ⥪áâã (F3 / Ctrl+F): ¨­ªà¥¬¥­â «ì­ë©, ¯®¤á¢¥âª  ᮢ¯ ¤¥­¨©
### Phase 8 - F8 Raw / Render toggle
¥à¥ª«îç â¥«ì ०¨¬  ®â®¡à ¦¥­¨ï: ¯à¨ ¢ª«îçñ­­®¬ Raw ¯®ª §ë¢ ¥âáï ¨á室­ë©
⥪áâ ä ©«  ª ª ¥áâì - ¢á¥ markdown-¬ àª¥àë (`#`, `**`, `_`, `` ` ``, `|`,
`>`, `-`, etc.) ७¤¥àïâáï «¨â¥à « ¬¨ á `ATTR_TEXT`, ¡¥§ ª« áá¨ä¨ª æ¨¨.
®«¥§­® ª®£¤ :
- ­ã¦­® 㢨¤¥âì â®ç­ãî à §¬¥âªã (®â« ¦¨¢ ­¨¥ .md, áªà¨­è®âë, ª®¯¨à®¢ ­¨¥)
- markdown-ª« áá¨ä¨ª â®à ®è¨¡áï ¨ å®ç¥âáï 㢨¤¥âì ®à¨£¨­ «
- å®ç¥âáï ¡ëáâà® áà ¢­¨âì "¤®/¯®á«¥" ७¤¥à 
**®¢¥¤¥­¨¥:**
- F8 ¯¥à¥ª«îç ¥â `render_mode` (1=render, 0=raw); ¬¥­î ¯®ª §ë¢ ¥â ®¡à â­®¥
¤¥©á⢨¥ ("Raw" ª®£¤  ᥩç á render, "Render" ª®£¤  ᥩç á raw) - ⮩ ¦¥
«®£¨ª®© çâ® F2/Wrap/Unwrap.
- Raw ०¨¬¥: `render_line()` ¨¤ñâ ¯® ª®à®âª®¬ã ¯ã⨠- ­¨ª ª®£®
`classify_line`, `is_fence_delim`, `is_code_body`, inline-í¬ä §¨á ; ¯à®áâ®
¡ ©â®¢ë© ¤ ¬¯ FILE_BUF ®â seg-offset ¤® next-seg/EOL á tab-expansion ¨
ATTR_TEXT.
-  §¤¥«ì­® ®â F2: ®¡  ०¨¬  ­¥§ ¢¨á¨¬ë (¬®¦­® Raw+Wrap, Raw+Truncate,
Render+Wrap, Render+Truncate). Wrap-«®£¨ª  ¢ `index_lines` à ¡®â ¥â ¢
®¡®¨å á«ãç ïå ®¤¨­ ª®¢® (®¯¨à ¥âáï ­  ¢¨§ã «ì­ë¥ ª®«®­ª¨ ­¥§ ¢¨á¨¬® ®â
à áªà áª¨).
- ‘â âãá-¡ à: ¤®¡ ¢¨âì ¨­¤¨ª â®à `[R]` / `[V]` (Raw / View) ¨«¨ ⥪á⮬
`RAW` à冷¬ á ¨¬¥­¥¬ ä ©« .
**Œ¨­¨¬ «ì­ ï ॠ«¨§ æ¨ï:**
- ޤ¨­ ­®¢ë© static `uint8_t render_mode = 1;`
- `render_line()`: ­  á ¬®¬ ¢¥àåã `if (!render_mode) { ... raw render ... return; }`
- `render_menu()`: ¤®¡ ¢¨âì ïà«ëª F8 à冷¬ á F2.
- ‚ £« ¢­®¬ 横«¥: `case KEY_F8: toggle_render(); break;`
- `toggle_render()` ®â«¨ç ¥âáï ®â `toggle_wrap()` ⥬, çâ® … ¯¥à¥áâà ¨¢ ¥â
`line_offset[]` (wrap-ᥣ¬¥­â æ¨ï ­¥ ¬¥­ï¥âáï), ⮫쪮 ¯¥à¥à¥­¤¥à¨â íªà ­.
---
## API-­®¢¨­ª¨ ¢ libc (¬¨­¨¬ «ì­ë¥)
### `getkey()` - extended key reader (¢ libc)
’¥ªã騩 `getch()` â¥àï¥â scan code à áè¨à¥­­ëå ª« ¢¨è (¢®§¢à é ¥â ⮫쪮 E=ASCII). „®¡ ¢«ï¥¬ **áà §ã ¢ `libc/conio/conio.c`** ­®¢ãî äã­ªæ¨î à冷¬ á `getch()`:
```c
// Returns scan in high byte, ASCII in low byte.
// Extended keys (arrows, F-keys, PgUp/PgDn, Home/End): ASCII=0, scan code ¢ high byte.
// Plain keys: ASCII ¢ low byte; high byte ᮤ¥à¦¨â positional scan (¡¨â 7 = Ctrl/Alt/Shift modifier).
uint16_t getkey(void) __naked {
__asm
ld c, #0x30 ; ESTEX WAITKEY
rst #0x10 ; A=ASCII, D=scan, E=ASCII
ld e, a ; ensure E=ASCII even if E clobbered
ret ; SDCC __sdcccall(1): ¢®§¢à â uint16_t ¢ DE (D=scan, E=ASCII)
__endasm;
}
```
„®¯®«­¨â¥«ì­® - **¢ `libc/include/conio.h`** ¯à®¯¨á âì ¯à®â®â¨¯ ¨ ª®­áâ ­âë scan-ª®¤®¢:
```c
uint16_t getkey(void);
/* Scan codes for getkey() high byte when ASCII=0 (extended keys). */
#define KEY_F1 0x0E
#define KEY_F2 0x0F
#define KEY_F3 0x10
#define KEY_F4 0x11
#define KEY_F5 0x12
#define KEY_F6 0x13
#define KEY_F7 0x14
#define KEY_F8 0x15
#define KEY_F9 0x16
#define KEY_F10 0x17
#define KEY_F11 0x18
#define KEY_F12 0x19
#define KEY_END 0x24
#define KEY_DOWN 0x25
#define KEY_PGDN 0x26
#define KEY_LEFT 0x27
#define KEY_RIGHT 0x29
#define KEY_HOME 0x2A
#define KEY_UP 0x2B
#define KEY_PGUP 0x2C
#define KEY_INS 0x23
#define KEY_DEL 0x22
```
**‘ª ­-ª®¤ë (¨§ docs/converted/ProgrammerManual.txt:2143-2323):**
| Š« ¢¨è  | scan | Š« ¢¨è  | scan |
|---|---|---|---|
| F1 | 0x0E | Up | 0x2B |
| F10 | 0x17 | Down | 0x25 |
| F11 | 0x18 | Left | 0x27 |
| F12 | 0x19 | Right | 0x29 |
| | | PgUp | 0x2C |
| | | PgDn | 0x26 |
| | | Home | 0x2A |
| | | End | 0x24 |
### —â® **¯¥à¥¨á¯®«ì§ã¥¬** ¨§ áãé¥áâ¢ãî饩 libc
- `open/read/lseek/close` - `libc/io/{open,read,lseek}.c` (POSIX wrappers)
- `mem_alloc_pages/mem_free_block/mem_get_page` - `libc/mem/mem_alloc.c`
- `sprinter_page_w3()` - inline `__sfr` write ¢ `libc/include/sprinter.h:113`
- `bank_read_w3/bank_write_w3` - `libc/mem/bank_io_w3.c` (¤«ï fallback ¨«¨ v2 multi-page)
- `wrchar(x, y, ch, attr)` - `libc/conio/conio.c:476` (¡¥§ auto-scroll, ¨¤¥ «ì­® ¤«ï viewport)
- `clrscr_attr(attr)` - `libc/conio/conio.c:395`
- `gotoxy/wherex/wherey` - `libc/conio/conio.c:412-462` (¥á«¨ ­ã¦­®)
- `kbhit()` - `libc/conio/conio.c:22` (¤«ï non-blocking ®¯à®á , ®¯æ¨®­ «ì­®)
- `dec16/dec8` - `libc/stdio/dec_print.c` (¤«ï status bar: ⥪ãé ï áâப  / total / %)
- `COLOR(fg, bg)` ¬ ªà®á - `libc/include/conio.h:152`
- –¢¥â®¢ë¥ ª®­áâ ­âë `COLOR_*` - `libc/include/conio.h:145`
- `strlen/memcpy/memset` - z80.lib (… ¯¥à¥¯¨á뢠âì)
---
## ‘âàãªâãà  ¨á室­¨ª®¢
```
examples/mdview/
ÃÄÄ Makefile # áâ ­¤ àâ­ë© pattern (á¬. examples/cat/Makefile)
ÃÄÄ mdview.c # Phase 1: ¢áñ ¢ ®¤­®¬ ä ©«¥ (main, keys, indexing, render)
ÃÄÄ SAMPLE.MD # â¥áâ®¢ë© markdown
ÀÄÄ README.md # ®¯¨á ­¨¥ ¨ controls
```
®á«¥ Phase 3 à áª¨¤ âì ¯® ¬®¤ã«ï¬ (¥á«¨ á㬬 à­ë© à §¬¥à > ~6KB):
```
mdview.c - main loop, status/menu bars, key dispatch
mdrender.c - line rendering with MD inline parser
mdindex.c - file load + line indexing
```
### ‘¡®àª 
```makefile
PROJ ?= ../..
SPRINTER_CC := $(PROJ)/bin/sprinter-cc
mdview.exe: mdview.c
$(SPRINTER_CC) --memory small -o $@ mdview.c
```
`--memory small`: ª®¤+¤ ­­ë¥ ¢ W2 (DSS ¤ ñ⠭㦭®¥ ç¨á«® áâà ­¨æ); ä ©« - ®â¤¥«ì­ ï EMM-áâà ­¨æ  ¢ W3.
---
## ‘âàãªâãà  mdview.c (Phase 1, í᪨§)
```c
#include <stdint.h>
#include <stdio.h>
#include <conio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sprinter.h>
#include <sprinter_mem.h>
#define VIEW_TOP 1
#define VIEW_BOT 30 // inclusive
#define VIEW_H 30
#define SCREEN_W 80
#define MAX_LINES 2048
#define FILE_BUF ((char*)0xC000) /* W3 - EMM page mapped here */
#define TAB_STOP 4
#define ATTR_TEXT COLOR(COLOR_WHITE, COLOR_BLACK)
#define ATTR_BAR COLOR(COLOR_WHITE, COLOR_BLUE)
#define ATTR_MENU_K COLOR(COLOR_YELLOW, COLOR_BLUE)
#define ATTR_MENU_T COLOR(COLOR_WHITE, COLOR_BLUE)
static uint16_t line_off[MAX_LINES];
static uint16_t n_lines;
static uint16_t top_line;
static uint16_t file_size;
static uint8_t file_blk;
static char filename[64];
static int load_file(const char *path); // open, alloc EMM page, map W3, read, close
static void index_lines(void); // scan FILE_BUF, fill line_off[]
static void render_status(void); // row 0
static void render_menu(void); // row 31
static void render_line(uint16_t idx, uint8_t row); // one line @ row
static void render_viewport(void); // VIEW_H lines starting from top_line
static void scroll_up(uint16_t n);
static void scroll_down(uint16_t n);
static void help_screen(void); // F1
int main(int argc, char **argv) {
if (argc < 2) { puts("Usage: mdview <file.md>"); return 1; }
if (load_file(argv[1]) < 0) { puts("load error"); return 1; }
index_lines();
clrscr_attr(ATTR_TEXT);
render_menu();
render_status();
render_viewport();
for (;;) {
uint16_t k = getkey();
uint8_t ascii = k & 0xFF;
uint8_t scan = (k >> 8) & 0x7F; // strip mod bit
if (ascii) {
if (ascii == 0x1B) break; // Esc -> exit
continue;
}
switch (scan) {
case KEY_F10: goto exit;
case KEY_F1: help_screen(); break;
case KEY_UP: scroll_up(1); break;
case KEY_DOWN: scroll_down(1); break;
case KEY_PGUP: scroll_up(VIEW_H); break;
case KEY_PGDN: scroll_down(VIEW_H); break;
case KEY_HOME: top_line = 0;
render_viewport(); break;
case KEY_END: /* clamp to last viewport */ break;
}
render_status();
}
exit:
mem_free_block(file_blk);
clrscr_attr(ATTR_TEXT);
return 0;
}
```
---
## Verification
### Phase 1
1. ®¤£®â®¢¨âì `SAMPLE.MD` ~5 KB (§ £®«®¢ª¨,  ¡§ æë, ᯨ᪨) - ७¤¥à¨âìáï ¡ã¤¥â plain.
2. `cd examples/mdview && make`
3. `python make_disk.py mdview.exe SAMPLE.MD -> mc.img && ./run_mame.sh`
4. ஢¥à¨âì:
- Status bar ¯®ª §ë¢ ¥â `SAMPLE.MD L 1-30 / N X%`
- Menu bar ¢­¨§ã
- ^/V: 1 áâப 
- PgUp/PgDn: 30 áâப, ª®à४⭮¥ clamp ­  £à ­¨æ å
- Home: top_line=0
- End: top_line = total_lines - VIEW_H
- F1: ¯®ª §ë¢ ¥â help, «î¡ ï ª« ¢¨è  ¢®§¢à é ¥â
- F10 / Esc: ¢ë室, íªà ­ ®ç¨é¥­
5. Edge cases: ¯ãá⮩ ä ©«, ä ©« ¨§ ®¤­®© áâப¨, ä ©« á ®ç¥­ì ¤«¨­­®© áâப®© (>80), CRLF ¨ LF mixed.
### Phase 2-4
 áè¨àïâì `SAMPLE.MD` á ä¨ç ¬¨ ¯® ¬¥à¥ ¤®¡ ¢«¥­¨ï, ¢¨§ã «ì­® ¢¥à¨ä¨æ¨à®¢ âì ¢ MAME. ‘ªà¨­è®â-áà ¢­¥­¨¥ ®¯æ¨®­ «ì­®.
### ¥£à¥áᨨ
- ¨ª ª¨å ¨§¬¥­¥­¨© ¢ libc ­  Phase 1 ªà®¬¥ (¯®â¥­æ¨ «ì­®) ¤®¡ ¢«¥­¨ï `getkey()` ¢ `libc/conio/conio.c` - ¥á«¨ â ª, ¯à®£­ âì `examples/conio2` ¨ `examples/filetest` ç⮡ë ã¡¥¤¨âìáï çâ® ­¨ç¥£® ­¥ á«®¬ «®áì.
---
## ¥è¥­¨ï ¯® ­¥®¤­®§­ ç­®áâï¬
1. **Word-wrap vs truncate**: v1 = truncate (¯à®áâ®). v2 - F2 toggle wrap.
2. **ƒ®à¨§®­â «ì­ë© áªà®««¨­£**: v1 - ­¥â; v2 - `<-/->` ᤢ¨£ viewport ¯® á⮫¡æ ¬.
3. **Tab handling**: ¯à¥®¡à §®¢ ­¨¥ ¯à¨ ७¤¥à¥, **tabstop = 4** (áâ ­¤ àâ MD). Žà¨£¨­ « ¢ W3 ­¥ âண ¥¬.
4. **UTF-8**: ७¤¥à¨¬ ¡ ©âë ª ª ¥áâì. …᫨ ä ©« ¢ CP866 - ®âà¨áã¥âáï ª¨à¨««¨æ¥© ç¥à¥§ á¨á⥬­ë© ä®­â. UTF-8 - ¯®ª  ­¥ ¯®¤¤¥à¦¨¢ ¥¬ (¢¨§ã «ì­® ¡ã¤¥â ª è  ­  ­¥-ASCII ᨬ¢®« å; ¤¥â¥ªâ ¨ ¯à¥¤ã¯à¥¦¤¥­¨¥ - ¢ v2).
---
### Phase 6 - ®«­ë© layout â ¡«¨æ (deferred)
‘¥©ç á (Phase 4-tables) â ¡«¨æë à¨áãîâáï "¤¥ª®à â¨¢­®" - ¯ ©¯ë ¨ â¨à¥
§ ¬¥­ïîâáï ­  box-drawing chars, ­® è¨à¨­ë ª®«®­®ª ¡¥àãâáï ª ª ¥áâì ¨§
¨á室­¨ª . –¥«ì Phase 6 - ¯¥à¥áç¨â âì â ¡«¨æã ¢ ­®à¬ «¨§®¢ ­­ë© ¢¨¤:
- **Pre-scan â ¡«¨æë**: ¯à®©â¨ ¢á¥ áâப¨ ®¤­®£® table-¡«®ª , ­ ©â¨
¬ ªá¨¬ «ì­ãî è¨à¨­ã ª ¦¤®© ª®«®­ª¨ (á ãçñ⮬ áꥤ¥­­ëå inline-¬ àª¥à®¢ -
¢¨§ã «ì­ë© à §¬¥à, ­¥ ¡ ©â®¢ë©).
- **Re-emit ¢ ¡ãä¥à¥**: ¯à¨ § £à㧪¥ ä ©«  (¨«¨ ¯à¨ ¯¥à¢®© ¢áâà¥ç¥ â ¡«¨æë)
¯¥à¥¯¨á âì áâப¨ ¢ FILE_BUF â ª, çâ®¡ë ¢á¥ ï祩ª¨ ®¤­®© ª®«®­ª¨ ¨¬¥«¨
®¤¨­ ª®¢ãî è¨à¨­ã; ¤®¡ ¢¨âì top/bottom à ¬ª¨ (`ÚÄÂÄ¿` / `ÀÄÁÄÙ`) ª ª
ᨭâ¥â¨ç¥áª¨¥ áâப¨. â® ¯®§¢®«¨â á®åà ­¨âì 1:1 ᮮ⢥âá⢨¥ "«®£¨ç¥áª ï
áâப  -> ®¤­  viewport row" ¡¥§ ᯥ樠«ì­®© «®£¨ª¨ ¯à¨ ७¤¥à¥.
- ** ¬ïâì**: re-emit ¬®¦¥â “‚…‹ˆ—ˆ’œ ä ©« §  áçñâ padding ¨ ¤®¯.à ¬®ª.
…᫨ ¡ãä¥à ¡«¨§®ª ª 16KB - ®â१ âì â ¡«¨æã ¨ ¯®¬¥â¨âì ¥ñ overflow'®¬.
- **ƒ®à¨§®­â «ì­ë© áªà®««¨­£**: ¥á«¨ ¨â®£®¢ ï è¨à¨­  â ¡«¨æë (¨«¨ «î¡®©
áâப¨) > SCREEN_W = 80 - ¤®¡ ¢¨âì <-/-> ¤«ï horizontal pan. â® ¡ã¤¥â
®¡é¨© ¬¥å ­¨§¬ ¤«ï ¤«¨­­ëå áâப (á¬. â ª¦¥ wrap mode), ­¥ ⮫쪮 â ¡«¨æ.
- **‚ëà ¢­¨¢ ­¨¥ ¨§ separator-row**: `:-` -> left, `-:` -> right, `:-:` ->
center; ãç¨â뢠âì ¯à¨ padding'¥ ᮤ¥à¦¨¬®£® ï祩ª¨.
- **˜ £¨ ॠ«¨§ æ¨¨**:
1. Walking pass ¯® 䥭ᠬ/â ¡«¨æ ¬ ¯àאַ ¢ `index_lines()` - ᮡà âì
extents ¢á¥å â ¡«¨æ.
2. „«ï ª ¦¤®© â ¡«¨æë - ®¯à¥¤¥«¨âì è¨à¨­ë ª®«®­®ª.
3. ¥è¥­¨¥: rewrite-in-buffer (¯à®é¥ ¤«ï ७¤¥à , ­® ¬ãâ¨àã¥â ¨á室­¨ª)
vs render-time layout (cleaner, ­® âॡã¥â ®â¤¥«ì­®© áâàãªâãàë
®¯¨á ­¨ï layout'  ­  ª ¦¤ãî â ¡«¨æã).
4. Hpan: ®¡é¨© `viewport_x_offset` ¤«ï ¢á¥£® íªà ­ , ¨«¨ ®â¤¥«ì­ë©
"è¨à®ª¨© ०¨¬" ⮫쪮 ¢­ãâਠ⠡«¨æ.
> ¥ ¡«®ª¨àãîé ï ä¨ç . ‡ ¯ã᪠âì ª®£¤  áâ ­¥â ¯®­ï⥭ ⨯®¢®© ¨áâ®ç­¨ª
> markdown-ä ©«®¢ (㧪¨¥ ç¨â «ª¨ -> ¤®áâ â®ç­® ⥪ã饣® ¤¥ª®à â®à ;
> è¨à®ª¨¥ README á ¡®«ì訬¨ â ¡«¨æ ¬¨ -> ­ã¦¥­ ¯®«­ë© layout).
---
### Phase - Šíè ७¤¥à¥­­ëå áâப (low priority)
Žâ«®¦¥­®: ⥪ãé ï ᪮à®áâì ¡®«¥¥ 祬 ¤®áâ â®ç­ . €ªâ¨¢¨à®¢ âì ¥á«¨ ¯®ï¢¨âáï
áæ¥­ à¨©, £¤¥ ¢¨¤­  § ¤¥à¦ª  PgUp/PgDn (­ ¯à¨¬¥à, ¯à¨ âï¦ñ«®¬ inline-¯ àá¥à¥
v2 á UTF-8 / linkifier / â ¡«¨æ ¬¨).
**Šíè ®âä®à¬ â¨à®¢ ­­ëå áâப** (W3, ®â¤¥«ì­ ï EMM-áâà ­¨æ ):
```
Cache layout (16 KB EMM page, ¢á¥£® 16000 ¡ ©â ¨á¯®«ì§ã¥âáï):
slot 0: 80 chars + 80 attrs = 160 bytes @ offset 0
...
slot 99: 80 chars + 80 attrs = 160 bytes @ offset 15840
Cache tags (W2 static): uint16_t cache_tag[100] = 200 bytes
cache_tag[i] = line_id, ¨«¨ 0xFFFF = invalid
```
‘âà â¥£¨ï - **direct map (no LRU)**: `slot = line_id % 100`. Š®««¨§¨ï -> ¢ëâ¥á­¥­¨¥.
**Batched viewport render**: 2 page-swap'  ­  ‚…‘œ viewport (cache -> file -> cache),
­¥ 60 ª ª ¯à¨ ­ ¨¢­®© ॠ«¨§ æ¨¨.
ਠᡮથ: `cache_blk = mem_alloc_pages(1)` ¯®á«¥ `file_blk`; `mem_free_block` ­  exit.
---
## —â® ®â«®¦¥­® ¢ v2
- ~~” ©«ë >16 KB~~ - **ᤥ« ­® ¢ v1.5** (¤® 128 KB ç¥à¥§ 1-8 EMM-áâà ­¨æ + lazy map ¢ W3).
- ~~Word wrap~~ - **ᤥ« ­®** (Phase 5, F2 toggle).
- Search (Find / Find next) - F3 / F4.
- F8 Raw / Render toggle - ᯥæ¨ä¨ª æ¨ï ¢ Phase 8.
- Links `[text](url)` + image alt - Phase 7.
- Tables - Phase 6 (¯®«­ë© layout).
- Toggle highlight on/off - ç áâ­ë© á«ãç © F8 Raw.
+133
View File
@@ -0,0 +1,133 @@
# 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 Окно справки
F10 / Esc Выход из программы
```
## Синтаксис 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 (DOS Cyrillic)
- **Максимальный размер файла:** 128 КБ
- **Максимальное число строк в индексе:** 16 384
- **Режим памяти:** small
- Код программы, cтек, данные, куча — окнa W1-W2 (32 КБ, адреса 0x40000xBFFF).
- Буфер файла — страницы EMM, отображаемые в W3 (0xC0000xFFFF)
## TODO
1. **Увеличение размера документов.** Снять лимит 128 КБ: Достаточно
разрешить работать с большим кол-вом страниц памяти, пока оттестированно
на работе с 8-мю страницами по 16Кб.
2. **Форматированные таблицы.** Разбирать строки вида `| ячейка | ячейка |`
с автоматическим выравниванием столбцов и отрисовкой разделительных
линий (строки `|---|---|`). На текущий момент таблицы отображаются
как обычные nowrap-строки без выравнивания.
3. **Поддержка кодировок CP1251 и UTF-8.** Автоопределение кодировки
по BOM, явное указание через аргумент командной строки (`--encoding cp1251`),
возможность переключения кодировки во время просмотра (`F8`).
Нужно прежде всего для документов на русском языке; CP866 кодировака уже поддерживается.
4. **Ускорение рендеринга.** Кэш строк экрана. Оптимизация цикла вывода
символов через BIOS WRCHAR (пакетный вывод, DMA).
---
*MDView v0.2 · (c) 2026 Петров А.Г.*
-165
View File
@@ -1,165 +0,0 @@
# 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.
---
## Section: Inline emphasis
Plain words mixed with **bold words**, *italic words*, _underscore
words_ and `code words` to verify all four styles render with their
own colours.
A single **bold** stretch, then a single *italic* stretch, then a single
_underscore_ stretch, then a single `code` stretch, all on the same line.
A *long italic run that spans multiple words and several columns before
it closes here* and continues plain.
Inline code with punctuation: call `printf("%d\n", x)` then check the
result; or use `argv[0]` to grab the program name.
Unclosed emphasis (open **bold left dangling) — should auto-close at the
end of the line so the next line starts clean.
Conflict cases: **bold with a stray * inside** stays bold, and *italic
with a stray _ inside* stays italic, and `code with **bold** inside`
stays code.
## Section: Lorem ipsum
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
## Section: Lists
Unordered (dash):
- alpha
- bravo
- charlie
Unordered (asterisk):
* delta
* echo
Unordered (plus):
+ foxtrot
+ golf
Numbered list (digits + period highlighted):
1. first
2. second
3. third
10. tenth — verifies multi-digit numbering
99. ninety-ninth
Numbered with parens:
1) parenthesised one
2) parenthesised two
Nested lists (light v1 — leading spaces enable detection at non-zero col;
no hanging indent in wrap continuation yet):
- top level alpha
- nested level 1 bravo
- nested level 1 charlie
- nested level 2 delta
- nested level 2 echo
- nested level 3 foxtrot
- nested level 1 golf
- top level hotel
Mixed nested:
1. first top
1. nested numeric
2. nested numeric
- mixed bullet under numeric
- another bullet
3. nested numeric
2. second top
## Section: Blockquote
> Single-line blockquote.
> Multi-line blockquote starts here.
> Each line keeps the > prefix highlighted
> while the body stays plain text.
## Section: Code
Inline code: `printf("hello")`.
Fenced block (lines inside should render with code attribute on the
full row, no inline parsing — note the **stars** and *italics* below
remain literal):
```c
int main(void) {
/* **not bold**, *not italic*, `not nested` */
return 42;
}
```
After the block, normal **bold** parsing resumes.
## Section: Long line truncation
This line is intentionally long to verify that the viewer truncates at 80 columns instead of wrapping or scrolling horizontally. Anything past the 80th visible column should simply not appear on screen.
## Section: Tabs
Indented with tabs:
tab-indent level 1
tab-indent level 2
tab-indent level 3
## Section: Blockquote
> Markdown blockquotes start with a greater-than sign at column zero.
> Multiple lines look like this.
### Subsection: deeper heading (H3)
The line above is an H3 — it should render in a colour distinct from
H1 and H2.
#### Subsubsection: H4 and below
H4 (and the rarely-seen H5/H6) all share the H4 colour slot.
***
## Section: Filler
The remaining content exists to make the document scroll past one viewport.
Line 50 ........... approx.
Line 51 ........... .
Line 52 ........... ..
Line 53 ........... ...
Line 54 ........... ....
Line 55 ........... .....
Line 56 ........... ......
Line 57 ........... .......
Line 58 ........... ........
Line 59 ........... .........
Line 60 ........... ..........
End-of-document marker. If you can see this line you can use Home / End
to bounce between the start and finish of the file.
@@ -0,0 +1,103 @@
# mdview — модель документа и рендеринг (единый смешанный режим)
Дизайн-документ переработки `mdview.c` под новое ТЗ форматирования (`examples/mdview/todo2`). Описывает решение по хранению данных, разбор документа при загрузке, упрощение рендера и снятие лимита на число строк.
## 1. Контекст и главный вопрос
Мы отказываемся от двух режимов показа (Wrap/UnWrap-переключатель) и переходим к единому смешанному режиму: тип переноса задаётся типом блока (обычный текст/заголовки/списки/цитаты — Wrap; код/таблицы — UnWrap).
Главный вопрос: нужно ли хранить оригинальный байтовый контент файла, или при загрузке сразу преобразовать его в «готовый к показу» текст (склеить строки параграфов, убрать маркеры, развернуть отступы и т.п.)?
Ответ: **оригинал храним; отдельный «готовый» текстовый/ячеечный буфер не строим.** «Подготовка при загрузке» реализуется как построение компактного *индекса метаданных*, а не как преобразование *содержимого*.
## 2. Решение по архитектуре
### 2.1 Текущее состояние (база)
* Файл целиком лежит в EMM-страницах (до 8×16 КБ). В окно W3 (`0xC000`) в каждый момент замаплена ровно одна страница; доступ к байту — через `fb()`/`map_page()` (`mdview.c:198-213`).
* Индекс — это параллельные массивы по экранным сегментам: `line_offset[]` (смещение в файле) + битфлаги `cont/nowrap/blank/in_code`, 2-битный `line_kind[]`, `init_style[]` (`mdview.c:122-135`).
* `index_lines()` за один проход уже делает склейку параграфов обычного текста, перенос по словам на 80 колонок и перенос emphasis через soft/hard break (`mdview.c:692-911`).
* `render_line()` при каждой отрисовке заново читает байты из оригинала и заново парсит inline-разметку (`mdview.c:918-1156`).
Вывод: «готовим при загрузке» мы уже делаем — но готовим **индекс**, а не текст.
### 2.2 Почему не материализуем содержимое
* **Одно свободное окно W3.** Режим памяти `small`: код в W1, данные/стек/куча в W2, для банкуемых данных свободен только W3. Преобразование «оригинал → готовый буфер» требует одновременно держать замапленными исходную страницу (чтение) и страницу-приёмник (запись). При единственном окне это поток swap-ов на каждую границу. In-place преобразование тоже невозможно: склейка меняет длины, смещения «съезжают», параграф пересекает границу 16 КБ.
* **Удвоение памяти и срыв гарантии 128 КБ.** Оригинал может занимать все 8 страниц; готовой копии нужны свои страницы — гарантировать, что влезут обе, нельзя.
* **Готовая форма не обязательно меньше.** Снятие маркеров экономит байты, но добавляются отступы-продолжения у переносов списков/цитат. В лучшем случае ≈ размер оригинала, в худшем — больше. Ячеечная модель (символ+атрибут) — это ×2 (до 256 КБ), невозможно.
* **Покадровая стоимость и так мала.** За кадр рисуются только 30 видимых строк (~30×80 чтений). Единственный дорогой проход — `index_lines()` — неизбежен в любой архитектуре (нужен полный скан для `n_lines` и процента прокрутки).
### 2.3 Что реально оптимизировать
Не текст, а повторную работу `render_line()`:
* вызов `classify_line()` на каждый кадр (`mdview.c:984`);
* обратный проход к первому не-cont сегменту ради отступа продолжения (`mdview.c:944-970`).
Это снимается переносом результата классификации (`kind`, ширина отступа/контент-колонка) в сам индекс на этапе `index_lines()`.
## 3. Модель данных индекса
### 3.1 Запись сегмента
Единая запись на экранный сегмент полностью заменяет нынешние параллельные массивы (`line_offset`, `cont_flag`, `in_code`, `nowrap_flag`, `blank_flag`, `line_kind`, `init_style`); `index_lines()` переписывается с нуля под эту модель. Цель размера записи — 5–6 байт:
* `offset` — 3 байта (24-битное смещение в файле, покрывает 128 КБ).
* `flags` — 1 байт: биты `cont`, `nowrap`, `blank`, `in_code` + 2-битный `ckind` (тип продолжения: PLAIN/QUOTE/LIST/OTHER).
* `style` — 1 байт: стартовый стиль сегмента (`init_style`, теперь включая STRIKE) + при необходимости глубина вложенности.
* `indent` — 1 байт: предвычисленная контент-колонка/ширина префикса, чтобы рендер не вызывал `classify_line()` и не делал обратный проход.
### 3.2 Размещение и снятие лимита `MAX_LINES`
Проблема: `line_offset` сейчас `uint32_t[2048]` = 8 КБ, `init_style` = 2 КБ; суммарно статический индекс ~11.6 КБ near-памяти (W2). Рост лимита в near невозможен — W2 переполнится.
Решение:
* **Индекс храним в отдельном EMM-блоке** (свои страницы, помимо файловых), доступ — через тот же W3.
* **Near-кэш viewport**: перед отрисовкой кадра разово вычитываем записи для `VIEW_H+1` видимых сегментов в маленький near-массив (≈ `(VIEW_H+1)×6` ≈ 186 байт). Рендер работает по near-кэшу + читает только контент-страницы. Это устраняет per-byte thrashing между страницей индекса и страницей контента: переключений на кадр — единицы, а не тысячи.
* **Динамический размер**: число страниц под индекс выделяем пропорционально размеру файла (число сегментов ∈ размеру). `MAX_LINES` становится функцией от выделенных страниц индекса. Это прямо ложится на заметку v2 («чем больше банков под файл, тем больше буферы»).
* **Без регресса скорости при индексе в EMM**: чтобы вынос индекса в банки не замедлил сборку (W3 делится между чтением контента и записью индекса), записи копим в near-буфере батчами и сбрасываем в EMM-блок через `bank_write()` (он сам сохраняет/восстанавливает маппинг W3). Переключений окна на всю сборку — единицы, а не на каждый сегмент.
* Если индекс-страницы выделить не удалось — деградируем до текущего near-лимита и показываем явную диагностику обрезки (а не молчаливый обрыв в `emit_seg()``mdview.c:574`).
## 4. `index_lines()` — разбор по новому ТЗ
Единый проход по файлу строит сегменты. Деление на параграфы — по пустым строкам; несколько пустых строк подряд схлопываются в одну.
### 4.0 Скорость подготовки — главный приоритет
Требование: подготовка максимально быстрая (сейчас ~25 КБ готовятся 6–10 с). Две структурные причины медленности и их устранение:
* **Per-byte `fb(uint32_t)`.** Каждый доступ к байту пересчитывает страницу 32-битными `p >> 14` и `p & 0x3FFF` (`mdview.c:209-213`). На Z80 32-битная арифметика — это программные подпрограммы на каждый символ. Замена: **потоковый разбор** — мапим страницу один раз, идём по окну `char *`/16-битным индексом, страницу переключаем только на границе 16 КБ. 32-битным остаётся лишь сохраняемый в индекс `offset`.
* **Многократные пере-сканы.** На каждом `\n` внутри параграфа вызываются `is_fence_raw()`, `is_hr_raw()`, `classify_line()`, `is_line_blank()` (`mdview.c:800-809`) — каждая заново сканирует следующую строку, а `classify_line()` ещё и повторяет цикл детекции HR. Для параграфа из N строк — O(N×длина) лишней работы. Замена: **один проход** — каждую строку классифицируем ровно один раз в момент её начала, lookahead — минимальный (несколько первых байт).
* **32-битные сравнения.** Курсор скана — (страница:8 бит, смещение:16 бит); сравнение с концом — сначала по странице, потом 16-битно.
Ожидаемый эффект: подготовка — по сути один линейный проход с 16-битными операциями, кратное ускорение относительно текущего multi-pass + 32-bit. (`render_line()` может остаться на `fb()` — там только ~30×80 байт за кадр.)
### 4.1 Обычный текст (Wrap)
* **Soft break** (одиночный `\n`): склейка, следующая строка продолжается через пробел.
* **Wide break**: 2+ пробелов перед `\n` **или** символ `\` перед `\n` → принудительный перенос внутри параграфа, стиль сохраняется. (Текущий код ловит только 2 пробела — `mdview.c:813-818`; добавить ветку для `\`.)
* **Hard break** (пустая строка): новый параграф, отделяется ОДНОЙ пустой строкой независимо от числа пустых строк в оригинале.
* **Модификаторы** bold/italic/strike/code действуют через soft/wide break внутри параграфа и сбрасываются на границе параграфа. (Добавить STRIKE `~~…~~` — сейчас его нет в `INIT_STYLE_*`/`ATTR_*`.)
### 4.2 Заголовки (Wrap)
Один оригинальный абзац-строка; стартовый стиль по уровню. Во входе распознаём H1–H6, но H4/H5/H6 далее обрабатываются одинаково как H4 (сливаются в один стиль; `classify_line()` уже сворачивает `lvl>4``LK_H4`, `mdview.c:488`). Внутри допустимы bold/italic/code/strike. После заголовка всегда пустая строка.
### 4.3 Горизонтальный разделитель HR
Всегда одна строка, после неё всегда пустая строка (новый абзац).
### 4.4 Списки (Wrap) — НОВОЕ: многострочная склейка
* Пункт может занимать несколько оригинальных строк; soft break внутри пункта склеивается через пробел (как обычный текст). Сейчас списки эмитятся построчно (`mdview.c:762-772`) — переписать на paragraph-модель.
* Новая строка с префиксом пункта → новый пункт.
* Пустая строка завершает пункт. Следующая непустая НЕ-пункт строка не является продолжением.
* **Группировка**: если после ОДНОЙ пустой строки идёт снова пункт — это тот же список, пустая строка в показе подавляется (пункты идут вплотную). Только ДВЕ+ пустые строки между пунктами разрывают на разные списки (в показе — одна пустая строка между ними).
* Перенос продолжения пункта печатается с отступом до контент-колонки (для уровня 1 — 2 пробела).
* Незакрытые модификаторы НЕ переносятся на следующий пункт (каждый пункт — свой параграф).
* Вложенные списки поддерживаются (отступ растёт с ведущими пробелами).
* Отдельные стили: префикс маркера и текст списка.
Пример соответствует разделу «Списки» в `todo2` (строки 16).
### 4.5 Цитаты (Wrap)
* Отдельный параграф; перед текстом — префикс цитаты, перенесённые строки тоже предваряются префиксом.
* Многострочная склейка как у текста; пустая строка-цитата (`>`) показывается как пустая строка с префиксом.
* Вложенность (`> >` → двойной префикс). Отдельные стили: префикс и текст цитаты.
### 4.6 Блок кода ``` ``` ``` (UnWrap)
Весь блок одним стилем кода, без inline-модификаторов. После блока обязательна пустая строка. Строки не переносятся (truncate + горизонтальный скролл).
### 4.7 Таблицы (UnWrap)
Пока as-is, без переноса. Выравнивание столбцов — v2.
## 5. `render_line()` — упрощение
* Убрать ветку truncate-режима и `wrap_mode` (уже частично снято; `toggle_wrap()` — мёртвая заглушка `mdview.c:1286-1291`, удалить вместе с упоминаниями F2).
* Не вызывать `classify_line()` и не делать обратный проход: использовать `kind`/`indent`/`style` из индекса.
* Для cont-сегментов списков/цитат — печать отступа/префикса по `kind`+`indent` из записи сегмента.
* Inline-парсинг emphasis выполняется только в пределах видимого сегмента (дёшево); для кода/таблиц — отключён.
## 6. Горизонтальный скроллинг (UnWrap)
* Скроллится только UnWrap-текст (код/таблицы). Грануляция 8 символов (`HPAN_STEP`).
* Правый край: индикатор `>` своим стилем, если есть скрытый контент справа (есть — `mdview.c:1135-1155`).
* **Добавить** левый индикатор `<` в первой колонке, когда `viewport_x > 0`.
* **Границы скролла по факту**: текущий кламп жёстко до 240 (`mdview.c:1275`). Заменить на вычисление максимального переполнения среди UnWrap-строк в текущем viewport, чтобы вправо нельзя было уйти за самую длинную строку, а влево — до колонки 0.
## 7. Чеклист расхождений с текущим кодом
* [индекс] Перейти на запись-на-сегмент в EMM-банке + near-кэш viewport; снять `MAX_LINES=2048`.
* [скорость] Потоковый разбор: один проход, 16-битный курсор в окне (без per-byte `fb()` с 32-битной арифметикой), классификация строки один раз, минимальный lookahead; батч-флеш индекса.
* [текст] Wide break по символу `\`.
* [текст] Модификатор strikethrough `~~…~~` (+ стиль).
* [списки] Многострочная склейка пунктов и правило группировки по одной/двум пустым строкам.
* [цитаты] Многострочная склейка и повтор префикса (в т.ч. вложенные) на переносах.
* [заголовки] Читать H1–H6; H4/H5/H6 трактовать как H4 (частично уже есть — `mdview.c:488`).
* [скролл] Индикатор `<` и корректные границы по фактическому переполнению.
* [рендер] Снять per-кадровый `classify_line()` и обратный проход (данные — из индекса).
* [очистка] Удалить `toggle_wrap()` и упоминания F2 (`mdview.c:1286-1291`, `1311`).
## 8. Память: бюджет
* near (W2): текущий статический индекс ~11.6 КБ — у предела окна. После переноса `line_offset`/`init_style` в EMM в near остаётся near-кэш viewport (~0.2 КБ) + мелкие флаги → запас под стек/кучу растёт.
* EMM: файл до 8 страниц + индекс ~1–2 страницы (при 5–6 байт/сегмент и нескольких тысячах сегментов). Перед выделением проверять `mem_info()` на доступность страниц.
## 9. Заметки для v2
* **Прогрессивный показ**: отрисовать первую страницу (первые ~30 сегментов) ДО завершения полной подготовки; остальное доиндексировать дальше или по мере прокрутки. Процент и `End` показывать как «вычисляется», пока не готов полный `n_lines`. (Синергия с потоковым разбором §4.0: первый экран готов после разбора лишь нескольких КБ.)
* Разделители переноса Wrap: точка/запятая/`!`/`?`/дефис; правило «новая строка не начинается с разделителя», серия разделителей остаётся на первой строке.
* Таблицы: вычисление ширины столбцов и выравнивание.
* Подсветка синтаксиса внутри блоков кода.
* URL/Images и прочие типы строк.
@@ -0,0 +1,90 @@
# mdview — унифицированный рендеринг (Unified Wrap + Paragraph Model)
## Цель
Отказаться от двух режимов Wrap/Unwrap. Ввести единую модель отображения Markdown-документов, соответствующую стандарту CommonMark:
* Обычный текст: параграфы склеиваются по soft breaks (`\n` → пробел), перенос по словам на 80 колонок.
* Hard break (` \n`): принудительный перенос внутри параграфа.
* Paragraph break (`\n\n`): новый абзац (пустая строка между блоками).
* Fenced code block и таблицы (будущее): не переносятся, работают как truncate + горизонтальный скроллинг + индикатор `>`.
* Горизонтальный скроллинг активен только когда в текущем viewport есть nowrap-строки.
## Текущее состояние
* `mdview.c` использует `wrap_mode` (0 = truncate, 1 = wrap) и `toggle_wrap()` (F2).
* `index_lines()` делает два разных прохода: truncate (1 строка файла = 1 строка экрана) и wrap (сегментация по SCREEN_W).
* `render_line()` имеет ветвление по `wrap_mode` и `cont`.
* `line_offset[]`, `cont_flag[]`, `in_code[]`, `init_style[]` — существующие структуры.
## Предлагаемые изменения
### 1. Новые структуры данных
* Добавить `nowrap_flag[]` (битмап, аналогично `cont_flag[]`): строка не должна переноситься (code block, HR, таблица).
* Добавить `line_kind[]` (2 бита на строку, 512 байт для 2048 строк): хранит классификацию (PLAIN, H1-H4, HR, ULIST, OLIST, QUOTE, CODE, TABLE). Нужен для continuation-сегментов, чтобы render_line знал, какой префикс/отступ повторять.
* Убрать `wrap_mode` и `toggle_wrap()`.
* Убрать truncate-ветку из `index_lines()` и `render_line()`.
* `viewport_x` остаётся глобальным, но применяется только к `nowrap` строкам.
### 2. Новый `index_lines()` — paragraph scanner
Единый проход по файлу, без двух режимов.
#### 2.1 Сканирование «сущностей»
Walk по файлу от `p = 0` до `file_size`:
1. Если `fb(p) == '`' × 3 → Fenced code block. Все строки до закрывающего ` ``` ``nowrap_flag = 1`, `in_code = 1` (для body), `line_kind = CODE`. Длинные строки не wrap'аются.
2. Если строка начинается с `|...|` (будущее) → Table. `nowrap_flag = 1`, `line_kind = TABLE`.
3. Если строка — HR (`---`/`***`/`___`) → `nowrap_flag = 1`, `line_kind = HR`. 1 строка экрана.
4. Если строка — Header (`#...`), List (`- ` / `* ` / `+ ` / `N. ` / `N) `), Quote (`> `) → начало **Normal paragraph / block**. Обрабатывается как единый параграф до `\n\n` или EOF или code block.
5. Иначе — Normal paragraph (plain text).
#### 2.2 Normal paragraph — обработка
Внутри параграфа символы читаются как единый поток (не прерываясь на `\n`):
* `\n\n` → конец параграфа. Emit текущую сегментную строку (если есть). Добавить пустую строку (1 строка экрана с нулевой длиной, или offset указывающий на второй `\n`).
* ` \n` (два пробела или таб + пробел перед `\n`) → **Hard break**. Emit текущую сегментную строку. Начать новую сегментную строку с того же абзаца, **сохранив `line_style`**. `prev_ch = ' '`.
* Обычный `\n`**Soft break**. Считаем как 1 пробел: `visible col++`, `prev_ch = ' '`. Продолжаем текущую сегментную строку.
* Wrap at `SCREEN_W` — так же как сейчас, по `last_space`. При создании continuation-сегмента: `set_cont()`, `set_init_style_raw()` (carry emphasis), `set_line_kind()` (carry list/quote kind для отступа/префикса).
* Emphasis tracking (`line_style`) работает через весь параграф, через soft/hard breaks.
#### 2.3 Fenced code block — обработка
* Delimiter строка (` `): `line_kind = CODE`, `nowrap_flag = 1`, рендерится как пустая строка (или строка с ` ` — но сейчас пустая).
* Body строки: каждая физическая строка = 1 строка экрана. `nowrap_flag = 1`, `in_code = 1`, `line_kind = CODE`. Нет inline parsing, нет wrap.
* Если строка длиннее `SCREEN_W` — truncation (не wrap). `render_line()` рисует `>` в последней колонке, если есть content за пределами `SCREEN_W + viewport_x`.
#### 2.4 List continuation
* Первая строка list item: `line_kind = ULIST/OLIST`, `marker_visible_col` определяет отступ. Текст начинается после маркера.
* Wrap внутри list item: `set_line_kind()` = тот же `ULIST`/`OLIST`. `render_line()` для continuation-сегмента не рисует маркер, но добавляет отступ = `marker_visible_col` (т.е. количество пробелов до текста первой строки).
* Для вложенных списков отступ растёт, потому что `marker_visible_col` учитывает ведущие пробелы.
#### 2.5 Quote continuation
* Первая строка quote: `line_kind = QUOTE`, `marker_visible_col` = отступ + 2 (для `│ `).
* Wrap внутри quote: `set_line_kind()` = `QUOTE`. `render_line()` для continuation рисует префикс `│ ` (или пробелы, если символ не нужен на всех кроме первой строки).
### 3. Изменения в `render_line()`
* Убрать ветку `if (!wrap_mode)` (truncate mode больше не существует).
* Добавить `if (is_nowrap(line_idx))`: применяется `viewport_x`, отображается `>` в колонке 79 если контент выходит за пределы.
* Для `cont` сегментов (wrap continuation):
* Если `line_kind` == `QUOTE`: повторить префикс отступа + `│ ` (или только отступ, если решено показывать `│` только на первой строке).
* Если `line_kind` == `ULIST`/`OLIST`: повторить отступ = `marker_visible_col` (без маркера/цифр). Текст начинается с той же колонки, что и на первой строке item.
* Для plain text: просто продолжить текст без префиксов.
* Для `nowrap` строк: применять `viewport_x` как смещение. Текст сдвигается влево, скрытые символы не рисуются. Если за пределами видимой зоны есть ещё контент — `>` в колонке 79.
### 4. Горизонтальный скроллинг
* `scroll_h()` проверяет: есть ли в текущем viewport (top_line .. top_line+VIEW_H-1) хотя бы одна строка с `nowrap_flag == 1`. Если нет — return (no-op).
* Для `nowrap` строк `render_line()` использует `viewport_x` при отображении: символы с `cc < viewport_x` пропускаются, `cc >= viewport_x` рисуются.
* Для wrap-строк `viewport_x` игнорируется (эффективно = 0).
* При `Home`/`End`/`PgUp`/`PgDn`/`Up`/`Down``viewport_x` не сбрасывается (пользователь может скроллить по вертикали, оставаясь на горизонтальном смещении для code block).
### 5. Очистка и навигация
* Убрать `wrap_mode`, `toggle_wrap()`.
* Убрать F2 из меню и help.
* `scroll_up`/`scroll_down`/`scroll_h`/`clamp_top` — обновить без ссылок на `wrap_mode`.
* `calc_pct` и статусная строка — обновить, убрать ссылку на wrap/unwrap.
### 6. Порядок реализации (по файлу mdview.c)
1. **Структуры данных**: добавить `nowrap_flag[]`, `line_kind[]`, убрать `wrap_mode`.
2. **`index_lines()`**: полностью переписать в paragraph scanner. Это самый сложный и объёмный блок.
3. **`render_line()`**: убрать truncate, добавить nowrap/quote/list continuation, добавить `viewport_x` для nowrap.
4. **`scroll_h()`**: добавить проверку наличия nowrap в viewport.
5. **Очистка**: убрать toggle_wrap, обновить меню/help, обновить main() (убрать F2).
6. **Тестирование**: soft breaks, hard breaks, paragraph breaks, emphasis через soft breaks, fenced code blocks, list wrap с отступом, quote wrap с префиксом, горизонтальный скролл только для code.
## Orchestration
План реализуется одним агентом (последовательно в одном файле), параллелизм не требуется. Дочерние агенты не используются.
+1407 -607
View File
File diff suppressed because it is too large Load Diff
+180
View File
@@ -0,0 +1,180 @@
Новое требование к первоначальной подготовке документа.
Правила форматирования и показа -
1) Деление на параграфы. Параграфы разделяются пустыми строками.
2) Заголовки H1-H6 - отдельные параграфы (после них всегда пустые строки)
3) Разделители (---) - тоже - после них всегда пустые строки.
4) Таблица/код (через ```)/списки - это единый параграф.
Типы текста (строк) -
Обычный текст
Может в оригинале находиться на нескольких строках.
Строки могут разделяться
- обычный break - это когда на строке после текста идет перевод строки -
такой текст просто объединяется (в нашем случае если возможно то следующая строка продолжает
выводиться на той же строке что и предыдущая, только отделяется от нее уже не переводом строки
а через пробел.
- широкий break - это когда перед переводом строки есть два или более пробелов или перед переводом
строки находится символ обратный слэш '\' - в этом случае и когда мы выводим текст на экран следующая
строка начинается с новой строки.
- жесткий break - это когда две строки обычного текста разделяются двумя или более переводами
стрки (то есть между ними как минимум есть одна пустая строка) - в этом случае вторая строка
будет считаться новым параграфом и отделяться от предыдущей строки ОДНОЙ пустой строкой (вне
зависимости от того сколько пустых строк в оригинале.
Должно сохраняться действие модификатора bold/italic/strike начатое на одной строке на следующие
если они так же находятся в этом параграфе. Новый параграф (после пустой строки) теряет воздействие
незакрытого модификатора из предыдущего параграфа.
Тип переноса строк - Wrap.
Заголовки -
абзацы из одной оригинальной строки. Начальный стиль зависит от типа заголовка (в нем могут встречаться
модификаторы bold/italic/code/strike.
Тип переноса строк - Wrap.
Разделитель (horizontal rules) -
Всегда одна строка. После нее выводим пустую строку всегда (следующий текст - новый абзац)
Списки
как и в обычном тексте - один пункт списка может находиться в оригинале на нескольких строках.
если появляется новая строка с префиксом пункта списка - это означает что с этой строки начинается
новый пункт списка. если появляется пустая строка - это означает что следующая за пустой строкой
непустая строка уже не является продолжением пункта списка.
Если следующая за пустой строкой строка так же является пунктом списка то такая строка считается
продолжением текущего списка. Только если две строки списков разделены ДВУМЯ и более пустыми
строками то вторая строка с пунктом списка будет считаться началом нового списка -
Пример -
- строка 1
- строка 2
строка 3
- строка 4
- строка 5
- строка 6
Должно отображаться так -
- строка 1
- строка 2 строка 3
- строка 4
- строка 5
- строка 6
Тип переноса строк - Wrap.
Замечание - перенесенная строка продолжение пункта списка должна начинаться с отступа в
несколько пробелов (для списка первого уровня - два пробела) - то есть с той же позиции
что и начальный текст строки -
Пример -
- длинная строка которая не может поместиться и будет перенесена по слову 'будет'
Должно отображаться так -
- длинная строка которая не может поместиться и
будет перенесена по слову 'будет'
Незакрытые модификаторы типа (bold/etc.) не переносят свою модификацию не последующие пункты списка.
Действуют только в пределах одного пункта.
(то есть фактически - пункт списка - это отдельный параграф но следующий пункт списка (тоже отдельный
параграф) не отделяется от него пустой строкой.
Вложенные списки - поддерживаются.
Для отображения префиксов пунктов списка используется свой стиль. Так же для текста списков используется
отдельный (от обычного текста) стиль.
Блок Кода -
Весь блок кода отображается только одним стилем - стилем Кода. В нем не действуют модификаторы
bold/italic/etc (возможно в дальнейшей использование парсера языка для кода что бы отобразить
этот код с подсветкой синтаксиса этого языка, но сейчас весь блок рисуется только одним стилем)
После блока кода - обязательна пустая строка.
Тип переноса строк - UnWrap. - То есть строки НЕ ПЕРЕНОСЯТСЯ.
Quoted -
Отображается как отдельный параграф. Перед отображением текста отображается символ префикса Цитирования
Способы переноса текста аналогичны обычному тексту, за исключение того что перенесенные строки так же
предваряются префиксом Цитирования
Для отображения префиксов Цитирования используется свой стиль. Так же для текста цитирования используется
отдельный (от обычного текста) стиль.
Тип переноса строк - Wrap.
Пример форматирования -
> Первый параграф
>
> Второй параграф
> > Вложенный параграф
>
> Продолжение основной цитаты - длинная строка (переносится по слову 'строка')
Будет отображаться так -
| Первый параграф
|
| Второй параграф
| | Вложенный параграф
|
| Продолжение основной цитаты - длинная
| строка (переносится по слову 'строка')
Таблицы -
Пока отображаются as is.
Дальше возможно предусмотрим вариант вычисления ширины столбцов и форматирование
вывода что бы все ячейки столбца имели одинаковую ширину.
Тип переноса строк - UnWrap.
Типы переноса строк -
Wrap -
происходит перенос текста с одной строки на другую по разделителям пробелам (предусмотреть
во второй версии возможность использовать разделителем знаков точка, запятая, восклицательный
знак, вопросительный знак, дефис. Замечание - новая строка не может начинаться со знаков
разделителей - то есть если у нас идет многоточие (три точки) то нельзя что бы одна точка
была в конце первой строки а остальные две на другой - если идут несколько разделителей подряд
то они считаются как один и должны оставаться на первой строке).
UnWrap -
Текст НЕ ПЕРЕНОСИТСЯ.
Если строка не помещается на экране - то в конец строки на экране выводим символ-знак наличия
продолжения строки справа за краем экрана (свой стиль для этого символа).
Символ '>'.
Если есть хотя бы одна строка которая не помещается на экране - то разрешаем горизонтальный
скроллинг.
ЗАМЕЧАНИЕ - скроллируется только текст выводящийся в режиме UnWrap.
Если произведен скроллинг влево (строки UnWrap начинаются показываться не с первой позиции)
то на первой позиции отображем другой символ '<' сообщающий пользователю что есть текст
за левым краем экрана.
Скроллирование не бесконечно - если на экране нет строк UnWrap для которых есть скрытый текст
за правым краем экрана то скроллирование влево больше не возможно, и наоборот - если при
скроллировании вправо дошли до показа строк UnWrap с первой позиции то дальше скроллинг в этом
направлении так же невозможен. Скроллинг идет с грануляцией по 8 символов.
(сейчас UnWrap текст это только блоки кода и таблицы).
Проанализируй данную постановку задачи.
Что требуется -
Мы более не поддерживаем два режима показа - Wrap/UnWrap - только один (смешанный).
Потому вопрос - надо ли нам сохранять оригинальный контент считанного файла с диска ?
Или при подготовке данных считываемых с диска можно сразу преобразовывать его в формат
готовый для показа (объединять строки в одном параграфе и так далее) ?
Ограничения - не забывать о том что это все работает на компьютере с 8-ми битным процессором.
Потому очень экономно относимся к памяти и лишней работе процессора - все должно быть весьма
быстрым и компактным.
Требования для работы с файлами 128 Кб сохраняется.
Для версии 2 -
Подумай о возможности использования банков памяти не только для содержимого файла а например
для буферов типа line_offset/init_style и прочих (тогда их размер может так же быть динамическим
и чем больше банков памяти будет использовано для чтения файла тем больше станут размеры этих
буферов.
+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 × количество строк/столбцов экономии. Применимо ко всем композитным примитивам.
+57
View File
@@ -0,0 +1,57 @@
# Build mdview2.exe — Markdown viewer for Sprinter (render-cache version).
#
# Фаза 0: скаффолдинг — копия mdview.c как baseline, без изменений
# логики. План — docs/mdview2-plan.md.
#
# small memory mode: code in W1, data/stack/heap in W2 (32 KB total).
# W3 stays free for the file buffer (EMM-mapped).
PROJ_ROOT := $(abspath $(CURDIR)/../..)
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
# --max-allocs 100000: агрессивнее аллокация регистров SDCC — минус ~850 байт
# кода на всём приложении (замер 2026-07-04) ценой более долгой компиляции.
EXTRA_FLAGS := --max-allocs 100000
include $(PROJ_ROOT)/app.mk
# ------------------------------------------------------------------
# Образ дискеты: mdview2.exe + документация. Каждый документ — в СВОЕЙ
# кодировке, чтобы дискета заодно покрывала все пути автодетекта:
# README.MD — документация, ОРИГИНАЛЬНЫЙ UTF-8 (как в репозитории);
# CHANGES.MD — история версий, CP866 (хранится так, кладётся как есть;
# имя укорочено: "CHANGELOG" не влезает в FAT 8.3);
# DEMO.MD — демонстрация разметки, конвертируется UTF-8 → CP1251.
#
# iconv -c возвращает ненулевой код, если хоть один символ отброшен
# (даже с -c) — это ОЖИДАЕМО при потере символов без аналога в целевой
# кодировке, не ошибка конвертации; вывод при этом корректно записан.
# Поэтому код возврата iconv игнорируется (|| true).
#
# Тестовые файлы лимита 256 КБ лежат в testfiles/ (генератор gen_testfiles.py);
# на диск НЕ кладутся — при необходимости добавить вручную в список ниже.
# ------------------------------------------------------------------
DISK_TMP := .disk_tmp
DEMO_DISK := $(DISK_TMP)/DEMO.MD
CHLOG_DISK := $(DISK_TMP)/CHANGES.MD
$(DISK_TMP):
mkdir -p $@
$(CHLOG_DISK): CHANGELOG.md | $(DISK_TMP)
cp CHANGELOG.md $@
$(DEMO_DISK): DEMO.MD | $(DISK_TMP)
iconv -c -f UTF-8 -t CP1251 DEMO.MD > $@ || true
floppy: $(EXAMPLE).exe $(DEMO_DISK) $(CHLOG_DISK)
python3 $(MAKE_DISK) $(FLOPPY_IMG) $(EXAMPLE).exe README.md $(DEMO_DISK) $(CHLOG_DISK)
@echo
@echo "Floppy ready: $(FLOPPY_IMG)"
@echo "Run: cd $(MAME_DIR) && ./run_mame.sh"
clean:
rm -rf .sprinter-cc-* $(EXAMPLE).exe $(DISK_TMP)
.PHONY: all clean floppy run
+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 в корне репозитория).
+799
View File
@@ -0,0 +1,799 @@
# MDView — Просмотрщик Markdown для Sprinter
**MDView** — программа для просмотра документов в формате *Markdown* на компьютере
Sprinter (процессор Z80). Документ хранится в отдельном W3 окне и не занимает
основную RAM программы.
## Возможности
- Документы до **128 КБ** (8 страниц EMM по 16 КБ каждая)
- До **16 384** экранных строк в индексе
- Автоматический перенос слов по ширине экрана (80 столбцов)
- Горизонтальный сдвиг для широких строк (блоки кода, таблицы)
- Статус-бар: имя файла, диапазон строк, процент прокрутки
- Спиннер в строке состояния во время загрузки и индексации
- Поддержка «мягкого» склеивания строк в абзацах и цитатах
## Запуск
```
mdview [имя_файла.md]
```
Если имя файла не задано, загружается `README.MD`.
## Управление
```
Клавиша Действие
───────────── ────────────────────────────────────────
Up Down Прокрутка на одну строку вверх / вниз
PgUp PgDn Прокрутка на страницу (30 строк)
Home Начало документа
End Конец документа
Left Right Горизонтальный сдвиг (только nowrap-строки)
F1 Окно справки
F8 Кодировка: CP866 → CP1251 → KOI8-R → UTF-8 → ...
F10 / Esc Выход из программы
```
Кодировка определяется автоматически при открытии (BOM + эвристика по
первым 4 КБ); `F8` переключает её вручную, если детекция ошиблась.
8-битные кодировки (CP866/CP1251/KOI8-R) переключаются мгновенно (ремап на
отрисовке). Второй набор индекс/кэша (например UTF-8) строится лениво — при
первом переключении в него (короткая пауза со спиннером), дальше мгновенно.
## Синтаксис Markdown
### Заголовки
Поддерживаются уровни H1–H4. Уровни H5 и H6 отображаются как H4.
# Заголовок первого уровня
## Заголовок второго уровня
### Заголовок третьего уровня
#### Заголовок четвёртого уровня
### Текстовое форматирование
**Жирный текст** выделяется двойными звёздочками: `**текст**`
*Курсив* выделяется одиночными звёздочками `*текст*` или знаком подчёркивания `_текст_`
`Встроенный код` обозначается обратными кавычками
~~Зачёркнутый текст~~ — двойные тильды: `~~текст~~`
### Ненумерованный список
Маркеры `-`, `*` или `+`:
- Первый пункт списка
- Второй пункт списка
- Третий пункт с достаточно длинным текстом, который при необходимости
будет перенесён на следующую строку с сохранением отступа
### Нумерованный список
1. Первый элемент
2. Второй элемент
3. Третий элемент
### Цитата
> Блок цитаты начинается с символа `>`. Несколько последовательных
> строк одной цитаты склеиваются в единый абзац с автоматическим
> переносом слов.
### Блок кода (verbatim)
Блок кода заключается в тройные обратные кавычки. Внутри блока
текст отображается «как есть» без разбора Markdown:
```
#include <stdio.h>
#include <sprinter.h>
int main(void) {
puts("Hello, Sprinter!");
return 0;
}
```
### Горизонтальная линия
Три или более символов `---`, `***` или `___` на отдельной строке:
---
## Технические характеристики
- **Платформа:** Sprinter, процессор Z80 @ 21 МГц
- **Кодировки:** CP866 / CP1251 / KOI8-R / UTF-8 (автоопределение, `F8`)
- **Максимальный размер файла:** 128 КБ
- **Максимальное число строк в индексе:** 16 384
- **Режим памяти:** small
- Код программы, cтек, данные, куча — окнa W1-W2 (32 КБ, адреса 0x40000xBFFF).
- Буфер файла — страницы EMM, отображаемые в W3 (0xC0000xFFFF)
## TODO
1. **Увеличение размера документов.** Снять лимит 128 КБ: Достаточно
разрешить работать с большим кол-вом страниц памяти, пока оттестированно
на работе с 8-мю страницами по 16Кб.
Сделано: форматированные таблицы с рамкой; поддержка кодировок
CP866 / CP1251 / KOI8-R / UTF-8 с автоопределением и переключением по `F8`
(второй набор строится лениво, по первому переключению).
4. **Ускорение рендеринга.** Кэш строк экрана. Оптимизация цикла вывода
символов через BIOS WRCHAR (пакетный вывод, DMA).
---
*MDView v0.2 · (c) 2026 Петров А.Г.*
---
# Sprinter C Compiler — v1.0
C toolchain for **Sprinter** — the Z80-based home computer by Peters Plus, running
ESTEX DSS. Host: macOS / Linux. Target: `.EXE` files in SprintEXE format.
Built on top of **SDCC 4.5** (vendored in `third_party/sdcc/`). This repository adds
everything Sprinter-specific: crt0, linker integration, libc wrappers over ESTEX,
banked-call trampolines, graphics & accelerator API, mouse driver wrappers, and the
`mkexe` utility for producing SprintEXE images.
## What you get
* **`bin/sprinter-cc`** — one-line driver: `sprinter-cc -o foo.exe foo.c`
* **Memory modes**: `tiny`, `small`, `big`, `huge`, `manual` — see below.
* **stdio + conio**: printf, puts, putchar, getchar, fopen/fread/..., cprintf, cputs, putch, textcolor/textbackground/textattr, gotoxy, kbhit/getch.
* **Graphics**: 320×256×256 and 640×256×16 modes, accelerator-backed primitives (hline / vline / rect / fill_rect / line via Bresenham, plus clear), bitmap-font text in both modes via BIOS character generator.
* **File I/O**: POSIX (`open`/`read`/`write`/`close`/`lseek`/`unlink`/`creat`), FILE\* streams (`fopen`/`fgets`/`fwrite`/...), directory listing (`ffirst`/`fnext`), `chdir`/`getcwd`/`mkdir`/`rmdir`, `stat`/`fstat`.
* **Memory**: 32 KB heap (W2-resident), banking-aware page allocator (`mem_alloc_pages`/`bank_read`/`bank_write`), explicit memory modes for sub-16 KB programs.
* **Mouse**: full Sprinter driver wrapper (14 functions including custom cursor bitmaps).
* **Environment**: `getenv`/`putenv`/`sysenv` over ESTEX `$46`.
* **Time**: `getdatetime`/`setdatetime` + POSIX `time`/`localtime`/`mktime`/`asctime`/`ctime`.
* **Misc**: `errno`/`strerror`/`perror`, `atexit`, `setjmp`/`longjmp`, `sleep`, full argv parsing in crt0.
## Quick start
```sh
git clone <this repo> sprinter-c
cd sprinter-c
make sdcc # one-time: fetch SDCC 4.5 binary (~25 MB)
make all # build mkexe + libsprinter.lib + 27 examples
make floppy # pack everything into mame/v306/IMG/mc.img
cd mame/v306 && ./run_mame.sh # boot Sprinter in MAME
```
Compile a single program:
```sh
cat > hello.c <<EOF
#include <stdio.h>
int main(void) { puts("Hello, Sprinter!"); return 0; }
EOF
bin/sprinter-cc -o hello.exe hello.c
```
That's it — `hello.exe` is now a valid SprintEXE you can `RUN HELLO` from the ESTEX shell.
## Memory modes
Sprinter's address space is four 16 KB windows (W0 / W1 / W2 / W3). DSS allocates
pages by program size — small programs get only one page. Pick a memory mode based
on what your program needs:
| Mode | Code lives in | Banking | Use when | Note |
|---|---|---|---|---|
| `tiny` (default) | W2 (0x8100+) | no | code+data < 14 KB | |
| `small` | W1-W2 (0x4100+) | no | code+data < 30 KB | |
| `big` | W2 + W1 banking | yes (W1) | tiny + extra code modules | |
| `huge` | W1-W2 + W3 banking | yes (W3) | small + extra code modules | |
| `manual` | user-specified | optional | special layouts | Not implemented |
```sh
sprinter-cc --memory small -o big.exe bigprog.c
sprinter-cc --memory huge -o app.exe main.c --bank 1=engine.c --bank 2=ai.c
```
Banked functions are declared with `__banked`:
```c
void engine_tick(int dt) __banked; // lives in BANK1, automatically swapped
```
## Examples (27 total)
| Example | What it demonstrates |
|---|---|
| `hello` | Hello world with stdio + conio Turbo-C-style colors |
| `argv` | argv parsing in crt0 |
| `cat` | File I/O — read & print TEST.TXT |
| `seek` | 32-bit lseek over a 100 KB file |
| `ls` | Directory listing via ffirst/fnext |
| `filetest` | FILE\* streams (fopen/fread/...) |
| `stattest` | `stat`/`fstat` on files and directories |
| `errno` | errno / strerror / perror |
| `mem_test` | Page allocator + bank\_read/bank\_write |
| `malloc` | Heap stress test (200+ allocations) |
| `banked` | Banked code in W3 (huge mode) |
| `bankedbg` | Banked code in W1 (big mode) |
| `banklocl` | Bank-local static data and BSS |
| `mouse` | Mouse driver in text mode |
| `gfx_mous` | Mouse with custom bitmap cursor in graphics mode |
| `gfx_demo` | 320×256×256 lines / rects / accelerator |
| `gfx_d16` | 640×256×16 same primitives |
| `gfx_text` | Bitmap-font text on graphics screen |
| `timedir` | Date/time + directory listing |
| `ptime` | POSIX time API |
| `openenv` | open() flags + environment vars |
| `conio` | conio API smoke test |
| `attrprob` | Probe Sprinter text attribute byte layout |
| `strtest` | string.h test (from SDCC's z80.lib) |
| `stdlib` | stdlib.h test (qsort / rand / strtol / etc.) |
| `assrtest` | assert() |
| `rt_test` | Runtime helpers (sleep, setjmp, atexit) |
## Headers
Standard:
* `<stdio.h>` — puts / printf / FILE\* + Sprinter-specific dec/hex helpers
* `<stdlib.h>` — atoi / atof / malloc / qsort / ... (from SDCC z80.lib)
* `<string.h>` / `<ctype.h>` / `<math.h>` — from SDCC z80.lib
* `<unistd.h>` — read / write / close / lseek / unlink
* `<fcntl.h>` — open / creat + O\_RDONLY / O\_CREAT / ...
* `<errno.h>` — errno + error names + strerror
* `<sys/stat.h>` — stat / fstat
* `<setjmp.h>` / `<assert.h>` — from SDCC
Sprinter-specific:
* `<conio.h>` — putch / cputs / cprintf, textcolor / textbackground / textattr, kbhit / getch, clrscr, gotoxy, wherex/y
* `<gfx.h>` — gfx_init/done, palette, putpixel, hline/vline/rect/fill_rect/line, text — for both 320 and 640 modes (gfx_\*16 variants)
* `<mouse.h>` — full 14-function driver wrapper + mouse_cursor_t with bitmap support
* `<dir.h>` — chdir / getcwd / mkdir / rmdir / ffirst / fnext / ffblk
* `<time.h>` — getdatetime / setdatetime + POSIX time / localtime / etc.
* `<sprinter.h>` — raw ports, ESTEX/BIOS function numbers, env API
* `<sprinter_exit.h>` — exit / \_exit / atexit
* `<sprinter_mem.h>` — mem\_alloc\_pages / mem\_free\_block / bank\_read / bank\_write
* `<sprinter_compat.h>` — Solid-C compatibility layer (aliases + BOOL/WORD/uint types)
## Toolchain commands
```sh
make all # build mkexe + lib + every example
make floppy # repack mame/v306/IMG/mc.img with all .exe files
make check # 17 mkexe unit-tests
make clean # remove all build artefacts
make sdcc # one-time: fetch SDCC 4.5 binary
```
### sprinter-cc options
```
sprinter-cc -o foo.exe foo.c [more.c ...] [options]
--memory MODE tiny | small | big | huge | manual (default: tiny)
--memory-manual SPEC explicit placement (CODE=W1|W2,DATA=W1|W2|SAME,BANKED=W1|W3)
--stack-size N bytes reserved for the stack (default ~1278)
--crt0=TYPE default | minimal | banked | small
--bank N=FILE.c compile FILE.c into bank N (repeatable, max 15)
--debug enable runtime diagnostics (defines DEBUG_RT)
-I PATH extra include path
-L 0xADDR / -E / -S override load / entry / stack addresses
-Wl FLAG pass FLAG to sdldz80
--mkexe FLAG pass FLAG to mkexe (e.g. --mkexe -p --mkexe 0 for bank padding)
-v verbose
```
## Status
What works in v1.0:
* Compile / link / pack to SprintEXE — verified on all 27 examples
* Four memory modes (tiny / small / big / huge)
* Graphics (both modes) with accelerator
* Mouse (text + graphics cursor)
* File I/O, directories, environment, time
* All headers listed above
Deferred to v2.0 (see `docs/TODO.md`):
* **Turbo-C-style BGI graphics API** — `initgraph` / `setcolor` / `circle` /
`getimage` / `putimage` / etc. on top of our `gfx_*` primitives
* Remaining Solid-C compatibility gaps (Phase 2/3) — see `docs/solid_c_compatibility.md`
* Manual memory mode
* Rewrite FILE\* stream API (current implementation is very primitive and doesn't use buffers)
Deferred to v3.0:
* **IM2 interrupt handlers** — research complete (`docs/im2_isr_design.md`),
implementation scheduled for v3
* **Audio API** (AY-3-8910 + COVOX) — requires IM2
* **ISA-8 slot drivers** — requires IM2 (???)
## Documentation
* `docs/TODO.md` — roadmap and open work items
* `docs/solid_c_compatibility.md` — gap analysis vs Solid-C 2004
* `docs/im2_isr_design.md` — interrupt handler design (v2)
* `docs/converted/` — source documentation (ESTEX, BIOS, architecture)
converted to plain text for `grep`
* `docs/reference/`, `docs/samples/`, `docs/memory management/` — original
Russian docs and code samples from Peters Plus
## Repository layout
```
bin/sprinter-cc one-line compiler driver (bash)
toolchain/mkexe/ host-side tool: .ihx -> .exe SprintEXE
toolchain/check_banks.py post-link bank size enforcer
runtime/ crt0 variants (default, minimal, small, banked)
bank trampolines, heap, heap_top
libc/include/ headers
libc/io|stdio|mem|gfx/ C and asm sources for libsprinter.lib
lib/ Makefile that archives libsprinter.lib via sdar
examples/ 27 example programs
mame/v306/ MAME binary + Sprinter ROM/HDD images + floppy script
third_party/sdcc/ vendored SDCC 4.5 (fetched via `make sdcc`)
third_party/solid-c/ reference: original Sprinter native C (for compat target)
docs/ documentation
```
## License
This repository contains:
* Original code in `bin/`, `toolchain/`, `runtime/`, `libc/`, `lib/`, `examples/`
MIT-licensed.
* `third_party/sdcc/` — SDCC 4.5 under GPLv2 with linking exception
(see `third_party/sdcc/COPYING.txt`)
* `third_party/solid-c/` — original Sprinter Solid C, used only as a reference
## Credits
* **Sprinter / Peters Plus** — Иван Мак, Дмитрий Паринов and the original team
* **SDCC** — for the underlying Z80 compiler
* **MAME** — for the Sprinter Sp2000 emulation
---
For questions / patches: see CONTRIBUTING.md (TBD) or open an issue.
---
# TODO / Roadmap
Открытые задачи в порядке убывания приоритета. По мере появления реальных программ — приоритеты будут смещаться.
## Этап 5 — malloc / free + banking-aware page allocator ✅ ГОТОВО
- [x] SDCC's `malloc`/`free` + наш `runtime/heap.s` (полностью заменяет library heap.rel, 14000-байтный heap в окне 2)
- [x] `libc/mem/mem_alloc.c` — page allocator: `mem_alloc_pages`/`mem_free_block`/`mem_get_page`/`mem_info` через ESTEX `$3C/$3D/$3E` + BIOS `$C4`
- [x] `libc/mem/bank_io.c` — HOME-резидентные `bank_read`/`bank_write`/`bank_load_byte`/`bank_store_byte` со свопом W3 внутри
- [x] `examples/malloc_test/` — проверка SDCC's malloc (~210 64-байтных allocations через всю heap)
- [x] `examples/mem_test/` — проверка page allocator: 3 страницы, разные паттерны через bank_write, верификация через bank_read
## Этап 6 — argv parsing + sprinter-cc wrapper ✅ ГОТОВО
- [x] crt0 парсит ESTEX command-line из IX-prefix (inline asm в `runtime/crt0.s`)
- [x] Strip leading CP/M-style space (DSS quirk)
- [x] Передача `argc`/`argv` в main() через HL/DE (SDCC __sdcccall(1) ABI)
- [x] argv[0] = basename .EXE через ESTEX APPINFO ($47 subfn 2)
- [x] `runtime/crt0_minimal.s` — opt-out для очень маленьких программ
- [x] `runtime/crt0_banked.s` — теперь тоже парсит argv (parse_argv + get_progname скопированы из crt0.s; будет factored в argv.s когда возьмёмся за libsprinter.lib)
- [x] Bash-обёртка `bin/sprinter-cc`: `sprinter-cc -o foo.exe foo.c` одной строкой
- [x] Поддержка опций: `--memory`, `--memory-manual`, `--stack-size`, `--crt0=`, `--bank N=FILE.c`, `--debug`, `-I`, `-L`/`-E`/`-S`, `-Wl`, `--mkexe`
## Этап 8 — графика (320×256×256 + 640×256×16 + accel + bitmap font) ✅ ГОТОВО
- [x] **8a** Graphics core: `gfx_init`/`gfx_done`/`gfx_clear`/`gfx_putpixel`/`gfx_pal_load`/`gfx_pal_set` (libc/gfx/gfx_core.c). Палитра через BIOS PIC_SET_PAL ($A4). Verified 320×256×256.
- [x] **8b** Линии/прямоугольники/fill через accelerator (libc/gfx/gfx_lines.c): `gfx_hline`/`gfx_vline` через accel Fill (LD C,C / LD E,E + SMC block-size), `gfx_rect`/`gfx_fill_rect` с heuristic выбором ориентации (h/v bursts count), `gfx_line` с Bresenham для диагоналей. `gfx_clear` тоже переписан на column-major accel (~4× быстрее).
- [x] **8c** 640×256×16 mode (libc/gfx/gfx_16.c): `gfx_*16` API, HIGH nibble = LEFT pixel (документация misleading), per-row RMW для vline (один байт = 2 горизонтальных пикселя).
- [x] **8d** Bitmap font + gfx_text (libc/gfx/gfx_text.c): шрифт через BIOS WIN_GET_ZG ($B8), interleaved layout `font[row*256+char]`, `gfx_text`/`gfx_putchar` для 320 mode, `gfx_text16`/`gfx_putchar16` для 640 mode с pair-table lookup.
См. memory/sprinter_graphics.md, sprinter_accelerator.md, sprinter_graphics_16.md, sprinter_font_format.md.
Открытые мелочи (не блокируют):
- [ ] Шрифт-quad для 640: per-cell палитра (mode 0x82 разрешает 1 из 4 палитр per 16×8 cell) — через прямой доступ к area-описания экрана 0x0300..0x039F
## Auto-banking (см. `memory/banking_roadmap.md` для деталей)
Phase 1 — file-level bin-packing — реализовывать когда проект перерастёт ~30 KB кода.
- [ ] `toolchain/auto_bank.py`:
- Парсит размеры из `.rel`-файлов (или из .map после dry-run link'а)
- First-fit-decreasing bin-packing
- Уважает `#pragma codeseg BANKn` как manual override
- Перелинковывает с новыми `-Wl-b_BANKn=...` параметрами
- Печатает план распределения
Phase 2-5: incremental rebalance, declarative `banks.toml`, function-level, call-graph-aware. Только если/когда понадобится.
## Bank-local static data (mutable data в том же банке что и код) — ✅ ГОТОВО
- [x] Пример `examples/bank_local_data/` — функция в BANK1 со своим writable BSS array + const table + malloc-тест
- [x] `mkexe -p 0` для нулевого padding банков (BSS-storage обнуляется при загрузке)
- [x] Канонический рецепт: `--codeseg BANK1 --constseg BANK1 --dataseg BANK1` для bank1.c + `-Wl-b_BANK1=0x1C000` для линковки. **`--dataseg BANK1` РАБОТАЕТ** — раньше казалось обратное из-за trampoline bug который маскировал результат.
- [x] **Критичный фикс trampoline'a в runtime/bank.s** — старый `pop af; out (n), a` клобберил A → все banked-функции возвращающие uint8_t тихо возвращали мусор. Новый `pop bc; out (c), b` сохраняет A.
- [x] **malloc из banked-функции работает прозрачно** — heap живёт в W2 (HOME), W2 никогда не свапается trampoline'ом, pointer валиден из любого контекста. См. memory/bank_local_data_pattern.md.
- [x] Документация в memory: `memory/bank_local_data_pattern.md` (полный рецепт + malloc + nuances), `memory/sdcc_banking.md` (trampoline fix)
- [ ] Опционально — расширить `check_banks.py` чтобы показывать разбивку size = code + const + bss per bank (cosmetic)
Зачем: для модулей с большим private state (level loader, audio engine, scene data). Экономит W2 heap для динамики, а статика остаётся в бэке.
## Подсказки из solid-c (нативный Sprinter C — `third_party/solid-c/`)
После анализа solid-c'овской libc (см. `memory/solid_c_findings.md`) выявлены готовые паттерны для следующих недостающих функций. Приоритет от **высокого** к низкому:
### High-priority gaps (легко портировать, большая польза)
- [x] **`errno` + `strerror`/`perror`** — табличка 32 ошибок (libc/io/errno.c)
- [x] **Расширенный `open()`** для O_CREAT/O_TRUNC/O_APPEND/O_EXCL state machine
- [x] **`atexit`** — 8-callback LIFO + `exit()` + `_exit()` (libc/io/atexit.c)
- [x] **`setjmp`/`longjmp`** — 6-байт jmp_buf={sp,ix,pc} (libc/io/setjmp.c)
- [x] **`sleep(seconds)`** — 50Hz halt-loop (libc/io/sleep.c)
- [x] **ESTEX ENV API** ($46, getenv/putenv) — libc/io/env.c. Учли doc-bug: реально A=0 это NOT FOUND
### Medium-priority (нужно для shell-like утилит)
- [ ] **Mouse driver**`rst $30h`, 17 функций. **Сначала тест что работает в MAME**.
- [x] **`ffirst`/`fnext` + ffblk_t struct** для directory listing — реализовано, demo: ls.exe
- [x] **`getdatetime`/`setdatetime`** через ESTEX $21/$22 — libc/io/time.c, demo: time_dir_test
- [x] **`chdir`/`getcwd`/`mkdir`/`rmdir`** — wrappers для ESTEX $1B-$1E — libc/io/fsdir.c
- [x] **conio: `kbhit`/`getch`/`getche`/`cputs`/`clrscr`/`gotoxy`** — реализовано
- [x] **conio extras**: `wherex`/`wherey` ($53), `wrchar`/`rdchar` ($58/$57), `textmode_get/set` ($50/$51), `clrscr_attr` ($56) + COLOR macros
### Low-priority — ✅ FILE* stack ГОТОВО
- [x] **Минимальный unbuffered FILE\*** — `libc/stdio/file.c` + `libc/include/stdio.h`. fopen/fclose/fputs/fgets/fread/fwrite/fseek/ftell/rewind/feof/ferror/clearerr/fflush + stdin/stdout/stderr как pseudo-streams. См. `memory/file_star_design.md` и `examples/filetest`.
- [ ] fprintf / fscanf — нужна printf-через-callback machinery. Пока пользователь может `sprintf(buf, ...) + fputs(buf, fp)`.
- [ ] Опциональный buffered mode (setvbuf, line/block buffering) — если когда-то понадобится.
### POSIX time API — ✅ ГОТОВО
- [x] `libc/io/posix_time.c` — time/localtime/gmtime/mktime/asctime/ctime поверх getdatetime. SDCC's time.rel избегаем (нельзя override _RtcRead). См. `examples/ptime`.
### sys/stat — ✅ ГОТОВО
- [x] `libc/io/stat.c` — POSIX stat/fstat. Гибрид open+fstat для файлов, ffirst+iter для папок (включая "."/".."). См. `examples/stattest` и `memory/estex_ffirst_dotdot.md`.
### assert — ✅ ГОТОВО (используем SDCC's __assert через fallback include path)
## libc/stdlib — ✅ не нужно делать (см. memory/sdcc_stdlib_works.md)
Проверено через `examples/stdlib_test/`: SDCC z80.lib содержит работающие реализации:
- `atoi/atol/atof, strtol/strtoul, rand/srand, qsort/bsearch, abs/labs, div/ldiv`
- Полный `<string.h>` (memchr/cmp/set/cpy, strcat/cmp/cpy/len/chr/spn/etc.)
- `<ctype.h>` (toupper/tolower)
- `<math.h>` (sinf/cosf/sqrtf/etc.)
Линкер автоматически тянет из z80.lib когда нужно. **НЕ переписывать**.
Наши Sprinter-specific обязательные модули остаются: atexit, env, errno, setjmp, putchar/puts/getchar, conio, fsdir, time, mouse, open/read/lseek/close.
## Build-system: libsprinter.lib + sprinter-cc — ✅ ГОТОВО
- [x] `lib/Makefile` — собирает каждый libc/*.c в `.rel`, архивирует через sdar в `lib/sprinter.lib`
- [x] Включает runtime/bank.s и runtime/heap.s (auto-pulled при __banked/malloc)
- [x] `bin/sprinter-cc` — bash-wrapper: `sprinter-cc -o foo.exe foo.c` одной строкой
- [x] Поддержка опций `--crt0=default|minimal|banked`, `--bank N=FILE.c`, `-I`, `-L`/`-E`/`-S`, `-Wl`, `--mkexe`
- [x] `examples/hello_sccc/` — демо: `hello.c` собирается за один shell-вызов, размер совпадает с ручным Makefile (925 байт)
- [x] Split `putchar.c``putchar.c` + `puts.c` для per-function granularity (puts override SDCC's z80.lib version)
- [x] Включён в `make all` (зависимость `lib` перед `examples`)
Возможные улучшения (опционально):
- [ ] Мигрировать остальные examples на sprinter-cc вместо ручных Makefile (косметика)
- [ ] Дальнейшая декомпозиция libc/*.c per-function (но текущая granularity уже даёт нужный размер — линкер пакетует .rel целиком, и для большинства файлов это одна функция)
## Этап 9 — memory modes для sprinter-cc
DSS выделяет страницы памяти по размеру приложения: < 16 KB → одна страница, в остальные окна подключается «страница #FF» (read=0xFF, write игнорится). Из-за этого CODE-в-W1 + DATA-в-W2 для маленькой программы молча ломается. См. [memory/sprinter_memory_modes.md](../../.claude/projects/-Volumes-SAM8-Projects-DIY-Z80-Sprinter-C-Compiler/memory/sprinter_memory_modes.md).
- [x] **`tiny`**: всё (CODE+DATA+стек) в W2. Default. Verified hello/argv/conio/malloc/file/etc.
- [x] **`--memory MODE` флаг в sprinter-cc**: parser + per-mode дефолты CODE_LOC/DATA_LOC, override через явные `--code-loc`/`--data-loc`. tiny работает; small/big/huge компилируются с warning'ом (runtime не готов). Реализовано 2026-05-30.
- [x] **`--memory-manual SPEC`**: парсит `CODE=W1|W2,DATA=W1|W2|SAME,BANKED=W1|W3`. Реализовано 2026-05-30.
- [x] **`small` runtime**: `runtime/crt0_small.s` использует ESTEX `$3D GETMEM` + `$3A SETWIN2` чтобы выделить и замапить W2-страницу ДО gsinit. **НЕ** BIOS `$C4` — стек на этом этапе в W1 (boot_stack в HOME), а BIOS требует стек в W2. После маппинга SP переключается на 0xBFFE, дальше стандартный flow. Реализовано 2026-05-30, verified hello.exe.
- [x] **`small` auto-detect для >16 KB программ**: `crt0_small.s` читает порт `0xC2` (текущая страница в W2 — не `0xA2`! это W1). Если 0xFF — выделяет page; иначе DSS уже сделала это (программа сама вылезла в W2). Один crt0 покрывает 0..30 KB. mkexe также разрешает HOME span W1+W2 (0x4000..0xBFFF). Verified hello: small (5 KB файл, SETWIN2 path) + 32 KB файл (auto-skip). Реализовано 2026-05-30.
- [x] **`big` runtime** (tiny + banked code в W1): параметризовали `crt0_banked.s` + `bank.s` через `.ifdef BANK_W1` — другой banking port (0xA2 vs 0xE2), другой load-addr (0x4000 vs 0xC000). sprinter-cc prepend'ит `BANK_W1 = 1` при `--memory big`, передаёт `mkexe -B 0x4000`. Пример `examples/banked_big/`. Реализовано 2026-05-30.
- [x] **`huge` runtime** (small + banked code в W3): merge W2-detect логики из `crt0_small.s` в `crt0_banked.s`. Существующий пример `examples/banked/` теперь использует MEMORY=huge. Реализовано 2026-05-30.
- [x] **`--debug` флаг**: prepend `DEBUG_RT = 1` в crt0 + `-DDEBUG_RT` в sdcc. Открывает symbol `_w2_self_allocated` (uint8_t) — runtime diagnostic кто аллоцировал W2. Реализовано 2026-05-30.
### Дизайн-решения по libc и crt0
**Одна `sprinter.lib`** работает для всех memory mode — `.rel`-члены relocatable, SDLD делает dead-code elimination per-member (без графики не подтягивает `gfx_core.rel` и т.д.). Verified hello vs malloc_test через map-файлы.
**`gfx.lib` отдельно — НЕ нужен**: dead-code elimination уже работает.
**`libc_banked` (libc в bank вместо HOME)** — идея на потом, когда HOME (16 KB) забит user-кодом + libc в `huge` mode. Реализуется через `--codeseg BANK0` при компиляции libc; trade-off: trampoline ~30 циклов на каждый libc-вызов. Триггер: реальная программа упрётся в HOME budget.
**HW-зависимые модули — `sprinter_home.lib` отдельно.** Часть libc физически не может быть забанкована в W3, потому что она РАБОТАЕТ с W3:
- `gfx_*` — пишет в видеопамять `0xC000+` после swap W3 на video page
- `bank_io` (mem_alloc_pages/bank_read/bank_write) — swap'ит W3 через `OUT (0xE2)`
- Будущие ISR — прерывание может прийти когда W3 на чём угодно
В huge mode эти модули ДОЛЖНЫ остаться в HOME (W1). Когда будем делать `libc_banked`, **одновременно** выделяем `sprinter_home.lib` (HOME-only) из `sprinter.lib` (bankable). Финальная схема:
```
sprinter_home.lib HOME-only: gfx, bank_io, ISR shims
sprinter.lib bankable: printf, malloc, string, conio, stdio, env, ...
sprinter_banked.lib тот же sprinter.lib но --codeseg BANK0 (для huge)
```
Триггер: реализация `--memory huge` runtime.
**crt0 — по одному на mode:**
- `crt0.s` — текущий, для **tiny/big**: SP=0xBFFE, парсит argv (W2-ресурс уже выделен DSS).
- `crt0_minimal.s` — текущий, для tiny без argv.
- `crt0_small.s`**новый, step 3**: для **small/huge**, аллоцирует W2 через `mem_alloc_pages` ДО gsinit, маппит в порт `0xA2`, потом стандартный flow.
- `crt0_banked.s` — текущий, для **big**: trampoline-таблица для W3 банков, CODE в W2.
- `crt0_banked_small.s`**новый**: huge = small (W2-alloc) + banked (W3 trampolines).
sprinter-cc подбирает crt0 по `--memory` mode (сейчас `--crt0=` это override).
- [x] **Настраиваемый размер стека**: флаг `sprinter-cc --stack-size BYTES`. Wrapper генерирует `heap_top.s` с `___sdcc_heap_end = stack_top + 1 - stack_size`, отдельный .rel линкуется per-program. Default ≈1278 байт (heap_top=0xBB00) из `runtime/heap_top.s`. Реализовано 2026-05-30.
Интерфейс: `sprinter-cc --memory [tiny|small|big|huge|manual] [--memory-manual SPEC] [--stack-size N] foo.c`. `--memory-manual` имеет смысл только с `--memory manual`.
## Known issues / quirks
- **ESTEX $46 ENV API**: ✅ работает. Док-ция в `DiskSyscalls.txt v1.6` ошибочно описывает return-status — A=0 это NOT FOUND, не FOUND. Зафиксировано в `memory/sprinter_platform.md`.
## ОБЯЗАТЕЛЬНЫЕ ЗАДАЧИ ДЛЯ V2 (после релиза v1)
### Turbo-C-style graphics API (BGI-like) — **MUST для v2**
Расширить наш `gfx_*` API до уровня **Turbo-C `<graphics.h>`** (BGI) для MS-DOS.
Программисты привыкшие к Turbo-C должны переносить графический код 1-в-1.
**Что должно быть** (на основе Borland BGI):
Setup/teardown:
- `initgraph()` / `closegraph()`у нас сейчас `gfx_init`/`gfx_done`, добавить alias
- `getmaxx()` / `getmaxy()` — макрос на GFX_WIDTH-1 / GFX_HEIGHT-1
- `cleardevice()` — alias to gfx_clear
- `getgraphmode()` / `setgraphmode()`у нас get_videomode/set_videomode
Color/palette:
- `setcolor(c)`, `getcolor()` — current draw color
- `setbkcolor(c)`, `getbkcolor()` — background color
- `setpalette(idx, c)` — палитра entry
- `getpalette(&info)` — read all palette
Primitives (мы уже имеем эквиваленты — добавить BGI-имена как aliases):
- `putpixel(x, y, c)` — есть как gfx_putpixel
- `getpixel(x, y)` — нужно реализовать (RMW обратное — IN)
- `moveto(x, y)`, `lineto(x, y)`, `linerel(dx, dy)` — current point + line drawing
- `line(x1, y1, x2, y2)` — есть как gfx_line
- `rectangle(x1, y1, x2, y2)` — есть как gfx_rect (но другой API: x1,y1,x2,y2 vs x,y,w,h!)
- `bar(x1, y1, x2, y2)` — есть как gfx_fill_rect
- `bar3d(x1, y1, x2, y2, depth, topflag)` — новое: rect + 3d edges
- `circle(x, y, r)`, `arc(...)`, `ellipse(...)`, `pieslice(...)` — новые primitives
- `fillpoly()`, `drawpoly()` — полигоны
- `floodfill(x, y, border_color)` — заливка
Text on graphics screen:
- `outtext(s)` / `outtextxy(x, y, s)` — есть как gfx_text (alias)
- `settextstyle(font, dir, size)` — multiple bitmap fonts
- `gettextsettings(&info)`
- `textwidth(s)` / `textheight(s)` — measure
Image manipulation:
- `imagesize(x1, y1, x2, y2)` — bytes needed for getimage
- `getimage(x1, y1, x2, y2, buf)` — save rect to buffer
- `putimage(x, y, buf, op)` — paste back with COPY_PUT/XOR_PUT/AND_PUT/OR_PUT/NOT_PUT
Clipping/viewport:
- `setviewport(x1, y1, x2, y2, clip)` — drawing clip rect
- `getviewsettings(&info)`
- `clearviewport()`
- `setactivepage(p)` / `setvisualpage(p)` — двойная буферизация (Sprinter имеет 2 screen)
Line style:
- `setlinestyle(style, pattern, thickness)` — SOLID_LINE / DOTTED_LINE / etc.
- `getlinesettings(&info)`
**Acceptance:** перенос типичной Turbo-C BGI программы (рисующей с использованием
moveto/lineto/circle/bar/setcolor) должен работать без существенных правок.
**Notes:**
- BGI fonts (TRIPLEX/SANS_SERIF/GOTHIC) — у нас один BIOS font, остальные нужно
добавить (как bitmap data в lib)
- imagesize/getimage/putimage — самые востребованные для game/animation
- Active/visual page (двойная буферизация) — Sprinter поддерживает 2 graphics pages,
нужен API switching
См. также `examples/` Turbo C 2.x BGIDEMO как reference что нужно.
### IM2 Interrupt Handlers — **MUST для v2**
User-задаваемые ISR через Z80 IM 2 mode. Нужны для:
- Timer ticks (50 Hz frame counter, плавная анимация)
- Music playback (AY, COVOX)
- Real-time games (input + game logic + render в interrupt-driven)
- Async keyboard / mouse handling
**Status:** ОТЛОЖЕНО до v2. Полный research + design в `docs/im2_isr_design.md`.
**Решение по архитектуре:** реализовать как отдельный memory mode `--memory im2`
(вместо того чтобы лезть во все существующие crt0). Detail'и в design-doc.
**Резюме research'а** (полный текст в `docs/im2_isr_design.md`):
- Vector 0xFF — frame + keyboard + CBL. Disambiguation по портам 0x19 / 0xFE
- Mouse hardware-IRQ не приходит (на текущей плате)
- Vector table / ISR / stack ОБЯЗАНЫ быть в W2 (0x8000..0xBFFF)
- DSS имеет свой IM 2 handler — нужно chain'иться (иначе клавиатура / SYSTIME ломаются)
### Прочие крупные пункты для v2
- [ ] **FILE API rewrite — buffered streams** — текущая реализация в
`libc/stdio/file.c` это provisional unbuffered shim (каждый fputc/fgetc
= один read/write syscall). Нужна полноценная buffered семантика
как в Solid-C:
```c
typedef struct {
uint flags; // +0..1 file status flags
int level; // +2..3 empty/fill level of buffer
char *curp; // +4..5 current active pointer
int fd; // +6..7 underlying low-level fd
char *buffer; // +8..9 data transfer buffer
char hold; // +10 ungetc byte if no buffer
short token; // +11..12 reserved
char dummy; // +13 reserved
} FILE;
```
stdin/stdout/stderr — fd-маркеры `0 / -1 / -2`. Отрицательные для
stdout/stderr выбраны намеренно: ESTEX OPEN может вернуть positive
small fd (1, 2, …) для обычного файла → если бы stdout=1, реальный
fd=1 сталкивался бы с идентификатором. fd=0 для stdin безопасно
(ESTEX 0 не возвращает). Сами fd не передаются в syscall'ы —
диспетчеризация по флагам `_F_CONIN/_F_CONOUT`.
Принтер-потоки (stdaux/stdprn) НЕ реализуем — Sprinter принтерной
API не имеет.
Альтернатива — взять реализацию из third_party/solid-c (sources в
`SRC/CLIB/`); там есть готовый buffered FILE + fopen/fread/fwrite/
fseek/setvbuf и т.д. Адаптировать к нашим open/read/write/lseek.
При rewrite заодно решить deferred issues stdio-review:
- `fwrite` short-write должен ставить `_F_ERROR`
- `fgets(buf, 1, fp)` — стандарт говорит "empty string", мы вернули NULL
- `mode_to_flags` — break-out на '+' (cosmetic)
- [ ] **Audio API** — AY-3-8910 + COVOX через прерывания (требует IM2)
- [ ] **ISA-8 slot support** — ZX-Bus карты (sound, network, etc.) — требует IM2 + чтения portов
## Прочие задачи (v1 backlog, не блокирующие)
- [x] **#9: text I/O split (Turbo-C style)** — stdio (puts/printf/putchar) теперь fast no-attr через PCHARS/PUTCHAR. conio (cputs/cprintf/putch) применяет attr через textcolor/textbackground/textattr. KEEP_EXIST_ATTR → conio fallback на fast path. Verified в hello.exe. См. `memory/text_output_api_split.md`. Реализовано 2026-05-31.
- [x] **Mouse API полный** (резидентный driver, RST 30h) — все 14 функций обёрнуты (init/show/hide/refresh/read/goto/bounds/text_cursor/load_cursor/get_cursor/get/set_sensitivity/video_mode_changed). См. `memory/mouse_api.md`. Verified в MAME 2026-05-31. Sensitivity = divider (меньше = быстрее).
- [ ] Interrupt handlers — IM 2 vector table в HOME для user ISR'ов
- [ ] Поддержка `restore SP on EXIT` (паттерн из z88dk +pps) — проверить нужно ли
- [ ] CI: автоматически запускать MAME с `-aviwrite` для screenshot-сравнения, чтобы тесты примеров проходили без человека
## Идеи на потом
- Поддержка `<setjmp.h>` (есть в SDCC stdlib — нужно протестировать что наш crt0 совместим)
- `<time.h>` через ESTEX SYSTIME (`$21`) и CMOS BIOS-функции
- ZX Spectrum-совместимый режим как отдельный target (для портирования спектрумовских программ)
- Поддержка ZX-Bus карт (sound, network, etc.) — нужны драйверы
- Profile-guided optimization tools (hot/cold detection) для крупных программ
## Linker duplicate-symbol warnings (благоприятные, отфильтрованы)
Когда мы сознательно overrides'им SDCC z80.lib функции собственной версией в `sprinter.lib`, `sdldz80` пишет `?ASlink-Warning-Definition of public symbol '...' found more than once`. Линкер берёт первое найденное определение (наше), поэтому поведение корректное — warning только noise.
Текущие overrides:
- `_puts` — наша версия через PCHARS+\r\n vs SDCC posix puts
- `___sdcc_heap` — наш heap в W2 vs SDCC's стандартный
- `_asctime`, `_localtime` (и возможно другие из time) — наш `posix_time.c` через ESTEX SYSTIME vs SDCC's `time.rel` который зависит от `_RtcRead`
**Текущее решение:** `bin/sprinter-cc` отфильтровывает warning-блок (warning + 2 follow-up `Library:` строки) из вывода `sdcc`. Через `-v` (verbose) всё показывается. Реализовано через awk-pipe.
**Возможные улучшения:**
- Перейти на explicit `--nostdlib` + ручной список нужных модулей из z80.lib (string, math, stdlib без override'нутых) — убрать ИСТОЧНИК warning'ов, не маскировать
- Или: переименовать наши `_puts` → `_puts_sprinter` + alias через linker flag (не уверен что SDCC поддерживает)
- Или: оставить как сейчас (рабочее и benign) — приоритет низкий
## TODO: проверить на реальном железе
- [ ] **Port_Y banking trick** (`docs/part2/SprinterGraphics programming.txt`):
доку утверждает что после `OUT (0x89), Y` адреса 0xC000+0x400*N в окне W3
маппятся на строки Y..Y+15 (одно программирование → 16 строк).
Empirical 2026-06-01 в MAME 0.283 этот trick **не работает** — пиксели
по адресам выше 0xC000+row_width уходят в невидимую область. Канонический
`docs/samples/plasma2.asm` тоже не использует banking, переустанавливает
Port_Y per row.
План:
1. Получить доступ к реальному Sprinter
2. Запустить тест dual-write (`_gfx_putpixel_raw` + второй write в `0xD000+x`)
3. Если на железе видны двойные линии → бага MAME, открыть issue с
минимальным репро
4. Если на железе тоже одна линия → документ неверный, удалить упоминание
из доки и просто оставить текущую реализацию (Port_Y per pixel)
5. Если banking работает на железе → внедрить кэширование Port_Y в
`_gfx_putpixel_raw` (sentinel out-of-range, см. memory/gfx_port_y_banking.md)
Связанный выигрыш для Bresenham (60-pixel диагональ) — около 8× меньше
OUT (0x89) операций, для `gfx_fill_rect 320x256` — 16× меньше. Не блокирует
release v1.
## GFX: расширения по `docs/part2/accelerator_doc.txt`
После прочтения детального accelerator doc выявлены незакрытые направления.
Сейчас в коде используется только горизонтальный/вертикальный Fill mode.
### Quick wins для текущих primitives
- [ ] **Заменить SMC на `LD A, (var)` для block-size**. Документ явно
разрешает `LD A, (HL)`, `LD A, (BC)`, `LD A, (DE)` (но не `LD A, r`).
Это уберёт SMC complexity в `gfx_lines.c:hfill_chunk/vfill_chunk` и
`gfx_16.c:g16_hfill_chunk`. Запрещено только register-to-register.
- [ ] **Кэширование block-size**. Документ показывает что accel запоминает
block size между bursts (см. `Horizontal_Line_Fill`: устанавливают
size + `LD B,B` отключение, потом включают Fill mode и используют
сохранённый size). Для `gfx_fill_rect` с 100 одинаковыми
строками — установить size 1 раз, а не 100.
### Bank-prefix modes (port 0xE2 bits)
Документ показывает три варианта банка видеостраницы помимо стандартного 0x50:
| Bank byte | Effect |
|---|---|
| 0x50 | Normal write — пишется в shadow + видимый |
| 0x54 | "no copy in main shadow RAM" |
| 0x58 | **"FF is transparent"** — байт 0xFF при write оставляет background |
| 0x5C | both |
Bank 0x58 объясняет почему mouse cursor рисуется с 0xFF-прозрачностью.
Это путь к **sprite-blending через accel block copy**:
- [ ] **`gfx_set_bank_transparent(on)`** или флаг в `gfx_set_bank` для
выбора 0x50/0x58 при отрисовке sprite'ов
- [ ] Использовать в новом `gfx_blit()` чтобы по факту получать
transparent sprites через accel-копию
### Block copy mode (sprite blit'ы)
`LD L,L` (horizontal) и `LD A,A` (vertical) — режим копирования блока через
256-байтную accel memory. Это базис для blit'ов.
- [ ] **`gfx_blit(src_data, x, y, w, h)`** — копирование sprite'а
(произвольный размер, через accel)
- [ ] **`gfx_blit_transparent(src, x, y, w, h)`** — с использованием bank 0x58
См. `Draw_Restangle_Data` в accelerator_doc.txt как референс.
### AND / OR / XOR operations через accel
Документ показывает что accel поддерживает логические операции с блоками
данных. Применения:
- XOR — инверсия области (выделение selection в UI)
- OR / AND — masking, alpha-style blending
- См. пример в accelerator_doc.txt: "256 bytes block coding via XOR"
- [ ] **`gfx_xor_rect`** / **`gfx_or_rect`** / **`gfx_and_rect`** —
примитивы логических операций над прямоугольником
- [ ] **`gfx_invert_rect(x, y, w, h)`** — alias на xor с 0xFF
### Bitmap fonts разных размеров
Сейчас `gfx_text` / `gfx_putchar` хардкоженно работают с 8×8 шрифтом
(BIOS WIN_GET_ZG возвращает 256×8 байт). Для будущих UI / титульников
нужны:
- [ ] **`gfx_set_font_size(w, h)`** — переключить ширину/высоту glyph'а
- [ ] **`gfx_set_font_data(ptr, w, h, advance)`** — заменить указатель
на пользовательский шрифт + размеры
- [ ] Поддержка **proportional** (advance != w) шрифтов — добавить
array advance[256] на ширину каждого glyph'а
- [ ] **Big-font режимы**: 8×16, 16×16, 16×8 (для титульников)
- [ ] Возможно отдельный API `gfx_text_ex(x, y, str, font_id)` где
font_id выбирает один из загруженных шрифтов
- [ ] **Anti-alias 2-bit шрифты** (бит фон / бит граница / 2-бит alpha?)
— far future, для smooth UI
## Финальный этап оптимизаций (не сейчас)
- **`gfx_line` через accel для пологих диагоналей** — Bresenham для линии с |dy| << |dx| (или наоборот) выдаёт длинные runs одинакового Y (или X): пиксель, пиксель, пиксель, шаг Y, пиксель, пиксель... Каждый такой run — это готовый аргумент для `gfx_hline` (или `vline`).
План исследования: посчитать длину runs как функцию от наклона; решить минимальный run length, при котором выгоднее accel hline чем N×putpixel (overhead accel ~20µs, putpixel ~5µs — accel выгоднее при run ≥ 4-5 px); для крутых диагоналей (dx ≈ dy) оставить Bresenham, для пологих — run-length-based fill.
Сейчас `gfx_line` orthogonal cases уже через accel — оптимизировать только косые.
- **`gfx_fill_rect` с одним W3-swap на всю операцию** — сейчас каждый внутренний `gfx_hline`/`gfx_vline` делает свой DI/save-W3/restore-W3/EI. Можно сделать internal `_fill_rect_inner` который держит W3 замапленным и DI весь цикл; ~20µs × количество строк/столбцов экономии. Применимо ко всем композитным примитивам.
+28
View File
@@ -0,0 +1,28 @@
# Тест UTF-8
Это **проверка** кодировки _UTF-8_ в `mdview2`.
Кириллица должна читаться: съешь же ещё этих мягких булок.
## Символы и пунктуация
Тире — длинное, и – короткое. Кавычки: «ёлочки» и “лапки”.
Многоточие… стрелки → ← ↑ ↓, галочка ✓ и крестик ✗.
Градус 25°, пункт списка • буллет, номер № 7.
## Список
- Первый пункт
- Второй пункт с длинным текстом, чтобы проверить перенос строки по словам на границе экрана восемьдесят символов
- Ёжик, ёлка, объём
> Цитата: «Краткость — сестра таланта».
## Таблица
| Язык | Привет | Число |
|----------|---------------|-------|
| Русский | Здравствуйте | 42 |
| English | Hello | 7 |
| Deutsch | Grüße | 100 |
Конец файла.
+144
View File
@@ -0,0 +1,144 @@
# mdview2 — поддержка кодировок (CP866 / CP1251 / KOI8-R / UTF-8)
Статус: РЕАЛИЗОВАНО (2026-06-25). Все фазы сделаны; Фаза 4 — eager-кооперативным
вариантом (см. ниже), а не idle-build. Осталась только проверка на железе и
возможная оптимизация «не строить заведомо бесполезный вторичный набор».
## Цель
- CP1251, KOI8-R — простой 1:1 маппинг байтов [128-255] в CP866 (кириллица
+ основная пунктуация). KOI8-R равнозначна, входит в общий цикл.
- UTF-8 — декодирование с маппингом кириллицы в CP866 + подстановка части
некириллических символов (стрелки, галочки, тире, буллеты, box-drawing) в
CP866/ASCII-глифы.
- Автоопределение кодировки при открытии (BOM + дешёвая эвристика).
- F8 (Codepage) — переключение по циклу CP866 → CP1251 → KOI8-R → UTF8.
## Ключевые наблюдения (определяют архитектуру)
1. **8-битные кодировки имеют ОДНУ структуру.** markdown-разметка вся в ASCII
(`# * | -` …); 8-битные различаются только глифами [128-255]. Значит индекс
и кэш (смещения, переносы, ширины таблиц) для CP866/CP1251/KOI8-R —
**общие**; переключение между ними = ремап глифов [128-255], БЕЗ
переиндексации.
2. **UTF-8 — другая структура** (кириллица 2 байта). Нужен отдельный
конвертированный CP866-буфер со своим индексом/кэшем.
3. **Проблема рамок таблиц / маркеров.** В кэше намешаны контентные глифы
(байты-из-источника [128-255] — НАДО ремапить) и вставленные нами CP866-
глифы (box `│─┼…`, маркер списка 0x07, цитата 0xB3, HR 0xC4 — ремапить
НЕЛЬЗЯ). По значению байта не различить → различаем по **атрибуту**.
## Архитектура
### Буферы
- `src_phys[]` — оригинальные байты файла (грузим как сейчас; храним всегда —
под ремап и raw-view, см. memory/mdview2_file_phys_preserve).
- 8-битный режим: `fb()` читает `src_phys` напрямую. Кэш контентных ячеек
хранит **исходный байт** (не пред-конвертированный).
- UTF-8 режим: отдельные EMM-страницы `utf_phys[]` = UTF8→CP866 конвертация
src; свой индекс/кэш; `fb()` читает `utf_phys`.
### Различение контент/структура по attr
Каждый структурный глиф получает НЕ-контентный attr:
- box-рамка таблиц → новый `ATTR_BOX` (сейчас TBL_ATTR=ATTR_TEXT — поменять);
- HR (0xC4) → ATTR_HR; маркер списка (0x07) → ATTR_LIST_MARKER; цитата
(0xB3) → ATTR_QUOTE_MARKER — уже различимы.
Правило ремапа: ремапить `char>=128` только если attr ∈ контентных
(TEXT/BOLD/ITALIC/UNDER/CODE/STRIKE/TITLE1-4). Структурные — как есть.
### Ремап на этапе отрисовки
`draw_line_from_cache`:
- encoding == CP866 или UTF-8: прямой `win_rest` из кэш-страницы (ремап не
нужен — CP866 identity; UTF-8-кэш уже CP866).
- encoding == CP1251/KOI8-R: читаем кэш-строку в near-буфер, ремапим
контентные байты [128-255] через активную таблицу (структурные пропускаем
по attr), пишем в scratch-страницу, `win_rest` из неё. Только видимые ~30
строк, на скролле — дёшево.
Итого: переключение между 8-битными — мгновенно (меняем активную таблицу +
redraw, draw ремапит). Переиндексация только при переходе в/из UTF-8.
## Детекция кодировки (дешёвый скан байтов, ДО построения)
1. **BOM**: первые 3 байта EF BB BF → UTF8 (и пропустить BOM).
2. **UTF-8 валидность** (если нет BOM): проход, проверка структуры
(лид-байты 0xC2-0xDF/0xE0-0xEF/0xF0-0xF4 + континюэйшны 0x80-0xBF;
одиночный 0x80-0xBF, 0xC0/0xC1, 0xF5+ → нарушение). 0 нарушений И есть
≥1 multibyte → UTF8. Любое нарушение → 8-бит.
3. **8-бит дизамбигуация** (CP866/CP1251/KOI8-R): счёт попаданий в байты
самых ходовых строчных русских букв (о е а и н т с р в л) каждой кодировки;
максимум выигрывает. (Предрасчёт байт-наборов по таблицам.)
4. **Фолбэк**: нет байт ≥0x80 или неоднозначно → CP866 (родная).
## Таблицы (static const, CODE/const-сегмент)
- `cp1251_to_866[256]`, `koi8r_to_866[256]` — байт→байт (ASCII identity;
кириллица по раскладкам; en/em-dash, «ёлочки», … → CP866-аналоги или '?').
- UTF-8: `utf_cyr_to_866[]` для U+0400..U+045F + компактная таблица символов
`utf_sym[]` (codepoint→CP866): U+2192→'>'/стрелка, U+2190→'<', U+2713/14 ✓
→'v'/box, U+2022 •→0x07/0xF9, U+2014/2013 —→'-', U+2026 …→"...",
U+00A0→' ', U+2500.. box→CP866 box; прочее → '?'.
## Поток загрузки
1. Грузим в `src_phys`. Детект-скан → кодировка E.
2. Если E ∈ {UTF8}: строим UTF-8-набор (utf_phys + конвертация + индекс),
показываем UTF-8. Иначе: показываем 8-битный (общий индекс на src,
активная таблица = E).
3. **Ленивый build второго набора в простое.** Главный цикл — НЕ блокирующий
getkey, а kbhit-поллинг: пока нет клавиш и второй набор (UTF-8 при
стартовом 8-бит, либо 8-бит при стартовом UTF-8) не построен — докручиваем
его инкрементально. После — F8 в любую сторону мгновенно.
## F8 — переключение
- Цикл g_encoding: CP866 → CP1251 → KOI8-R → UTF8 → CP866.
- 8-бит↔8-бит: сменить активную таблицу + redraw (без переиндексации).
- в/из UTF-8: переключить активный индекс/кэш на соответствующий набор
(если построен; иначе достроить — но при ленивом build обычно уже готов).
- Статус-бар: имя кодировки; меню (строка 31): «F8 Codepage».
## Фазы реализации (ИТОГ)
1.**Ядро 8-бит**: ATTR_BOX; кэш хранит исходный байт; таблицы CP1251/KOI8R;
ремап в draw (`win_rest_remap`).
2.**UTF-8 набор**: `alloc_and_convert_utf8` + `utf8_convert` (декодер 1/2/3-
байт, 4-байтные/битые → `?`); `utf_cyr_to_866[96]` + символьные подстановки
в `conv_emit_cp` (стрелки 0x18-0x1B, галка 0xFB, буллет 0xF9, тире/кавычки/
box/° и т.п.; «» → `<`/`>`, т.к. в CP866 гильеметов нет).
3.**Детекция** (BOM + эвристика), сэмпл — первые **4 КБ** (быстро); хвост-
обрезка multibyte на границе сэмпла не штрафуется.
4.**Сосуществование 2 наборов** (`docset_t g_doc[2]` + `doc_save/load/switch`,
свап «живых» глобалов). Сборка — **ленивая (build-on-demand)**: при старте
строится только первичный (показываемый) набор; UTF-8 конвертация тоже
ленивая (в `build_doc`, не до первого экрана → старт быстрый). Второй набор
достраивается `switch_encoding()` при первом F8-переходе в него (спиннер,
потом кэш). Eager-вариант отвергнут: удваивал старт и блокировал F8 на время
фоновой сборки.
5.**F8** полный цикл CP866→CP1251→KOI8R→UTF8→CP866 (внутри 8-бит — ремап,
на границе — `doc_switch`/ленивая сборка). Статус: `enc_name` кол.37. Меню
«F8 Codepage» показывается только когда переключение возможно (`g_f8_enabled`):
во время сборки 8-битного первичного F8 разрешён в `load_key` (только цикл
8-бит); во время сборки UTF-8 первичного метка F8 скрыта. F1-справка: секция
Encoding.
NB: меню-строка разбита на 10 блоков по 8 колонок, метки Fn кладутся в блок
(n-1)*8 (F1→0, F8→7, F10→9).
## Память
- 8-бит: src + scratch-страница для ремапа (1 стр.). Доп. индекса нет.
- UTF-8 набор: utf_phys (≤8 стр.) + свой индекс/кэш-контент. Строится лениво.
- Бюджет 215 свободных страниц (sprinter_emm_budget) — с запасом.
## Открытые вопросы (к реализации)
1. Имя кодировки в статусе — где (зона имени файла / отдельный слот).
2. Набор UTF-8-подстановок символов — приоритет (→ ← ✓ ✗ • — … « » box).
3. Глубина таблиц пунктуации CP1251/KOI8 (минимум кириллица+dash или полнее).
4. Объём детект-сэмпла (весь файл или первые N КБ).
## Риски
- Корректность различения контент/структура по attr — нужно, чтобы ВСЕ
вставленные глифы имели не-контентный attr (проверить box/markers).
- Два набора индекс/кэш (8-бит + UTF-8) + переключение активного — учёт
страниц, чтобы не течь и не путать.
- Точность таблиц (особенно UTF-8 символы) — итеративно по факту.
+164
View File
@@ -0,0 +1,164 @@
# mdview2 — план: рендер-кэш вместо живого парсинга на каждый скролл
Контекст и обоснование — см. обсуждение 2026-06-23 (после оптимизации mdview через
`<bios/text.h>`, см. `mdview-модель-документа-и-рендеринг.md`). Идея: вместо того
чтобы при каждом скролле повторно идти в файл (`fb()`, W3-банкинг) и гонять
markdown inline-парсер (`handle_inline_marker`/emphasis state machine), один раз
отрендерить каждую логическую строку в готовый байтовый буфер и дальше выводить
его на экран напрямую — без парсинга, без обращения к исходному файлу.
Ключевые подтверждённые факты (эмпирически в MAME, не из документации):
- **Формат буфера ESTEX `WINCOPY`(59h)/`WINREST`(5Ah)** — пара байт `(char, attr)`
на ячейку, по строкам, шаг строки = `width*2`, без паддинга. Можно генерировать
самим, не вызывая `WINCOPY`. `WINREST` копирует сразу `H` строк одним вызовом.
Детали ABI и регистров — см. `tests/winrest/winrest.c`.
- **Бюджет EMM**: 256 страниц (4 МБ) total, 215 (3440 КБ) free на старте программы.
Файл+индекс в худшем случае (128 КБ файл) съедают 16 страниц (256 КБ) — остаётся
≈199 страниц (3184 КБ). Кэш всего документа целиком (даже худший случай:
16384 строк × 160 байт = 2.6 МБ) укладывается без LRU/частичного кэша.
## Единый формат строки (пересмотрено 2026-06-23)
Исходно планировалось два типа строк (тип 1 — многоцветные, помещающиеся;
тип 2 — nowrap/code, один стиль, чистый текст для экономии памяти). От этого
деления отказались: строки таблиц (`IF_NOWRAP`, но не `IF_CODE`) всё равно
проходят inline-парсер и могут содержать несколько атрибутов (bold/italic в
ячейках) — предположение "один стиль" для них неверно. Бюджет EMM
([[sprinter_emm_budget]]) с большим запасом покрывает (char,attr)-формат для
ВСЕХ строк без исключения, поэтому усложнение не оправдано.
**Единый формат**: кэш-запись = `len` пар `(char,attr)` — реальная длина
контента в ячейках, без паддинга до 80, капается на `MAX_CACHE_LINE_LEN`=255.
Вывод: `bios_fillcharattr(' ', base_attr, SCREEN_W)` (очистить строку) →
`win_rest(row, left_margin, 1, len, page)` (контент). Для широких nowrap-строк
горизонтальный скролл (Фаза 5) — это просто смещение НАЧАЛА среза внутри ТОГО
ЖЕ (char,attr)-буфера на `hscroll*2` байт, тот же `win_rest`, без отдельного
плain-текстового формата и без необходимости на лету "разворачивать" текст+
атрибут в пары.
## Фазы реализации
### Фаза 0 — скаффолдинг `examples/mdview2/` [СДЕЛАНО 2026-06-23]
Новая директория со своим `Makefile`/`app.mk` (по аналогии с `mdview/`, не
модифицируем `mdview.c`). `mdview2.c` — копия `mdview.c` без изменений логики
(только usage-строка/заголовок комментария). Собирается чисто, дискета собрана.
### Фаза 1 — формат кэша и директория строк [СДЕЛАНО и ЗАКРЫТО 2026-06-23]
Реализовано в `mdview2.c`:
- `cache_rec_t` (РОВНО 8 байт: `page`, `off` (uint16_t), `len`, `flags`,
`reserved`, `pad[2]`; размер задаётся `CACHE_DIR_REC_SIZE = sizeof(cache_rec_t)`,
НЕ хардкодом — см. разобранный инцидент ниже) — **отдельная** директория
(`cache_dir_phys[]`/`cache_dir_get`/`cache_dir_put`), своя ёмкость =
`file_pages+1` страниц (как у индекса). **Важное уточнение к исходному тексту
плана ниже**: директория НЕ переиспользует слоты `idx_rec_t` in-place —
рендер-воркер (Фаза 2) при обработке строки N может заглядывать в idx-записи
СОСЕДНИХ строк (откат cont-сегментов, line_idx+1 для границы сегмента); если
бы строка N-1 была перезатёрта сразу после своего рендера, воркер строки N
прочитал бы уже не исходные off/flags, а указатель в кэш — поломав откат.
Бюджет EMM ([[sprinter_emm_budget]]) позволяет отдельный массив — он безопаснее.
- Пул контента — отдельные EMM-страницы (`cache_content_phys[]`), ленивый рост
по 1 странице в `cache_reserve()` (bump-allocator, запись никогда не
разбивается через границу страницы; длина капается на `MAX_CACHE_LINE_LEN`=255
ячеек).
- `cache_commit()` = один `bank_write()` на строку (буфер строки собирается
локально в W1/W2 заранее, не в W3 — иначе конфликт с `fb()` при чтении
исходника во время рендера).
- `win_rest()` (ESTEX WINREST 5Ah) — вывод готового буфера на экран.
- `phase1_selftest()` — самопроверка (резервирует/коммитит 4 тестовые ячейки,
кладёт запись в директорию по индексу 0, рисует через `win_rest` дважды
подряд с координатами через параметры функции) — убрать в Фазе 2.
**Подтверждено визуально в MAME (2026-06-23)**: оба квадрата 2×2 на месте, без
единой задержки, с координатами через переменные — Фаза 1 полностью закрыта.
**Разобранный инцидент (НЕ платформенный квирк)**: по пути долго казалось, что
`win_rest()` после "всплеска" обычных BIOS print-вызовов рисует буфер в
неправильном месте, причём воспроизводилось только когда `row`/`col` приходили
через переменные, а не как константы — это и было ключом. Настоящая причина:
`cache_rec_t` фактически занимал 7 байт (SDCC z80 не паддит структуры), а
`CACHE_DIR_REC_SIZE` был захардкожен как 8 — `bank_read`/`bank_write` копировали
8 байт в 7-байтный буфер на стеке, затирая соседнюю переменную (параметр `col`)
вызывающей функции. Фикс — `cache_rec_t` явно до 8 байт + размер через
`sizeof()`. Полная история и общий урок — [[sprinter_winrest_format]] и
[[defer_unexplained_quirks]].
### Фаза 2 — рендер-воркер (бывший `render_line`) [СДЕЛАНО 2026-06-23]
`render_line_to_cache(line_idx)` — адаптация `render_line()`: та же классификация
строк/префиксов/inline-парсинг (`handle_inline_marker`, emphasis state machine,
soft-wrap join) БЕЗ ИЗМЕНЕНИЙ, но вместо `bios_writeattr`/`flush_run`/`wrchar`/
`bios_fillcharattr` на экран — пишем `(char,attr)` пары через `cc_put`/`cc_fill`
в локальный `cellbuf` (без батчинга через `runbuf`/`flush_run` — тот паттерн был
нужен только чтобы минимизировать число BIOS-вызовов, при записи в локальный
буфер смысла нет, пишем посимвольно сразу по месту), затем ОДНИМ
`cache_reserve()`+`cache_commit()`+`cache_dir_put()` коммитим всю строку.
Кэшируется ПОЛНЫЙ контент строки до `MAX_CACHE_LINE_LEN`, без обрезки по
`SCREEN_W` и без среза по `viewport_x` (view/scroll-time понятия, Фаза 4-5).
Также найден и исправлен по ходу баг в `win_rest()`: `IX` всегда указывал на
начало страницы (`#0xC000`), полностью игнорируя `off` — работало только в
Фазе 1, где в кэше была ровно одна запись со смещением 0; как только Фаза 2
начала пакетировать много строк на одной странице с разными `off`, все строки
стали читаться с начала страницы. Фикс: `win_rest()` принимает `off`
(uint16_t), `IX = 0xC000 + off` (новая раскладка ABI сверена через `sdcc -S`
с непустым телом — у `__naked` с пустым телом SDCC не генерирует код доступа
к параметрам, нужен пробный non-naked враппер). Подтверждено визуально в MAME.
### Фаза 3 — фоновый пре-рендер с прогрессом [СДЕЛАНО 2026-06-23]
`emit_seg()` вызывает `render_line_to_cache()` **interleaved** с построением
индекса — но с отставанием на одну строку: рендер строки N требует уже
существующей idx-записи N+1 (источник `seg_end`), которой ещё нет в момент,
когда строка N только создана. Поэтому `emit_seg()` для новой строки рендерит
ПРЕДЫДУЩУЮ (`n_lines-2` после инкремента) — её флаги (`IF_NOWRAP`/`IF_CODE`/
`IF_BLANK`) к этому моменту уже дописаны вызовом `set_*_cur()` на предыдущей
итерации `index_lines()`. Последнюю строку файла (у которой "следующей" не
будет) дорендеривает сам `index_lines()` после выхода из цикла, как и
`render_line()` делал для последней строки при живом рендере (`seg_end =
file_size`). Требует, чтобы `cache_dir_phys[]` был выделен ДО `index_lines()`
это уже так (`load_file()` выделяет директорию кэша, затем вызывается
`index_lines()`).
**Важное сужение скоупа относительно исходного текста плана ниже**: пункты
"спиннер крутится, пока `rendered_up_to < n_lines`" и "скролл ограничен
диапазоном `[0, rendered_up_to]`" **не реализованы и не нужны** в этой
архитектуре — рендеринг происходит СИНХРОННО внутри той же однопроходной
`index_lines()`, без событийного цикла во время загрузки; пользователь
физически не может начать скроллить, пока `index_lines()` не вернёт
управление, а к этому моменту весь документ уже полностью в кэше. Спиннер
из `index_lines()` (каждые 16 строк) сохранён как есть — он покрывает
индексацию+рендер вместе, отдельный прогресс-индикатор не нужен.
### Фаза 4 — cache-only draw path при скролле
Цикл перерисовки видимой области (после пре-рендера) идёт **только** по
директории строк → `win_rest` (тип 1) или срез текста+`bios_writeattr` (тип 2).
Никаких обращений к `fb()`/исходному файлу в steady-state скролле.
### Фаза 5 — горизонтальный скролл для широких nowrap-строк
Срез ТОГО ЖЕ (char,attr)-кэш-буфера по текущему `hscroll`-офсету (смещение
начала на `hscroll*2` байт внутри буфера строки), `win_rest(row, col, 1,
visible_len, page)` с новым `off`. Никакого отдельного плоско-текстового
формата не нужно (см. пересмотр "Единый формат строки" выше).
### Фаза 6 — тестирование в MAME
Тот же набор паттернов, что использовался для mdview1 (заголовки/списки/цитаты/
code-block/bold-italic/nowrap-обрезка), ПЛЮС: реальный замер EMM на большом
документе (не синтетика — проверить, что бюджет из [[sprinter_emm_budget]]
действительно держится на чём-то близком к 128 КБ); UX пре-рендера (спиннер +
ограничение скролла, отсутствие "дыр" в недорендеренной области); проверка
`win_rest` на РЕАЛЬНОМ отрендеренном контенте (не только синтетический A/B/C/D
тест из `tests/winrest`).
## Открытые вопросы (решить по ходу, не блокируют старт Фазы 0)
- ~~Хранение длины строки в директории~~ — решено в Фазе 1: поле `len` (uint8_t)
прямо в `cache_rec_t`.
- ~~Деление строк на тип 1/тип 2~~ — отказались (см. "Единый формат строки"
выше): единый (char,attr)-формат для всех строк, капается на
`MAX_CACHE_LINE_LEN`=255 ячеек (с тем же индикатором обрезки на этапе вывода,
что уже есть в рендере для nowrap-строк).
- Освобождать ли страницы исходного файла после того, как все его строки
отрендерены (вернуть EMM в пул) — даёт больше места про запас, но усложняет
(нужна гарантия, что назад к файлу обращаться больше не придётся — а это не
так, если позже добавится поиск по тексту). Не делать в v1.
- BIOS-вариант `WIN_COPY_WIN`/`WIN_RESTORE_WIN` (0B2h/0B3h, RST 8) не проверен
(см. [[sprinter_winrest_format]]) — ESTEX-варианта достаточно для v1, проверять
BIOS-вариант только если понадобится экономия на RST-диспетчеризации.
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 — без отдельного инкрементального счётчика,
* который рассинхронизировался, когда контент кончается в середине экрана
* (короче окна после wrap→unwrap у конца файла). */
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 MD→RAW и 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')
+24 -41
View File
@@ -20,46 +20,10 @@ CC_FLAGS := -mz80 --no-std-crt0 --std-c99 --opt-code-size $(INC)
BUILD := $(PROJ_ROOT)/lib/build BUILD := $(PROJ_ROOT)/lib/build
# All libc C modules. # All libc C modules — every .c under libc/<area>/ becomes its own .rel.
LIBC_C := \ # One PUBLIC function per file (see docs/libc-split-asm-cases.md): the
libc/sys/atexit.c \ # linker pulls whole .rel modules, so file granularity == DCE granularity.
libc/conio/conio.c \ LIBC_C := $(patsubst $(PROJ_ROOT)/%,%,$(wildcard $(PROJ_ROOT)/libc/*/*.c))
libc/conio/cprintf.c \
libc/io/dir.c \
libc/video/videomode_raw.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_alloc.c \
libc/gfx/gfx_core.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). # 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 # NOTE: runtime/bank.s is NOT bundled — its trampoline depends on the banking
@@ -72,6 +36,16 @@ RUNTIME_RELS := $(patsubst runtime/%.s,$(BUILD)/%.rel,$(RUNTIME_S))
ALL_RELS := $(LIBC_RELS) $(RUNTIME_RELS) ALL_RELS := $(LIBC_RELS) $(RUNTIME_RELS)
# Штамп списка модулей: обновляется при любом изменении состава
# (добавление/удаление/переименование исходника) — иначе удаление
# исходника не триггерит перелинковку и stale .rel остаётся в архиве.
# Сразу сносим и архив: на exFAT (SAM8) гранулярность mtime грубая,
# и «список изменился» через сравнение времён можно проглядеть.
MODLIST := $(BUILD)/.modules
ifneq ($(shell cat $(MODLIST) 2>/dev/null),$(ALL_RELS))
_ := $(shell mkdir -p $(BUILD); echo "$(ALL_RELS)" > $(MODLIST); rm -f sprinter.lib)
endif
LIB := sprinter.lib LIB := sprinter.lib
all: $(LIB) all: $(LIB)
@@ -88,7 +62,16 @@ $(BUILD)/%.rel: $(PROJ_ROOT)/runtime/%.s
$(SDASZ80) -o $@ $< $(SDASZ80) -o $@ $<
# Archive — sdar with `rcs` = replace/create/symtab. # Archive — sdar with `rcs` = replace/create/symtab.
$(LIB): $(ALL_RELS) # Перед упаковкой удаляем stale .rel: wildcard не знает об удалённых/
# переименованных исходниках, и осиротевший .rel из build/ иначе жил бы
# вечно (уже кусало: старый file.rel после сплита).
$(LIB): $(ALL_RELS) $(MODLIST)
@find $(BUILD) -name '*.rel' -print | sort > $(BUILD)/.have
@printf '%s\n' $(ALL_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 $@ rm -f $@
$(SDAR) -rcs $@ $(ALL_RELS) $(SDAR) -rcs $@ $(ALL_RELS)
@echo @echo
+33
View File
@@ -0,0 +1,33 @@
/*
* bios_clearwin очистить окно (row, col, height, width) пробелами с
* атрибутом attr. BIOS LP_CLS_WIN ($89, rst 8): D=row, E=col,
* H=height, L=width, B=attr.
*/
#include <bios/text.h>
void bios_clearwin(uint8_t row, uint8_t col, uint8_t height, uint8_t width,
uint8_t attr) __naked
{
(void)row; (void)col; (void)height; (void)width; (void)attr;
__asm
;; rowA, colL, height/width/attr на стеке [SP+2..4].
ld iy, #2
add iy, sp
ld h, 0 (iy) ; H = height
ld c, 1 (iy) ; спрятать width (L пока держит col)
ld e, l ; E = col
ld d, a ; D = row
ld l, c ; L = width
ld b, 2 (iy) ; B = attr
push ix
ld c, #0x89
rst #0x08
pop ix
pop hl
inc sp
inc sp
inc sp ; съесть height + width + attr
jp (hl)
__endasm;
}
+35
View File
@@ -0,0 +1,35 @@
/*
* bios_clearwin_ch залить окно (row, col, height, width) символом
* fillch с атрибутом attr. BIOS LP_CLS_WIN2 ($8D, rst 8): D=row,
* E=col, H=height, L=width, B=attr, A=fillch.
*/
#include <bios/text.h>
void bios_clearwin_ch(uint8_t row, uint8_t col, uint8_t height,
uint8_t width, uint8_t attr, char fillch) __naked
{
(void)row; (void)col; (void)height; (void)width; (void)attr; (void)fillch;
__asm
;; rowA, colL, height/width/attr/fillch на стеке [SP+2..5].
ld iy, #2
add iy, sp
ld d, a ; D = row (освободить A под fillch)
ld e, l ; E = col (освободить L под width)
ld h, 0 (iy) ; H = height
ld c, 1 (iy) ; спрятать width
ld b, 2 (iy) ; B = attr
ld a, 3 (iy) ; A = fillch
ld l, c ; L = width
push ix
ld c, #0x8D
rst #0x08
pop ix
pop hl
inc sp
inc sp
inc sp
inc sp ; съесть height + width + attr + fillch
jp (hl)
__endasm;
}
+20
View File
@@ -0,0 +1,20 @@
/*
* bios_fillattr залить count позиций атрибутом attr (символы не
* трогаются) от текущего place. BIOS LP_PRINT_ATR ($83, rst 8):
* E=attr, B=count. Сохраняет HL и IX.
*/
#include <bios/text.h>
void bios_fillattr(uint8_t attr, uint8_t count) __naked
{
(void)attr; (void)count;
__asm
;; attrA, countL.
ld e, a
ld b, l
ld c, #0x83
rst #0x08
ret
__endasm;
}
+19
View File
@@ -0,0 +1,19 @@
/*
* bios_fillchar вывести символ ch count раз (атрибут не трогается)
* от текущего place. BIOS LP_PRINT_SYM ($82, rst 8): A=ch, B=count.
* Сохраняет HL и IX.
*/
#include <bios/text.h>
void bios_fillchar(char ch, uint8_t count) __naked
{
(void)ch; (void)count;
__asm
;; chA, countL.
ld b, l
ld c, #0x82
rst #0x08
ret
__endasm;
}
+26
View File
@@ -0,0 +1,26 @@
/*
* bios_fillcharattr вывести символ ch с атрибутом attr count раз от
* текущего place. BIOS LP_PRINT_ALL ($81, rst 8): A=ch, E=attr,
* B=count. Сохраняет HL и IX push/pop ix не нужен.
*
* SDCC __sdcccall(1): chA, attrL, count на стеке [SP+2]; стековый
* байт съедает callee (inc sp), IY скретч для чтения без порчи IX.
*/
#include <bios/text.h>
void bios_fillcharattr(char ch, uint8_t attr, uint8_t count) __naked
{
(void)ch; (void)attr; (void)count;
__asm
ld iy, #2
add iy, sp
ld b, 0 (iy) ; B = count
ld e, l ; E = attr
ld c, #0x81
rst #0x08
pop hl ; адрес возврата
inc sp ; съесть байт count
jp (hl)
__endasm;
}
+16
View File
@@ -0,0 +1,16 @@
/*
* bios_get_place текущая позиция BIOS-вывода: (row << 8) | col.
* BIOS LP_GET_PLACE ($8E, rst 8) возвращает D=row, E=col это уже
* конвенция возврата uint16_t (DE), перестановок не нужно.
*/
#include <bios/text.h>
uint16_t bios_get_place(void) __naked
{
__asm
ld c, #0x8E
rst #0x08
ret
__endasm;
}
+27
View File
@@ -0,0 +1,27 @@
/*
* bios_scrollwin прокрутить экран от строки row на count строк.
* BIOS LP_SCROLL_UD ($8A, rst 8): B=dir (1=вверх, 2=вниз), D=row,
* E=count.
*/
#include <bios/text.h>
void bios_scrollwin(uint8_t dir, uint8_t row, uint8_t count) __naked
{
(void)dir; (void)row; (void)count;
__asm
;; dirA, rowL, count на стеке [SP+2].
ld iy, #2
add iy, sp
ld e, 0 (iy) ; E = count
ld d, l ; D = row
ld b, a ; B = dir
push ix
ld c, #0x8A
rst #0x08
pop ix
pop hl
inc sp
jp (hl)
__endasm;
}
+20
View File
@@ -0,0 +1,20 @@
/*
* bios_set_place установить позицию вывода BIOS-текста (place).
* BIOS LP_SET_PLACE ($84, rst 8): D=row, E=col. Используется без
* push/pop ix (как gotoxy в conio эмпирически безопасно).
*/
#include <bios/text.h>
void bios_set_place(uint8_t row, uint8_t col) __naked
{
(void)row; (void)col;
__asm
;; rowA, colL.
ld d, a
ld e, l
ld c, #0x84
rst #0x08
ret
__endasm;
}
+25
View File
@@ -0,0 +1,25 @@
/*
* bios_write вывести len символов строки s (атрибут не трогается).
* BIOS LP_PRINT_LN2 ($86, rst 8): HL=s, B=len. Place продвигается.
* s должен лежать в #4000-#BFFF.
*/
#include <bios/text.h>
void bios_write(const char *s, uint8_t len) __naked
{
(void)s; (void)len;
__asm
;; sHL, len на стеке [SP+2].
ld iy, #2
add iy, sp
ld b, 0 (iy) ; B = len
push ix
ld c, #0x86
rst #0x08
pop ix
pop hl
inc sp
jp (hl)
__endasm;
}
+27
View File
@@ -0,0 +1,27 @@
/*
* bios_write_stop вывести строку s до разделителя sep БЕЗ добивки
* (максимум maxlen символов, атрибут не трогается).
* BIOS LP_PRINT_LN6 ($8C, rst 8): HL=s, B=maxlen, D=sep.
*/
#include <bios/text.h>
void bios_write_stop(const char *s, uint8_t maxlen, char sep) __naked
{
(void)s; (void)maxlen; (void)sep;
__asm
;; sHL, maxlen/sep на стеке [SP+2..3].
ld iy, #2
add iy, sp
ld b, 0 (iy) ; B = maxlen
ld d, 1 (iy) ; D = sep
push ix
ld c, #0x8C
rst #0x08
pop ix
pop hl
inc sp
inc sp
jp (hl)
__endasm;
}
+27
View File
@@ -0,0 +1,27 @@
/*
* bios_write_until вывести строку s до разделителя sep, ДОБИВ
* пробелами до len (атрибут не трогается). BIOS LP_PRINT_LN4 ($88,
* rst 8): HL=s, B=len, D=sep.
*/
#include <bios/text.h>
void bios_write_until(const char *s, uint8_t len, char sep) __naked
{
(void)s; (void)len; (void)sep;
__asm
;; sHL, len/sep на стеке [SP+2..3].
ld iy, #2
add iy, sp
ld b, 0 (iy) ; B = len
ld d, 1 (iy) ; D = sep
push ix
ld c, #0x88
rst #0x08
pop ix
pop hl
inc sp
inc sp
jp (hl)
__endasm;
}
+29
View File
@@ -0,0 +1,29 @@
/*
* bios_writeattr вывести len символов строки s с атрибутом attr.
* BIOS LP_PRINT_LN ($85, rst 8): HL=s, B=len, E=attr. Place
* продвигается после печати. s должен лежать в #4000-#BFFF.
*
* SDCC __sdcccall(1): sHL, len/attr на стеке [SP+2]/[SP+3];
* стековые байты съедает callee.
*/
#include <bios/text.h>
void bios_writeattr(const char *s, uint8_t len, uint8_t attr) __naked
{
(void)s; (void)len; (void)attr;
__asm
ld iy, #2
add iy, sp
ld b, 0 (iy) ; B = len
ld e, 1 (iy) ; E = attr
push ix
ld c, #0x85
rst #0x08
pop ix
pop hl ; адрес возврата
inc sp
inc sp ; съесть len + attr
jp (hl)
__endasm;
}
+30
View File
@@ -0,0 +1,30 @@
/*
* bios_writeattr_stop вывести строку s с атрибутом attr до
* разделителя sep БЕЗ добивки пробелами (максимум maxlen символов).
* BIOS LP_PRINT_LN5 ($8B, rst 8): HL=s, B=maxlen, E=attr, D=sep.
*/
#include <bios/text.h>
void bios_writeattr_stop(const char *s, uint8_t maxlen, uint8_t attr,
char sep) __naked
{
(void)s; (void)maxlen; (void)attr; (void)sep;
__asm
;; sHL, maxlen/attr/sep на стеке [SP+2..4].
ld iy, #2
add iy, sp
ld b, 0 (iy) ; B = maxlen
ld e, 1 (iy) ; E = attr
ld d, 2 (iy) ; D = sep
push ix
ld c, #0x8B
rst #0x08
pop ix
pop hl
inc sp
inc sp
inc sp
jp (hl)
__endasm;
}
+29
View File
@@ -0,0 +1,29 @@
/*
* bios_writeattr_until вывести строку s с атрибутом attr до
* разделителя sep, ДОБИВ пробелами до len. BIOS LP_PRINT_LN3 ($87,
* rst 8): HL=s, B=len, E=attr, D=sep.
*/
#include <bios/text.h>
void bios_writeattr_until(const char *s, uint8_t len, uint8_t attr, char sep) __naked
{
(void)s; (void)len; (void)attr; (void)sep;
__asm
;; sHL, len/attr/sep на стеке [SP+2..4].
ld iy, #2
add iy, sp
ld b, 0 (iy) ; B = len
ld e, 1 (iy) ; E = attr
ld d, 2 (iy) ; D = sep
push ix
ld c, #0x87
rst #0x08
pop ix
pop hl
inc sp
inc sp
inc sp ; съесть len + attr + sep
jp (hl)
__endasm;
}
+21
View File
@@ -0,0 +1,21 @@
/*
* _bios_putchar быстрый вывод символа через ESTEX PUTCHAR ($5B):
* без атрибута, CR/LF/скролл и курсор ведёт сам ESTEX. Используется
* (задуман) для пути g_text_attr == KEEP_EXIST_ATTR.
* PUTCHAR не меняет IX (проверено), поэтому push/pop ix не нужен.
*/
#include "_conio.h"
char _bios_putchar(char ch) __naked
{
(void)ch;
__asm
;; ch в A. push af сохраняет его через RST (который портит A).
push af
ld c, #0x5B ; ESTEX PUTCHAR
rst #0x10
pop af
ret
__endasm;
}
+49
View File
@@ -0,0 +1,49 @@
/*
* _conio.h внутренние состояние и хелперы conio.
*
* НЕ публичный заголовок: живёт рядом с исходниками. После сплита
* «1 функция = 1 модуль» каждый элемент лежит в своём модуле, чтобы
* приложение тянуло из архива только то, что реально вызывает:
*
* _g_text_attr.c g_text_attr (текущий атрибут, KEEP_EXIST_ATTR)
* _pc_place.c pc_place (курсор col:row; global читают тесты)
* _pc_raw_mode.c pc_raw_mode (режим управляющих символов putch)
* _get_cursor.c чтение курсора BIOS pc_place
* _set_cursor.c запись pc_place курсор BIOS
* _putch_wrchar.c воркер вывода mode-0 (BS/TAB/LF/CR интерпретируются)
* _putch_wrchar_raw.c воркер mode-1 (всё как глифы)
* _bios_putchar.c быстрый вывод через ESTEX PUTCHAR (без атрибута)
*/
#ifndef _CONIO_INTERNAL_H
#define _CONIO_INTERNAL_H
#include <conio.h> /* two_bytes, KEEP_EXIST_ATTR */
#include <stdint.h>
/* Текущий текстовый атрибут: 0x00..0xFF — реальный байт атрибута,
* KEEP_EXIST_ATTR (0xFFFF) быстрый путь без атрибута. */
extern two_bytes g_text_attr;
/* Позиция курсора: .byte.low = col, .byte.high = row.
* Поддерживается gotoxy/wherex/wherey/wherexy и воркерами putch. */
extern two_bytes pc_place;
/* 0 — BS/TAB/LF/CR интерпретируются; 1 — печатаются как глифы. */
extern uint8_t pc_raw_mode;
/* Прочитать курсор BIOS (rst 8, $8E) в pc_place. */
void _get_cursor(void);
/* Установить курсор BIOS (rst 8, $84) из pc_place. */
void _set_cursor(void);
/* Быстрый вывод символа через ESTEX PUTCHAR ($5B) — атрибут не
* трогается, CR/LF/скролл делает ESTEX. */
char _bios_putchar(char ch);
/* Воркеры вывода в (pc_place) атрибутом attr; курсор BIOS НЕ трогают —
* вызывающий делает _get_cursor()/_set_cursor() один раз на операцию. */
void _putch_wrchar(char ch, uint8_t attr);
void _putch_wrchar_raw(char ch, uint8_t attr);
#endif
+12
View File
@@ -0,0 +1,12 @@
/*
* g_text_attr текущий текстовый атрибут conio (Turbo-C-стиль: stdio
* putchar/puts быстрые и без атрибута; putch/cputs/cprintf применяют
* этот атрибут). Модуль только с данными.
*
* 0x00..0xFF реальный атрибут (4 бита FG | 3 бита BG | 1 бит blink)
* KEEP_EXIST_ATTR (0xFFFF) putch/cputs уходят на быстрый путь
*/
#include "_conio.h"
two_bytes g_text_attr;
+16
View File
@@ -0,0 +1,16 @@
/*
* _get_cursor прочитать текущий курсор BIOS GetCursor (rst 8, $8E)
* в pc_place (D=row, E=col сохраняются парой).
*/
#include "_conio.h"
void _get_cursor(void) __naked
{
__asm
ld c, #0x8e ; BIOS GetCursor
rst #0x08
ld (_pc_place), de
ret
__endasm;
}
+10
View File
@@ -0,0 +1,10 @@
/*
* pc_place кэш позиции курсора conio: .byte.low = col (0..79),
* .byte.high = row (0..31). Обновляется gotoxy/wherex/wherey/wherexy
* и воркерами putch; глобальный (не `_`-имя) читается и тестами
* (tests/conio2). Модуль только с данными.
*/
#include "_conio.h"
two_bytes pc_place;
+13
View File
@@ -0,0 +1,13 @@
/*
* pc_raw_mode режим обработки управляющих символов (< 0x20) в
* воркерах putch/cputs:
* 0 (по умолчанию) BS/TAB/LF/CR интерпретируются (без глифа);
* 1 все символы печатаются как глифы CP437.
* Действует только на WRCHAR-пути (attr 0xFF); при KEEP_EXIST_ATTR
* курсором и управляющими символами занимается сам ESTEX.
* Модуль только с данными.
*/
#include "_conio.h"
uint8_t pc_raw_mode;
+98
View File
@@ -0,0 +1,98 @@
/*
* _putch_wrchar воркер вывода mode-0: пишет символ в (pc_place)
* через ESTEX WRCHAR ($58) атрибутом attr, интерпретируя управляющие:
*
* 0x08 BS pc_col-- (если не 0)
* 0x09 TAB pc_col к следующему кратному 8 (потолок 80)
* 0x0A LF pc_row++ (потолок 32; без глифа)
* 0x0D CR pc_col = 0
* прочее WRCHAR + pc_col++
*
* WRCHAR подавляется при pc_col 80 или pc_row 32 (вне экрана)
* координаты только [0..79] × [0..31].
*
* Курсор BIOS НЕ трогает вызывающий читает его один раз до серии
* вызовов и записывает один раз после: плата BIOS за ОПЕРАЦИЮ,
* а не за символ.
*/
#include "_conio.h"
void _putch_wrchar(char ch, uint8_t attr) __naked
{
(void)ch; (void)attr;
__asm
;; __sdcccall(1): ch в A, attr в L.
;; Диспетчеризация по управляющим, пока A держит ch (cp не
;; трогает A). B/C грузятся только на пути вывода, поэтому
;; ветки управляющих символов дешевле.
cp #0x08
jr z, _rp0_bs
cp #0x09
jr z, _rp0_tab
cp #0x0A
jr z, _rp0_lf
cp #0x0D
jr z, _rp0_cr
;; Всё остальное (печатное или незнакомый ctrl) глиф.
_rp0_pri:
ld c, a ; C = ch (сохранить до порчи A)
ld a, (_pc_place + 1)
cp #32
ret nc ; за нижним краем молча пропустить
ld d, a ; D = row (конвенция ESTEX WRCHAR)
ld a, (_pc_place)
cp #80
ret nc ; за правым краем молча пропустить
ld e, a ; E = col
inc a
ld (_pc_place), a ; pc_col++
_rp0_wr:
ld b, l ; B = attr
ld a, c ; A = ch
push ix
ld c, #0x58 ; ESTEX WRCHAR
rst #0x10
pop ix
ret
_rp0_bs:
ld a, (_pc_place)
or a, a
ret z ; уже в колонке 0 без изменений
dec a
ld (_pc_place), a
ld e, a ; E = col
ld a, (_pc_place + 1)
ld d, a ; D = row (конвенция ESTEX WRCHAR)
ld c, #0x20
jr _rp0_wr
_rp0_tab:
ld a, (_pc_place)
or #0x07 ; округлить вниз к кратному 8
inc a ; следующее кратное 8
cp #81 ; сравнить A с 81 (0x51)
jr c, _rp0_tab_skip ; если A < 81 (т.е. A 80), пропустить загрузку
ld a, #80 ; иначе A > 80 установить A = 80
_rp0_tab_skip:
ld (_pc_place), a
ret
_rp0_lf:
ld a, (_pc_place + 1)
cp #32
ret nc ; уже у нижнего края
inc a
ld (_pc_place + 1), a
ret
_rp0_cr:
xor a, a
ld (_pc_place), a
ret
__endasm;
}
+37
View File
@@ -0,0 +1,37 @@
/*
* _putch_wrchar_raw воркер вывода mode-1: КАЖДЫЙ байт идёт через
* ESTEX WRCHAR ($58) как глиф CP437 (включая 0x08/0x09/0x0A/0x0D),
* атрибутом attr, в позицию (pc_place) с pc_col++.
* Вне экрана (col 80 / row 32) вывод молча подавляется.
* Курсор BIOS не трогает см. _putch_wrchar.
*/
#include "_conio.h"
void _putch_wrchar_raw(char ch, uint8_t attr) __naked
{
(void)ch; (void)attr;
__asm
;; __sdcccall(1): ch в A, attr в L.
ld c, a ; C = ch (сохранить)
ld a, (_pc_place + 1)
cp #32
ret nc ; за нижним краем молча пропустить
ld d, a ; D = row
ld a, (_pc_place)
cp #80
ret nc ; за правым краем молча пропустить
ld e, a ; E = col
inc a
ld (_pc_place), a ; pc_col++
ld b, l ; B = attr
ld a, c ; A = ch
push ix
ld c, #0x58 ; ESTEX WRCHAR
rst #0x10
pop ix
ret
__endasm;
}
+16
View File
@@ -0,0 +1,16 @@
/*
* _set_cursor установить курсор BIOS SetCursor (rst 8, $84) из
* pc_place (D=row, E=col).
*/
#include "_conio.h"
void _set_cursor(void) __naked
{
__asm
ld de, (_pc_place)
ld c, #0x84 ; BIOS SetCursor
rst #0x08
ret
__endasm;
}
+33
View File
@@ -0,0 +1,33 @@
/*
* cgets построчный ввод в стиле Solid-C / Turbo-C:
* buf[0] = максимум символов (вход)
* buf[1] = фактическое количество (выход)
* buf[2..] = символы + NUL
* Ввод через getche() (с эхом), Enter завершает строку (выводится
* "\r\n"), Backspace откатывает символ. Возвращает &buf[2].
*/
#include <conio.h>
#include <stdint.h>
char *cgets(char *buf)
{
uint8_t maxlen = (uint8_t)buf[0];
uint8_t n = 0;
while (n < maxlen) {
int ch = getche();
if (ch == '\n' || ch == '\r') {
putch('\r'); putch('\n');
break;
}
if (ch == 8) { /* backspace */
if (n > 0) { n--; }
continue;
}
buf[2 + n] = (char)ch;
n++;
}
buf[1] = (char)n;
buf[2 + n] = 0;
return &buf[2];
}
+15
View File
@@ -0,0 +1,15 @@
/*
* clrscr очистить экран атрибутом по умолчанию 0x0F (ярко-белый на
* чёрном). Tail-call в clrscr_attr (jp по глобальному имени работает
* через границу модулей).
*/
#include "_conio.h"
void clrscr(void) __naked
{
__asm
ld a, #0x0F
jp _clrscr_attr
__endasm;
}
+24
View File
@@ -0,0 +1,24 @@
/*
* clrscr_attr очистить экран (80×32) пробелами с заданным атрибутом.
* ESTEX CLEAR ($56): A=символ-заполнитель, B=атрибут, DE=верхний левый
* угол, H=строк, L=колонок.
*/
#include "_conio.h"
void clrscr_attr(uint8_t attr) __naked
{
(void)attr;
__asm
push ix
;; SDCC __sdcccall(1): первый uint8_t аргумент в A.
ld b, a ; B = атрибут (цвет заливки)
ld de, #0x0000 ; верхний левый угол
ld hl, #0x2050 ; H=32 строки, L=80 колонок
ld a, #0x20 ; заполнение пробелом
ld c, #0x56 ; ESTEX CLEAR
rst #0x10
pop ix
ret
__endasm;
}
-684
View File
@@ -1,684 +0,0 @@
/*
* conio.c console I/O wrappers around ESTEX kbd/screen syscalls.
*
* $30 WAITKEY blocking read, returns scan / ASCII / modifiers
* $31 SCANKEY non-blocking poll
* $32 ECHOKEY blocking read + auto-echo to the screen
* $52 LOCATE set cursor to (D=row, E=col)
* $56 CLEAR fill a window with (A=char, B=attr)
* $5B PUTCHAR write single character (CR/LF/scroll handled by ESTEX)
*
* Every RST 10h is bracketed with push/pop IX (caller's frame pointer).
*/
#include <conio.h>
#include <stdint.h>
#include <errno.h>
/* Forward extern — definition is further down (after putch/cputs which
* reference it from asm by linker-symbol name). */
extern int16_t g_text_attr;
char kbhit(void) __naked
{
__asm
push ix
ld c, #0x33 ; ESTEX CTRLKEY peeks without consuming
rst #0x10
pop ix
;; A=0 no key waiting; non-zero there is one.
or a, a
ret z
ld a, #0x01
ret
__endasm;
}
char getch(void) __naked
{
__asm
push ix
ld c, #0x30 ; ESTEX WAITKEY (no echo)
rst #0x10
pop ix
;; ESTEX returns ASCII in E (and copy in A)
ret
__endasm;
}
char getche(void) __naked
{
__asm
push ix
ld c, #0x32 ; ESTEX ECHOKEY (echo to console)
rst #0x10
pop ix
;; ESTEX returns ASCII in E (and copy in A)
ret
__endasm;
}
/* getkey — like getch() but exposes BOTH the ASCII value and the
* positional scan code, so callers can distinguish extended keys
* (arrows, F1..F12, PgUp/PgDn, Home/End, Ins/Del all of which carry
* ASCII == 0 from ESTEX) from plain ASCII keys.
*
* return = (scan << 8) | ascii
*
* For plain keys: ascii holds the character, scan holds the positional
* code (bit 7 set when Ctrl/Alt/Shift is held).
* For extended keys: ascii == 0, scan identifies the key (see KEY_* in
* <conio.h>).
*/
uint16_t getkey(void) __naked
{
__asm
push ix
ld c, #0x30 ; ESTEX WAITKEY: A=ASCII, D=scan, E=ASCII
rst #0x10
pop ix
ld e, a ; E = ASCII (defensive: A is the canonical copy)
ret ; __sdcccall(1) returns uint16_t in DE
__endasm;
}
/* ---- putch / cputs: Turbo-C conio convention ---------------------- *
* Both APPLY the current text attribute (g_text_attr). When attr is
* KEEP_EXIST_ATTR (>0xFF), they short-circuit to the FAST stdio path
* (putchar / puts-like raw PCHARS).
*
* No '\n' to CR LF translation here Turbo-C cputs/putch require the
* caller to use "\r\n" explicitly. Stdio puts/putchar do translate.
*/
static uint8_t pc_ch = 0;
static uint8_t pc_attr = 0;
static uint8_t pc_row = 0;
static uint8_t pc_col = 0;
/* Controls how _raw_putch treats control characters (< 0x20):
* 0 (default) BS/TAB/LF/CR are interpreted (no glyph output);
* other chars print as glyphs via WRCHAR.
* 1 all characters print as glyphs, no interpretation.
*
* Only takes effect on the WRCHAR (attr 0xFF) path. When
* g_text_attr is KEEP_EXIST_ATTR, ESTEX's own PUTCHAR/PCHARS handle
* cursor and control chars pc_raw_mode is irrelevant. */
static uint8_t pc_raw_mode = 0;
void set_putch_raw_mode(uint8_t mode) { pc_raw_mode = mode; }
uint8_t get_putch_raw_mode(void) { return pc_raw_mode; }
/* ---- Internal helpers ------------------------------------------- */
/* Read current cursor into pc_row / pc_col via ESTEX CURSOR ($53). */
static void _get_cursor(void) __naked
{
__asm
push ix
ld c, #0x53 ; ESTEX CURSOR
rst #0x10
ld a, d
ld (_pc_row), a
ld a, e
ld (_pc_col), a
pop ix
ret
__endasm;
}
/* Move cursor to (pc_col, pc_row) via ESTEX LOCATE ($52). */
static void _set_cursor(void) __naked
{
__asm
push ix
ld a, (_pc_row)
ld d, a
ld a, (_pc_col)
ld e, a
ld c, #0x52 ; ESTEX LOCATE
rst #0x10
pop ix
ret
__endasm;
}
/* ESTEX PUTCHAR ($5B) — fast no-attr path; ESTEX handles CR/LF/scroll
* and cursor itself. Used when g_text_attr = KEEP_EXIST_ATTR. */
static char _bios_putchar(char ch) __naked
{
(void)ch;
__asm
;; c in A. push af stashes it across the RST (which clobbers A).
;; push ix ; PUTCHAR не меняет IX
push af
ld c, #0x5B ; ESTEX PUTCHAR
rst #0x10
pop af
;; pop ix
ret
__endasm;
}
/* Raw putch: low-level WRCHAR-based output at (pc_col, pc_row) using
* the given attribute byte (caller has already verified that the high
* byte of g_text_attr is zero this function takes only the low byte).
* Updates pc_col / pc_row per pc_raw_mode:
*
* pc_raw_mode == 0: BS/TAB/LF/CR are INTERPRETED:
* 0x08 BS pc_col-- (if not already 0)
* 0x09 TAB pc_col rounded up to next multiple of 8 (capped 80)
* 0x0A LF pc_row++ (capped at 32; no glyph)
* 0x0D CR pc_col = 0
* other WRCHAR + pc_col++
*
* pc_raw_mode == 1: ALL characters print as glyphs via WRCHAR
* + pc_col++ (including 0x08, 0x09, 0x0A, 0x0D they render as
* their CP437 glyphs).
*
* WRCHAR itself is suppressed when pc_col 80 or pc_row 32 (off-
* screen) coordinates [0..79] × [0..31] only.
*
* Does NOT call CURSOR / LOCATE caller is expected to fetch cursor
* once before a sequence of _raw_putch calls and write it back once
* after, so we pay the BIOS overhead per OPERATION instead of per CHAR. */
/* Mode-0 worker: interprets BS/TAB/LF/CR, outputs other chars as glyphs. */
static void _raw_putch_raw0(char ch, uint8_t attr) __naked
{
(void)ch; (void)attr;
__asm
;; __sdcccall(1): ch in A, attr in L.
;; Dispatch on control chars while A still holds ch (cp does not
;; modify A). B/C only get loaded on the output path so the
;; ctrl-char paths are cheaper.
cp #0x08
jr z, _rp0_bs
cp #0x09
jr z, _rp0_tab
cp #0x0A
jr z, _rp0_lf
cp #0x0D
jr z, _rp0_cr
;; Anything else (printable or unrecognised ctrl) glyph.
ld c, a ; C = ch (save before A is clobbered)
ld a, (_pc_row)
cp #32
ret nc ; off-screen bottom silently skip
ld d, a ; D = row (ESTEX WRCHAR convention)
ld a, (_pc_col)
cp #80
ret nc ; off-screen right silently skip
ld e, a ; E = col
inc a
ld (_pc_col), a ; pc_col++
ld b, l ; B = attr
ld a, c ; A = ch
push ix
ld c, #0x58 ; ESTEX WRCHAR
rst #0x10
pop ix
ret
_rp0_bs:
ld a, (_pc_col)
or a, a
ret z ; already at col 0 no change
dec a
ld (_pc_col), a
ret
_rp0_tab:
ld a, (_pc_col)
and #0xF8 ; floor to mult of 8
add a, #8 ; next mult of 8
cp #80
jr c, _rp0_tab_store
ld a, #80 ; cap at off-screen right
_rp0_tab_store:
ld (_pc_col), a
ret
_rp0_lf:
ld a, (_pc_row)
cp #32
ret nc ; already at bottom edge
inc a
ld (_pc_row), a
ret
_rp0_cr:
xor a, a
ld (_pc_col), a
ret
__endasm;
}
/* Mode-1 worker: every byte goes through WRCHAR as a glyph. */
static void _raw_putch_raw1(char ch, uint8_t attr) __naked
{
(void)ch; (void)attr;
__asm
;; __sdcccall(1): ch in A, attr in L.
ld c, a ; C = ch (save)
ld a, (_pc_row)
cp #32
ret nc ; off-screen bottom silently skip
ld d, a ; D = row
ld a, (_pc_col)
cp #80
ret nc ; off-screen right silently skip
ld e, a ; E = col
inc a
ld (_pc_col), a ; pc_col++
ld b, l ; B = attr
ld a, c ; A = ch
push ix
ld c, #0x58 ; ESTEX WRCHAR
rst #0x10
pop ix
ret
__endasm;
}
/* PCHARS (no attr) — used by cputs when KEEP_EXIST_ATTR is in effect. */
static void _cputs_pchars(const char *s) __naked
{
(void)s;
__asm
push ix
ld c, #0x5C ; ESTEX PCHARS
rst #0x10
pop ix
ret
__endasm;
}
/* ---- Public putch / cputs --------------------------------------- *
*
* KEEP_EXIST_ATTR (high byte != 0) fast PUTCHAR/PCHARS through
* ESTEX, which manages its own cursor.
*
* Otherwise fetch cursor ONCE via CURSOR ($53), run one or many
* _raw_putch calls, write cursor back ONCE via LOCATE ($52). This
* folds the per-char CURSOR/LOCATE pair from the old design into a
* single pair per operation. */
char putch(char ch) __naked
{
(void)ch;
__asm
;; A = ch on entry; char return A.
ld (_pc_ch), a ; stash c (for both return and re-load)
;; KEEP_EXIST_ATTR? high byte of g_text_attr != 0
ld a, (_g_text_attr + 1)
or a, a
jr nz, _putch_fast
;; --- WRCHAR path: cursor worker cursor ---
call __get_cursor
;; Worker ABI (2 char/uint8 args): ch in A, attr in L.
ld a, (_g_text_attr) ; A = low byte = attr
ld l, a ; L = attr
ld a, (_pc_raw_mode)
or a, a ; Z = (mode == 0)
ld a, (_pc_ch) ; A = ch (`ld a,(nn)` does not touch flags)
jr nz, _putch_use_raw1
call __raw_putch_raw0
jr _putch_after_raw
_putch_use_raw1:
call __raw_putch_raw1
_putch_after_raw:
call __set_cursor
ld a, (_pc_ch) ; return value
ret
_putch_fast:
ld a, (_pc_ch)
call __bios_putchar ; __bios_putchar keeps AF
ret
__endasm;
}
char cputs(const char *s) __naked
{
(void)s;
__asm
;; HL = s on entry; char return A.
;; NULL-check: cputs(NULL) return 0 immediately.
ld a, h
or a, l
ret z
;; KEEP_EXIST_ATTR? high byte of g_text_attr != 0
ld a, (_g_text_attr + 1)
or a, a
jr nz, _cputs_fast
;; --- WRCHAR path: cursor worker cursor ---
call __get_cursor
;; Worker walks the string via DE; HL is used to carry attr in L
;; across iterations (RST 10 inside worker clobbers it, so we
;; push/pop hl around each call).
ex de, hl ; DE = s
ld a, (_g_text_attr) ; A = attr (low byte)
ld l, a ; L = attr (worker ABI: arg2 in L)
;; Pick worker once based on pc_raw_mode; IX = function pointer.
ld a, (_pc_raw_mode)
or a, a
jr z, _cputs_use_raw0
ld ix, #__raw_putch_raw1
jr _cputs_loop
_cputs_use_raw0:
ld ix, #__raw_putch_raw0
_cputs_loop:
ld a, (de)
or a, a
jr z, _cputs_loop_end
inc de
;; Z80 has no "call (ix)" emulate via push-of-ret + jp (ix).
push de ; save string pointer
push hl ; save attr (in L)
ld de, #_cputs_after_worker
push de ; push return address
jp (ix) ; "call" worker
_cputs_after_worker:
pop hl
pop de
jr _cputs_loop
_cputs_loop_end:
call __set_cursor
xor a, a ; return 0
ret
_cputs_fast:
call __cputs_pchars
xor a, a ; return 0
ret
__endasm;
}
void clrscr(void) __naked
{
__asm
ld a, #0x0F
jp _clrscr_attr
__endasm;
}
void clrscr_attr(uint8_t attr) __naked
{
(void)attr;
__asm
push ix
;; SDCC __sdcccall(1): uint8_t 1st arg is in A.
ld b, a ; B = attribute (mode-fill colour)
ld de, #0x0000 ; top-left
ld hl, #0x2050 ; H=32 rows, L=80 cols
ld a, #0x20 ; space fill
ld c, #0x56 ; ESTEX CLEAR
rst #0x10
pop ix
ret
__endasm;
}
void gotoxy(uint8_t x, uint8_t y) __naked
{
(void)x; (void)y;
__asm
;; __sdcccall(1) 2 uint8 args: x in A, y in L.
;; ESTEX LOCATE ($52) wants: D = row, E = col.
push ix
ld d, l ; D = row (y)
ld e, a ; E = col (x)
ld c, #0x52
rst #0x10
pop ix
ret
__endasm;
}
uint8_t wherex(void) __naked
{
__asm
;; ESTEX CURSOR ($53): D = row, E = col. Return col in DE.
push ix
ld c, #0x53
rst #0x10
pop ix
ld a, e
ret
__endasm;
}
uint8_t wherey(void) __naked
{
__asm
push ix
ld c, #0x53
rst #0x10
pop ix
ld a, d
ret
__endasm;
}
uint16_t wherexy(void) __naked
{
__asm
;; ESTEX CURSOR ($53): D = row, E = col. Return col in DE.
push ix
ld c, #0x53
rst #0x10
pop ix
ret
__endasm;
}
/* wrchar(uint8_t x, uint8_t y, char ch, uint8_t attr)
*
* SDCC __sdcccall(1): x in A, y in L (2 uint8 A, L); ch and attr
* packed and pushed on the stack as a single 16-bit value (caller does
* `ld hl, #(attr<<8)|ch; push hl`). Layout after CALL:
* [SP+0..1] = return address
* [SP+2] = ch (low half of pushed pair)
* [SP+3] = attr (high half)
* Void return callee-pops the 2 stack-arg bytes via `pop bc` + jp (iy).
*/
void scroll(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t direction, uint8_t clear) __naked
{
(void)x; (void)y; (void)w; (void)h; (void)direction; (void)clear;
__asm
pop iy ; return address
ld d, l ; D = row (y)
ld e, a ; E = col (x)
pop hl ; H = heigth(h), L = width(w)
pop bc ; C = direction, B = clear
ld a, b ; A = clear(B)
ld b, c ; B = direction(C)
push ix
ld c, #0x55 ; ESTEX SCROLL
rst #0x10
pop ix
jp (iy)
__endasm;
}
/* wrchar(uint8_t x, uint8_t y, char ch, uint8_t attr)
*
* SDCC __sdcccall(1): x in A, y in L (2 uint8 A, L); ch and attr
* packed and pushed on the stack as a single 16-bit value (caller does
* `ld hl, #(attr<<8)|ch; push hl`). Layout after CALL:
* [SP+0..1] = return address
* [SP+2] = ch (low half of pushed pair)
* [SP+3] = attr (high half)
* Void return callee-pops the 2 stack-arg bytes via `pop bc` + jp (iy).
*/
void wrchar(uint8_t x, uint8_t y, char ch, uint8_t attr) __naked
{
(void)x; (void)y; (void)ch; (void)attr;
__asm
pop iy ; return address
pop bc ; C = ch, B = attr
push ix
ld d, l ; D = row (y)
ld e, a ; E = col (x)
ld a, c ; A = ch
ld c, #0x58 ; ESTEX WRCHAR
rst #0x10
pop ix
jp (iy)
__endasm;
}
/* rdchar(int x, int y) → (attr << 8) | ch */
uint16_t rdchar(uint8_t x, uint8_t y) __naked
{
(void)x; (void)y;
__asm
push ix
ld d, l ; D = row
ld e, a ; E = col
ld c, #0x57 ; ESTEX RDCHAR
rst #0x10
;; A = ch, B = attr
ld d, b ; high byte attr
ld e, a ; low byte ch
pop ix
ret
__endasm;
}
/* Public text-mode video API — defined here so it's pulled in with the
* rest of conio. The raw setters/getters live in videomode_raw.c so
* pure graphics programs can pick them up without conio's other
* dependencies. */
extern uint8_t _videomode_raw_get(void);
extern int _videomode_raw_set(uint8_t mode);
uint8_t get_videotextmode(void)
{
return _videomode_raw_get();
}
int set_videotextmode(uint8_t mode)
{
/* Refuse anything that isn't a known text mode — otherwise a stray
* GFX_MODE_* value could swap the screen out from under text I/O. */
if (mode != TEXT_MODE_40x32 && mode != TEXT_MODE_80x32) {
errno = EINVAL;
return -1;
}
return _videomode_raw_set(mode);
}
/* ---- text attribute state ----------------------------------------
* g_text_attr is owned by conio.c now (Turbo-C-style: stdio putchar/puts
* are fast and attribute-free; only conio's putch/cputs/cprintf apply
* the attribute). Default = 0x0F (bright white on black).
*
* 0x00..0xFF real attribute (4-bit FG | 3-bit BG | 1-bit blink)
* KEEP_EXIST_ATTR (0xFFFF) putch/cputs fall back to fast no-attr path */
int16_t g_text_attr = 0x0F;
int16_t set_text_attr(int16_t attr)
{
int16_t prev = g_text_attr;
g_text_attr = attr;
return prev;
}
int16_t get_text_attr(void)
{
return g_text_attr;
}
/* ---- Turbo-C-style palette helpers --------------------------------
* textcolor / textbackground touch only their nibble; the other nibble
* (and the blink bit) are preserved. textattr replaces the whole byte. */
void textcolor(uint8_t fg)
{
/* If we were KEEP_EXIST_ATTR, switch to a real attr first. */
uint8_t cur = ((uint16_t)g_text_attr > 0xFF) ? 0x00 : (uint8_t)g_text_attr;
g_text_attr = (int16_t)((cur & 0xF0) | (fg & 0x0F));
}
void textbackground(uint8_t bg)
{
uint8_t cur = ((uint16_t)g_text_attr > 0xFF) ? 0x00 : (uint8_t)g_text_attr;
/* Background uses 3 bits (4..6); preserve blink (bit 7) too. */
g_text_attr = (int16_t)((cur & 0x8F) | ((bg & 0x07) << 4));
}
void textattr(uint8_t attr)
{
g_text_attr = (int16_t)attr;
}
/* ---- Solid-C compatibility ---------------------------------------- */
/* Direct port I/O. Z80 has 256 IN/OUT ports; we wrap the Z80 IN/OUT
* opcodes with a stable C API. Names match Solid-C / MS-DOS Turbo-C. */
uint8_t z80_inp(uint8_t port) __naked
{
(void)port;
__asm
;; SDCC __sdcccall(1): single uint8_t arg in A; uint8_t return in A.
ld c, a
in a, (c)
ret
__endasm;
}
void z80_outp(uint8_t port, uint8_t value) __naked
{
(void)port; (void)value;
__asm
;; __sdcccall(1): 2 uint8 args arg1 in A, arg2 in L.
ld c, a ; C = port
out (c), l ; out (port), value
ret
__endasm;
}
/* cgets — Solid-C / Turbo-C style line input.
* buf[0] = max characters (in)
* buf[1] = actual count (out)
* buf[2..] = chars + NUL
* Returns &buf[2]. */
char *cgets(char *buf)
{
uint8_t maxlen = (uint8_t)buf[0];
uint8_t n = 0;
while (n < maxlen) {
int ch = getche();
if (ch == '\n' || ch == '\r') {
putch('\r'); putch('\n');
break;
}
if (ch == 8) { /* backspace */
if (n > 0) { n--; }
continue;
}
buf[2 + n] = (char)ch;
n++;
}
buf[1] = (char)n;
buf[2 + n] = 0;
return &buf[2];
}
+7 -6
View File
@@ -1,12 +1,13 @@
/* /*
* cprintf printf for the conio output set. Formats into a static * cprintf printf для conio-набора вывода. Форматирует в статический
* buffer with vsprintf (from SDCC's stdlib), then emits via cputs which * буфер через vsprintf (из stdlib SDCC), затем выводит cputs'ом,
* applies the current text attribute per character. * который применяет текущий текстовый атрибут посимвольно.
* *
* No '\n' to CR LF translation Turbo-C convention: callers write * '\n' в CR LF НЕ транслируется конвенция Turbo-C: перевод строки
* "\r\n" explicitly in the format string for line breaks. * пишется в format-строке явно как "\r\n".
* *
* Not reentrant (single static buffer) but Z80 single-threaded is fine. * Не реентерабельно (один статический буфер), но на однопоточном Z80
* это не проблема.
*/ */
#include <conio.h> #include <conio.h>
+72
View File
@@ -0,0 +1,72 @@
/*
* cputs вывод ASCIIZ-строки с текущим атрибутом (Turbo-C conio).
*
* Курсор BIOS читается один раз до строки и записывается один раз
* после. Три пути:
* g_text_attr == KEEP_EXIST_ATTR быстрый ESTEX PCHARS ($5C);
* pc_raw_mode == 1 BIOS LP_PRINT_LN5 (глифы с атрибутом);
* иначе цикл _putch_wrchar (интерпретация
* BS/TAB/LF/CR).
* Трансляции '\n' CR LF нет вызывающий пишет "\r\n" сам.
* cputs(NULL) no-op. Возвращает 0.
*/
#include "_conio.h"
char cputs(const char *s) __naked
{
(void)s;
__asm
;; HL = s на входе; возврат char A.
;; NULL-check: cputs(NULL) сразу вернуть 0.
ld a, h
or a, l
ret z
push ix
call __get_cursor
;; KEEP_EXIST_ATTR? старший байт g_text_attr != 0
ld a, (_g_text_attr + 1)
or a, a
jr nz, _cputs_fast
ld a, (_pc_raw_mode)
or a, a
jr nz, _cputs_bios
push hl
pop de
_cputs_wrloop:
ld a, (_g_text_attr)
ld l, a
ld a, (de) ; загрузить байт
or a ; установить флаг Z, если A == 0
jr z, _cputs_ex ; завершить подпрограмму (конец строки)
push de
call __putch_wrchar ; вывести символ (A передаётся как аргумент)
pop de
inc de ; перейти к следующему байту
jr _cputs_wrloop ; повторить
_cputs_bios:
ld a, (_g_text_attr)
ld b, #0xFF
ld d, #0x0
ld e, a
ld c, #0x8B ; BIOS LP_PRINT_LN5
rst #0x08
jr _cputs_ex
_cputs_fast:
ld c, #0x5C ; ESTEX PCHARS
rst #0x10
_cputs_ex:
call __set_cursor
pop ix ; восстановить IX вызывающего
xor a, a ; вернуть 0
ret
__endasm;
}
+11
View File
@@ -0,0 +1,11 @@
/*
* get_putch_raw_mode текущий режим обработки управляющих символов
* putch/cputs (см. set_putch_raw_mode).
*/
#include "_conio.h"
uint8_t get_putch_raw_mode(void)
{
return pc_raw_mode;
}
+10
View File
@@ -0,0 +1,10 @@
/*
* get_text_attr текущий 16-битный атрибут conio (см. set_text_attr).
*/
#include "_conio.h"
int16_t get_text_attr(void)
{
return g_text_attr.value;
}
+18
View File
@@ -0,0 +1,18 @@
/*
* getch блокирующее чтение клавиши БЕЗ эха на экран.
* ESTEX WAITKEY ($30); ASCII возвращается в A.
*/
#include "_conio.h"
char getch(void) __naked
{
__asm
push ix
ld c, #0x30 ; ESTEX WAITKEY (без эха)
rst #0x10
pop ix
;; ESTEX возвращает ASCII в E (и копию в A)
ret
__endasm;
}
+18
View File
@@ -0,0 +1,18 @@
/*
* getche блокирующее чтение клавиши С эхом на экран.
* ESTEX ECHOKEY ($32); ASCII возвращается в A.
*/
#include "_conio.h"
char getche(void) __naked
{
__asm
push ix
ld c, #0x32 ; ESTEX ECHOKEY (эхо на консоль)
rst #0x10
pop ix
;; ESTEX возвращает ASCII в E (и копию в A)
ret
__endasm;
}
+26
View File
@@ -0,0 +1,26 @@
/*
* getkey как getch(), но возвращает И ASCII, И позиционный скан-код,
* чтобы различать расширенные клавиши (стрелки, F1..F12, PgUp/PgDn,
* Home/End, Ins/Del у них ASCII == 0 из ESTEX) и обычные символы.
*
* возврат = (scan << 8) | ascii
*
* Обычные клавиши: ascii символ, scan позиционный код (бит 7
* взведён при Ctrl/Alt/Shift).
* Расширенные: ascii == 0, клавишу определяет scan (см. KEY_* в
* <conio.h>).
*/
#include "_conio.h"
uint16_t getkey(void) __naked
{
__asm
push ix
ld c, #0x30 ; ESTEX WAITKEY: A=ASCII, D=scan, E=ASCII
rst #0x10
pop ix
ld e, a ; E = ASCII (защитно: канонична копия в A)
ret ; __sdcccall(1) возвращает uint16_t в DE
__endasm;
}
+15
View File
@@ -0,0 +1,15 @@
/*
* gettextmode текущий видеорежим (обёртка _videomode_raw_get из
* video/videomode_raw.c: сырые геттер/сеттер живут отдельно, чтобы
* чисто графические программы не тянули conio).
*/
#include <conio.h>
#include <stdint.h>
extern uint8_t _videomode_raw_get(void);
uint8_t gettextmode(void)
{
return _videomode_raw_get();
}
+20
View File
@@ -0,0 +1,20 @@
/*
* gotoxy установить курсор в (x=col, y=row), 0-based.
* Обновляет pc_place и ставит курсор BIOS SetCursor (rst 8, $84).
*/
#include "_conio.h"
void gotoxy(uint8_t x, uint8_t y) __naked
{
(void)x; (void)y;
__asm
;; __sdcccall(1), 2 аргумента uint8: x в A, y в L.
ld d, l ; D = row (y)
ld e, a ; E = col (x)
ld (_pc_place), de
ld c, #0x84 ; BIOS SetCursor
rst #0x08
ret
__endasm;
}
+23
View File
@@ -0,0 +1,23 @@
/*
* kbhit 1, если в буфере клавиатуры есть символ, иначе 0.
* ESTEX CTRLKEY ($33) подглядывает, не забирая символ из буфера.
*/
#include "_conio.h"
// TODO - проверить - ф-ии 30h-33h (kbhit/getch/getche/getkey)
// не должны менять IX и им можно не делать push ix / pop ix
char kbhit(void) __naked
{
__asm
push ix
ld c, #0x33 ; ESTEX CTRLKEY подглядывает без изъятия
rst #0x10
pop ix
;; A=0 клавиши нет; ненулевой есть.
or a, a
ret z
ld a, #0x01
ret
__endasm;
}
+41
View File
@@ -0,0 +1,41 @@
/*
* putch вывод символа с текущим атрибутом (Turbo-C conio).
*
* Применяет g_text_attr; при pc_raw_mode == 1 управляющие символы
* печатаются как глифы (воркер _putch_wrchar_raw), иначе
* интерпретируются (_putch_wrchar). После вывода записывает курсор
* BIOS один раз (_set_cursor). Трансляции '\n' CR LF нет
* Turbo-C-конвенция, вызывающий пишет "\r\n" сам.
* Возвращает выведенный символ.
*/
#include "_conio.h"
/* Стэш символа на время вызовов воркеров (только для putch). */
static uint8_t pc_ch;
char putch(char ch) __naked
{
(void)ch;
__asm
;; A = ch на входе; возврат char A.
ld (_pc_ch), a ; спрятать ch (для возврата и перезагрузки)
ld a, (_g_text_attr) ; A = младший байт = attr
ld l, a ; L = attr
ld a, (_pc_raw_mode)
or a, a ; Z = (mode == 0)
ld a, (_pc_ch) ; A = ch (`ld a,(nn)` не трогает флаги)
jr nz, _putch_use_raw
call __putch_wrchar
jr _putch_after_raw
_putch_use_raw:
call __putch_wrchar_raw
_putch_after_raw:
call __set_cursor
ld a, (_pc_ch) ; возвращаемое значение
ret
__endasm;
}
+23
View File
@@ -0,0 +1,23 @@
/*
* rdchar прочитать символ и атрибут из позиции (x, y):
* возврат (attr << 8) | ch. ESTEX RDCHAR ($57).
*/
#include "_conio.h"
uint16_t rdchar(uint8_t x, uint8_t y) __naked
{
(void)x; (void)y;
__asm
push ix
ld d, l ; D = row
ld e, a ; E = col
ld c, #0x57 ; ESTEX RDCHAR
rst #0x10
;; A = ch, B = attr
ld d, b ; старший байт attr
ld e, a ; младший байт ch
pop ix
ret
__endasm;
}
+30
View File
@@ -0,0 +1,30 @@
/*
* scroll прокрутка окна (x, y, w, h) на 1 позицию в направлении
* direction, с очисткой освободившейся строки/колонки атрибутом clear.
* ESTEX SCROLL ($55).
*
* SDCC __sdcccall(1): x в A, y в L; w,h и direction,clear запушены
* парами на стек. Void-возврат callee снимает стековые аргументы
* (pop до RST) и уходит через jp (iy).
*/
#include "_conio.h"
void scroll(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t direction, uint8_t clear) __naked
{
(void)x; (void)y; (void)w; (void)h; (void)direction; (void)clear;
__asm
pop iy ; адрес возврата
ld d, l ; D = row (y)
ld e, a ; E = col (x)
pop hl ; H = высота (h), L = ширина (w)
pop bc ; C = direction, B = clear
ld a, b ; A = clear(B)
ld b, c ; B = direction(C)
push ix
ld c, #0x55 ; ESTEX SCROLL
rst #0x10
pop ix
jp (iy)
__endasm;
}
+12
View File
@@ -0,0 +1,12 @@
/*
* set_putch_raw_mode режим обработки управляющих символов в
* putch/cputs: 0 BS/TAB/LF/CR интерпретируются, 1 все байты
* печатаются как глифы CP437 (см. pc_raw_mode).
*/
#include "_conio.h"
void set_putch_raw_mode(uint8_t mode)
{
pc_raw_mode = mode;
}
+14
View File
@@ -0,0 +1,14 @@
/*
* set_text_attr установить полный 16-битный атрибут conio
* (0x00..0xFF реальный атрибут; KEEP_EXIST_ATTR быстрый путь без
* атрибута). Возвращает прежнее значение.
*/
#include "_conio.h"
int16_t set_text_attr(int16_t attr)
{
int16_t prev = g_text_attr.value;
g_text_attr.value = attr;
return prev;
}

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