16 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
335 changed files with 9126 additions and 5153 deletions
+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.
# 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 \
rt_test openenv ls conio attrprob timedir mouse banklocl stdlib \
assrtest ptime stattest filetest gfx_demo gfx_d16 gfx_text gfx_mous
TESTS := hello hello2 simple banked bankedbg banktest strtest cat seek \
malloc mem_test argv errno rt_test openenv ls conio conio2 \
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/.
APPS := mdview
APPS := mdview mdview2
MAME_DIR := mame/v306
FLOPPY_IMG := $(MAME_DIR)/IMG/mc.img
@@ -33,7 +36,8 @@ DATA_FILES := \
tests/seek/big.txt \
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
@@ -66,6 +70,14 @@ floppy: tests examples tests/seek/big.txt
@echo "Floppy ready: $(FLOPPY_IMG)"
@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:
$(MAKE) -C toolchain/mkexe clean
$(MAKE) -C lib clean
+6 -1
View File
@@ -51,7 +51,12 @@ CC_FLAGS += $(EXTRA_FLAGS)
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)
$(MKEXE):
+6
View File
@@ -34,6 +34,9 @@
# --bank N=FILE.c compile FILE.c as bank N; repeatable; pulls crt0_banked
# automatically and adds -Wl-b_BANKN=0x{N}C000
# --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
# sdcc (-DDEBUG_RT) and the crt0 assembly (prepended
# `DEBUG_RT = 1`). Exposes runtime introspection symbols
@@ -82,6 +85,7 @@ SOURCES=()
LD_EXTRA=()
MKEXE_EXTRA=()
BANK_SPECS=() # entries like "1=engine.c"
MAX_ALLOCS="" # if set, passed to sdcc as --max-allocs-per-node
# ------- Parse args ----------------------------------------------------------
usage() {
@@ -105,6 +109,7 @@ while [[ $# -gt 0 ]]; do
-Wl) LD_EXTRA+=("$2"); shift 2;;
--bank) BANK_SPECS+=("$2"); shift 2;;
--mkexe) MKEXE_EXTRA+=("$2"); shift 2;;
--max-allocs) MAX_ALLOCS="$2"; shift 2;;
--debug) DEBUG_RT=1; shift;;
-v) VERBOSE=1; shift;;
-h|--help) usage 0;;
@@ -272,6 +277,7 @@ run "$SDASZ80" -o "$HEAP_TOP_REL" "$HEAP_TOP_SRC"
USER_RELS=()
CC_FLAGS=(-mz80 --no-std-crt0 --std-c99 --opt-code-size -I "$INC_DIR" "${USER_INCS[@]}")
[[ $DEBUG_RT -eq 1 ]] && CC_FLAGS+=(-DDEBUG_RT)
[[ -n "$MAX_ALLOCS" ]] && CC_FLAGS+=(--max-allocs-per-node "$MAX_ALLOCS")
for src in "${SOURCES[@]}"; do
rel="$WORK/$(basename "$src" .c).rel"
run "$SDCC" "${CC_FLAGS[@]}" -c -o "$rel" "$src"
+150 -433
View File
@@ -1,436 +1,153 @@
# TODO / Roadmap
Открытые задачи в порядке убывания приоритета. По мере появления реальных программ — приоритеты будут смещаться.
Открытые задачи в порядке убывания приоритета; закрытые этапы — в
«Истории» внизу. Текущий срез libc-работ: docs/libc-roadmap.md.
## Этап 5 — malloc / free + banking-aware page allocator ✅ ГОТОВО
- [x] SDCC's `malloc`/`free` + наш `runtime/heap.s` (полностью заменяет library heap.rel, 14000-байтный heap в окне 2)
- [x] `libc/mem/mem_alloc.c` — page allocator: `mem_alloc_pages`/`mem_free_block`/`mem_get_page`/`mem_info` через ESTEX `$3C/$3D/$3E` + BIOS `$C4`
- [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:` строки) из вывода `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 × количество строк/столбцов экономии. Применимо ко всем композитным примитивам.
## Ближайшее
- [ ] **П6/железо**: MAME-смоук всех тестов после libc-сплита (conio,
ptime, stattest, mouse, gfx_demo/gfx_d16/gfx_text/gfx_mous —
трогался asm акселератора); затем прогон на реальном Sprinter
(mdview2 + FILE* v2 + fdmax — подтвердить лимит 8 манипуляторов
и зависание DSS на 9-м OPEN)
- [ ] Мигрировать оставшиеся examples на sprinter-cc вместо ручных
Makefile (косметика)
- [ ] check_banks.py: разбивка size = code + const + bss per bank
## Auto-banking (memory/banking_roadmap.md)
Phase 1 — file-level bin-packing (`toolchain/auto_bank.py`) — когда
проект перерастёт ~30 KB кода: парсинг размеров из .rel/.map,
first-fit-decreasing, уважение `#pragma codeseg BANKn`, перелинковка,
печать плана. Phase 2-5 (rebalance, banks.toml, function-level) —
по потребности.
## ОБЯЗАТЕЛЬНОЕ ДЛЯ V2
### Turbo-C-style graphics API (BGI-like)
Расширить `gfx_*` до уровня Turbo-C `<graphics.h>`: initgraph/
closegraph/cleardevice-алиасы, current point (moveto/lineto/linerel),
setcolor/setbkcolor, getpixel, circle/arc/ellipse/pieslice, drawpoly/
fillpoly, floodfill, bar/bar3d, outtext[xy]/settextstyle/textwidth,
imagesize/getimage/putimage (COPY/XOR/AND/OR/NOT_PUT), setviewport/
клиппинг, setactivepage/setvisualpage (2 страницы есть), setlinestyle.
Acceptance: типичная BGI-программа переносится без существенных
правок. Референс: Turbo C 2.x BGIDEMO.
### IM2 Interrupt Handlers
User-ISR через IM 2 — timer ticks, музыка (AY/COVOX), real-time
игры, async input. Решение: отдельный memory mode `--memory im2`.
Полный research/design: docs/im2_isr_design.md (vector 0xFF общий,
disambiguation по портам 0x19/0xFE; таблица/ISR/стек в W2; chain к
DSS-хендлеру обязателен).
### Прочее v2
- [ ] **Audio API** — AY-3-8910 + COVOX (требует IM2)
- [ ] **ISA-8 slot support** — ZX-Bus карты (требует IM2)
## GFX: расширения по accelerator_doc.txt
Quick wins:
- [ ] block-size через `LD A,(nn)` вместо SMC (док разрешает LD A,(HL/BC/DE))
- [ ] кэширование block-size между burst'ами (accel помнит размер)
Новые возможности:
- [ ] `gfx_blit` / `gfx_blit_transparent` — block copy (LD L,L / LD A,A),
прозрачность через bank 0x58 («FF is transparent»)
- [ ] `gfx_xor_rect` / `gfx_or_rect` / `gfx_and_rect` / `gfx_invert_rect`
- [ ] шрифты ≠ 8×8: gfx_set_font_data(ptr,w,h,advance), proportional,
8×16/16×16, отдельный font_id API; font-quad для 640×256
(per-cell палитра через дескрипторы 0x0300..0x039F)
Оптимизации (не сейчас):
- [ ] gfx_line через accel для пологих диагоналей (runs ≥ 4-5 px)
- [ ] композитные примитивы с одним W3-swap на операцию
## Прочий backlog
- [ ] factoring parse_argv из crt0/crt0_banked в общий argv.s
- [ ] `restore SP on EXIT` (паттерн z88dk +pps) — проверить нужность
- [ ] CI: MAME с -aviwrite для screenshot-сравнения без человека
- [ ] linker duplicate-symbol warnings: сейчас фильтруются в
sprinter-cc (наши overrides _puts/___sdcc_heap/_asctime/…);
радикально — --nostdlib с ручным списком модулей z80.lib
- [ ] ZX Spectrum-совместимый target; ZX-Bus драйверы; PGO-tools
## Проверить на реальном железе
- [ ] **Port_Y banking trick** (адреса 0xC000+0x400*N → строки
Y..Y+15): в MAME 0.283 НЕ работает (2026-06-01). На железе:
dual-write тест → если работает, кэшировать Port_Y в putpixel
(~8× меньше OUT для Брезенхэма); если нет — вычистить из доков.
- [ ] fdmax: лимит манипуляторов и зависание 9-го OPEN — MAME vs железо.
## Known quirks (зафиксированы, обходы в libc)
- ESTEX $46 ENV: A=0 это NOT FOUND (док врёт) — memory/sprinter_platform
- ESTEX WRITE $14: на успехе DE=0, не счётчик; успех = CF=0 & A=0 —
memory/estex_write_de_quirk
- DSS: 8 манипуляторов, 9-й OPEN вешает систему; _fd_guard в libc —
memory/dss_fd_limit
- SDCC z80 `if(n!=g)g=n;` пишет (n-g) — memory/sdcc_z80_cmp_store_a_bug
---
# История — закрытые этапы
## Этап 10 — libc: сплит + FILE v2 + Solid-C (2026-07-05/06) ✅
Полный план/итоги: docs/libc-roadmap.md. Кратко:
- вся libc разложена «1 функция = 1 модуль» (~250 модулей, wildcard-
сборка, DCE на уровне файлов): gfx_text 4.4 КБ, timedir/ls/stattest
−3 КБ и т.д.; правила asm-связок: docs/libc-split-asm-cases.md
- **FILE* v2 (B+)**: ленивый буфер 512 на чтение/запись с
автопереключением, таблица OPEN_MAX=8, flush-on-exit, ungetc,
fprintf/vfprintf, fdopen/freopen/fclosall/fgetpos/fsetpos; горячие
пути fgetc/fputc/fgets на asm (fgets 100 КБ: 144с unbuffered-оценка
→ ~1 с). Дизайн: docs/file-buffering-design.md
- **scanf/fscanf/sscanf** — своё C-ядро (в SDCC z80 нет)
- **Solid-C совместимость закрыта**: <dos.h> (даты/диски/absread),
errno-алиасы, <sprinter_solid.h> — docs/solid_c_compatibility.md
- гигиена: stale .rel чистка, все 43 теста в make all, размерный
регресс (make size-check), контракт заголовков docs/libc-headers.md
- справочник API: docs/libc-reference.md
## Этап 9 — memory modes (tiny/small/big/huge/manual) ✅ 2026-05-30
`--memory MODE` в sprinter-cc; crt0-семейство (crt0/minimal/small/
banked); small: ESTEX GETMEM+SETWIN2 до gsinit, auto-detect W2 по
порту 0xC2; big/huge: параметризация crt0_banked/bank.s через
BANK_W1; --debug, --stack-size. Детали: memory/memory_modes_
implemented, memory/sprinter_memory_modes.
Дизайн-решения: одна sprinter.lib на все режимы (DCE per-member);
gfx.lib отдельно не нужен; libc_banked + sprinter_home.lib — идея
на потом (триггер: HOME забит user-кодом).
## Этап 8 — графика ✅
320×256×256 + 640×256×16, акселератор (Fill h/v, SMC block-size),
Брезенхэм, bitmap font (WIN_GET_ZG, interleaved), gfx_text.
memory/sprinter_graphics*, sprinter_accelerator, sprinter_font_format.
## Bank-local data ✅
--codeseg/--constseg/--dataseg BANKn + mkexe -p 0; фикс трамплина
(pop bc/out (c),b — сохраняет A); malloc из банка прозрачен (heap в
W2). memory/bank_local_data_pattern.
## Этапы 5-7 и ранняя libc ✅
- malloc/free (SDCC + runtime/heap.s в W2), page allocator
(mem_alloc_pages, ESTEX $3C-$3E + BIOS $C4), bank_read/bank_write
- crt0 argv-парсинг (IX-prefix, CP/M-space quirk, APPINFO basename),
sprinter-cc wrapper со всеми опциями
- errno+strerror/perror, open state-machine, atexit, setjmp/longjmp,
sleep, ENV API ($46), ffirst/fnext, getdatetime/setdatetime,
chdir/getcwd/mkdir/rmdir, conio (полный), mouse (RST 30h, 14 ф-й),
POSIX time API, sys/stat, assert
- text I/O split (stdio fast / conio attr) — memory/text_output_api_split
- SDCC stdlib НЕ переписываем — memory/sdcc_stdlib_works
+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) ведут на метки внутри своей же функции — сплиту не мешают.
+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.
## 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
- 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
+5
View File
@@ -0,0 +1,5 @@
MDVIEW Sample Document
This is a sample Markdown file for testing the Sprinter *mdview text
viewer*. Phase 3 adds inline emphasis: bold, italic and underscore
runs render with distinct background colours.
+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á ­  ¡®«ì讬 ä ©«¥).
+24 -20
View File
@@ -8,41 +8,45 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := mdview2
EXTRA_SRCS := mdview2_raw.c mdview2_help.c mdview2_table.c mdview2_enc.c mdview2_md.c mdview2_status.c mdview2_index.c
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 в CP866 — рабочую кодировку Sprinter).
#
# README.MD хранится в репозитории в UTF-8; iconv -c конвертирует
# его в CP866, отбрасывая символы без аналога в целевой кодировке.
# Результат кладётся в .disk_tmp/README.MD, чтобы make_disk.py
# использовал правильное имя файла на диске.
# Образ дискеты: mdview2.exe + документация. Каждый документ — в СВОЕЙ
# кодировке, чтобы дискета заодно покрывала все пути автодетекта:
# README.MD — документация, ОРИГИНАЛЬНЫЙ UTF-8 (как в репозитории);
# CHANGES.MD — история версий, CP866 (хранится так, кладётся как есть;
# имя укорочено: "CHANGELOG" не влезает в FAT 8.3);
# DEMO.MD — демонстрация разметки, конвертируется UTF-8 → CP1251.
#
# iconv -c возвращает ненулевой код, если хоть один символ отброшен
# (даже с -c) — это ОЖИДАЕМО при потере символов без аналога в CP866,
# не ошибка конвертации; вывод при этом всё равно корректно записан.
# (даже с -c) — это ОЖИДАЕМО при потере символов без аналога в целевой
# кодировке, не ошибка конвертации; вывод при этом корректно записан.
# Поэтому код возврата iconv игнорируется (|| true).
#
# Тестовые файлы лимита 256 КБ лежат в testfiles/ (генератор gen_testfiles.py);
# на диск НЕ кладутся — при необходимости добавить вручную в список ниже.
# ------------------------------------------------------------------
DISK_TMP := .disk_tmp
README_DISK := $(DISK_TMP)/README.MD
DEMO_DISK := $(DISK_TMP)/DEMO.MD
CHLOG_DISK := $(DISK_TMP)/CHANGES.MD
$(DISK_TMP):
mkdir -p $@
$(README_DISK): README.MD | $(DISK_TMP)
iconv -c -f UTF-8 -t CP866 README.MD > $@ || true
$(CHLOG_DISK): CHANGELOG.md | $(DISK_TMP)
cp CHANGELOG.md $@
# UTF8TEST.MD кладётся на диск КАК ЕСТЬ (в UTF-8, без перекодировки) —
# это тестовый вход для проверки UTF-8 рендеринга (Фаза 2 кодировок).
#
# Тестовые файлы лимита 256 КБ лежат в testfiles/ (генератор gen_testfiles.py);
# на диск НЕ кладутся — при необходимости добавить вручную в список ниже.
floppy: $(EXAMPLE).exe $(README_DISK)
python3 $(MAKE_DISK) $(FLOPPY_IMG) $(EXAMPLE).exe $(README_DISK) UTF8TEST.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"
+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 в корне репозитория).
+301 -47
View File
@@ -101,15 +101,15 @@ static const uint8_t md_pallete[16 * 4] = {
/* MAX_CACHE_LINE_LEN — в mdview2.h (буфер ячеек g_cells живёт в mdview2_index.c). */
#define MAX_CACHE_CONTENT_PAGES 40u /* кап контента на набор: 40 * 16 КБ = 640 КБ (80 на оба набора) */
static uint8_t cache_dir_blk = 0;
static uint8_t cache_dir_pages = 0;
static uint8_t cache_dir_phys[MAX_CACHE_DIR_PAGES] = {0};
static uint8_t cache_dir_blk;
static uint8_t cache_dir_pages;
static uint8_t cache_dir_phys[MAX_CACHE_DIR_PAGES];
static uint8_t cache_content_blk[MAX_CACHE_CONTENT_PAGES] = {0}; /* id блока на каждую выделенную страницу */
static uint8_t cache_content_phys[MAX_CACHE_CONTENT_PAGES] = {0};
static uint8_t cache_content_n_pages = 0; /* сколько страниц уже выделено */
static uint8_t cache_cur_page_idx = 0; /* индекс текущей страницы записи в cache_content_phys */
static uint16_t cache_cur_off = 0; /* текущее смещение записи в этой странице */
static uint8_t cache_content_blk[MAX_CACHE_CONTENT_PAGES]; /* id блока на каждую выделенную страницу */
static uint8_t cache_content_phys[MAX_CACHE_CONTENT_PAGES];
static uint8_t cache_content_n_pages; /* сколько страниц уже выделено */
static uint8_t cache_cur_page_idx; /* индекс текущей страницы записи в cache_content_phys */
static uint16_t cache_cur_off; /* текущее смещение записи в этой странице */
/* Доступ к директории рендер-кэша — тот же приём, что у idx_get/idx_put. */
void cache_dir_get(uint16_t idx, cache_rec_t *r)
@@ -175,41 +175,46 @@ void cache_commit(uint8_t page, uint16_t off, const void *buf, uint16_t len)
/* emph_to_attr определена ниже (рядом с inline-логикой), нужна в inline_scan. */
/* emph_to_attr — в mdview2_index.c. */
uint16_t n_lines = 0; /* extern: всего строк в индексе текущего набора */
uint16_t max_lines = 0; /* extern: ёмкость индекса: index_pages * 2048 */
uint16_t top_line = 0; /* extern: верхняя видимая строка */
uint32_t file_size = 0; /* extern: размер активного буфера документа */
static uint8_t file_blk = 0;
static uint8_t file_pages = 0;
static uint8_t file_phys[MAX_PAGES] = {0};
uint8_t index_blk = 0; /* extern: дескриптор EMM-блока индекса */
uint8_t index_pages = 0; /* */
uint8_t index_phys[MAX_INDEX_PAGES] = {0}; /* extern: физ. страницы индекса */
uint8_t index_truncated = 0; /* выставляется при исчерпании ёмкости индекса */
uint8_t g_file_clamped = 0; /* extern: файл >256 КБ обрезан до MAX_FILE при загрузке */
uint16_t n_lines; /* extern: всего строк в индексе текущего набора */
uint16_t max_lines; /* extern: ёмкость индекса: index_pages * 2048 */
uint16_t top_line; /* extern: верхняя видимая строка */
uint32_t file_size; /* extern: размер активного буфера документа */
static uint8_t file_blk;
static uint8_t file_pages;
static uint8_t file_phys[MAX_PAGES];
uint8_t index_blk; /* extern: дескриптор EMM-блока индекса */
uint8_t index_pages; /* */
uint8_t index_phys[MAX_INDEX_PAGES]; /* extern: физ. страницы индекса */
uint8_t index_truncated; /* выставляется при исчерпании ёмкости индекса */
uint8_t g_file_clamped; /* extern: файл >256 КБ обрезан до MAX_FILE при загрузке */
/* cur_seg_off/cur_rec (near-состояние индексатора) — в mdview2_index.c. */
uint8_t cur_page = 0xFF; /* extern: 0xFF = в W3 ещё не показана ни одна страница (extern: кодек инвалидирует) */
uint8_t viewport_x = 0; /* extern: горизонтальный сдвиг для nowrap-строк */
char filename[64] = {0}; /* extern: статус-бар */
uint8_t viewport_x; /* extern: горизонтальный сдвиг для nowrap-строк */
char filename[64]; /* extern: статус-бар */
/* Прогрессивная загрузка: рисуем первый экран и пускаем скролл, пока
* index_lines() ещё досканирует файл (кооперативно, без прерываний). */
uint8_t g_view = VIEW_MD; /* extern: активный режим просмотра (существует даже когда доступен только MD режим) */
#if WITH_RAW
uint8_t g_ready = 0; /* extern: 1 когда первичный документ построен — F2 (RAW) рабочий */
uint8_t g_ready; /* extern: 1 когда первичный документ построен — F2 (RAW) рабочий */
static uint8_t g_raw_last = VIEW_RAW_WRAP; /* последний под-режим RAW (возврат по F2) */
#if WITH_HEX
static uint8_t g_hex_last = VIEW_MD; /* вид, из которого вошли в HEX (возврат по F4) */
#endif
uint8_t g_loading = 0; /* extern: 1 во время index_lines() */
static uint8_t g_first_drawn = 0; /* первый экран уже нарисован */
uint8_t g_f8_enabled = 0; /* extern: можно ли сейчас переключать кодировку */
static uint8_t g_utf_failed = 0; /* UTF-8 набор не удалось построить (нет EMM) — пропускаем UTF-8 в цикле F8 */
uint8_t g_abort = 0; /* extern: F10/Esc во время загрузки → прервать индексацию */
static uint16_t g_last_prog = 0; /* n_lines на последнем обновлении статуса */
#endif
uint8_t g_loading; /* extern: 1 во время видимой индексации (build_doc) */
static uint8_t g_first_drawn; /* первый экран уже нарисован */
uint8_t g_f8_enabled; /* extern: можно ли сейчас переключать кодировку */
static uint8_t g_utf_failed; /* UTF-8 набор не удалось построить (нет EMM) — пропускаем UTF-8 в цикле F8 */
uint8_t g_bg_building; /* extern: 1 во время фонового index_step — гасит спиннер индексатора */
static uint8_t g_bg_doc = 0xFF; /* слот, строящийся в фоне (bg_step); 0xFF = фоновой сборки нет */
uint8_t g_abort; /* extern: F10/Esc во время загрузки → прервать индексацию */
static uint16_t g_last_prog; /* n_lines на последнем обновлении статуса */
/* Кодировки (ENC_*) и g_encoding — в mdview2.h / mdview2_enc.c. */
const uint8_t *g_remap = 0; /* extern: активная таблица ремапа (CP1251/KOI8), 0 = без ремапа (CP866)*/
uint8_t g_scratch_phys = 0; /* extern: EMM-страница под ремап-буфер строки для win_rest */
const uint8_t *g_remap; /* extern: активная таблица ремапа (CP1251/KOI8), 0 = без ремапа (CP866)*/
uint8_t g_scratch_phys; /* extern: EMM-страница под ремап-буфер строки для win_rest */
/* UTF-8 набор (utf_blk/utf_pages/utf_phys/utf_size/utf_avail, конвертер и его
* состояние) — в mdview2_enc.c; разделяемые символы и API — в mdview2.h.
@@ -218,10 +223,10 @@ uint8_t g_scratch_phys = 0; /* extern: EMM-страни
/* Снимок исходного (8-битного) файлового буфера — набор DOC_8BIT читает его
* как есть, набор DOC_UTF8 читает utf_phys. */
static uint8_t orig_file_blk = 0;
uint8_t orig_file_pages = 0; /* extern: читает кодек (utf_alloc) */
uint32_t orig_file_size = 0; /* extern: читает кодек (utf_convert_more) */
uint8_t orig_file_phys[MAX_PAGES] = {0}; /* extern: читает кодек (cv_read) */
static uint8_t orig_file_blk;
uint8_t orig_file_pages; /* extern: читает кодек (utf_alloc) */
uint32_t orig_file_size; /* extern: читает кодек (utf_convert_more) */
uint8_t orig_file_phys[MAX_PAGES]; /* extern: читает кодек (cv_read) */
/* ----------------- Два сосуществующих набора индекс/кэш -----------------
* DOC_8BIT — исходные байты (CP866/CP1251/KOI8R, различаются ремапом на
@@ -250,9 +255,9 @@ typedef struct {
#define DOC_8BIT 0
#define DOC_UTF8 1
static docset_t g_doc[2] = {{0}, {0}}; /* ОБЯЗАТЕЛЬНО инициализировать: built=0 для обоих
* (SDCC z80 не обнуляет неинициализированные статики
* надёжно — см. memory/sdcc_static_storage_gotcha) */
static docset_t g_doc[2]; /* built=0 для обоих: _DATA зануляется в crt0 gsinit
* (с 2026-07-04), явный {0}-инициализатор больше
* не нужен — он стоил ~260 байт _INITIALIZER */
static uint8_t g_active_doc = DOC_8BIT; /* какой набор сейчас в живых глобалах */
static uint8_t g_disp_doc = DOC_8BIT; /* какой набор показан пользователю */
@@ -284,6 +289,23 @@ void fill_row(uint8_t y, uint8_t attr)
bios_clearwin(y, 0, 1, SCREEN_W, attr);
}
/* Процент 0..100 = num*100/den на чистой 16-битной арифметике — чтобы НЕ
* тянуть 32-битное деление __divulong (~760 Б) ради показа процента в статусе.
* Оба вызова (MD/RAW) дают операнды, влезающие в 16 бит (≤18432 и ≤1024),
* переполняет только *100 — поэтому масштабируем оба вниз, пока num*100 не
* влезёт в uint16, затем считаем долю циклом-вычитанием (без div-хелпера).
* Инвариант вызовов: num ≤ den; результат всё равно ограничен 100. */
uint8_t pct16(uint16_t num, uint16_t den)
{
if (den == 0) return 0;
while (num > 655) { num >>= 1; den >>= 1; } /* num*100 ≤ 65500 < 65536 */
if (den == 0) return 100; /* страховка от num>den */
uint16_t n = (uint16_t)(num * 100u);
uint8_t p = 0;
while (n >= den && p < 100) { n = (uint16_t)(n - den); p++; }
return p;
}
/* Отображает указанную EMM-страницу файла в окно W3.
* Если эта страница уже активна, переключение не выполняется. */
@@ -703,6 +725,12 @@ void progress_tick(void)
* не MD-вид (иначе мелькал бы MD). Индекс при этом строится дальше (нужен
* для MD-вида). */
if (g_view != VIEW_MD) {
#if WITH_HEX
if (g_view == VIEW_HEX) { /* HEX показывает оригинал — экран уже актуален */
spinner_tick();
return;
}
#endif
if (!g_first_drawn) {
raw_draw();
// render_raw_status();
@@ -742,18 +770,26 @@ void progress_tick(void)
* Точка входа
* ================================================================== */
/* Строит набор `slot` кооперативно (index_lines + progress_tick).
/* Строит набор `slot` кооперативно (index_step + progress_tick).
* visible=1 — первичный (показываемый): прогрессивная отрисовка первого экрана
* и скролл по готовым строкам. visible=0 — вторичный: index_lines строит его в
* живых глобалах, а progress_tick через свап показывает/листает уже готовый
* и скролл по готовым строкам. visible=0 — вторичный: индексация идёт в живых
* глобалах, а progress_tick через свап показывает/листает уже готовый
* g_disp_doc. Возврат: 0 — успех, <0 — нехватка EMM (-5/-6/-7).
*
* Если slot прямо сейчас строится ФОНОМ (g_bg_doc, см. bg_step ниже) —
* не начинаем заново, а ДОКРУЧИВАЕМ: загружаем частично построенный набор
* и продолжаем с сохранённой позиции индексатора (его межшаговое состояние
* живёт в статиках модуля index и переживает свапы наборов).
*
* UTF-8 набор конвертируется ИНКРЕМЕНТАЛЬНО: выделяем страницы, переключаем
* живой буфер на utf и запускаем конвертацию первого чанка; дальше index_lines
* живой буфер на utf и запускаем конвертацию первого чанка; дальше index_step
* докручивает её впереди позиции чтения (g_utf_building). Конвертер читает
* оригинал из orig_file_phys[] через cv_read (не через fb, который теперь
* указывает на utf). */
static int build_doc(uint8_t slot, uint8_t enc, uint8_t visible)
/* Настраивает живые глобалы под сборку набора slot: файловый буфер (utf/orig),
* индекс + директория кэша, сброс индексатора. Общая часть build_doc и
* bg_build_start. Возврат: 0 — успех, <0 — нехватка EMM (-5/-6/-7). */
static int doc_setup(uint8_t slot)
{
if (slot == DOC_UTF8) {
if (!utf_avail) { /* первый вход: выделить страницы */
@@ -782,6 +818,20 @@ static int build_doc(uint8_t slot, uint8_t enc, uint8_t visible)
int rc = alloc_set_storage();
if (rc < 0)
return rc;
index_begin();
return 0;
}
static int build_doc(uint8_t slot, uint8_t enc, uint8_t visible)
{
if (g_bg_doc == slot) { /* фон уже строит его — продолжить */
doc_load_live(slot);
g_bg_doc = 0xFF;
} else {
int rc = doc_setup(slot);
if (rc < 0)
return rc;
}
set_encoding(enc);
top_line = 0;
@@ -790,11 +840,13 @@ static int build_doc(uint8_t slot, uint8_t enc, uint8_t visible)
if (visible) {
g_disp_doc = slot;
g_first_drawn = 0;
status_encoding(); /* показать кодировку сразу (детект/F8), не дожидаясь индексации */
cur_page = 0xFF; /* BIOS-вывод мог сбить W3-маппинг, на который опирается fb() */
}
g_loading = 1;
g_last_prog = 0;
index_lines();
while (!index_step(255)) {}
g_loading = 0;
/* Зафиксировать конвертацию (на случай обрыва индекса по max_lines до конца
@@ -809,6 +861,121 @@ static int build_doc(uint8_t slot, uint8_t enc, uint8_t visible)
return 0;
}
/* ==================================================================
* Фоновая сборка второго набора (idle-time, между клавишами)
* ==================================================================
* Пока пользователь читает документ, главный цикл в паузах между клавишами
* докручивает индексацию второго набора (UTF-8 при 8-битном первичном и
* наоборот) порциями index_step: свап на bg-набор → несколько итераций →
* свап обратно. Экран при этом не трогается (g_bg_building гасит спиннер),
* задержка реакции на клавишу — не больше одного шага. По готовности F8
* переключает наборы мгновенно; F8 ДО готовности докручивает сборку видимо
* (ветка resume в build_doc), не начиная её заново. */
#define BG_STEP_BUDGET 2u /* итераций индексатора на idle-шаг: латентность клавиши
* = длительность шага (~единицы мс на обычном абзаце) */
#define BG_KEY_HOLDOFF 300u /* пустых опросов kbhit после клавиши до возобновления
* фона — при автоповторе (скролл) фон не вклинивается */
/* Готовит фоновую сборку второго набора: настраивает живые глобалы (буфер,
* индекс, кэш, index_begin) так же, как build_doc, но БЕЗ индексации, и
* возвращает живым прежний набор. При нехватке EMM фон просто не стартует —
* остаётся сегодняшний ленивый путь по F8. */
static void bg_build_start(void)
{
uint8_t slot = (uint8_t)((g_active_doc == DOC_8BIT) ? DOC_UTF8 : DOC_8BIT);
uint8_t vis = g_active_doc;
if (g_doc[slot].built || (slot == DOC_UTF8 && g_utf_failed))
return; /* строить нечего */
doc_save_live(vis);
if (doc_setup(slot) < 0) { /* нет EMM — как и в ленивом пути */
if (slot == DOC_UTF8) g_utf_failed = 1;
doc_load_live(vis);
return;
}
top_line = 0;
viewport_x = 0;
g_active_doc = slot; /* живые глобалы = bg-набор... */
doc_switch(vis); /* ...сохранить его и вернуть видимый */
g_bg_doc = slot;
}
/* Один idle-шаг фона: свап на строящийся набор, порция индексации, свап
* обратно. Межшаговое состояние индексатора/UTF-конвертера — статики их
* модулей, обработчики клавиш видимого набора их не трогают. По завершении
* финализирует набор (дальше F8 переключается мгновенно). */
static void bg_step(void)
{
uint8_t vis = g_active_doc;
doc_switch(g_bg_doc);
g_bg_building = 1;
uint8_t done = index_step(BG_STEP_BUDGET);
g_bg_building = 0;
if (done) {
if (g_utf_building) { /* как build_doc: обрыв индекса до конца исходника */
utf_size = file_size;
g_utf_building = 0;
}
g_doc[g_bg_doc].built = 1; /* doc_save_live поле built не трогает */
g_bg_doc = 0xFF;
}
doc_switch(vis);
}
/* Пропорциональный перенос байт-позиции между буферами разного размера
* (8bit ↔ UTF-8 при F8): off * to / from БЕЗ __mullong/__divulong. Один
* цикл restoring-деления: бит k дроби off/from (сравнение с from/2^k)
* сразу добавляет to/2^k в результат — сумма to/2^k по установленным
* битам и есть off*to/from. Точность from/65536 (≤4 байта на максимальном
* файле); ляжет на границу строки через line_at_off / raw_reanchor. */
static uint32_t map_off(uint32_t off, uint32_t from, uint32_t to)
{
if (from == to || from == 0) return off;
uint32_t res = 0;
for (uint8_t i = 0; i < 16; i++) {
from >>= 1;
to >>= 1;
if (off >= from) { off -= from; res += to; }
}
return res;
}
/* Байт-позиция текущего вида (верх экрана) — единая «валюта» для переноса
* позиции между видами/наборами. Валюта = offset АКТИВНОГО буфера; HEX живёт
* в байтах оригинала, для UTF-набора конвертируем через map_off. */
static uint32_t view_pos(void)
{
#if WITH_HEX
if (g_view == VIEW_HEX)
return (g_active_doc == DOC_UTF8)
? map_off(hex_pos(), orig_file_size, file_size)
: hex_pos();
#endif
#if WITH_RAW
if (g_view != VIEW_MD) return raw_pos();
#endif
return seg_off(top_line);
}
/* Поставить текущий вид на байт-позицию off (активного буфера). */
static void view_reanchor(uint32_t off)
{
#if WITH_HEX
if (g_view == VIEW_HEX) {
hex_reanchor((g_active_doc == DOC_UTF8)
? map_off(off, file_size, orig_file_size)
: off);
return;
}
#endif
#if WITH_RAW
if (g_view != VIEW_MD) { raw_reanchor(off); return; }
#endif
top_line = line_at_off(off);
}
/* Переключает кодировку на `next` (вызывается из главного цикла по F8).
* 8-бит↔8-бит внутри одного набора — мгновенный ремап; переход в другой набор
* — мгновенный doc_switch, если он построен, иначе ЛЕНИВАЯ сборка по месту
@@ -825,13 +992,22 @@ static void switch_encoding(uint8_t next)
set_encoding(next);
g_doc[target].enc = next;
} else if (g_doc[target].built) { /* набор готов — мгновенный свап */
/* Позицию переносим пропорцией байтов (буферы наборов разного
* размера), а не из сохранённой в g_doc копии — иначе каждый набор
* скачет по своей старой позиции. */
uint32_t old_off = view_pos();
uint32_t old_size = file_size;
doc_switch(target);
g_disp_doc = target;
set_encoding(next);
g_doc[target].enc = next;
view_reanchor(map_off(old_off, old_size, file_size));
} else { /* строим набор лениво */
doc_save_live(g_active_doc); /* сохранить текущий (позиция и т.п.) */
#if WITH_RAW
uint32_t raw_save = raw_pos();
if (VIEW_IS_RAW(g_view)) /* HEX показывает оригинал — его позиция инвариантна */
raw_reanchor(0); /* прогрессивный показ нового буфера — с начала */
g_ready = 0;
render_menu(); /* идёт индексация — прячем F2 */
#endif
@@ -847,6 +1023,10 @@ static void switch_encoding(uint8_t next)
if (target == DOC_UTF8)
g_utf_failed = 1;
doc_load_live(g_active_doc);
#if WITH_RAW
if (VIEW_IS_RAW(g_view))
raw_reanchor(raw_save); /* буфер прежний — вернуть позицию RAW */
#endif
return;
}
g_disp_doc = target; /* build_doc уже выставил active/enc */
@@ -988,7 +1168,27 @@ int main(int argc, char **argv)
render_full_status();
draw_viewport_from_cache();
/* Второй набор (UTF-8 при 8-битном первичном и наоборот) готовим фоном
* в паузах между клавишами — к первому F8 он обычно уже построен. */
bg_build_start();
for (;;) {
/* Idle-фон: пока второй набор не достроен, вместо блокирующего getkey
* опрашиваем клавиатуру и тратим паузы на bg_step. Холдаун после
* каждой клавиши: при активном скролле (автоповтор) фон молчит и
* плавность не страдает. После достройки — снова блокирующий getkey. */
if (g_bg_doc != 0xFF) {
uint16_t idle = 0;
while (!kbhit()) {
if (idle < BG_KEY_HOLDOFF) {
idle++;
continue;
}
bg_step();
if (g_bg_doc == 0xFF)
break;
}
}
uint16_t k = getkey();
uint8_t ascii = (uint8_t)(k & 0xFF);
uint8_t scan = (uint8_t)((k >> 8) & 0x7F);
@@ -1005,7 +1205,8 @@ int main(int argc, char **argv)
show_help();
continue;
} /* show_help сам перерисует */
/* F2 — тумблер RAW ↔ MD (рабочий только после постройки документа). */
/* F2 — тумблер RAW ↔ MD (из HEX тоже уводит в MD).
* Рабочий только после постройки документа. */
if (scan == KEY_F2) {
if (!g_ready) continue;
if (g_view == VIEW_MD) {
@@ -1017,14 +1218,45 @@ int main(int argc, char **argv)
render_raw_status();
raw_draw();
} else {
g_raw_last = g_view; /* запомнить под-режим RAW */
uint32_t off = view_pos(); /* позиция из RAW/HEX */
if (VIEW_IS_RAW(g_view))
g_raw_last = g_view; /* запомнить под-режим RAW */
g_view = VIEW_MD;
top_line = line_at_off(off);
clamp_top();
render_full_status(); render_menu();
draw_viewport_from_cache();
}
continue;
}
#if WITH_HEX
/* F4 — тумблер HEX ↔ прежний вид (MD/RAW). */
if (scan == KEY_F4) {
if (!g_ready) continue;
uint32_t off = view_pos(); /* позиция текущего вида */
if (g_view == VIEW_HEX) { /* HEX → прежний вид */
g_view = g_hex_last;
view_reanchor(off);
if (g_view == VIEW_MD) {
clamp_top();
render_full_status(); render_menu();
draw_viewport_from_cache();
} else {
raw_screen_init();
status_encoding(); render_menu();
render_raw_status(); raw_draw();
}
} else { /* MD/RAW → HEX */
g_hex_last = g_view;
g_view = VIEW_HEX;
view_reanchor(off); /* active → байты оригинала */
hex_screen_init();
status_encoding(); render_menu();
render_hex_status(); hex_draw();
}
continue;
}
#endif
/* цикл CP866→CP1251→KOI8R→UTF8→CP866; набор дособерётся лениво */
if (scan == KEY_F8) {
uint8_t next = (g_encoding == ENC_CP866) ? ENC_CP1251 :
@@ -1035,7 +1267,24 @@ int main(int argc, char **argv)
switch_encoding(next);
if (g_abort)
exit(0); /* Esc/F10 во время ленивой досборки */
render_full_status(); /* перерисовка — в MD-режиме */
/* Перерисовка — по текущему виду (switch_encoding сам не рисует). */
#if WITH_HEX
if (g_view == VIEW_HEX) {
status_encoding();
render_hex_status();
hex_draw(); /* printable-колонка в новой кодировке */
continue;
}
#endif
#if WITH_RAW
if (g_view != VIEW_MD) {
status_encoding();
render_raw_status();
raw_draw();
continue;
}
#endif
render_full_status();
draw_viewport_from_cache();
continue;
}
@@ -1044,6 +1293,11 @@ int main(int argc, char **argv)
/* MD-режим: F1/F8/F10 здесь, навигация — в md_key() (peer к raw_key). */
md_key(scan); /* навигация MD (сама рисует+статус); незнакомые игнорит */
continue;
#if WITH_HEX
case VIEW_HEX:
hex_key(scan); /* навигация HEX (сама рисует+процент) */
continue;
#endif
#if WITH_RAW
case VIEW_RAW_WRAP:
case VIEW_RAW_HSCROLL:
+28 -3
View File
@@ -69,6 +69,8 @@
#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]
@@ -137,19 +139,33 @@ extern uint8_t g_scratch_phys; /* scratch EMM-страница для wi
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 */
void raw_home(void); /* сброс в начало (смена буфера при F8 в/из UTF-8) */
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).
@@ -186,6 +202,7 @@ void prerender_status(void); /* фикс. часть: фон,
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); /* число строк, готовых к показу */
@@ -228,7 +245,14 @@ 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 (рисует/листает) */
void index_lines(void); /* построить индекс по загруженному файлу */
/* Резюмируемая индексация (для фоновой сборки второго набора): 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) — общий с модулем таблиц.
@@ -275,5 +299,6 @@ 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 */
+1 -1
View File
@@ -11,6 +11,6 @@
#define MDVIEW2_CONF_H
#define WITH_RAW 1 /* RAW-просмотр исходника (F2): mdview2_raw.c */
#define WITH_HEX 0 /* HEX-просмотр (зарезервировано): mdview2_hex.c */
#define WITH_HEX 1 /* HEX-дамп оригинального файла (F4): mdview2_hex.c */
#endif /* MDVIEW2_CONF_H */
+24 -20
View File
@@ -23,16 +23,16 @@
/* ---- Состояние кодировки/UTF (владелец — этот модуль) ------------- */
uint8_t g_encoding = ENC_CP866; /* активная кодировка */
uint8_t utf_blk = 0;
uint8_t utf_pages = 0;
uint8_t utf_phys[MAX_PAGES] = {0};
uint32_t utf_size = 0;
uint8_t utf_avail = 0; /* 1 = страницы UTF-8 выделены */
uint8_t g_utf_building = 0; /* 1 пока конвертация не дошла до конца исходника */
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 = 0; /* позиция чтения ОРИГИНАЛА (utf-8) */
static uint32_t utf_src; /* позиция чтения ОРИГИНАЛА (utf-8) */
static uint8_t cv_page = 0xFF; /* orig-страница в W3 для cv_read (своя от cur_page) */
/* Таблицы ремапа СТАРШИХ байтов [128-255] в CP866. Индексируются (ch-0x80):
@@ -180,9 +180,9 @@ static const uint8_t utf_cyr_to_866[96] = {
/* Поток вывода конвертера: пишем в utf_phys чанками по странице, буфер в
* near-памяти, флаш через один bank_write (не побайтовый swap W3). */
static uint8_t conv_buf[256];
static uint16_t conv_n = 0; /* байт в буфере, ещё не сброшено */
static uint8_t conv_page = 0; /* индекс текущей страницы в utf_phys */
static uint16_t conv_off = 0; /* уже сброшенное смещение в этой странице */
static uint16_t conv_n; /* байт в буфере, ещё не сброшено */
static uint8_t conv_page; /* индекс текущей страницы в utf_phys */
static uint16_t conv_off; /* уже сброшенное смещение в этой странице */
static void conv_flush(void)
{
@@ -230,20 +230,24 @@ static const utf_sym_t utf_sym[] = {
};
#define UTF_SYM_N (uint8_t)(sizeof(utf_sym) / sizeof(utf_sym[0]))
/* Кодпойнт → CP866-глиф(ы). Кириллица по таблице; спецслучаи (…, BOM) явно;
* остальные ходовые символы по таблице utf_sym[]; прочее → '?'. */
/* Кодпойнт → ОДИН 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 < 0x80) { conv_put((uint8_t)cp); return; }
if (cp >= 0x0400 && cp <= 0x045F) { conv_put(utf_cyr_to_866[cp - 0x0400]); return; }
if (cp == 0x2026) { conv_put('.'); conv_put('.'); conv_put('.'); return; } /* … → "..." */
if (cp == 0xFEFF) return; /* BOM/ZWNBSP — выкинуть */
for (uint8_t i = 0; i < UTF_SYM_N; i++)
if (utf_sym[i].utf8 == (uint16_t)cp) {
conv_put(utf_sym[i].cp866);
return;
}
conv_put('?');
conv_put(utf_cp_glyph(cp));
}
/* Последовательное чтение байта ОРИГИНАЛА (utf-8) конвертером. Оригинал лежит
+2 -2
View File
@@ -73,7 +73,7 @@ void show_help(void)
/* Содержимое (20 внутренних строк) */
uint8_t r = 0;
help_line(r++, "", ATTR_HELP_BG);
help_line(r++, " MDView v1.0 (a3) -- Markdown Viewer for Sprinter", ATTR_HELP_HDR);
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);
@@ -92,11 +92,11 @@ void show_help(void)
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);
help_line(r++, "", ATTR_HELP_BG);
(void)getkey();
+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 */
+214 -209
View File
@@ -1,7 +1,7 @@
/*
* mdview2_index.c — парсер/индексатор markdown (сердце приложения).
*
* Единственный проход по исходному файлу (index_lines): классификация строк
* Единственный проход по исходному файлу (index_begin/index_step): классификация строк
* (classify_line, is_*_raw), сборка абзацев с переносом (scan_join_stream),
* inline-разметка (inline_marker/inline_scan) и эмиссия готовых (char,attr)-
* ячеек в рендер-кэш (gc_put → seg_commit_cells → cache_*). Ведёт индекс
@@ -9,17 +9,25 @@
* позиции чтения. Разделяемые символы и API — в mdview2.h.
*
* Приватно (не торчит в .h): idx_rec_t, cur_rec/cur_seg_off, буфер ячеек
* g_cells, styles_map, idx_get/idx_put и все классификаторы/сканеры.
* g_cells, idx_get/idx_put и все классификаторы/сканеры.
*/
#include <stdint.h>
#include <string.h>
#include <conio.h> /* COLOR()/COLOR_* для ATTR_* в styles_map */
#include <conio.h> /* COLOR()/COLOR_* для ATTR_* в сканерах */
#include <sprinter.h>
#include <sprinter_mem.h> /* bank_read/bank_write (idx_get/idx_put) */
#include "mdview2.h"
static uint8_t emph_to_attr(uint8_t emph, uint8_t base_attr); /* forward (нужна в inline-сканере) */
static uint8_t is_hr_raw(uint32_t p); /* forward (нужна в classify_line) */
/* Начало следующей физической строки (байт после '\n', либо file_size). */
static uint32_t next_line(uint32_t p)
{
p = row_end(p);
return (p < file_size) ? p + 1 : p;
}
typedef struct idx_rec_s {
uint32_t off; /* смещение первого байта исходного текста для сегмента */
@@ -31,7 +39,7 @@ typedef struct idx_rec_s {
#define INDEX_REC_SIZE 8u
static uint32_t cur_seg_off = 0; /* смещение последнего emit_seg (кэш в near-памяти) */
static uint32_t cur_seg_off; /* смещение последнего emit_seg (кэш в near-памяти) */
static idx_rec_t cur_rec; /* near-копия последней записанной idx-записи */
/* Для проверки границ выделения считаем пробел/таб/конец строки/0 пробельными. */
@@ -90,12 +98,23 @@ uint32_t seg_off(uint16_t idx)
return r.off;
}
/* Установка флагов всегда идёт в последний emit_seg (n_lines-1).
/* Последняя строка индекса с исходным offset <= off — обратный перевод
* байт-позиции в MD-строку (позиционирование при F2/F8). Оффсеты сегментов
* монотонны, поэтому бинарный поиск (~15 bank-чтений). */
uint16_t line_at_off(uint32_t off)
{
uint16_t lo = 0, hi = n_lines;
while (lo < hi) {
uint16_t mid = (uint16_t)((lo + hi) >> 1);
if (seg_off(mid) <= off) lo = (uint16_t)(mid + 1);
else hi = mid;
}
return (lo > 0) ? (uint16_t)(lo - 1) : 0;
}
/* Установка флагов (маска IF_*) всегда идёт в последний emit_seg (n_lines-1).
* Используется near-копия cur_rec, чтобы не делать обратное чтение из банка. */
static void set_nowrap_cur(void) { cur_rec.flags |= IF_NOWRAP; idx_put((uint16_t)(n_lines - 1), &cur_rec); }
static void set_blank_cur(void) { cur_rec.flags |= IF_BLANK; idx_put((uint16_t)(n_lines - 1), &cur_rec); }
static void set_code_cur(void) { cur_rec.flags |= IF_CODE; idx_put((uint16_t)(n_lines - 1), &cur_rec); }
static void set_hscroll_cur(void){ cur_rec.flags |= IF_HSCROLL; idx_put((uint16_t)(n_lines - 1), &cur_rec); }
static void set_cur_flags(uint8_t f) { cur_rec.flags |= f; idx_put((uint16_t)(n_lines - 1), &cur_rec); }
/* ==================================================================
* Индексация строк
@@ -103,12 +122,6 @@ static void set_hscroll_cur(void){ cur_rec.flags |= IF_HSCROLL; idx_put((uint16_
/* INIT_STYLE_* — в mdview2.h (общие с модулем таблиц). */
/* Таблица сопоставления init-style -> атрибут.
* Порядок должен совпадать с INIT_STYLE_* / EM_* ниже. */
static const uint8_t styles_map[] = {
ATTR_TEXT, ATTR_TEXT_BOLD, ATTR_TEXT_ITALIC, ATTR_TEXT_UNDERSORE, ATTR_TEXT_CODE, ATTR_TEXT_STRIKE
};
/* Тип строки, который возвращает classify_line(). */
#define LK_PLAIN 0
#define LK_H1 1
@@ -157,22 +170,7 @@ static uint8_t classify_line(uint32_t p_start, uint32_t *out_content)
/* Горизонтальный разделитель.
* Проверяется до списков, чтобы `- - -` не стало маркером списка. */
if (c0 == '-' || c0 == '*' || c0 == '_') {
char marker = 0;
uint8_t count = 0;
uint8_t ok = 1;
uint32_t p = p_start;
while (p < file_size) {
char ch = fb(p++);
if (ch == '\n' || ch == '\r') break;
if (ch == ' ' || ch == '\t') continue;
if (ch != '-' && ch != '*' && ch != '_') { ok = 0; break; }
if (marker == 0) marker = ch;
else if (ch != marker) { ok = 0; break; }
count++;
}
if (ok && count >= 3) return LK_HR;
}
if (is_hr_raw(p_start)) return LK_HR;
/* Для списков/цитат дополнительно допускаем ведущие пробелы
* (базовая поддержка вложенности). */
@@ -237,15 +235,15 @@ static uint8_t marker_visible_col(uint8_t kind, uint32_t p_start, uint32_t conte
* g_ncells_at_space — снимок длины на последнем пробеле для усечения буфера
* при переносе строки (отбрасываем уехавшее за границу слово). */
static uint8_t g_cells[MAX_CACHE_LINE_LEN * 2];
uint8_t g_ncells = 0; /* ячеек в текущем сегменте (общий с таблицами) */
static uint8_t g_ncells_at_space = 0; /* снимок g_ncells на последнем пробеле (для усечения при переносе) */
static uint8_t g_skip_flush = 0; /* следующий emit_seg пропустит lag-флаш (pending уже зафлашен вручную, напр. перед таблицей) */
uint8_t g_ncells; /* ячеек в текущем сегменте (общий с таблицами) */
static uint8_t g_ncells_at_space; /* снимок g_ncells на последнем пробеле (для усечения при переносе) */
static uint8_t g_skip_flush; /* следующий emit_seg пропустит lag-флаш (pending уже зафлашен вручную, напр. перед таблицей) */
/* Обрыв индексации: причина (0 / TRUNC_CONTENT / TRUNC_LINES) и строка, на
* которой упёрлись. Выставляются ОДИН раз (первая причина побеждает); по ним
* index_lines() в конце пишет строку-сообщение (IF_TRUNC_MSG, рисуется вживую). */
static uint8_t g_trunc_cause = 0;
static uint16_t g_trunc_line = 0;
static uint8_t g_trunc_cause;
static uint16_t g_trunc_line;
/* Пишет одну (char,attr)-ячейку в g_cells, молча игнорируя переполнение
* MAX_CACHE_LINE_LEN. Оставлена ФУНКЦИЕЙ намеренно: инлайн в сильно
@@ -383,17 +381,18 @@ static uint8_t is_table_raw(uint32_t p)
* маркер обработан (вызывающий делает continue), 0 — символ обычный (пробел/
* текст), его кладёт сам вызывающий (и проверяет перенос). Меняет
* *pq/*pls/*pseg/*pprev. attr берётся как emph_to_attr(line_style, base) —
* для join base=ATTR_TEXT, что тождественно styles_map[line_style]. */
* для join base=ATTR_TEXT (атрибут «обычного» текста). */
static uint8_t inline_marker(uint32_t *pq, uint32_t bound, uint8_t *pls,
uint8_t *pseg, char *pprev, uint8_t base)
{
uint32_t q = *pq;
uint8_t ls = *pls;
char ch = fb(q);
uint8_t at = emph_to_attr(ls, base); /* атрибут литералов текущего стиля */
if (ls != INIT_STYLE_CODE && ch == '\\' && (q + 1) < bound && is_escapable(fb(q + 1))) {
char nb = fb(q + 1);
gc_put(nb, emph_to_attr(ls, base));
gc_put(nb, at);
*pseg += 1; *pq = q + 2; *pprev = nb; return 1;
}
if (ls != INIT_STYLE_CODE && ch == '[' && (q + 2) < bound && fb(q + 2) == ']') {
@@ -403,7 +402,7 @@ static uint8_t inline_marker(uint32_t *pq, uint32_t bound, uint8_t *pls,
if (ch == '`') {
if (ls == INIT_STYLE_PLAIN) { *pls = INIT_STYLE_CODE; *pq = q + 1; return 1; }
if (ls == INIT_STYLE_CODE) { *pls = INIT_STYLE_PLAIN; *pq = q + 1; return 1; }
gc_put('`', emph_to_attr(ls, base)); *pseg += 1; *pq = q + 1; *pprev = '`'; return 1;
gc_put('`', at); *pseg += 1; *pq = q + 1; *pprev = '`'; return 1;
}
if (ch == '*' && (q + 1) < bound && fb(q + 1) == '*') {
char nx = ((q + 2) < bound) ? fb(q + 2) : '\n';
@@ -411,7 +410,7 @@ static uint8_t inline_marker(uint32_t *pq, uint32_t bound, uint8_t *pls,
if (ls == INIT_STYLE_PLAIN && ws_or_eol(*pprev)) { *pls = INIT_STYLE_BOLD; *pq = q + 2; return 1; }
if (ls == INIT_STYLE_BOLD && ws_or_eol_or_delim(nx)) { *pls = INIT_STYLE_PLAIN; *pq = q + 2; return 1; }
}
gc_put('*', emph_to_attr(ls, base)); gc_put('*', emph_to_attr(ls, base));
gc_put('*', at); gc_put('*', at);
*pseg += 2; *pq = q + 2; *pprev = '*'; return 1;
}
if (ch == '~' && (q + 1) < bound && fb(q + 1) == '~') {
@@ -420,7 +419,7 @@ static uint8_t inline_marker(uint32_t *pq, uint32_t bound, uint8_t *pls,
if (ls == INIT_STYLE_PLAIN && ws_or_eol(*pprev)) { *pls = INIT_STYLE_STRIKE; *pq = q + 2; return 1; }
if (ls == INIT_STYLE_STRIKE && ws_or_eol_or_delim(nx)) { *pls = INIT_STYLE_PLAIN; *pq = q + 2; return 1; }
}
gc_put('~', emph_to_attr(ls, base)); gc_put('~', emph_to_attr(ls, base));
gc_put('~', at); gc_put('~', at);
*pseg += 2; *pq = q + 2; *pprev = '~'; return 1;
}
if (ch == '*' || ch == '_') {
@@ -430,99 +429,57 @@ static uint8_t inline_marker(uint32_t *pq, uint32_t bound, uint8_t *pls,
if (ls == INIT_STYLE_PLAIN && ws_or_eol(*pprev)) { *pls = ns; *pq = q + 1; return 1; }
if (ls != INIT_STYLE_PLAIN && ws_or_eol_or_delim(nx)) { *pls = INIT_STYLE_PLAIN; *pq = q + 1; return 1; }
}
gc_put(ch, emph_to_attr(ls, base)); *pseg += 1; *pq = q + 1; *pprev = ch; return 1;
gc_put(ch, at); *pseg += 1; *pq = q + 1; *pprev = ch; return 1;
}
if (ch == '\t') {
uint8_t old_sc = *pseg;
uint8_t tgt_full = (uint8_t)((old_sc & (uint8_t)~(TAB_STOP - 1)) + TAB_STOP);
uint8_t tgt = tgt_full; if (tgt > SCREEN_W) tgt = SCREEN_W;
gc_fill(' ', emph_to_attr(ls, base), (uint8_t)(tgt_full - old_sc));
gc_fill(' ', at, (uint8_t)(tgt_full - old_sc));
*pseg = tgt; *pq = q + 1; *pprev = ' '; return 1;
}
return 0; /* пробел или обычный символ — обрабатывает вызывающий */
}
/* Общий сканер inline-форматирования и переносов.
* Идёт от q до q_end, учитывает стартовую колонку col и при необходимости
* эмитит continuation-сегменты. Возвращает итоговый line_style. */
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)
{
uint32_t last_space = 0xFFFFFFFFu;
uint8_t last_space_style = line_style; /* снимок line_style на момент last_space */
char prev_ch = ' ';
uint8_t seg_col = col;
/* base — атрибут «обычного» текста сегмента. Для заголовка первый сегмент
* имеет title-attr, а continuation-сегменты render рисует как PLAIN
* (ATTR_TEXT, kind=LK_PLAIN при cont) — переключаем base после 1-го переноса. */
uint8_t base = base_attr;
while (q < q_end && n_lines < max_lines) {
char ch = fb(q);
if (ch == '\n') {
/* Жёсткий перенос (уже проверен снаружи) или конец потока. */
q++;
emit_seg(q, line_style, ckind, (ckind == CK_LIST || ckind == CK_QUOTE) ? 1 : 0);
base = ATTR_TEXT;
last_space = 0xFFFFFFFFu; prev_ch = ' '; seg_col = col;
continue;
}
/* Inline-маркеры (\X [x] ` ** ~~ * _ \t) — общий разбор. */
if (inline_marker(&q, q_end, &line_style, &seg_col, &prev_ch, base)) continue;
if (ch == ' ') {
gc_put(' ', emph_to_attr(line_style, base));
last_space = q + 1;
last_space_style = line_style;
g_ncells_at_space = g_ncells;
seg_col++; q++; prev_ch = ' ';
} else {
gc_put(ch, emph_to_attr(line_style, base));
seg_col++; q++; prev_ch = ch;
}
if (!nowrap && seg_col >= SCREEN_W) {
uint8_t use_ls = (uint8_t)(last_space != 0xFFFFFFFFu && last_space > cur_seg_off);
uint32_t wrap_at = use_ls ? last_space : q;
if (wrap_at >= q_end || wrap_at == cur_seg_off) break;
uint8_t wrap_style = use_ls ? last_space_style : line_style;
if (use_ls) g_ncells = g_ncells_at_space;
emit_seg(wrap_at, wrap_style, ckind, 1);
base = ATTR_TEXT; /* continuation заголовка → PLAIN */
seg_col = col; last_space = 0xFFFFFFFFu; prev_ch = ' '; q = wrap_at;
line_style = wrap_style;
}
}
return line_style;
}
#define JOIN_MODE_NONE 0u
#define JOIN_MODE_LIST 1u
#define JOIN_MODE_QUOTE 2u
#define JOIN_MODE_PLAIN 3u
/* Общий потоковый сканер для index_lines().
* Убирает дублирование между ветками list/quote/plain:
* - mode задаёт правила обработки перевода строки,
* - остальная inline-логика (emphasis/таб/перенос) едина.
* Возвращает позицию q, на которой сканер остановился, и обновляет стиль. */
static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col,
uint8_t ckind, uint8_t *io_line_style, uint8_t mode)
/* ================== Единый сканер сегмента ================================
* Общий цикл inline-форматирования, переносов и (в join-режимах) склейки
* физических строк — бывшие inline_scan + scan_join_stream. mode:
* JOIN_MODE_NONE — сканировать РОВНО [q, bound) без склейки; '\n' в
* диапазоне не ожидается (вызывающие дают строку/ячейку до EOL);
* активны base_attr (title-attr заголовка) и nowrap (замер таблиц);
* ckind всегда CK_OTHER (continuation-префикс не эмитится).
* JOIN_MODE_LIST/QUOTE/PLAIN — правила '\n'-склейки; bound == file_size,
* base_attr == ATTR_TEXT (emph_to_attr(ls, ATTR_TEXT) тождественно
* бывшему styles_map[ls]), nowrap == 0.
* Возвращает позицию останова; итоговый стиль — через *io_line_style. */
static uint32_t scan_stream(uint32_t q, uint32_t bound, uint32_t para_start,
uint8_t col, uint8_t ckind, uint8_t *io_line_style,
uint8_t base_attr, uint8_t nowrap, uint8_t mode)
{
uint32_t last_space = 0xFFFFFFFFu;
uint8_t last_space_style = *io_line_style; /* снимок line_style на момент last_space */
char prev_ch = ' ';
uint8_t seg_col = col;
uint8_t line_style = *io_line_style;
/* base — атрибут «обычного» текста сегмента. Для заголовка первый сегмент
* имеет title-attr, а continuation-сегменты render рисует как PLAIN —
* переключаем base после 1-го переноса (в join-режимах base == ATTR_TEXT
* и присваивание ничего не меняет). */
uint8_t base = base_attr;
while (q < file_size && n_lines < max_lines) {
while (q < bound && n_lines < max_lines) {
char ch = fb(q);
uint8_t soft_break = 0;
if (ch == '\n') {
uint32_t next = q + 1;
if (next >= file_size) break;
if (mode == JOIN_MODE_NONE) break; /* NONE: '\n' в [q,bound) не ожидается */
if (next >= bound) break;
if (mode == JOIN_MODE_LIST) {
if (is_line_blank(next)) break;
@@ -538,7 +495,7 @@ static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col,
break;
}
}
while (next < file_size) {
while (next < bound) {
char ws = fb(next);
if (ws == ' ' || ws == '\t') next++;
else break;
@@ -556,8 +513,7 @@ static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col,
if (next_kind != LK_QUOTE) break;
{
uint32_t next_line_end = next_content;
while (next_line_end < file_size && fb(next_line_end) != '\n') next_line_end++;
uint32_t next_line_end = row_end(next_content);
{
uint8_t empty_quote = 1;
@@ -622,7 +578,7 @@ static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col,
q++;
/* как render: пропускаем ведущие пробелы/табы строки-продолжения,
* склейка даёт РОВНО один пробел (иначе merge эмитит отступ как лишние ячейки) */
while (q < file_size && (fb(q) == ' ' || fb(q) == '\t')) q++;
while (q < bound && (fb(q) == ' ' || fb(q) == '\t')) q++;
soft_break = 1;
ch = ' ';
}
@@ -631,39 +587,39 @@ static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col,
/* Inline-маркеры (\X [x] ` ** ~~ * _ \t) — общий разбор; при
* soft_break ch==' ' (синтетический пробел склейки) — маркеры
* пропускаем, его кладёт ветка пробела ниже. */
if (!soft_break && inline_marker(&q, file_size, &line_style, &seg_col, &prev_ch, ATTR_TEXT))
if (!soft_break && inline_marker(&q, bound, &line_style, &seg_col, &prev_ch, base))
continue;
if (ch == ' ') {
gc_put(' ', styles_map[line_style]);
gc_put(' ', emph_to_attr(line_style, base));
if (soft_break) {
last_space = q;
last_space_style = line_style;
seg_col++;
last_space = q; /* синтетический пробел склейки */
} else {
last_space = q + 1;
last_space_style = line_style;
seg_col++;
q++;
}
g_ncells_at_space = g_ncells; /* снимок для усечения */
last_space_style = line_style;
g_ncells_at_space = g_ncells; /* снимок для усечения */
seg_col++;
prev_ch = ' ';
} else {
gc_put(ch, styles_map[line_style]);
gc_put(ch, emph_to_attr(line_style, base));
seg_col++;
q++; /* soft_break здесь всегда 0 (ch!=' ') */
q++; /* soft_break здесь всегда 0 (ch != ' ') */
prev_ch = ch;
}
if (seg_col >= SCREEN_W) {
if (!nowrap && seg_col >= SCREEN_W) {
uint8_t use_ls = (uint8_t)(last_space != 0xFFFFFFFFu && last_space > cur_seg_off);
uint32_t wrap_at = use_ls ? last_space : q;
if (wrap_at >= file_size || wrap_at == cur_seg_off) break;
if (wrap_at >= bound || wrap_at == cur_seg_off) break;
uint8_t wrap_style = use_ls ? last_space_style : line_style;
if (use_ls) g_ncells = g_ncells_at_space; /* усечь до пробела */
emit_seg(wrap_at, wrap_style, ckind, 1);
base = ATTR_TEXT; /* continuation заголовка → PLAIN (в join base и так ATTR_TEXT) */
/* Префикс continuation-сегмента: список и отступленный абзац (#4) —
* col пробелов выравнивания; цитата — отступ + маркер 0xB3 + пробел.
* Для обычного абзаца без отступа col==0 → префикса нет. */
* Для обычного абзаца без отступа col==0 → префикса нет; CK_OTHER
* (заголовки/таблицы, NONE-режим) — префикса нет. */
if (ckind == CK_LIST || ckind == CK_PLAIN) {
gc_fill(' ', ATTR_TEXT, col);
} else if (ckind == CK_QUOTE) {
@@ -692,60 +648,132 @@ static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col,
return q;
}
/* Тонкая обёртка прежнего API: скан ровно [q, q_end) без склейки строк.
* Вызывается для заголовков и ячеек таблиц (всегда CK_OTHER, col=0). */
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)
{
(void)scan_stream(q, q_end, q, col, ckind, &line_style, base_attr, nowrap,
JOIN_MODE_NONE);
return line_style;
}
/* Потоковый сканер для index_lines(): mode задаёт правила '\n'-склейки. */
static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col,
uint8_t ckind, uint8_t *io_line_style, uint8_t mode)
{
return scan_stream(q, file_size, para_start, col, ckind, io_line_style,
ATTR_TEXT, 0, mode);
}
/* Отрисовка таблиц (row_end/table_*) — в модуле mdview2_table.c. */
/* Строит индекс сегментов рендера по загруженному файлу. */
void index_lines(void)
/* ================== Резюмируемый проход индексации =======================
* Бывший монолит index_lines() порезан на begin/step/finish, чтобы фоновая
* сборка второго набора (bg_step ядра) могла выполнять индексацию порциями
* в паузах между клавишами. Всё межшаговое состояние — в статиках модуля
* (idx_p/idx_in_block ниже + g_cells/cur_rec/g_trunc_* выше); оно НЕ входит
* в docset_t и переживает свапы наборов, потому что обработчики клавиш
* видимого набора индексатор не трогают. */
static uint32_t idx_p; /* позиция чтения исходника между шагами */
static uint8_t idx_in_block; /* внутри fenced code-блока */
static uint8_t idx_active; /* 1 = проход начат и ещё не финализирован */
/* Начинает новый проход (сброс позиции/буферов/причин обрыва). */
void index_begin(void)
{
uint8_t in_block = 0;
idx_p = 0;
idx_in_block = 0;
n_lines = 0;
index_truncated = 0;
g_trunc_cause = 0;
g_trunc_line = 0;
cur_seg_off = 0xFFFFFFFFu; /* пока не эмитили ни одного сегмента */
if (file_size == 0) return;
g_ncells = 0;
g_ncells_at_space = 0;
g_skip_flush = 0;
idx_active = 1;
}
uint32_t p = 0;
while (p < file_size && n_lines < max_lines && !g_abort && !g_trunc_cause) {
/* Хвост прохода: финальный флаш ячеек + строка-обрыв (бывший финал index_lines). */
static void index_finish(void)
{
idx_active = 0;
/* emit_seg() флашит ячейки с отставанием на одну строку (см. её
* комментарий) — последняя эмитированная строка файла так и остаётся
* незафлашенной, её ячейки ещё в g_cells; коммитим здесь. */
if (n_lines > 0) seg_commit_cells((uint16_t)(n_lines - 1));
/* UTF-8: держим конвертацию впереди позиции чтения (включая look-ahead
* склейки/классификации ~ один абзац). file_size при этом = доступная
* сконвертированная часть и растёт по ходу. */
if (g_utf_building && (uint32_t)(p + CONV_MARGIN) > file_size)
utf_convert_more((uint32_t)(p + CONV_MARGIN));
/* Лимит строк ловим ЗДЕСЬ, а не в emit_seg: главный цикл выходит по
* `n_lines < max_lines` ДО того, как emit_seg будет вызван в переполненном
* состоянии (для code-block — один emit_seg на строку, эта ветка не
* достигается). Признак — остановились, а файл не кончился; g_abort и
* исчерпание контента (TRUNC_CONTENT) уже обработаны/исключены. */
if (!g_trunc_cause && !g_abort && idx_p < file_size) {
g_trunc_cause = TRUNC_LINES;
g_trunc_line = (uint16_t)(max_lines - 1);
}
if (g_loading)
progress_tick(); /* первый экран + статус + скролл по готовым данным */
else if ((n_lines & 15) == 0)
spinner_tick();
/* Файл был обрезан до 256 КБ при загрузке, а индексация дошла до конца
* клампнутых данных без исчерпания контента/строк → ДОПИСЫВАЕМ строку
* (место есть: n_lines < max_lines). Приоритет ниже content/lines —
* те означают, что обрезали ещё раньше, их сообщение точнее. */
if (!g_trunc_cause && !g_abort && g_file_clamped) {
g_trunc_cause = TRUNC_FILE;
g_trunc_line = n_lines;
}
/* Обрыв по контенту/лимиту строк (не путать с пользовательским g_abort):
* документ заканчиваем строкой-сообщением на g_trunc_line. Сообщение
* рисуется вживую (IF_TRUNC_MSG) — место в контент-кэше ему не нужно, что
* и снимает тупик «кэш полон, а сообщение тоже надо закэшировать». */
if (g_trunc_cause && !g_abort) {
cache_rec_t r;
r.page = 0; r.off = 0; r.len = 0;
r.flags = IF_TRUNC_MSG; r.reserved = g_trunc_cause;
r.pad[0] = 0; r.pad[1] = 0;
cache_dir_put(g_trunc_line, &r);
n_lines = (uint16_t)(g_trunc_line + 1); /* документ кончается на строке-обрыве */
}
}
/* Одна итерация бывшего главного цикла index_lines(): классифицирует и
* эмитит очередной блок исходника (параграф/строку кода/таблицу целиком).
* do{}while(0) сохраняет исходные `continue` тела как выход из итерации
* с записью локальной p обратно в idx_p. */
static void index_iter(void)
{
if (g_loading)
progress_tick(); /* первый экран + статус + скролл по готовым данным */
else if (!g_bg_building && (n_lines & 15) == 0)
spinner_tick();
uint32_t p = idx_p;
do {
/* Пустая строка — разделитель параграфов.
* В индекс добавляем не более одной пустой экранной строки подряд. */
if (is_line_blank(p)) {
while (p < file_size && fb(p) != '\n') p++;
if (p < file_size) p++;
p = next_line(p);
/* Серия пустых строк схлопывается в одну визуальную строку. */
if (n_lines == 0 || !(cur_rec.flags & IF_BLANK)) {
emit_seg(p, INIT_STYLE_PLAIN, CK_PLAIN, 0);
set_blank_cur();
set_cur_flags(IF_BLANK);
}
continue;
}
/* ---- fenced code-блок (граница или тело) ---- */
if (is_fence_raw(p)) {
in_block = !in_block;
idx_in_block = !idx_in_block;
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
set_nowrap_cur();
while (p < file_size && fb(p) != '\n') p++;
if (p < file_size) p++;
set_cur_flags(IF_NOWRAP);
p = next_line(p);
continue;
}
if (in_block) {
if (idx_in_block) {
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
set_nowrap_cur();
set_code_cur();
set_hscroll_cur(); /* код скроллится целым блоком */
set_cur_flags(IF_NOWRAP | IF_CODE | IF_HSCROLL); /* код скроллится целым блоком */
{
uint8_t ccx = 0;
uint32_t pp = p;
@@ -763,18 +791,16 @@ void index_lines(void)
pp++;
}
}
while (p < file_size && fb(p) != '\n') p++;
if (p < file_size) p++;
p = next_line(p);
continue;
}
/* ---- горизонтальный разделитель ---- */
if (is_hr_raw(p)) {
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
set_nowrap_cur();
set_cur_flags(IF_NOWRAP);
gc_fill(0xC4, ATTR_HR, SCREEN_W);
while (p < file_size && fb(p) != '\n') p++;
if (p < file_size) p++;
p = next_line(p);
continue;
}
@@ -814,21 +840,21 @@ void index_lines(void)
/* Проход 2: верхняя рамка → строки/разделитель → нижняя рамка. */
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
set_nowrap_cur(); set_hscroll_cur();
set_cur_flags(IF_NOWRAP | IF_HSCROLL);
table_border(widths, ncols, TBL_TL, TBL_TM, TBL_TR);
r = p;
while (r < tbl_end) {
uint32_t le = row_end(r);
emit_seg(r, INIT_STYLE_PLAIN, CK_OTHER, 0);
set_nowrap_cur(); set_hscroll_cur();
set_cur_flags(IF_NOWRAP | IF_HSCROLL);
if (table_is_sep_row(r, le)) table_border(widths, ncols, TBL_ML, TBL_MM, TBL_MR);
else table_data_row(r, le, widths, ncols);
r = (le < file_size) ? le + 1 : le;
}
emit_seg(tbl_end, INIT_STYLE_PLAIN, CK_OTHER, 0);
set_nowrap_cur(); set_hscroll_cur();
set_cur_flags(IF_NOWRAP | IF_HSCROLL);
table_border(widths, ncols, TBL_BL, TBL_BM, TBL_BR);
p = tbl_end;
@@ -849,11 +875,9 @@ void index_lines(void)
default: hbase = ATTR_TEXT_TITLE4; break;
}
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
uint32_t line_end = content_off;
while (line_end < file_size && fb(line_end) != '\n') line_end++;
uint32_t line_end = row_end(content_off);
(void)inline_scan(content_off, line_end, 0, CK_OTHER, INIT_STYLE_PLAIN, hbase, 0);
while (p < file_size && fb(p) != '\n') p++;
if (p < file_size) p++;
p = next_line(p);
continue;
}
@@ -882,10 +906,7 @@ void index_lines(void)
q = scan_join_stream(q, p, col, CK_LIST, &line_style, JOIN_MODE_LIST);
/* Переходим к началу следующей физической строки после пункта. */
if (q < file_size) {
while (q < file_size && fb(q) != '\n') q++;
if (q < file_size) q++;
}
q = next_line(q);
/* Одна пустая строка между соседними маркерами списка подавляется.
* Две и более пустых строки оставляют видимый разделитель. */
@@ -893,8 +914,7 @@ void index_lines(void)
uint32_t t = q;
uint8_t blanks = 0;
while (t < file_size && is_line_blank(t)) {
while (t < file_size && fb(t) != '\n') t++;
if (t < file_size) t++;
t = next_line(t);
blanks++;
// if (blanks >= 2) break;
}
@@ -934,9 +954,7 @@ void index_lines(void)
}
q = scan_join_stream(q, p, col, CK_QUOTE, &line_style, JOIN_MODE_QUOTE);
while (q < file_size && fb(q) != '\n') q++;
if (q < file_size) q++;
p = q;
p = next_line(q);
continue;
}
@@ -962,48 +980,35 @@ void index_lines(void)
/* q стоит на завершающем newline параграфа.
* Переходим на следующую строку, чтобы внешняя петля её
* корректно переклассифицировала. */
while (q < file_size && fb(q) != '\n') q++;
if (q < file_size) q++;
p = q;
p = next_line(q);
}
}
}
/* emit_seg() флашит ячейки с отставанием на одну строку (см. её
* комментарий) — последняя эмитированная строка файла так и остаётся
* незафлашенной, её ячейки ещё в g_cells; коммитим здесь. */
if (n_lines > 0) seg_commit_cells((uint16_t)(n_lines - 1));
} while (0);
idx_p = p;
}
/* Лимит строк ловим ЗДЕСЬ, а не в emit_seg: главный цикл выходит по
* `n_lines < max_lines` ДО того, как emit_seg будет вызван в переполненном
* состоянии (для code-block — один emit_seg на строку, эта ветка не
* достигается). Признак — остановились, а файл не кончился; g_abort и
* исчерпание контента (TRUNC_CONTENT) уже обработаны/исключены. */
if (!g_trunc_cause && !g_abort && p < file_size) {
g_trunc_cause = TRUNC_LINES;
g_trunc_line = (uint16_t)(max_lines - 1);
}
/* Выполняет до budget итераций индексации. Возврат: 1 — проход завершён
* (индекс финализирован index_finish), 0 — есть ещё работа.
* Конвертация UTF-8 держится впереди позиции чтения ЗДЕСЬ, до проверки
* условия останова: пока исходник не сконвертирован до конца, idx_p не
* может «догнать» file_size и оборвать проход преждевременно. */
uint8_t index_step(uint8_t budget)
{
if (!idx_active) return 1;
while (budget-- > 0) {
/* UTF-8: держим конвертацию впереди позиции чтения (включая look-ahead
* склейки/классификации ~ один абзац). file_size при этом = доступная
* сконвертированная часть и растёт по ходу. */
if (g_utf_building && (uint32_t)(idx_p + CONV_MARGIN) > file_size)
utf_convert_more((uint32_t)(idx_p + CONV_MARGIN));
/* Файл был обрезан до 256 КБ при загрузке, а индексация дошла до конца
* клампнутых данных без исчерпания контента/строк → ДОПИСЫВАЕМ строку
* (место есть: n_lines < max_lines). Приоритет ниже content/lines —
* те означают, что обрезали ещё раньше, их сообщение точнее. */
if (!g_trunc_cause && !g_abort && g_file_clamped) {
g_trunc_cause = TRUNC_FILE;
g_trunc_line = n_lines;
}
/* Обрыв по контенту/лимиту строк (не путать с пользовательским g_abort):
* документ заканчиваем строкой-сообщением на g_trunc_line. Сообщение
* рисуется вживую (IF_TRUNC_MSG) — место в контент-кэше ему не нужно, что
* и снимает тупик «кэш полон, а сообщение тоже надо закэшировать». */
if (g_trunc_cause && !g_abort) {
cache_rec_t r;
r.page = 0; r.off = 0; r.len = 0;
r.flags = IF_TRUNC_MSG; r.reserved = g_trunc_cause;
r.pad[0] = 0; r.pad[1] = 0;
cache_dir_put(g_trunc_line, &r);
n_lines = (uint16_t)(g_trunc_line + 1); /* документ кончается на строке-обрыве */
if (!(idx_p < file_size && n_lines < max_lines && !g_abort && !g_trunc_cause)) {
index_finish();
return 1;
}
index_iter();
}
return 0;
}
#define EM_NONE 0
#define EM_BOLD 1
+1 -1
View File
@@ -172,7 +172,7 @@ static uint8_t calc_md_pct(void)
{
uint16_t total = drawable_lines();
if (total <= VIEW_H) return 100;
return (uint8_t)((uint32_t)top_line * 100UL / (uint32_t)(total - VIEW_H));
return pct16(top_line, (uint16_t)(total - VIEW_H));
}
/* ---- навигация MD после загрузки (peer к raw_key) ---------------- */
+22 -13
View File
@@ -19,8 +19,8 @@
#if WITH_RAW /* весь модуль — опционален (mdview_conf.h); при 0 пустой объектник */
static uint32_t raw_top = 0; /* байт-offset верхней строки экрана */
static uint16_t raw_hpan = 0; /* гориз. сдвиг (только HSCROLL) */
static uint32_t raw_top; /* байт-offset верхней строки экрана */
static uint16_t raw_hpan; /* гориз. сдвиг (только HSCROLL) */
/* ---- примитивы навигации по физическим строкам / переносам -------- */
@@ -161,30 +161,39 @@ static uint32_t raw_prev(uint32_t off)
/* ---- статус-бар: % по байтам (отрисовка — render_raw_status_numbers
* в mdview2_status.c, читает позицию через этот raw_pct) ---- */
/* Процент по байтам без 32-битного деления: масштабируем offset'ы (>>8),
* дальше 32-битная арифметика. raw_top приватен модулю, поэтому % считаем
* здесь и отдаём готовым числом в статус-модуль. */
/* Процент по байтам без 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);
if (b == 0) return 0; /* файл < 1 КБ */
return (uint8_t)(a * 100UL / b);
return pct16(a, b); /* den==0 (файл < 1 КБ) → pct16 вернёт 0 */
}
/* ---- посев позиции / пересев при смене активного буфера ----------- */
void raw_seed_from(uint16_t md_top_line)
uint32_t raw_pos(void)
{
uint32_t off = seg_off(md_top_line);
if (off > file_size) off = 0;
return raw_top;
}
/* Поставить RAW на начало строки, содержащей байт off (позиционирование
* при F2 MDRAW и F8-переносе позиции между наборами). */
void raw_reanchor(uint32_t off)
{
if (off > file_size) off = file_size;
raw_top = raw_line_start(off);
raw_hpan = 0;
}
/* Сброс в начало — при смене активного буфера (F8 в/из UTF-8: размеры/offset'ы
* разные, старый raw_top невалиден). Без percent-математики. */
void raw_home(void)
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;
+23 -7
View File
@@ -79,15 +79,16 @@ void status_encoding(void)
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)
{
static uint16_t local_total = UINT_MAX;
static uint16_t local_last = UINT_MAX;
static uint8_t local_loading = UCHAR_MAX;
uint16_t total = drawable_lines();
uint16_t last = top_line + VIEW_H;
if (last > total)
@@ -113,12 +114,12 @@ void render_md_status_numbers(void)
/* Полное обновление переменной части (MD): кодировка + числа. */
void render_full_status(void)
{
local_loading = UCHAR_MAX;
status_encoding();
render_md_status_numbers();
}
void render_percent_progress(uint8_t pct) {
static uint8_t local_pct = UCHAR_MAX;
if (local_pct != pct) {
local_pct = pct;
gotoxy(DIV2_X + 2, 0);
@@ -144,6 +145,16 @@ void render_raw_status(void)
}
#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)
@@ -152,9 +163,10 @@ void render_menu(void)
* ATTR_MENU_K, ведущий «чёрный» пробел кроме F10) + 6 поз. ТЕКСТ-функция
* (ATTR_MENU_T) сразу за номером. Номера рисуем для ВСЕХ 10 клавиш; текст
* только у задействованных и доступных сейчас. */
char num[3];
fill_row(MENU_ROW, ATTR_MENU_T);
char num[3];
num[2] = 0;
for (uint8_t i = 0; i < 9; i++) { /* F1..F9: ' 1'..' 9' */
num[0] = ' '; num[1] = (char)('1' + i);
@@ -166,9 +178,13 @@ void render_menu(void)
#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 (g_view != VIEW_MD) /* Wrap/Unwrap — только в RAW */
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);
+24 -47
View File
@@ -20,52 +20,10 @@ CC_FLAGS := -mz80 --no-std-crt0 --std-c99 --opt-code-size $(INC)
BUILD := $(PROJ_ROOT)/lib/build
# All libc C modules.
LIBC_C := \
libc/sys/atexit.c \
libc/conio/conio.c \
libc/conio/cprintf.c \
libc/conio/text_palette.c \
libc/bios/text.c \
libc/io/dir.c \
libc/video/videomode_raw.c \
libc/video/palette.c \
libc/errno/_errno_set.c \
libc/env/env.c \
libc/errno/errno.c \
libc/io/fsdir.c \
libc/io/lseek.c \
libc/mouse/mouse.c \
libc/io/open.c \
libc/io/read.c \
libc/time/sleep.c \
libc/time/time.c \
libc/time/posix_time.c \
libc/io/unlink.c \
libc/io/stat.c \
libc/mem/bank_io_w3.c \
libc/mem/bank_io_w1.c \
libc/mem/mem_estex.c \
libc/mem/mem_bios.c \
libc/gfx/gfx_core.c \
libc/gfx/gfx_palette.c \
libc/gfx/gfx_raw_common.c \
libc/gfx/gfx_raw_256.c \
libc/gfx/gfx_raw_16.c \
libc/gfx/gfx_256.c \
libc/gfx/gfx_16.c \
libc/gfx/gfx_font.c \
libc/gfx/gfx_text_256.c \
libc/gfx/gfx_text_16.c \
libc/stdio/getchar.c \
libc/stdio/putchar.c \
libc/stdio/puts.c \
libc/stdlib/minmax.c \
libc/file/file.c \
libc/stdio/hex_print.c \
libc/stdio/dec_print.c \
libc/string/strlwr.c \
libc/string/strupr.c
# All libc C modules — every .c under libc/<area>/ becomes its own .rel.
# One PUBLIC function per file (see docs/libc-split-asm-cases.md): the
# linker pulls whole .rel modules, so file granularity == DCE granularity.
LIBC_C := $(patsubst $(PROJ_ROOT)/%,%,$(wildcard $(PROJ_ROOT)/libc/*/*.c))
# Runtime modules to bundle (pulled by symbol references from libc-using code).
# NOTE: runtime/bank.s is NOT bundled — its trampoline depends on the banking
@@ -78,6 +36,16 @@ RUNTIME_RELS := $(patsubst runtime/%.s,$(BUILD)/%.rel,$(RUNTIME_S))
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
all: $(LIB)
@@ -94,7 +62,16 @@ $(BUILD)/%.rel: $(PROJ_ROOT)/runtime/%.s
$(SDASZ80) -o $@ $<
# 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 $@
$(SDAR) -rcs $@ $(ALL_RELS)
@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;
}
-302
View File
@@ -1,302 +0,0 @@
/*
* text.c wrappers for the Sprinter BIOS text-output calls (RST 8,
* 081h..08Eh "LP_*"). See <bios/text.h> for the full opcode -> name
* mapping and semantics.
*
* ABI recap (docs/converted/bios.txt):
* - 081h/082h/083h explicitly preserve HL and IX -> no push/pop ix
* needed, and the result is the smallest/fastest of the set.
* - 084h/08Eh (place get/set) are used without push/pop ix elsewhere
* in this libc (libc/conio/conio.c gotoxy/wherex/wherey via the same
* opcodes) empirically safe, followed here too.
* - Everything else clobbers IX like any other BIOS/ESTEX call, so
* every RST 8 below is bracketed with push ix / pop ix.
*
* Stack-argument layout: SDCC __sdcccall(1) places the first 8-bit arg
* in A, the second (only if the first was also 8-bit) in L; every arg
* beyond that lands on the stack in left-to-right declaration order,
* starting at [SP+2] (right after the 2-byte return address) verified
* empirically via `sdcc -S` for 2..6 uint8 args and for pointer+N uint8
* args. IY is used as a scratch index register to read those without
* disturbing IX (the caller's frame pointer).
*/
#include <bios/text.h>
void bios_fillcharattr(char ch, uint8_t attr, uint8_t count) __naked
{
(void)ch; (void)attr; (void)count;
__asm
;; ch->A, attr->L, count on stack at [SP+2].
;; LP_PRINT_ALL (081h): A=ch, E=attr, B=count. Preserves HL,IX.
ld iy, #2
add iy, sp
ld b, 0 (iy) ; B = count
ld e, l ; E = attr
ld c, #0x81
rst #0x08
pop hl ; return address
inc sp ; consume count byte
jp (hl)
__endasm;
}
void bios_fillchar(char ch, uint8_t count) __naked
{
(void)ch; (void)count;
__asm
;; ch->A, count->L. LP_PRINT_SYM (082h): A=ch, B=count.
;; Preserves HL,IX.
ld b, l
ld c, #0x82
rst #0x08
ret
__endasm;
}
void bios_fillattr(uint8_t attr, uint8_t count) __naked
{
(void)attr; (void)count;
__asm
;; attr->A, count->L. LP_PRINT_ATR (083h): E=attr, B=count.
;; Preserves HL,IX.
ld e, a
ld b, l
ld c, #0x83
rst #0x08
ret
__endasm;
}
void bios_set_place(uint8_t row, uint8_t col) __naked
{
(void)row; (void)col;
__asm
;; row->A, col->L. LP_SET_PLACE (084h): D=row, E=col.
ld d, a
ld e, l
ld c, #0x84
rst #0x08
ret
__endasm;
}
uint16_t bios_get_place(void) __naked
{
__asm
;; LP_GET_PLACE (08Eh): returns D=row, E=col already the
;; uint16_t return convention (DE); nothing left to rearrange.
ld c, #0x8E
rst #0x08
ret
__endasm;
}
void bios_writeattr(const char *s, uint8_t len, uint8_t attr) __naked
{
(void)s; (void)len; (void)attr;
__asm
;; s->HL, len/attr on stack at [SP+2]/[SP+3].
;; LP_PRINT_LN (085h): HL=s, B=len, E=attr.
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 ; return address
inc sp
inc sp ; consume len + attr
jp (hl)
__endasm;
}
void bios_write(const char *s, uint8_t len) __naked
{
(void)s; (void)len;
__asm
;; s->HL, len on stack at [SP+2].
;; LP_PRINT_LN2 (086h): HL=s, B=len.
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;
}
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
;; s->HL, len/attr/sep on stack at [SP+2..4].
;; LP_PRINT_LN3 (087h): HL=s, B=len, E=attr, D=sep.
;; Pads with spaces after sep up to len.
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 ; consume len + attr + sep
jp (hl)
__endasm;
}
void bios_write_until(const char *s, uint8_t len, char sep) __naked
{
(void)s; (void)len; (void)sep;
__asm
;; s->HL, len/sep on stack at [SP+2..3].
;; LP_PRINT_LN4 (088h): HL=s, B=len, D=sep. Pads with spaces.
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;
}
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
;; row->A, col->L, height/width/attr on stack at [SP+2..4].
;; LP_CLS_WIN (089h): D=row, E=col, H=height, L=width, B=attr.
ld iy, #2
add iy, sp
ld h, 0 (iy) ; H = height
ld c, 1 (iy) ; stash width (L is still col for now)
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 ; consume height + width + attr
jp (hl)
__endasm;
}
void bios_scrollwin(uint8_t dir, uint8_t row, uint8_t count) __naked
{
(void)dir; (void)row; (void)count;
__asm
;; dir->A, row->L, count on stack at [SP+2].
;; LP_SCROLL_UD (08Ah): B=dir(1 up/2 down), D=row, E=count.
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;
}
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
;; s->HL, maxlen/attr/sep on stack at [SP+2..4].
;; LP_PRINT_LN5 (08Bh): HL=s, B=maxlen, E=attr, D=sep.
;; Stops at sep (no padding).
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;
}
void bios_write_stop(const char *s, uint8_t maxlen, char sep) __naked
{
(void)s; (void)maxlen; (void)sep;
__asm
;; s->HL, maxlen/sep on stack at [SP+2..3].
;; LP_PRINT_LN6 (08Ch): HL=s, B=maxlen, D=sep. Stops at sep.
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;
}
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
;; row->A, col->L, height/width/attr/fillch on stack [SP+2..5].
;; LP_CLS_WIN2 (08Dh): D=row, E=col, H=height, L=width, B=attr,
;; A=fillch.
ld iy, #2
add iy, sp
ld d, a ; D = row (free A for fillch later)
ld e, l ; E = col (free L for width later)
ld h, 0 (iy) ; H = height
ld c, 1 (iy) ; stash 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 ; consume height + width + attr + fillch
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;
}
-678
View File
@@ -1,678 +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). */
static two_bytes g_text_attr = {0};
static uint8_t pc_ch = 0;
extern two_bytes pc_place = {0};
static uint8_t pc_raw_mode = 0;
// TODO - проверить - ф-ии 30h-33h (kbhit/getch/getche/getkey)
// не должны менять IX и им можно не делать push ix / pop ix
//
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.
*/
/* 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. */
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, #0x8e ; BIOS GetCursor
rst #0x08
ld (_pc_place), de
; 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 de, (_pc_place)
ld c, #0x84 ; BIOS SetCursor
rst #0x08
; 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 _putch_wrchar(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.
;; ld c, a
;; ld a, (_pc_ch) ; A = ch (`ld a,(nn)` does not touch flags)
;; jr nz, _rp0_pri
;; ld a, 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
;; Anything else (printable or unrecognised ctrl) glyph.
_rp0_pri:
ld c, a ; C = ch (save before A is clobbered)
ld a, (_pc_place + 1)
cp #32
ret nc ; off-screen bottom silently skip
ld d, a ; D = row (ESTEX WRCHAR convention)
ld a, (_pc_place)
cp #80
ret nc ; off-screen right silently skip
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 ; already at col 0 no change
dec a
ld (_pc_place), a
ld e, a ; E = col
ld a, (_pc_place + 1)
ld d, a ; D = row (ESTEX WRCHAR convention)
ld c, #0x20
jr _rp0_wr
_rp0_tab:
ld a, (_pc_place)
or #0x07 ; floor to mult of 8
inc a ; next mult of 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 ; already at bottom edge
inc a
ld (_pc_place + 1), a
ret
_rp0_cr:
xor a, a
ld (_pc_place), a
ret
__endasm;
}
/* Mode-1 worker: every byte goes through WRCHAR as a glyph. */
static void _putch_wrchar_raw(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_place + 1)
cp #32
ret nc ; off-screen bottom silently skip
ld d, a ; D = row
ld a, (_pc_place)
cp #80
ret nc ; off-screen right silently skip
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;
}
/* ---- 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)
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_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) ; return value
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
push ix
call __get_cursor
;; KEEP_EXIST_ATTR? high byte of 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 ; restore callers IX
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 (_pc_place), de
ld c, #0x84 ; BIOS SetCursor
rst #0x08
;; 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, #0x8e ; BIOS GetCursor
rst #0x08
;; ld c, #0x53
;; rst #0x10
;; pop ix
ld a, e
ld (_pc_place), de
ret
__endasm;
}
uint8_t wherey(void) __naked
{
__asm
;; push ix
ld c, #0x8e ; BIOS GetCursor
rst #0x08
;; ld c, #0x53
;; rst #0x10
;; pop ix
ld a, d
ld (_pc_place), de
ret
__endasm;
}
uint16_t wherexy(void) __naked
{
__asm
;; ESTEX CURSOR ($53): D = row, E = col. Return col in DE.
;; push ix
ld c, #0x8e ; BIOS GetCursor
rst #0x08
;; ld c, #0x53
;; rst #0x10
;; pop ix
ld (_pc_place), de
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 gettextmode(void)
{
return _videomode_raw_get();
}
int settextmode(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 set_text_attr(int16_t attr)
{
int16_t prev = g_text_attr.value;
g_text_attr.value = attr;
return prev;
}
int16_t get_text_attr(void)
{
return g_text_attr.value;
}
/* ---- 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)
{
fg = (fg & 0x07);
g_text_attr.value = (g_text_attr.byte.low & 0xF0) | fg;
// g_text_attr.byte.high = 0;
}
void textbackground(uint8_t bg)
{
bg = (bg & 0x07) << 4;
g_text_attr.value = (g_text_attr.byte.low & 0x0F) | bg;
// g_text_attr.byte.high = 0;
}
void textattr(uint8_t attr)
{
g_text_attr.value = (uint16_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
* buffer with vsprintf (from SDCC's stdlib), then emits via cputs which
* applies the current text attribute per character.
* cprintf printf для conio-набора вывода. Форматирует в статический
* буфер через vsprintf (из stdlib SDCC), затем выводит cputs'ом,
* который применяет текущий текстовый атрибут посимвольно.
*
* No '\n' to CR LF translation Turbo-C convention: callers write
* "\r\n" explicitly in the format string for line breaks.
* '\n' в CR LF НЕ транслируется конвенция Turbo-C: перевод строки
* пишется в format-строке явно как "\r\n".
*
* Not reentrant (single static buffer) but Z80 single-threaded is fine.
* Не реентерабельно (один статический буфер), но на однопоточном Z80
* это не проблема.
*/
#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;
}
+22
View File
@@ -0,0 +1,22 @@
/*
* settextmode установить ТЕКСТОВЫЙ видеорежим (TEXT_MODE_40x32 /
* TEXT_MODE_80x32). Любое другое значение отвергается с EINVAL
* иначе случайный GFX_MODE_* мог бы выдернуть экран из-под текстового
* I/O. Возвращает 0 или -1 + errno.
*/
#include <conio.h>
#include <stdint.h>
#include <errno.h>
extern int _videomode_raw_set(uint8_t mode);
int settextmode(uint8_t mode)
{
/* Отвергаем всё, что не известный текстовый режим. */
if (mode != TEXT_MODE_40x32 && mode != TEXT_MODE_80x32) {
errno = EINVAL;
return -1;
}
return _videomode_raw_set(mode);
}
+12
View File
@@ -0,0 +1,12 @@
/*
* text_pal_get прочитать блок записей текстовой палитры;
* план 0..3 транслируется в палитровую страницу BIOS 4..7.
*/
#include <conio.h>
#include <palette.h>
void text_pal_get(uint8_t plane, uint8_t start, uint8_t count, uint8_t *data)
{
pal_get((uint8_t)(plane + 4u), start, count, data);
}
+13
View File
@@ -0,0 +1,13 @@
/*
* text_pal_get_color прочитать одну запись текстовой палитры в R/G/B;
* план 0..3 транслируется в палитровую страницу BIOS 4..7.
*/
#include <conio.h>
#include <palette.h>
void text_pal_get_color(uint8_t plane, uint8_t attr,
uint8_t *r, uint8_t *g, uint8_t *b)
{
pal_get_color((uint8_t)(plane + 4u), attr, r, g, b);
}
+14
View File
@@ -0,0 +1,14 @@
/*
* text_pal_load загрузить блок записей текстовой палитры.
* Текстовый «план» 0..3 (paper/ink/blink-paper/blink-ink) транслируется
* в палитровую страницу BIOS 4..7 (см. <palette.h>).
*/
#include <conio.h>
#include <palette.h>
void text_pal_load(uint8_t plane, uint8_t start, uint8_t count,
const uint8_t *data)
{
pal_load((uint8_t)(plane + 4u), start, count, data);
}
+12
View File
@@ -0,0 +1,12 @@
/*
* text_pal_reset восстановить системную текстовую палитру
* (BIOS $A6, type = PAL_CGA, страницы 4..7).
*/
#include <conio.h>
#include <palette.h>
void text_pal_reset(void)
{
pal_reset(PAL_CGA);
}
+13
View File
@@ -0,0 +1,13 @@
/*
* text_pal_set_color одна запись текстовой палитры из RGB;
* план 0..3 транслируется в палитровую страницу BIOS 4..7.
*/
#include <conio.h>
#include <palette.h>
void text_pal_set_color(uint8_t plane, uint8_t attr,
uint8_t r, uint8_t g, uint8_t b)
{
pal_set_color((uint8_t)(plane + 4u), attr, r, g, b);
}
-42
View File
@@ -1,42 +0,0 @@
/*
* text_palette.c text-mode palette wrappers.
*
* Thin layer over libc/video/palette.c. Translates the text "plane
* number" 0..3 (paper/ink/blink-paper/blink-ink) into the underlying
* BIOS palette page 4..7 used by $A4.
*
* For an introduction to the four-plane text colour model see
* <palette.h> (top-of-file doc-block).
*/
#include <stdint.h>
#include <conio.h>
#include <palette.h>
void text_pal_load(uint8_t plane, uint8_t start, uint8_t count,
const uint8_t *data)
{
pal_load((uint8_t)(plane + 4u), start, count, data);
}
void text_pal_set_color(uint8_t plane, uint8_t attr,
uint8_t r, uint8_t g, uint8_t b)
{
pal_set_color((uint8_t)(plane + 4u), attr, r, g, b);
}
void text_pal_get(uint8_t plane, uint8_t start, uint8_t count, uint8_t *data)
{
pal_get((uint8_t)(plane + 4u), start, count, data);
}
void text_pal_get_color(uint8_t plane, uint8_t attr,
uint8_t *r, uint8_t *g, uint8_t *b)
{
pal_get_color((uint8_t)(plane + 4u), attr, r, g, b);
}
void text_pal_reset(void)
{
pal_reset(PAL_CGA);
}
+11
View File
@@ -0,0 +1,11 @@
/*
* textattr заменить атрибут целиком одним байтом (fg | bg | blink).
* Turbo-C-стиль. Сбрасывает KEEP_EXIST_ATTR (старший байт = 0).
*/
#include "_conio.h"
void textattr(uint8_t attr)
{
g_text_attr.value = (uint16_t)attr;
}
+13
View File
@@ -0,0 +1,13 @@
/*
* textbackground установить цвет фона (биты 4..6 атрибута),
* цвет символов и бит blink сохраняются. Turbo-C-стиль. Попутно
* сбрасывает KEEP_EXIST_ATTR (старший байт становится 0).
*/
#include "_conio.h"
void textbackground(uint8_t bg)
{
bg = (bg & 0x07) << 4;
g_text_attr.value = (g_text_attr.byte.low & 0x0F) | bg;
}
+13
View File
@@ -0,0 +1,13 @@
/*
* textcolor установить цвет символов (нижний нибл атрибута),
* фон и бит blink сохраняются. Turbo-C-стиль. Попутно сбрасывает
* KEEP_EXIST_ATTR (старший байт становится 0).
*/
#include "_conio.h"
void textcolor(uint8_t fg)
{
fg = (fg & 0x07);
g_text_attr.value = (g_text_attr.byte.low & 0xF0) | fg;
}
+17
View File
@@ -0,0 +1,17 @@
/*
* wherex текущая колонка курсора (0..79).
* Читает курсор BIOS GetCursor (rst 8, $8E), попутно обновляя pc_place.
*/
#include "_conio.h"
uint8_t wherex(void) __naked
{
__asm
ld c, #0x8e ; BIOS GetCursor: D = row, E = col
rst #0x08
ld a, e
ld (_pc_place), de
ret
__endasm;
}
+17
View File
@@ -0,0 +1,17 @@
/*
* wherexy обе координаты курсора одним вызовом:
* (row << 8) | col в DE. Читает курсор BIOS GetCursor (rst 8, $8E),
* попутно обновляя pc_place.
*/
#include "_conio.h"
uint16_t wherexy(void) __naked
{
__asm
ld c, #0x8e ; BIOS GetCursor: D = row, E = col
rst #0x08
ld (_pc_place), de
ret ; __sdcccall(1): uint16_t в DE
__endasm;
}
+17
View File
@@ -0,0 +1,17 @@
/*
* wherey текущая строка курсора (0..31).
* Читает курсор BIOS GetCursor (rst 8, $8E), попутно обновляя pc_place.
*/
#include "_conio.h"
uint8_t wherey(void) __naked
{
__asm
ld c, #0x8e ; BIOS GetCursor: D = row, E = col
rst #0x08
ld a, d
ld (_pc_place), de
ret
__endasm;
}
+32
View File
@@ -0,0 +1,32 @@
/*
* wrchar записать символ ch с атрибутом attr в позицию (x, y), не
* трогая курсор. ESTEX WRCHAR ($58).
*
* SDCC __sdcccall(1): x в A, y в L (2 uint8 A, L); ch и attr
* упакованы и запушены на стек одной 16-битной парой (вызывающий
* делает `ld hl, #(attr<<8)|ch; push hl`). Раскладка после CALL:
* [SP+0..1] = адрес возврата
* [SP+2] = ch (младшая половина пары)
* [SP+3] = attr (старшая половина)
* Void-возврат callee снимает 2 байта стековых аргументов через
* `pop bc` + jp (iy).
*/
#include "_conio.h"
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 ; адрес возврата
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;
}
+17
View File
@@ -0,0 +1,17 @@
/*
* z80_inp прочитать байт из порта Z80 (опкод IN). Обёртка с
* стабильным C API; имя как в Solid-C / MS-DOS Turbo-C.
*/
#include "_conio.h"
uint8_t z80_inp(uint8_t port) __naked
{
(void)port;
__asm
;; SDCC __sdcccall(1): единственный uint8_t аргумент в A; возврат в A.
ld c, a
in a, (c)
ret
__endasm;
}
+17
View File
@@ -0,0 +1,17 @@
/*
* z80_outp записать байт value в порт Z80 (опкод OUT). Обёртка с
* стабильным C API; имя как в Solid-C / MS-DOS Turbo-C.
*/
#include "_conio.h"
void z80_outp(uint8_t port, uint8_t value) __naked
{
(void)port; (void)value;
__asm
;; __sdcccall(1): 2 аргумента uint8 arg1 в A, arg2 в L.
ld c, a ; C = port
out (c), l ; out (port), value
ret
__endasm;
}
-93
View File
@@ -1,93 +0,0 @@
/*
* getenv / putenv via ESTEX ENVIRON ($46).
*
* ESTEX $46 subfn 1 (getenv):
* in: HL = name (ASCIIZ), DE = output buffer (caller-owned)
* out: CF=0 + A != 0 variable found, value written into [DE..end-1]
* CF=0 + A == 0 variable not present (note: this is the
* opposite of what DiskSyscalls.txt v1.6 docs
* claim verified empirically and against
* solid-c's IO.ASM:763-766 implementation)
* CF=1 error, A = code
* DE on exit points at one past the last byte written
*
* ESTEX $46 subfn 2 (putenv):
* in: HL = "NAME=value" (NUL-terminated)
* out: CF=1 / A = error code on failure
*
* We hand back a pointer into a private 128-byte buffer for getenv().
* Caller must copy the bytes before the next getenv() call if they
* need to outlive it.
*/
#include <stdint.h>
#include <sprinter.h>
static char env_buf[128];
char *getenv(const char *name) __naked
{
(void)name;
__asm
push ix
;; HL = name on entry; we need to also load DE = env_buf.
ld de, #_env_buf
ld bc, #0x0146 ; ESTEX ENVIRON; subfn: getenv
rst #0x10
pop ix
jr c, _getenv_err
or a, a
jr Z, _getenv_miss
ld de, #_env_buf
ret
_getenv_err:
call __errno_set
_getenv_miss:
ld de, #0
ret
__endasm;
}
int putenv(const char *namevalue) __naked
{
(void)namevalue;
__asm
push ix
ld bc, #0x0246 ; ESTEX ENVIRON; subfn: setenv
rst #0x10
pop ix
jr c, _putenv_err
ld de, #0
ret
_putenv_err:
call __errno_set
ld de, #-1
ret
__endasm;
}
/* ESTEX $46 subfn 0 (sysenv):
* in: HL = caller-owned buffer
* out: caller's buffer is filled with one NUL-terminated "NAME=value"
* per env var, then a trailing extra NUL marks the end:
* "PATH=...\0SOLID=H\0\0"
* return: buf on success, -1 on error (errno set).
* Buffer must be large enough for the whole environment.
*/
char *sysenv(char *buf) __naked
{
(void)buf;
__asm
push ix
push hl ; stash buffer pointer (= return value)
ld bc, #0x0046 ; ESTEX ENVIRON; subfn 0 = sysenv
rst #0x10
pop de ; DE = buffer pointer
pop ix
ret nc ; success: DE already has buf
;; CF=1 A = ESTEX error code.
call __errno_set
ld de, #-1
ret
__endasm;
}
+41
View File
@@ -0,0 +1,41 @@
/*
* getenv значение переменной окружения через ESTEX ENVIRON ($46, subfn 1):
* in: HL = имя (ASCIIZ), DE = выходной буфер
* out: CF=0 + A != 0 найдена, значение записано в [DE..end-1]
* CF=0 + A == 0 переменной нет (примечание: это ПРОТИВОПОЛОЖНО
* тому, что пишет DiskSyscalls.txt v1.6 проверено
* эмпирически и по solid-c IO.ASM:763-766)
* CF=1 ошибка, A = код
*
* Возвращает указатель в приватный 128-байтный буфер скопируй до
* следующего вызова getenv(), если значение должно пережить его.
* NULL нет переменной или ошибка (errno).
*/
#include <stdint.h>
#include <sprinter.h>
static char env_buf[128];
char *getenv(const char *name) __naked
{
(void)name;
__asm
push ix
;; HL = имя на входе; дополнительно DE = env_buf.
ld de, #_env_buf
ld bc, #0x0146 ; ESTEX ENVIRON; subfn: getenv
rst #0x10
pop ix
jr c, _getenv_err
or a, a
jr Z, _getenv_miss
ld de, #_env_buf
ret
_getenv_err:
call __errno_set
_getenv_miss:
ld de, #0
ret
__endasm;
}
+26
View File
@@ -0,0 +1,26 @@
/*
* putenv установить переменную окружения строкой "NAME=value" (ASCIIZ).
* ESTEX ENVIRON ($46, subfn 2): HL = строка; CF=1 / A = код при ошибке.
* Возвращает 0 или -1 + errno.
*/
#include <stdint.h>
#include <sprinter.h>
int putenv(const char *namevalue) __naked
{
(void)namevalue;
__asm
push ix
ld bc, #0x0246 ; ESTEX ENVIRON; subfn: setenv
rst #0x10
pop ix
jr c, _putenv_err
ld de, #0
ret
_putenv_err:
call __errno_set
ld de, #-1
ret
__endasm;
}
+28
View File
@@ -0,0 +1,28 @@
/*
* sysenv снять всё окружение в буфер вызывающего.
* ESTEX ENVIRON ($46, subfn 0): HL = буфер; на выходе буфер заполнен
* ASCIIZ-строками "NAME=value" подряд, конец дополнительный NUL:
* "PATH=...\0SOLID=H\0\0"
* Буфер должен вмещать всё окружение. Возвращает buf или -1 + errno.
*/
#include <stdint.h>
#include <sprinter.h>
char *sysenv(char *buf) __naked
{
(void)buf;
__asm
push ix
push hl ; спрятать указатель (= возврат)
ld bc, #0x0046 ; ESTEX ENVIRON; subfn 0 = sysenv
rst #0x10
pop de ; DE = указатель буфера
pop ix
ret nc ; успех: DE уже держит buf
;; CF=1 A = код ошибки ESTEX.
call __errno_set
ld de, #-1
ret
__endasm;
}
+22 -22
View File
@@ -1,40 +1,40 @@
/*
* _errno_set set `errno` from an ESTEX error code (0..255 in A).
* _errno_set установить `errno` из кода ошибки ESTEX (0..255 в A).
*
* Replaces the inline pattern
* Заменяет inline-паттерн
* ld (_errno), a
* xor a, a
* ld (_errno+1), a ; 7 bytes per error path
* with a single
* call __errno_set ; 3 bytes per error path
* ld (_errno+1), a ; 7 байт на каждый error path
* одним
* call __errno_set ; 3 байта на error path
*
* Saves ~4 bytes at every libc error handler that converts an ESTEX
* code into the C-side `errno`. Helper itself is 7 bytes; with 10+
* error paths in our libc the size win is net-positive.
* Экономит ~4 байта в каждом обработчике ошибок libc, конвертирующем
* код ESTEX в C-шный errno. Сам хелпер 7 байт; при 10+ error path
* в нашей libc выигрыш по размеру суммарно положительный.
*
* ABI:
* in: A = ESTEX error code (0..255)
* out: HL = A (zero-extended); errno fully overwritten so a prior
* large value (e.g. errno = -1) can't leak its high byte.
* clobbers: HL, AF flags. Caller must not depend on HL afterwards.
* вход: A = код ошибки ESTEX (0..255)
* выход: errno перезаписан целиком, чтобы прежнее большое значение
* (например errno = -1) не оставило свой старший байт.
* клоббер: A, флаги. HL/BC/DE/IX/IY не трогаются.
*
* Defensive 16-bit store see chat 2026-06-02: if anyone ever assigns
* errno via C (`errno = -1`), the high byte becomes 0xFF, and a partial
* 8-bit write here would leave that 0xFF in place. Always writing the
* full word keeps errno honest regardless of who set it last.
* Защитная 16-битная запись см. чат 2026-06-02: если кто-то присвоит
* errno из C (`errno = -1`), старший байт станет 0xFF, и частичная
* 8-битная запись здесь оставила бы этот 0xFF на месте. Полное слово
* держит errno честным независимо от того, кто писал последним.
*/
void _errno_set(unsigned char code) __naked
{
(void)code;
__asm
;; __sdcccall(1): single uint8_t arg already in A.
;; Write the two bytes separately so HL/BC/DE/IX/IY remain
;; untouched. Only A is clobbered: it is the input register,
;; and ABI does not require preserving it across a void call.
ld (_errno), a ; low byte = code
;; __sdcccall(1): единственный uint8_t-аргумент уже в A.
;; Пишем два байта по отдельности, чтобы HL/BC/DE/IX/IY остались
;; нетронутыми. Клобберится только A: это входной регистр, и
;; ABI не требует сохранять его через void-вызов.
ld (_errno), a ; младший байт = код
xor a, a
ld (_errno+1), a ; high byte = 0
ld (_errno+1), a ; старший байт = 0
ret
__endasm;
}
+18
View File
@@ -0,0 +1,18 @@
/*
* perror напечатать "prefix: <strerror(errno)>\r\n" в stderr.
* Пустой/NULL prefix печатается только текст ошибки.
*/
#include <errno.h>
#include <stdio.h>
#include <string.h>
void perror(const char *prefix)
{
if (prefix && *prefix) {
fputs(prefix, stderr);
fputs(": ", stderr);
}
fputs(strerror(errno), stderr);
fputs("\r\n", stderr);
}
+10 -24
View File
@@ -1,24 +1,20 @@
/*
* errno.c strerror / perror over the SDCC-provided `errno` global.
* strerror текст ошибки по коду ESTEX (0..32); вне диапазона
* "Unknown error". Таблица повторяет solid-c IO.ASM (английские
* формулировки сохранены для grep-ability). Таблица и функция
* неразделимы живут одним модулем.
*
* The message table mirrors the one in solid-c's IO.ASM (we kept the
* English wording for grep-ability). ESTEX returns codes 0..32 in the
* meaningful range; anything beyond gets "Unknown error".
*
* Note: we deliberately do NOT define `_errno` here SDCC's
* z80.lib/errno.rel provides it (a single int in _DATA), and our libc
* wrappers (read.c, open.c, etc.) just assign to `errno`. This
* removes the "multiple definition of _errno" link warning.
* Примечание: `_errno` здесь сознательно НЕ определяется его даёт
* SDCC z80.lib/errno.rel (int в _DATA), наши обёртки просто пишут в
* `errno`. Это убирает варнинг "multiple definition of _errno".
*/
#include <errno.h>
#include <stdio.h>
#include <string.h>
/*
* Stored verbatim pointers in the lookup table cost 2 bytes each plus
* the message bytes themselves. Sentinel "" entries pad out gaps so
* indexing stays direct.
* Хранится как есть указатель в таблице стоит 2 байта плюс сами
* байты сообщения. Заглушки "RESERVED" затыкают дыры, чтобы
* индексирование оставалось прямым.
*/
static const char *const messages[] = {
/* 0 */ "No error",
@@ -65,13 +61,3 @@ const char *strerror(int err)
}
return messages[err];
}
void perror(const char *prefix)
{
if (prefix && *prefix) {
fputs(prefix, stderr);
fputs(": ", stderr);
}
fputs(strerror(errno), stderr);
fputs("\r\n", stderr);
}
+14
View File
@@ -0,0 +1,14 @@
/*
* _fclosall сбросить буферы и закрыть все открытые потоки таблицы.
* Регистрируется через atexit() первым fopen так exit() посреди
* программы не теряет несброшенную запись (требование стандарта).
*/
#include "_file.h"
void _fclosall(void)
{
for (uint8_t i = 0; i < OPEN_MAX; i++) {
if (_file_slots[i].flags) fclose(&_file_slots[i]);
}
}
+50
View File
@@ -0,0 +1,50 @@
/*
* _file.h внутренности буферизованного FILE* (вариант B+, НЕ
* публичный заголовок). Дизайн: docs/file-buffering-design.md.
*
* Инварианты:
* - слот таблицы свободен, когда flags == 0;
* - level < BUFSIZ на входе в любую операцию записи (конвенция
* «сброс ПЕРЕД записью в буфер»);
* - hold != EOF означает отложенный байт ungetc; логическая позиция
* потока на 1 меньше учтённой в level;
* - консольные псевдопотоки (_F_CONIN/_F_CONOUT) буфер не заводят
* и в _file_sync не попадают.
*/
#ifndef _FILE_INTERNAL_H
#define _FILE_INTERNAL_H
#include <stdio.h>
/* Таблица потоков (_file_slots.c); свободный слот: flags == 0. */
extern FILE _file_slots[OPEN_MAX];
/* Найти свободный слот и взвести atexit(_fclosall) при первом
* использовании. Слот НЕ помечается занятым вызывающий заполняет
* поля и последним ставит flags. NULL + errno=EMFILE, если все
* заняты. (_file_slot.c; используют fopen и fdopen.) */
FILE *_file_slot_take(void);
/* Транслировать строку режима fopen ("r/w/a", '+' в любом месте
* хвоста, 'b'/'t' игнорируются) в флаги open() (возврат) и наши
* _F_-флаги (*ff). -1 + errno=EINVAL при плохом режиме.
* (_file_mode.c; используют fopen, fdopen, freopen.) */
int _file_mode_flags(const char *mode, uint8_t *ff);
/* Обеспечить буфер: вернуть fp->buf, при необходимости malloc(BUFSIZ).
* NULL = malloc не дал вызывающий уходит на небуферизованный путь
* (прямой syscall на 1 байт), поток продолжает работать. */
uint8_t *_file_buf(FILE *fp);
/* Нейтрализовать буфер (перед seek/tell/close и при смене направления):
* - направление «запись»: write() несброшенного хвоста;
* - направление «чтение»: lseek(fd, -(level + hold), SEEK_CUR)
* откат непрочитанного readahead и ungetc-байта.
* После вызова: level = 0, hold = EOF, _F_DIROUT снят, curp = buf.
* Возврат 0 или EOF (ставит _F_ERROR). */
int _file_sync(FILE *fp);
/* fclose всех открытых слотов; вешается на atexit первым fopen. */
void _fclosall(void);
#endif
+18
View File
@@ -0,0 +1,18 @@
/*
* _file_buf ленивое выделение буфера потока: malloc(BUFSIZ) при
* первой буферизуемой операции. NULL при нехватке памяти поток
* НЕ ломается, вызывающие уходят на прямой syscall (небуферизованный
* fallback), попытка выделения повторится при следующей операции.
*/
#include <stdlib.h>
#include "_file.h"
uint8_t *_file_buf(FILE *fp)
{
if (!fp->buf) {
fp->buf = (uint8_t *)malloc(BUFSIZ);
fp->curp = fp->buf;
}
return fp->buf;
}
+43
View File
@@ -0,0 +1,43 @@
/*
* _file_mode_flags парсер строки режима fopen/fdopen/freopen.
* Поддержаны r, w, a; '+' в любом месте хвоста; 'b'/'t' игнорируются
* весь I/O на Sprinter бинарный.
*/
#include <fcntl.h>
#include <errno.h>
#include "_file.h"
int _file_mode_flags(const char *mode, uint8_t *ff)
{
if (!mode || !*mode) {
errno = EINVAL;
return -1;
}
int oflags = 0;
uint8_t f = 0;
char base = *mode;
int plus = 0;
for (const char *p = mode + 1; *p; p++) {
if (*p == '+') plus = 1;
}
switch (base) {
case 'r':
oflags = plus ? O_RDWR : O_RDONLY;
f = _F_READ | (plus ? _F_WRITE : 0);
break;
case 'w':
oflags = (plus ? O_RDWR : O_WRONLY) | O_CREAT | O_TRUNC;
f = _F_WRITE | (plus ? _F_READ : 0);
break;
case 'a':
oflags = (plus ? O_RDWR : O_WRONLY) | O_CREAT | O_APPEND;
f = _F_WRITE | _F_APPEND | (plus ? _F_READ : 0);
break;
default:
errno = EINVAL;
return -1;
}
*ff = f;
return oflags;
}
+30
View File
@@ -0,0 +1,30 @@
/*
* _file_slot_take найти свободный слот таблицы потоков и при первом
* обращении взвести atexit(_fclosall), чтобы exit() сбрасывал
* несброшенную запись всех потоков (требование стандарта).
* Слот НЕ помечается занятым вызывающий (fopen/fdopen) заполняет
* поля и последним выставляет flags.
*/
#include <sprinter_exit.h> /* atexit */
#include <errno.h>
#include "_file.h"
FILE *_file_slot_take(void)
{
static uint8_t atexit_armed;
FILE *fp = _file_slots;
for (uint8_t i = 0; ; i++, fp++) {
if (i >= OPEN_MAX) {
errno = EMFILE;
return 0;
}
if (fp->flags == 0) break;
}
if (!atexit_armed) {
atexit_armed = 1;
atexit(_fclosall);
}
return fp;
}
+12
View File
@@ -0,0 +1,12 @@
/*
* _file_slots статическая таблица потоков FILE (только данные).
*
* OPEN_MAX слотов; свободный слот распознаётся по flags == 0 (BSS
* зануляется crt0). malloc для структур не используется лимит
* одновременных файлов у DSS всё равно фиксированный (ошибка 06h
* "Too many open files"), таблица его зеркалит.
*/
#include "_file.h"
FILE _file_slots[OPEN_MAX];

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