16 Commits

Author SHA1 Message Date
snark13 977e2d3d4a mdview2: вернуть floppy к обычному README-диску (тест-каркас отработал)
Тест-файлы лимита 256 КБ (TABLES/LINES/HUGE) проверены в MAME и сняты с
диска. Сами файлы и генератор остаются в testfiles/ как архив для повтора.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 14:22:08 +03:00
25 changed files with 49730 additions and 2085 deletions
+195
View File
@@ -0,0 +1,195 @@
# Fast RAM (Быстрое ОЗУ / «КЭШ-ОЗУ») на Sprinter
Сводка по результатам изучения документации платформы. Источники:
- `docs/converted/Architecture.txt` — официальное «Описание архитектуры» (раздел
«Распределение основной памяти»).
- `docs/converted/ARHITECT.txt` — ранняя редакция того же документа (про загрузку
конфигураций ППЛМ).
- `docs/converted/IvanMak.txt` / `docs/converted/Parinov.txt` / `docs/converted/Forum.txt`
и `docs/part2/forum.txt` — форумные ответы Дениса Паринова (Sprinter Team) и
руководство Ивана Мака (раздел «7 КЭШ-ОЗУ»).
- `docs/part2/accelerator_doc.txt` — ограничение акселератора.
- `docs/samples/sprinterIntLib.asm` — практический пример temporary-off / restore.
> **Терминология.** В документации одно и то же ОЗУ называется тремя именами:
> **Fast RAM**, **Быстрое ОЗУ** и **«КЭШ-ОЗУ»**. Это *не* кэш в формальном смысле
> (нет автоматического заполнения/вытеснения) — это отдельный массив статической
> памяти, в котором процессор работает на полной частоте **без тактов ожидания**.
> Имя «КЭШ» — историческое, по аналогии с кэшем на КР537РУ10 в Pentagon-128.
---
## 1. Что это и зачем
* **Объём:** 64 КБ статической памяти (SRAM), отдельной от основного DRAM-SIMM
(4 МБ) и от видео-ОЗУ (256 КБ).
* **Скорость:** процессор обращается к Fast RAM на полной тактовой частоте
(21 МГц) **без wait-state'ов**. Основное ОЗУ (DRAM) требует тактов ожидания,
поэтому код и данные в Fast RAM исполняются/читаются заметно быстрее.
* **Назначение:** разместить «горячий» код или данные (внутренние циклы,
таблицы, буферы), которые критичны по скорости.
* **Системная роль:** Fast RAM также используется механизмом
переконфигурирования ППЛМ — именно в неё BIOS грузит данные новой
конфигурации и флаг `ACEX_30K_LOADING` (старое имя `FLEX_10K_LOADING`) перед
программным сбросом. Поэтому к Fast RAM нельзя относиться как к «своей» памяти,
которая всегда сохраняется (см. §5).
---
## 2. Карта физических страниц
Память делится на 16 КБ-блоки с однобайтовым физическим номером:
| Тип памяти | Физические номера страниц |
|---------------|---------------------------|
| Основное ОЗУ | `#00..#4F`, видео-область `#50..#5F`, ... |
| ПЗУ (ROM) | `#E0..#EF` |
| **Fast RAM** | `#F0..#FF` |
> Хотя диапазон номеров Fast RAM — `#F0..#FF` (16 значений), **реально
> используются только биты 1 и 2** номера страницы. То есть адресуются 4
> страницы × 16 КБ = **64 КБ**: `#F0`, `#F2`, `#F4`, `#F6`.
---
## 3. Как включать Fast RAM
Есть **два способа** подключить Fast RAM в адресное пространство Z80.
### Способ A. Pentagon-style через порт `#FB` / `#7B` (в окно 0)
Включается «как кэш в Pentagon»: подключает 16 КБ Fast RAM в **окно 0**
(`#0000..#3FFF`) вместо ПЗУ. Переключение — *побочный эффект чтения порта*
(значение в `A` после `IN` — мусор, важен сам факт обращения):
```asm
DI
IN A,(#FB) ; включить Fast-RAM — 16 КБ в окно 0 (#0000..#3FFF)
; ... ваш код / работа с Fast RAM ...
IN A,(#7B) ; выключить Fast-RAM (вернуть ПЗУ в окно 0)
EI
```
* `IN A,(#FB)`**включить**.
* `IN A,(#7B)`**выключить**.
> **Конфликт портов.** Порт `#FB` (и `#4F`) — это также порт COVOX/Blaster-а.
> Вывод (`OUT`) в `#FB` управляет звуком, а *чтение* (`IN`) — переключает
> Fast RAM. Не путать направления обращения.
### Способ B. Как ПЗУ — через PAGE0 (`#82`) + порт `#1FFD`
Fast RAM-страница (`#F0..#FF`) выбирается в PAGE0 и подключается на место ПЗУ
в окно 0 через спец-порт `#1FFD`:
```asm
; выбрать физическую страницу Fast RAM в PAGE0
LD A, #F0 ; номер страницы Fast RAM
OUT (#82), A ; PAGE0 = страница в окно 0
LD A,1 ; 1 → ОЗУ (выбранная страница) в #0000..#3FFF
LD BC,#1FFD
OUT (C),A
; ...
LD A,0 ; 0 → вернуть ПЗУ в #0000..#3FFF
LD BC,#1FFD
OUT (C),A
```
* Порты PAGE: `PAGE0=#82`, `PAGE1=#A2`, `PAGE2=#C2`, `PAGE3=#E2`.
**Чтение** порта PAGE возвращает текущий номер страницы.
* Эти адреса портов формально могут отличаться в других конфигурациях ППЛМ —
правильнее запрашивать их у BIOS и сверять (см. `docs/part2/bios_doc.txt`,
~строка 1033).
---
## 4. Преимущества
1. **Скорость без wait-state.** Главное и единственное предназначение — код и
данные исполняются на полной частоте 21 МГц без тактов ожидания, в отличие от
основного DRAM.
2. **Идеально для горячих участков.** Внутренние циклы, lookup-таблицы,
временные буферы рендера — то, к чему обращаются интенсивно и многократно.
3. **Отдельный массив.** Не отнимает страницы основного 4 МБ ОЗУ и не пересекается
с видео-областью.
---
## 5. Ограничения и подводные камни ⚠️
Это **самая важная часть** — Fast RAM небезопасна в обращении и легко даёт
«молча не работает».
1. **Акселератор НЕ работает с Fast RAM.**
Акселератор поддерживает пересылку блоков только для основного ОЗУ и
видео-ОЗУ. Пересылку **ROM и FastRAM он не поддерживает**. То есть нельзя
использовать accel-Fill/Copy для заполнения или копирования в/из Fast RAM —
только обычные `LD`-циклы процессора.
2. **Содержимое не сохраняется между процессами.**
Fast RAM может быть использована другими программами. При запуске любого
процесса через DSS (а также самим механизмом переконфигурирования ППЛМ)
**содержимое Fast RAM может быть затёрто**. Нельзя рассчитывать на
персистентность данных между вызовами системы.
3. **Перед вызовами DSS и BIOS Fast RAM надо ОТКЛЮЧАТЬ.**
Системные функции рассчитывают на стандартную карту памяти (ПЗУ в окне 0).
Вызывать `RST 10h` (ESTEX/DSS) или `RST 8` (BIOS) при включённой Fast RAM в
окне 0 — нельзя.
4. **Прерывания.**
Fast RAM (способ A) подключается в окно 0, перекрывая ПЗУ и системный вектор.
Если используются прерывания, программа **обязана установить свой обработчик
по адресу `#0038`**. На практике работу с Fast RAM ведут с `DI`, а на время
ожидания кадра/`halt` Fast RAM временно выключают и восстанавливают (см. §6).
5. **Окно 0 занято под DSS.**
В нашем C-toolchain'е окно 0 (`#0000..#3FFF`) — это ESTEX/DSS система
(см. `release_docs/ru/platform_reference.md`). Подключение Fast RAM в окно 0
вытесняет именно её, что усиливает требование п.3.
6. **Конфликт `#FB` с COVOX.** См. §3, способ A.
---
## 6. Канонический паттерн temporary-off / restore
Из реального резидента (`docs/samples/sprinterIntLib.asm`): перед `ei: halt`
(ожидание кадрового прерывания) Fast RAM временно выключается, после —
восстанавливается прежнее состояние:
```asm
_intWaitVsyncSys
call memCacheOffTemporary ; временно выключаем Fast RAM
ei
halt
jp memCacheRestoryState ; восстанавливаем прежнее состояние подключения
```
Идея паттерна: библиотека хранит флаг «было ли Fast RAM включено», умеет
безопасно его снять на время системных операций (прерывания, DSS/BIOS) и вернуть
обратно. При интеграции в C-toolchain эту логику следует обернуть так же:
сохранять состояние, отключать вокруг любого `RST`/`halt`, восстанавливать.
---
## 7. Выводы для нашего C-toolchain (SDCC + target-слой)
* **Из коробки сейчас не используется.** В `runtime/`, `lib/`, `libc/` обращений
к Fast RAM нет (порт `#FB`/`#7B` нигде не задействован под эту задачу).
* **Где могло бы пригодиться:** разместить «горячую» функцию или таблицу в
Fast RAM для ускорения. Но 64 КБ перекрывают окно 0, конфликтуют с DSS и не
переживают системные вызовы — это узкоспециализированный, ручной режим, не
кандидат на общий механизм линковки.
* **Реалистичный сценарий:** короткий самодостаточный inner-loop без вызовов
системы, с `DI`, со своим вектором `#0038`, скопированный в Fast RAM обычным
`LD`-циклом (не акселератором), исполняемый из окна 0, с гарантированным
восстановлением карты памяти перед любым `RST`.
* **Несовместимость с акселератором** означает, что для графики/блочных операций
Fast RAM бесполезна — там выигрывает accel по основному/видео-ОЗУ.
Если будем добавлять поддержку — делать это отдельным opt-in механизмом
(по аналогии с banked-режимами), с обязательной обёрткой off/restore вокруг всех
точек входа в систему.
+284
View File
@@ -0,0 +1,284 @@
Функция puts()
Функция puts() записывает символьную строку в стандартный
поток данных (т.е. выводит ее на экран). Функция puts()
возвращает код символа «\п».
int puts(const char *string);
После выполнения функции puts() курсор переводится на
новую строку.
Функция putchar()
Функция putchar() записывает символ в стандартный поток
данных (т.е. выводит его на экран). Функция putchar() возвращает
выведенный на экран символ.
int putchar(int ch);
Функция gets()
Функция gets() считывает символьную строку стандартного
входного потока и помещает ее по адресу, заданному указателем
buffer; прием строки заканчивается, если функция обнаруживает
символ конца строки «\п», данный символ удаляется и
заменяется нуль-терминатором «\0».
char *gets (char*buffer);
Функция gets() возвращает указатель на считанную строку.
Функция getchar()
Функция getchar() считывает символ из стандартного
входного потока.
int getchar(void);
Функция getchar() возвращает считанный символ.
=== Функции консольного ввода
char *cgets(char *str)
- помещает в буфер, на начало которого
указывает str, строку символов со стандартного ввода.
Запись символов начинается с str[l]; str[0] должен содержать
максимальное число символов, которое должно быть прочитано
и записано в строку. Функция возвращает указатель на начало
буфера str.
int getch(void)
- выполняет ввод символа с клавиатуры.
Turbo С не выполняет «эхо» ввода. В этой связи полезна для
организации интерфейса с пользователем, при котором нажатие
той или иной клавиши вызывает немедленную реакцию программы
без отображения введённого символа на экране.
int getche(void)
- выполняет небуферизуемый ввод символа
с клавиатуры. Turbo С «эхоирует» ввод на экране. Перевод
строки происходит при достижении правой вертикальной
границы текущего активного окна.
int kbhit(void)
- проверяет, пуст ли буфер клавиатуры.
Если в буфере есть символы, функция возвращает ненулевое
значение, в противном случае она возвращает О. Является
удобным средством предотвращения «зацикливания» или «по-
висания» при ожидании невозможного в данный момент события.
Кроме того, осуществляется проверка нажатия комбинации
клавиш «Ctrl-Break», что позволяет выполнить аварийное завершение
программы.
int ungetch(int ch)
- записывает непосредственно в буфер
клавиатуры символ ch. Он будет доступен при выполнении следующей
операции чтения с консоли (функциями файла
«conio.h»). Разрешает помещать только один символ, который
не должен совпадать с константой EOF, описанной в файле
«stdio.h». В случае успеха функция возвращает ch; в противном
случае возвращается -1.
=== Функции консольного вывода
void textmode(int newmode)
- изменяет текущий текстовый
режим. Новый режим указывается единственным параметром
newmode и может задаваться либо числом, либо с использованием
символических констант, значения которых определяет
перечислимый тип text_modes
Функции консольного вывода используют понятие активного
окна экрана. Активное окно - это прямоугольная область
экрана, в границах которой в данный момент работают функции.
Описание активного окна (или, как часто говорят, фрейм)
хранится во внутренней структурной переменной Turbo С. Установку
параметров активного текстового окна выполняет функция
window ().
void window(int l_t_col, int l_t_row, int r_b_col, int r_b_row)
- описывает активное текстовое окно: первая пара
аргументов задает столбец и строку левого верхнего угла, вторая
пара - правого нижнего угла. Строки и столбцы нумеруются,
начиная от 1. Поэтому, например, координаты левого верх-
него и правого нижнего углов экрана в режимах «25 строк х 80
столбцов» задаются парами (1,1) и (80,25). Ось X направлена
слева направо, а ось Y направлена сверху вниз. Следует обратить
внимание на то, как в Turbo С задаются координаты углов,
сначала столбец, затем строка.
Фрейм окна Turbo С имеет следующую структуру:
struct text_info {
unsigned char winleft; /* столбец, строка */
unsigned char wintop; /* левого верхнего угла */
unsigned char winright; /* столбец, строка */
unsigned char winbottom;/* правого верхнего угла */
unsigned char attribute; /* атрибуты */
unsigned char normattr; /* окна */
unsigned char screenheight; /* полная высота экрана */
unsigned char screenwidth; /* полная ширина экрана */
unsigned char curx; /* строка, столбец */
unsigned char сuгу /* текущей позиции курсора */
}
void gettextinfo(struct text_info *r)
- заполняет поля
структурной переменной по шаблону text_info, на которую
ссылается. Шаблон структуры text_info, описывающей текущее
окно экрана, содержится в заголовочном файле «conio.h».
Функция window() инициализирует поля координат фрейма
окна. Функции textcolor(), textbackground(), textattr() и
другие управляют цветом отображаемых символов окна.
void textattr(int newattr)
- устанавливает атрибут для
функций, работающих с текстовыми окнами. Атрибут хранится в
поле attribute структурной переменной по шаблону text_info,
доступной через функцию gettextinfo()
void textcolor(int newcolor)
- задает цвет символов, не
затрагивая установленный цвет фона. Цвет может быть или числом,
или формироваться из символических констант, значения
которых определяет перечисляемый тип COLORS.
void textbackground(int newcolor)
- задает цвет фона
символов, не затрагивая установленный цвет символа. Цвет может
быть или числом, или формироваться из символических
констант
void gotoxy(int х, int у)
- устанавливает курсор в строку
у и столбец х в текущем активном окне экрана. Верхний левый
угол окна имеет координаты (1,1). При попытке позиционировать
курсор за границы окна он останавливается на границе окна.
Особенностью функции является то, что координаты х и у
являются относительными, приведенными к левому верхнему
углу. Например, если текущее окно было описано функцией
window(1,8,80,25), обращение gotoxy(5,5); установит курсор
в пятый относительный столбец окна (совпадает с абсолютным
столбцом 4, отсчитываемым от О) в пятой относительной строке
(так как верхняя строка окна задана равной 5, то абсолютная
строка будет равна 5+8-1, если отсчет строк ведется от О)
int wherex(void),
int wherey(void)
- сообщают столбец и
строку текущей позиции курсора; возвращают целое число в
диапазоне
void clreol(void)
- стирает в текстовом окне строку, на которую
установлен курсор, начиная с текущей позиции курсора и
до конца строки (до правой вертикальной границы окна).
void clrscr(void)
- очищает все текстовое окно. Цвет «заливки»
окна при очистке будет соответствовать значению, установленному
символической переменной attribute в описании
окна (структурная переменная по шаблону text_info).
void delline(void)
- стирает в текстовом окне всю строку
текста, на которую установлен курсор.
void insline(void)
- вставляет пустую строку в текущей
позиции курсора со сдвигом всех остальных строк окна на одну
строку вниз. При этом самая нижняя строка текста окна теряется.
int cprintf(const char *format, ...)
- выполняет вывод
информации с преобразованием по заданной форматной строке,
на которую указывает format. Является аналогом функции
стандартной библиотеки printf(), но выполняет вывод в пределах
заданного окна. В отличие от printf() функция cprintf()
иначе реагирует на специальный символ '\п' - курсор переводится
на новую строку, но не возвращается к левой границе окна.
Поэтому для перевода курсора на начало новой строки текстового
окна следует вывести последовательность символов CR-
LF (OxOd,OxOa). Остальные специальные символы воздействуют
на курсор так же, как и в случае функций стандартного ввода-
вывода. Функция возвращает число выведенных байтов, а не
число обработанных полей, как это делает функция printf().
int cputs(const char *str)
- выводит строку символов в
текстовое окно, начиная с текущей позиции курсора. На начало
выводимой ASCIIZ-строки указывает str. Является аналогом
функции стандартной библиотеки puts(), выполняет вывод в
пределах заданного окна и при выводе не добавляет специальный
символ '\п'. Реакция cputs() на специальный символ '\п'
аналогична реакции cprintf(): курсор переводится на новую
строку, но не возвращается к левой границе окна. Поэтому для
перевода курсора на начало новой строки текстового окна следует
вывести последовательность символов CR-LF (OxOd,OxOa).
Остальные специальные символы воздействуют на курсор так
же, как и в случае функций стандартного ввода/вывода. Функция
возвращает ASCII-код последнего выведенного на экран
символа. В отличие от puts() в функции отсутствует возврат
символа EOF (вывод на экран происходит в любом случае).
int movetext(int left, int top, int right, int bottom, int destleft, int desttop)
- переносит окно, заданное координатами
левого верхнего (left, top) и правого нижнего (right, bottom)
углов, в другое место на экране, заданное координатами левого
верхнего угла нового положения окна. Размеры окна по горизонтали
и вертикали сохраняются. Все координаты задаются относительно
координат верхнего левого угла экрана (1,1). Функция
возвращает ненулевое значение, если перенос заданного
окна выполнен. В противном случае возвращается О. Функция
корректно выполняет перекрывающиеся переносы, т.е. переносы,
в которых прямоугольная область-источник и область, в которую
окно переносится, частично покрывают друг друга.
int putch(int ch)
- выводит символ в текущей позиции
текстового окна экрана. Как и для функций cprintf(), cputs(),
специальный символ '\п' вызывает только переход курсора на
новую строку текстового окна без возврата к его левой вертикальной
границе. Остальные специальные символы воздействуют
на курсор так же, как и для функций стандартного ввода-
вывода.
int puttext(int left, int top, int right, int bottom, void source)
- выводит на экран текстовое окно, заданное координатами
левого верхнего (left, top) и правого нижнего (right,
bottom) углов. Символы и атрибуты располагаются в буфере,
адрес начала которого задаёт указатель source (функция «открывает»
или «восстанавливает» текстовое окно экрана). Обычно
используется вместе с функцией gettext(), выполняющей
обратную операцию - запись в source символов/атрибутов,
полностью описывающих все знакоместа текстового окна. Функция
проверяет по заданным координатам окна, можно ли разместить
окно на экране для текущего режима видеоадаптера и
корректны ли эти координаты. В случае, когда окно успешно
выведено, возвращается ненулевое значение.
int gettext(int left, int top, int right, int bottom, void destin)
- записывает в буфер destin символы и атрибуты текстового
окна, заданного строкой и столбцом левого верхнего
(left, top) и правого нижнего (right, bottom) углов. Первые два
слова буфера занимают ширина и длина скопированного окна.
Работает только в текстовых режимах видеоадаптера. Координаты
задаются относительно верхнего левого угла экрана (1,1). В
случае успеха возвращает ненулевое число.
=== Файловый ввод/вывод
Прототипы функций ввода-вывода и используемые для этого
типы данных описаны в стандартном заголовочном файле
«stdio.h».
Для файлового ввода/вывода в Си предусмотрены две основные
группы функций:
• функции низкоуровневого ввода/вывода, использующие
для доступа к файлам целочисленные файловые дескрипторы;
• функции более высокого уровня, осуществляющие буферизованный
ввод/вывод с применением потоков.
Поток в Си - это объект, служащий для доступа к файлам
как к упорядоченной последовательности символов.
Поток представляется структурой типа FILE, с которой ассоциирован
некоторый открытый файл. При необходимости несколько
потоков могут ссылаться на один и тот же файл.
+9 -9
View File
@@ -1758,7 +1758,7 @@ static void clamp_top(void)
} }
/* Прокрутка вверх на n строк с частичной перерисовкой при n == 1. */ /* Прокрутка вверх на n строк с частичной перерисовкой при n == 1. */
static void scroll_up(uint16_t n) static void md_scroll_up(uint16_t n)
{ {
uint16_t new_top_line = (top_line >= n) ? (uint16_t)(top_line - n) : 0; uint16_t new_top_line = (top_line >= n) ? (uint16_t)(top_line - n) : 0;
if( new_top_line != top_line) { if( new_top_line != top_line) {
@@ -1773,7 +1773,7 @@ static void scroll_up(uint16_t n)
} }
/* Прокрутка вниз на n строк с частичной перерисовкой при n == 1. */ /* Прокрутка вниз на n строк с частичной перерисовкой при n == 1. */
static void scroll_down(uint16_t n) static void md_scroll_down(uint16_t n)
{ {
uint16_t new_top_line = (top_line + n < n_lines - VIEW_H) ? uint16_t new_top_line = (top_line + n < n_lines - VIEW_H) ?
(uint16_t)(top_line + n) : n_lines - VIEW_H; (uint16_t)(top_line + n) : n_lines - VIEW_H;
@@ -1791,7 +1791,7 @@ static void scroll_down(uint16_t n)
/* Горизонтальный сдвиг (только если в окне есть nowrap-строки). /* Горизонтальный сдвиг (только если в окне есть nowrap-строки).
* Максимум сдвига ограничен самой широкой nowrap-строкой на экране. */ * Максимум сдвига ограничен самой широкой nowrap-строкой на экране. */
static void scroll_h(int8_t delta) static void md_scroll_horizon(int8_t delta)
{ {
uint16_t maxw = 0; uint16_t maxw = 0;
for (uint8_t i = 0; i < VIEW_H; i++) { for (uint8_t i = 0; i < VIEW_H; i++) {
@@ -1999,12 +1999,12 @@ int main(int argc, char **argv)
switch (scan) { switch (scan) {
case KEY_F1: show_help(); break; case KEY_F1: show_help(); break;
case KEY_F10: goto exit_loop; case KEY_F10: goto exit_loop;
case KEY_UP: scroll_up(1); break; case KEY_UP: md_scroll_up(1); break;
case KEY_DOWN: scroll_down(1); break; case KEY_DOWN: md_scroll_down(1); break;
case KEY_LEFT: scroll_h(-(int8_t)HPAN_STEP); break; case KEY_LEFT: md_scroll_horizon(-(int8_t)HPAN_STEP); break;
case KEY_RIGHT: scroll_h(+(int8_t)HPAN_STEP); break; case KEY_RIGHT: md_scroll_horizon(+(int8_t)HPAN_STEP); break;
case KEY_PGUP: scroll_up(VIEW_H); break; case KEY_PGUP: md_scroll_up(VIEW_H); break;
case KEY_PGDN: scroll_down(VIEW_H); break; case KEY_PGDN: md_scroll_down(VIEW_H); break;
case KEY_HOME: top_line = 0; case KEY_HOME: top_line = 0;
viewport_x = 0; viewport_x = 0;
render_viewport(); break; render_viewport(); break;
+4
View File
@@ -8,6 +8,7 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..) PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := mdview2 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
MEMORY := small MEMORY := small
include $(PROJ_ROOT)/app.mk include $(PROJ_ROOT)/app.mk
@@ -37,6 +38,9 @@ $(README_DISK): README.MD | $(DISK_TMP)
# UTF8TEST.MD кладётся на диск КАК ЕСТЬ (в UTF-8, без перекодировки) — # UTF8TEST.MD кладётся на диск КАК ЕСТЬ (в UTF-8, без перекодировки) —
# это тестовый вход для проверки UTF-8 рендеринга (Фаза 2 кодировок). # это тестовый вход для проверки UTF-8 рендеринга (Фаза 2 кодировок).
#
# Тестовые файлы лимита 256 КБ лежат в testfiles/ (генератор gen_testfiles.py);
# на диск НЕ кладутся — при необходимости добавить вручную в список ниже.
floppy: $(EXAMPLE).exe $(README_DISK) floppy: $(EXAMPLE).exe $(README_DISK)
python3 $(MAKE_DISK) $(FLOPPY_IMG) $(EXAMPLE).exe $(README_DISK) UTF8TEST.MD python3 $(MAKE_DISK) $(FLOPPY_IMG) $(EXAMPLE).exe $(README_DISK) UTF8TEST.MD
@echo @echo
+41
View File
@@ -112,10 +112,51 @@ int main(void) {
- **Кодировки:** CP866 / CP1251 / KOI8-R / UTF-8 (автоопределение, `F8`) - **Кодировки:** CP866 / CP1251 / KOI8-R / UTF-8 (автоопределение, `F8`)
- **Максимальный размер файла:** 128 КБ - **Максимальный размер файла:** 128 КБ
- **Максимальное число строк в индексе:** 16 384 - **Максимальное число строк в индексе:** 16 384
- **Максимальная длина строки в рендер-кэше:** 255 ячеек (см. ограничение ниже)
- **Режим памяти:** small - **Режим памяти:** small
- Код программы, cтек, данные, куча — окнa W1-W2 (32 КБ, адреса 0x40000xBFFF). - Код программы, cтек, данные, куча — окнa W1-W2 (32 КБ, адреса 0x40000xBFFF).
- Буфер файла — страницы EMM, отображаемые в W3 (0xC0000xFFFF) - Буфер файла — страницы EMM, отображаемые в W3 (0xC0000xFFFF)
## Известные ограничения
### Длина строки в MD-режиме — 255 ячеек
Рендер-кэш хранит каждую логическую строку как **не более 255 пар (символ, атрибут)**
— константа `MAX_CACHE_LINE_LEN`. Лимит задан типами: `g_ncells` и
`cache_rec_t.len``uint8_t`. Касается всех строк, но заметнее всего на
**горизонтально скроллируемых** строках (блоки кода и строки таблиц, флаг
`IF_HSCROLL`), которые в MD-режиме можно листать вправо.
**Что происходит с более длинной строкой:** при индексации `gc_put()` молча
отбрасывает каждую ячейку после 255-й (`if (g_ncells < MAX_CACHE_LINE_LEN)`).
В кэш попадают только первые 255 ячеек, остаток **теряется** — до него нельзя
доскроллить и **нет маркера обрезки** на 255-й позиции (маркер `>` означает лишь
«есть ещё в пределах кэша»). Переполнения буфера нет — `gc_put` проверяет границу.
Важно: «255 ячеек» — это **отрендеренная ширина**, не байты исходника. Табы в
коде разворачиваются в пробелы (до `TAB_STOP`), а ячейки таблицы добиваются
пробелами до ширины колонки + рамки `│` — поэтому кап достигается раньше, чем
255 «полезных» символов.
> **RAW-режим (`F2`) этого лимита не имеет** — он рисует прямо из файла
> побайтово, длинные строки видны целиком (через wrap `F3` или гориз. скролл).
**Идея снятия лимита** (оценка, не реализовано) — расширить длину до `uint16_t`:
- `cache_rec_t.len` `uint8_t→uint16_t` — структура остаётся **ровно 8 байт**
(len съедает один pad-байт), адресация `idx<<3` не меняется. Бесплатно.
- `g_ncells` / `g_ncells_at_space``uint16_t` — главная цена по **коду/скорости**:
16-битная арифметика на Z80 в горячем `gc_put` (вызов на каждую ячейку) и в
scan-циклах. Ориентир: **+0.2…0.4 КБ кода** + замедление индексации.
- Буфер `g_cells[MAX_CACHE_LINE_LEN*2]` в near-RAM (W2) — главная цена по
**памяти**: `2 × кап` байт. Сейчас 510 Б; кап 512 → +0.5 КБ, 1024 → +1.5 КБ.
EMM-кэш контента (1 МБ) длинные строки тянет легко — узкое место именно near.
- `viewport_x` (и копия в `docset_t`) + потолок `max_vx` (сейчас 248) → `uint16_t`,
иначе хранить >255 можно, а доскроллить нельзя. Плюс `widths[]`/`ccx` в таблицах/коде.
Реалистичный компромисс — кап 512–1023: хватит почти всем листингам/таблицам,
цена ~+0.3 КБ кода и +0.5…1.5 КБ near-RAM. **Дешёвая полумера без `uint16_t`**
ставить честный маркер обрезки на 255-й позиции, чтобы потеря была видна.
## TODO ## TODO
1. **Увеличение размера документов.** Снять лимит 128 КБ: Достаточно 1. **Увеличение размера документов.** Снять лимит 128 КБ: Достаточно
+799
View File
@@ -0,0 +1,799 @@
# MDView — Просмотрщик Markdown для Sprinter
**MDView** — программа для просмотра документов в формате *Markdown* на компьютере
Sprinter (процессор Z80). Документ хранится в отдельном W3 окне и не занимает
основную RAM программы.
## Возможности
- Документы до **128 КБ** (8 страниц EMM по 16 КБ каждая)
- До **16 384** экранных строк в индексе
- Автоматический перенос слов по ширине экрана (80 столбцов)
- Горизонтальный сдвиг для широких строк (блоки кода, таблицы)
- Статус-бар: имя файла, диапазон строк, процент прокрутки
- Спиннер в строке состояния во время загрузки и индексации
- Поддержка «мягкого» склеивания строк в абзацах и цитатах
## Запуск
```
mdview [имя_файла.md]
```
Если имя файла не задано, загружается `README.MD`.
## Управление
```
Клавиша Действие
───────────── ────────────────────────────────────────
Up Down Прокрутка на одну строку вверх / вниз
PgUp PgDn Прокрутка на страницу (30 строк)
Home Начало документа
End Конец документа
Left Right Горизонтальный сдвиг (только nowrap-строки)
F1 Окно справки
F8 Кодировка: CP866 → CP1251 → KOI8-R → UTF-8 → ...
F10 / Esc Выход из программы
```
Кодировка определяется автоматически при открытии (BOM + эвристика по
первым 4 КБ); `F8` переключает её вручную, если детекция ошиблась.
8-битные кодировки (CP866/CP1251/KOI8-R) переключаются мгновенно (ремап на
отрисовке). Второй набор индекс/кэша (например UTF-8) строится лениво — при
первом переключении в него (короткая пауза со спиннером), дальше мгновенно.
## Синтаксис Markdown
### Заголовки
Поддерживаются уровни H1–H4. Уровни H5 и H6 отображаются как H4.
# Заголовок первого уровня
## Заголовок второго уровня
### Заголовок третьего уровня
#### Заголовок четвёртого уровня
### Текстовое форматирование
**Жирный текст** выделяется двойными звёздочками: `**текст**`
*Курсив* выделяется одиночными звёздочками `*текст*` или знаком подчёркивания `_текст_`
`Встроенный код` обозначается обратными кавычками
~~Зачёркнутый текст~~ — двойные тильды: `~~текст~~`
### Ненумерованный список
Маркеры `-`, `*` или `+`:
- Первый пункт списка
- Второй пункт списка
- Третий пункт с достаточно длинным текстом, который при необходимости
будет перенесён на следующую строку с сохранением отступа
### Нумерованный список
1. Первый элемент
2. Второй элемент
3. Третий элемент
### Цитата
> Блок цитаты начинается с символа `>`. Несколько последовательных
> строк одной цитаты склеиваются в единый абзац с автоматическим
> переносом слов.
### Блок кода (verbatim)
Блок кода заключается в тройные обратные кавычки. Внутри блока
текст отображается «как есть» без разбора Markdown:
```
#include <stdio.h>
#include <sprinter.h>
int main(void) {
puts("Hello, Sprinter!");
return 0;
}
```
### Горизонтальная линия
Три или более символов `---`, `***` или `___` на отдельной строке:
---
## Технические характеристики
- **Платформа:** Sprinter, процессор Z80 @ 21 МГц
- **Кодировки:** CP866 / CP1251 / KOI8-R / UTF-8 (автоопределение, `F8`)
- **Максимальный размер файла:** 128 КБ
- **Максимальное число строк в индексе:** 16 384
- **Режим памяти:** small
- Код программы, cтек, данные, куча — окнa W1-W2 (32 КБ, адреса 0x40000xBFFF).
- Буфер файла — страницы EMM, отображаемые в W3 (0xC0000xFFFF)
## TODO
1. **Увеличение размера документов.** Снять лимит 128 КБ: Достаточно
разрешить работать с большим кол-вом страниц памяти, пока оттестированно
на работе с 8-мю страницами по 16Кб.
Сделано: форматированные таблицы с рамкой; поддержка кодировок
CP866 / CP1251 / KOI8-R / UTF-8 с автоопределением и переключением по `F8`
(второй набор строится лениво, по первому переключению).
4. **Ускорение рендеринга.** Кэш строк экрана. Оптимизация цикла вывода
символов через BIOS WRCHAR (пакетный вывод, DMA).
---
*MDView v0.2 · (c) 2026 Петров А.Г.*
---
# Sprinter C Compiler — v1.0
C toolchain for **Sprinter** — the Z80-based home computer by Peters Plus, running
ESTEX DSS. Host: macOS / Linux. Target: `.EXE` files in SprintEXE format.
Built on top of **SDCC 4.5** (vendored in `third_party/sdcc/`). This repository adds
everything Sprinter-specific: crt0, linker integration, libc wrappers over ESTEX,
banked-call trampolines, graphics & accelerator API, mouse driver wrappers, and the
`mkexe` utility for producing SprintEXE images.
## What you get
* **`bin/sprinter-cc`** — one-line driver: `sprinter-cc -o foo.exe foo.c`
* **Memory modes**: `tiny`, `small`, `big`, `huge`, `manual` — see below.
* **stdio + conio**: printf, puts, putchar, getchar, fopen/fread/..., cprintf, cputs, putch, textcolor/textbackground/textattr, gotoxy, kbhit/getch.
* **Graphics**: 320×256×256 and 640×256×16 modes, accelerator-backed primitives (hline / vline / rect / fill_rect / line via Bresenham, plus clear), bitmap-font text in both modes via BIOS character generator.
* **File I/O**: POSIX (`open`/`read`/`write`/`close`/`lseek`/`unlink`/`creat`), FILE\* streams (`fopen`/`fgets`/`fwrite`/...), directory listing (`ffirst`/`fnext`), `chdir`/`getcwd`/`mkdir`/`rmdir`, `stat`/`fstat`.
* **Memory**: 32 KB heap (W2-resident), banking-aware page allocator (`mem_alloc_pages`/`bank_read`/`bank_write`), explicit memory modes for sub-16 KB programs.
* **Mouse**: full Sprinter driver wrapper (14 functions including custom cursor bitmaps).
* **Environment**: `getenv`/`putenv`/`sysenv` over ESTEX `$46`.
* **Time**: `getdatetime`/`setdatetime` + POSIX `time`/`localtime`/`mktime`/`asctime`/`ctime`.
* **Misc**: `errno`/`strerror`/`perror`, `atexit`, `setjmp`/`longjmp`, `sleep`, full argv parsing in crt0.
## Quick start
```sh
git clone <this repo> sprinter-c
cd sprinter-c
make sdcc # one-time: fetch SDCC 4.5 binary (~25 MB)
make all # build mkexe + libsprinter.lib + 27 examples
make floppy # pack everything into mame/v306/IMG/mc.img
cd mame/v306 && ./run_mame.sh # boot Sprinter in MAME
```
Compile a single program:
```sh
cat > hello.c <<EOF
#include <stdio.h>
int main(void) { puts("Hello, Sprinter!"); return 0; }
EOF
bin/sprinter-cc -o hello.exe hello.c
```
That's it — `hello.exe` is now a valid SprintEXE you can `RUN HELLO` from the ESTEX shell.
## Memory modes
Sprinter's address space is four 16 KB windows (W0 / W1 / W2 / W3). DSS allocates
pages by program size — small programs get only one page. Pick a memory mode based
on what your program needs:
| Mode | Code lives in | Banking | Use when | Note |
|---|---|---|---|---|
| `tiny` (default) | W2 (0x8100+) | no | code+data < 14 KB | |
| `small` | W1-W2 (0x4100+) | no | code+data < 30 KB | |
| `big` | W2 + W1 banking | yes (W1) | tiny + extra code modules | |
| `huge` | W1-W2 + W3 banking | yes (W3) | small + extra code modules | |
| `manual` | user-specified | optional | special layouts | Not implemented |
```sh
sprinter-cc --memory small -o big.exe bigprog.c
sprinter-cc --memory huge -o app.exe main.c --bank 1=engine.c --bank 2=ai.c
```
Banked functions are declared with `__banked`:
```c
void engine_tick(int dt) __banked; // lives in BANK1, automatically swapped
```
## Examples (27 total)
| Example | What it demonstrates |
|---|---|
| `hello` | Hello world with stdio + conio Turbo-C-style colors |
| `argv` | argv parsing in crt0 |
| `cat` | File I/O — read & print TEST.TXT |
| `seek` | 32-bit lseek over a 100 KB file |
| `ls` | Directory listing via ffirst/fnext |
| `filetest` | FILE\* streams (fopen/fread/...) |
| `stattest` | `stat`/`fstat` on files and directories |
| `errno` | errno / strerror / perror |
| `mem_test` | Page allocator + bank\_read/bank\_write |
| `malloc` | Heap stress test (200+ allocations) |
| `banked` | Banked code in W3 (huge mode) |
| `bankedbg` | Banked code in W1 (big mode) |
| `banklocl` | Bank-local static data and BSS |
| `mouse` | Mouse driver in text mode |
| `gfx_mous` | Mouse with custom bitmap cursor in graphics mode |
| `gfx_demo` | 320×256×256 lines / rects / accelerator |
| `gfx_d16` | 640×256×16 same primitives |
| `gfx_text` | Bitmap-font text on graphics screen |
| `timedir` | Date/time + directory listing |
| `ptime` | POSIX time API |
| `openenv` | open() flags + environment vars |
| `conio` | conio API smoke test |
| `attrprob` | Probe Sprinter text attribute byte layout |
| `strtest` | string.h test (from SDCC's z80.lib) |
| `stdlib` | stdlib.h test (qsort / rand / strtol / etc.) |
| `assrtest` | assert() |
| `rt_test` | Runtime helpers (sleep, setjmp, atexit) |
## Headers
Standard:
* `<stdio.h>` — puts / printf / FILE\* + Sprinter-specific dec/hex helpers
* `<stdlib.h>` — atoi / atof / malloc / qsort / ... (from SDCC z80.lib)
* `<string.h>` / `<ctype.h>` / `<math.h>` — from SDCC z80.lib
* `<unistd.h>` — read / write / close / lseek / unlink
* `<fcntl.h>` — open / creat + O\_RDONLY / O\_CREAT / ...
* `<errno.h>` — errno + error names + strerror
* `<sys/stat.h>` — stat / fstat
* `<setjmp.h>` / `<assert.h>` — from SDCC
Sprinter-specific:
* `<conio.h>` — putch / cputs / cprintf, textcolor / textbackground / textattr, kbhit / getch, clrscr, gotoxy, wherex/y
* `<gfx.h>` — gfx_init/done, palette, putpixel, hline/vline/rect/fill_rect/line, text — for both 320 and 640 modes (gfx_\*16 variants)
* `<mouse.h>` — full 14-function driver wrapper + mouse_cursor_t with bitmap support
* `<dir.h>` — chdir / getcwd / mkdir / rmdir / ffirst / fnext / ffblk
* `<time.h>` — getdatetime / setdatetime + POSIX time / localtime / etc.
* `<sprinter.h>` — raw ports, ESTEX/BIOS function numbers, env API
* `<sprinter_exit.h>` — exit / \_exit / atexit
* `<sprinter_mem.h>` — mem\_alloc\_pages / mem\_free\_block / bank\_read / bank\_write
* `<sprinter_compat.h>` — Solid-C compatibility layer (aliases + BOOL/WORD/uint types)
## Toolchain commands
```sh
make all # build mkexe + lib + every example
make floppy # repack mame/v306/IMG/mc.img with all .exe files
make check # 17 mkexe unit-tests
make clean # remove all build artefacts
make sdcc # one-time: fetch SDCC 4.5 binary
```
### sprinter-cc options
```
sprinter-cc -o foo.exe foo.c [more.c ...] [options]
--memory MODE tiny | small | big | huge | manual (default: tiny)
--memory-manual SPEC explicit placement (CODE=W1|W2,DATA=W1|W2|SAME,BANKED=W1|W3)
--stack-size N bytes reserved for the stack (default ~1278)
--crt0=TYPE default | minimal | banked | small
--bank N=FILE.c compile FILE.c into bank N (repeatable, max 15)
--debug enable runtime diagnostics (defines DEBUG_RT)
-I PATH extra include path
-L 0xADDR / -E / -S override load / entry / stack addresses
-Wl FLAG pass FLAG to sdldz80
--mkexe FLAG pass FLAG to mkexe (e.g. --mkexe -p --mkexe 0 for bank padding)
-v verbose
```
## Status
What works in v1.0:
* Compile / link / pack to SprintEXE — verified on all 27 examples
* Four memory modes (tiny / small / big / huge)
* Graphics (both modes) with accelerator
* Mouse (text + graphics cursor)
* File I/O, directories, environment, time
* All headers listed above
Deferred to v2.0 (see `docs/TODO.md`):
* **Turbo-C-style BGI graphics API** — `initgraph` / `setcolor` / `circle` /
`getimage` / `putimage` / etc. on top of our `gfx_*` primitives
* Remaining Solid-C compatibility gaps (Phase 2/3) — see `docs/solid_c_compatibility.md`
* Manual memory mode
* Rewrite FILE\* stream API (current implementation is very primitive and doesn't use buffers)
Deferred to v3.0:
* **IM2 interrupt handlers** — research complete (`docs/im2_isr_design.md`),
implementation scheduled for v3
* **Audio API** (AY-3-8910 + COVOX) — requires IM2
* **ISA-8 slot drivers** — requires IM2 (???)
## Documentation
* `docs/TODO.md` — roadmap and open work items
* `docs/solid_c_compatibility.md` — gap analysis vs Solid-C 2004
* `docs/im2_isr_design.md` — interrupt handler design (v2)
* `docs/converted/` — source documentation (ESTEX, BIOS, architecture)
converted to plain text for `grep`
* `docs/reference/`, `docs/samples/`, `docs/memory management/` — original
Russian docs and code samples from Peters Plus
## Repository layout
```
bin/sprinter-cc one-line compiler driver (bash)
toolchain/mkexe/ host-side tool: .ihx -> .exe SprintEXE
toolchain/check_banks.py post-link bank size enforcer
runtime/ crt0 variants (default, minimal, small, banked)
bank trampolines, heap, heap_top
libc/include/ headers
libc/io|stdio|mem|gfx/ C and asm sources for libsprinter.lib
lib/ Makefile that archives libsprinter.lib via sdar
examples/ 27 example programs
mame/v306/ MAME binary + Sprinter ROM/HDD images + floppy script
third_party/sdcc/ vendored SDCC 4.5 (fetched via `make sdcc`)
third_party/solid-c/ reference: original Sprinter native C (for compat target)
docs/ documentation
```
## License
This repository contains:
* Original code in `bin/`, `toolchain/`, `runtime/`, `libc/`, `lib/`, `examples/`
MIT-licensed.
* `third_party/sdcc/` — SDCC 4.5 under GPLv2 with linking exception
(see `third_party/sdcc/COPYING.txt`)
* `third_party/solid-c/` — original Sprinter Solid C, used only as a reference
## Credits
* **Sprinter / Peters Plus** — Иван Мак, Дмитрий Паринов and the original team
* **SDCC** — for the underlying Z80 compiler
* **MAME** — for the Sprinter Sp2000 emulation
---
For questions / patches: see CONTRIBUTING.md (TBD) or open an issue.
---
# TODO / Roadmap
Открытые задачи в порядке убывания приоритета. По мере появления реальных программ — приоритеты будут смещаться.
## Этап 5 — malloc / free + banking-aware page allocator ✅ ГОТОВО
- [x] SDCC's `malloc`/`free` + наш `runtime/heap.s` (полностью заменяет library heap.rel, 14000-байтный heap в окне 2)
- [x] `libc/mem/mem_alloc.c` — page allocator: `mem_alloc_pages`/`mem_free_block`/`mem_get_page`/`mem_info` через ESTEX `$3C/$3D/$3E` + BIOS `$C4`
- [x] `libc/mem/bank_io.c` — HOME-резидентные `bank_read`/`bank_write`/`bank_load_byte`/`bank_store_byte` со свопом W3 внутри
- [x] `examples/malloc_test/` — проверка SDCC's malloc (~210 64-байтных allocations через всю heap)
- [x] `examples/mem_test/` — проверка page allocator: 3 страницы, разные паттерны через bank_write, верификация через bank_read
## Этап 6 — argv parsing + sprinter-cc wrapper ✅ ГОТОВО
- [x] crt0 парсит ESTEX command-line из IX-prefix (inline asm в `runtime/crt0.s`)
- [x] Strip leading CP/M-style space (DSS quirk)
- [x] Передача `argc`/`argv` в main() через HL/DE (SDCC __sdcccall(1) ABI)
- [x] argv[0] = basename .EXE через ESTEX APPINFO ($47 subfn 2)
- [x] `runtime/crt0_minimal.s` — opt-out для очень маленьких программ
- [x] `runtime/crt0_banked.s` — теперь тоже парсит argv (parse_argv + get_progname скопированы из crt0.s; будет factored в argv.s когда возьмёмся за libsprinter.lib)
- [x] Bash-обёртка `bin/sprinter-cc`: `sprinter-cc -o foo.exe foo.c` одной строкой
- [x] Поддержка опций: `--memory`, `--memory-manual`, `--stack-size`, `--crt0=`, `--bank N=FILE.c`, `--debug`, `-I`, `-L`/`-E`/`-S`, `-Wl`, `--mkexe`
## Этап 8 — графика (320×256×256 + 640×256×16 + accel + bitmap font) ✅ ГОТОВО
- [x] **8a** Graphics core: `gfx_init`/`gfx_done`/`gfx_clear`/`gfx_putpixel`/`gfx_pal_load`/`gfx_pal_set` (libc/gfx/gfx_core.c). Палитра через BIOS PIC_SET_PAL ($A4). Verified 320×256×256.
- [x] **8b** Линии/прямоугольники/fill через accelerator (libc/gfx/gfx_lines.c): `gfx_hline`/`gfx_vline` через accel Fill (LD C,C / LD E,E + SMC block-size), `gfx_rect`/`gfx_fill_rect` с heuristic выбором ориентации (h/v bursts count), `gfx_line` с Bresenham для диагоналей. `gfx_clear` тоже переписан на column-major accel (~4× быстрее).
- [x] **8c** 640×256×16 mode (libc/gfx/gfx_16.c): `gfx_*16` API, HIGH nibble = LEFT pixel (документация misleading), per-row RMW для vline (один байт = 2 горизонтальных пикселя).
- [x] **8d** Bitmap font + gfx_text (libc/gfx/gfx_text.c): шрифт через BIOS WIN_GET_ZG ($B8), interleaved layout `font[row*256+char]`, `gfx_text`/`gfx_putchar` для 320 mode, `gfx_text16`/`gfx_putchar16` для 640 mode с pair-table lookup.
См. memory/sprinter_graphics.md, sprinter_accelerator.md, sprinter_graphics_16.md, sprinter_font_format.md.
Открытые мелочи (не блокируют):
- [ ] Шрифт-quad для 640: per-cell палитра (mode 0x82 разрешает 1 из 4 палитр per 16×8 cell) — через прямой доступ к area-описания экрана 0x0300..0x039F
## Auto-banking (см. `memory/banking_roadmap.md` для деталей)
Phase 1 — file-level bin-packing — реализовывать когда проект перерастёт ~30 KB кода.
- [ ] `toolchain/auto_bank.py`:
- Парсит размеры из `.rel`-файлов (или из .map после dry-run link'а)
- First-fit-decreasing bin-packing
- Уважает `#pragma codeseg BANKn` как manual override
- Перелинковывает с новыми `-Wl-b_BANKn=...` параметрами
- Печатает план распределения
Phase 2-5: incremental rebalance, declarative `banks.toml`, function-level, call-graph-aware. Только если/когда понадобится.
## Bank-local static data (mutable data в том же банке что и код) — ✅ ГОТОВО
- [x] Пример `examples/bank_local_data/` — функция в BANK1 со своим writable BSS array + const table + malloc-тест
- [x] `mkexe -p 0` для нулевого padding банков (BSS-storage обнуляется при загрузке)
- [x] Канонический рецепт: `--codeseg BANK1 --constseg BANK1 --dataseg BANK1` для bank1.c + `-Wl-b_BANK1=0x1C000` для линковки. **`--dataseg BANK1` РАБОТАЕТ** — раньше казалось обратное из-за trampoline bug который маскировал результат.
- [x] **Критичный фикс trampoline'a в runtime/bank.s** — старый `pop af; out (n), a` клобберил A → все banked-функции возвращающие uint8_t тихо возвращали мусор. Новый `pop bc; out (c), b` сохраняет A.
- [x] **malloc из banked-функции работает прозрачно** — heap живёт в W2 (HOME), W2 никогда не свапается trampoline'ом, pointer валиден из любого контекста. См. memory/bank_local_data_pattern.md.
- [x] Документация в memory: `memory/bank_local_data_pattern.md` (полный рецепт + malloc + nuances), `memory/sdcc_banking.md` (trampoline fix)
- [ ] Опционально — расширить `check_banks.py` чтобы показывать разбивку size = code + const + bss per bank (cosmetic)
Зачем: для модулей с большим private state (level loader, audio engine, scene data). Экономит W2 heap для динамики, а статика остаётся в бэке.
## Подсказки из solid-c (нативный Sprinter C — `third_party/solid-c/`)
После анализа solid-c'овской libc (см. `memory/solid_c_findings.md`) выявлены готовые паттерны для следующих недостающих функций. Приоритет от **высокого** к низкому:
### High-priority gaps (легко портировать, большая польза)
- [x] **`errno` + `strerror`/`perror`** — табличка 32 ошибок (libc/io/errno.c)
- [x] **Расширенный `open()`** для O_CREAT/O_TRUNC/O_APPEND/O_EXCL state machine
- [x] **`atexit`** — 8-callback LIFO + `exit()` + `_exit()` (libc/io/atexit.c)
- [x] **`setjmp`/`longjmp`** — 6-байт jmp_buf={sp,ix,pc} (libc/io/setjmp.c)
- [x] **`sleep(seconds)`** — 50Hz halt-loop (libc/io/sleep.c)
- [x] **ESTEX ENV API** ($46, getenv/putenv) — libc/io/env.c. Учли doc-bug: реально A=0 это NOT FOUND
### Medium-priority (нужно для shell-like утилит)
- [ ] **Mouse driver**`rst $30h`, 17 функций. **Сначала тест что работает в MAME**.
- [x] **`ffirst`/`fnext` + ffblk_t struct** для directory listing — реализовано, demo: ls.exe
- [x] **`getdatetime`/`setdatetime`** через ESTEX $21/$22 — libc/io/time.c, demo: time_dir_test
- [x] **`chdir`/`getcwd`/`mkdir`/`rmdir`** — wrappers для ESTEX $1B-$1E — libc/io/fsdir.c
- [x] **conio: `kbhit`/`getch`/`getche`/`cputs`/`clrscr`/`gotoxy`** — реализовано
- [x] **conio extras**: `wherex`/`wherey` ($53), `wrchar`/`rdchar` ($58/$57), `textmode_get/set` ($50/$51), `clrscr_attr` ($56) + COLOR macros
### Low-priority — ✅ FILE* stack ГОТОВО
- [x] **Минимальный unbuffered FILE\*** — `libc/stdio/file.c` + `libc/include/stdio.h`. fopen/fclose/fputs/fgets/fread/fwrite/fseek/ftell/rewind/feof/ferror/clearerr/fflush + stdin/stdout/stderr как pseudo-streams. См. `memory/file_star_design.md` и `examples/filetest`.
- [ ] fprintf / fscanf — нужна printf-через-callback machinery. Пока пользователь может `sprintf(buf, ...) + fputs(buf, fp)`.
- [ ] Опциональный buffered mode (setvbuf, line/block buffering) — если когда-то понадобится.
### POSIX time API — ✅ ГОТОВО
- [x] `libc/io/posix_time.c` — time/localtime/gmtime/mktime/asctime/ctime поверх getdatetime. SDCC's time.rel избегаем (нельзя override _RtcRead). См. `examples/ptime`.
### sys/stat — ✅ ГОТОВО
- [x] `libc/io/stat.c` — POSIX stat/fstat. Гибрид open+fstat для файлов, ffirst+iter для папок (включая "."/".."). См. `examples/stattest` и `memory/estex_ffirst_dotdot.md`.
### assert — ✅ ГОТОВО (используем SDCC's __assert через fallback include path)
## libc/stdlib — ✅ не нужно делать (см. memory/sdcc_stdlib_works.md)
Проверено через `examples/stdlib_test/`: SDCC z80.lib содержит работающие реализации:
- `atoi/atol/atof, strtol/strtoul, rand/srand, qsort/bsearch, abs/labs, div/ldiv`
- Полный `<string.h>` (memchr/cmp/set/cpy, strcat/cmp/cpy/len/chr/spn/etc.)
- `<ctype.h>` (toupper/tolower)
- `<math.h>` (sinf/cosf/sqrtf/etc.)
Линкер автоматически тянет из z80.lib когда нужно. **НЕ переписывать**.
Наши Sprinter-specific обязательные модули остаются: atexit, env, errno, setjmp, putchar/puts/getchar, conio, fsdir, time, mouse, open/read/lseek/close.
## Build-system: libsprinter.lib + sprinter-cc — ✅ ГОТОВО
- [x] `lib/Makefile` — собирает каждый libc/*.c в `.rel`, архивирует через sdar в `lib/sprinter.lib`
- [x] Включает runtime/bank.s и runtime/heap.s (auto-pulled при __banked/malloc)
- [x] `bin/sprinter-cc` — bash-wrapper: `sprinter-cc -o foo.exe foo.c` одной строкой
- [x] Поддержка опций `--crt0=default|minimal|banked`, `--bank N=FILE.c`, `-I`, `-L`/`-E`/`-S`, `-Wl`, `--mkexe`
- [x] `examples/hello_sccc/` — демо: `hello.c` собирается за один shell-вызов, размер совпадает с ручным Makefile (925 байт)
- [x] Split `putchar.c``putchar.c` + `puts.c` для per-function granularity (puts override SDCC's z80.lib version)
- [x] Включён в `make all` (зависимость `lib` перед `examples`)
Возможные улучшения (опционально):
- [ ] Мигрировать остальные examples на sprinter-cc вместо ручных Makefile (косметика)
- [ ] Дальнейшая декомпозиция libc/*.c per-function (но текущая granularity уже даёт нужный размер — линкер пакетует .rel целиком, и для большинства файлов это одна функция)
## Этап 9 — memory modes для sprinter-cc
DSS выделяет страницы памяти по размеру приложения: < 16 KB → одна страница, в остальные окна подключается «страница #FF» (read=0xFF, write игнорится). Из-за этого CODE-в-W1 + DATA-в-W2 для маленькой программы молча ломается. См. [memory/sprinter_memory_modes.md](../../.claude/projects/-Volumes-SAM8-Projects-DIY-Z80-Sprinter-C-Compiler/memory/sprinter_memory_modes.md).
- [x] **`tiny`**: всё (CODE+DATA+стек) в W2. Default. Verified hello/argv/conio/malloc/file/etc.
- [x] **`--memory MODE` флаг в sprinter-cc**: parser + per-mode дефолты CODE_LOC/DATA_LOC, override через явные `--code-loc`/`--data-loc`. tiny работает; small/big/huge компилируются с warning'ом (runtime не готов). Реализовано 2026-05-30.
- [x] **`--memory-manual SPEC`**: парсит `CODE=W1|W2,DATA=W1|W2|SAME,BANKED=W1|W3`. Реализовано 2026-05-30.
- [x] **`small` runtime**: `runtime/crt0_small.s` использует ESTEX `$3D GETMEM` + `$3A SETWIN2` чтобы выделить и замапить W2-страницу ДО gsinit. **НЕ** BIOS `$C4` — стек на этом этапе в W1 (boot_stack в HOME), а BIOS требует стек в W2. После маппинга SP переключается на 0xBFFE, дальше стандартный flow. Реализовано 2026-05-30, verified hello.exe.
- [x] **`small` auto-detect для >16 KB программ**: `crt0_small.s` читает порт `0xC2` (текущая страница в W2 — не `0xA2`! это W1). Если 0xFF — выделяет page; иначе DSS уже сделала это (программа сама вылезла в W2). Один crt0 покрывает 0..30 KB. mkexe также разрешает HOME span W1+W2 (0x4000..0xBFFF). Verified hello: small (5 KB файл, SETWIN2 path) + 32 KB файл (auto-skip). Реализовано 2026-05-30.
- [x] **`big` runtime** (tiny + banked code в W1): параметризовали `crt0_banked.s` + `bank.s` через `.ifdef BANK_W1` — другой banking port (0xA2 vs 0xE2), другой load-addr (0x4000 vs 0xC000). sprinter-cc prepend'ит `BANK_W1 = 1` при `--memory big`, передаёт `mkexe -B 0x4000`. Пример `examples/banked_big/`. Реализовано 2026-05-30.
- [x] **`huge` runtime** (small + banked code в W3): merge W2-detect логики из `crt0_small.s` в `crt0_banked.s`. Существующий пример `examples/banked/` теперь использует MEMORY=huge. Реализовано 2026-05-30.
- [x] **`--debug` флаг**: prepend `DEBUG_RT = 1` в crt0 + `-DDEBUG_RT` в sdcc. Открывает symbol `_w2_self_allocated` (uint8_t) — runtime diagnostic кто аллоцировал W2. Реализовано 2026-05-30.
### Дизайн-решения по libc и crt0
**Одна `sprinter.lib`** работает для всех memory mode — `.rel`-члены relocatable, SDLD делает dead-code elimination per-member (без графики не подтягивает `gfx_core.rel` и т.д.). Verified hello vs malloc_test через map-файлы.
**`gfx.lib` отдельно — НЕ нужен**: dead-code elimination уже работает.
**`libc_banked` (libc в bank вместо HOME)** — идея на потом, когда HOME (16 KB) забит user-кодом + libc в `huge` mode. Реализуется через `--codeseg BANK0` при компиляции libc; trade-off: trampoline ~30 циклов на каждый libc-вызов. Триггер: реальная программа упрётся в HOME budget.
**HW-зависимые модули — `sprinter_home.lib` отдельно.** Часть libc физически не может быть забанкована в W3, потому что она РАБОТАЕТ с W3:
- `gfx_*` — пишет в видеопамять `0xC000+` после swap W3 на video page
- `bank_io` (mem_alloc_pages/bank_read/bank_write) — swap'ит W3 через `OUT (0xE2)`
- Будущие ISR — прерывание может прийти когда W3 на чём угодно
В huge mode эти модули ДОЛЖНЫ остаться в HOME (W1). Когда будем делать `libc_banked`, **одновременно** выделяем `sprinter_home.lib` (HOME-only) из `sprinter.lib` (bankable). Финальная схема:
```
sprinter_home.lib HOME-only: gfx, bank_io, ISR shims
sprinter.lib bankable: printf, malloc, string, conio, stdio, env, ...
sprinter_banked.lib тот же sprinter.lib но --codeseg BANK0 (для huge)
```
Триггер: реализация `--memory huge` runtime.
**crt0 — по одному на mode:**
- `crt0.s` — текущий, для **tiny/big**: SP=0xBFFE, парсит argv (W2-ресурс уже выделен DSS).
- `crt0_minimal.s` — текущий, для tiny без argv.
- `crt0_small.s`**новый, step 3**: для **small/huge**, аллоцирует W2 через `mem_alloc_pages` ДО gsinit, маппит в порт `0xA2`, потом стандартный flow.
- `crt0_banked.s` — текущий, для **big**: trampoline-таблица для W3 банков, CODE в W2.
- `crt0_banked_small.s`**новый**: huge = small (W2-alloc) + banked (W3 trampolines).
sprinter-cc подбирает crt0 по `--memory` mode (сейчас `--crt0=` это override).
- [x] **Настраиваемый размер стека**: флаг `sprinter-cc --stack-size BYTES`. Wrapper генерирует `heap_top.s` с `___sdcc_heap_end = stack_top + 1 - stack_size`, отдельный .rel линкуется per-program. Default ≈1278 байт (heap_top=0xBB00) из `runtime/heap_top.s`. Реализовано 2026-05-30.
Интерфейс: `sprinter-cc --memory [tiny|small|big|huge|manual] [--memory-manual SPEC] [--stack-size N] foo.c`. `--memory-manual` имеет смысл только с `--memory manual`.
## Known issues / quirks
- **ESTEX $46 ENV API**: ✅ работает. Док-ция в `DiskSyscalls.txt v1.6` ошибочно описывает return-status — A=0 это NOT FOUND, не FOUND. Зафиксировано в `memory/sprinter_platform.md`.
## ОБЯЗАТЕЛЬНЫЕ ЗАДАЧИ ДЛЯ V2 (после релиза v1)
### Turbo-C-style graphics API (BGI-like) — **MUST для v2**
Расширить наш `gfx_*` API до уровня **Turbo-C `<graphics.h>`** (BGI) для MS-DOS.
Программисты привыкшие к Turbo-C должны переносить графический код 1-в-1.
**Что должно быть** (на основе Borland BGI):
Setup/teardown:
- `initgraph()` / `closegraph()`у нас сейчас `gfx_init`/`gfx_done`, добавить alias
- `getmaxx()` / `getmaxy()` — макрос на GFX_WIDTH-1 / GFX_HEIGHT-1
- `cleardevice()` — alias to gfx_clear
- `getgraphmode()` / `setgraphmode()`у нас get_videomode/set_videomode
Color/palette:
- `setcolor(c)`, `getcolor()` — current draw color
- `setbkcolor(c)`, `getbkcolor()` — background color
- `setpalette(idx, c)` — палитра entry
- `getpalette(&info)` — read all palette
Primitives (мы уже имеем эквиваленты — добавить BGI-имена как aliases):
- `putpixel(x, y, c)` — есть как gfx_putpixel
- `getpixel(x, y)` — нужно реализовать (RMW обратное — IN)
- `moveto(x, y)`, `lineto(x, y)`, `linerel(dx, dy)` — current point + line drawing
- `line(x1, y1, x2, y2)` — есть как gfx_line
- `rectangle(x1, y1, x2, y2)` — есть как gfx_rect (но другой API: x1,y1,x2,y2 vs x,y,w,h!)
- `bar(x1, y1, x2, y2)` — есть как gfx_fill_rect
- `bar3d(x1, y1, x2, y2, depth, topflag)` — новое: rect + 3d edges
- `circle(x, y, r)`, `arc(...)`, `ellipse(...)`, `pieslice(...)` — новые primitives
- `fillpoly()`, `drawpoly()` — полигоны
- `floodfill(x, y, border_color)` — заливка
Text on graphics screen:
- `outtext(s)` / `outtextxy(x, y, s)` — есть как gfx_text (alias)
- `settextstyle(font, dir, size)` — multiple bitmap fonts
- `gettextsettings(&info)`
- `textwidth(s)` / `textheight(s)` — measure
Image manipulation:
- `imagesize(x1, y1, x2, y2)` — bytes needed for getimage
- `getimage(x1, y1, x2, y2, buf)` — save rect to buffer
- `putimage(x, y, buf, op)` — paste back with COPY_PUT/XOR_PUT/AND_PUT/OR_PUT/NOT_PUT
Clipping/viewport:
- `setviewport(x1, y1, x2, y2, clip)` — drawing clip rect
- `getviewsettings(&info)`
- `clearviewport()`
- `setactivepage(p)` / `setvisualpage(p)` — двойная буферизация (Sprinter имеет 2 screen)
Line style:
- `setlinestyle(style, pattern, thickness)` — SOLID_LINE / DOTTED_LINE / etc.
- `getlinesettings(&info)`
**Acceptance:** перенос типичной Turbo-C BGI программы (рисующей с использованием
moveto/lineto/circle/bar/setcolor) должен работать без существенных правок.
**Notes:**
- BGI fonts (TRIPLEX/SANS_SERIF/GOTHIC) — у нас один BIOS font, остальные нужно
добавить (как bitmap data в lib)
- imagesize/getimage/putimage — самые востребованные для game/animation
- Active/visual page (двойная буферизация) — Sprinter поддерживает 2 graphics pages,
нужен API switching
См. также `examples/` Turbo C 2.x BGIDEMO как reference что нужно.
### IM2 Interrupt Handlers — **MUST для v2**
User-задаваемые ISR через Z80 IM 2 mode. Нужны для:
- Timer ticks (50 Hz frame counter, плавная анимация)
- Music playback (AY, COVOX)
- Real-time games (input + game logic + render в interrupt-driven)
- Async keyboard / mouse handling
**Status:** ОТЛОЖЕНО до v2. Полный research + design в `docs/im2_isr_design.md`.
**Решение по архитектуре:** реализовать как отдельный memory mode `--memory im2`
(вместо того чтобы лезть во все существующие crt0). Detail'и в design-doc.
**Резюме research'а** (полный текст в `docs/im2_isr_design.md`):
- Vector 0xFF — frame + keyboard + CBL. Disambiguation по портам 0x19 / 0xFE
- Mouse hardware-IRQ не приходит (на текущей плате)
- Vector table / ISR / stack ОБЯЗАНЫ быть в W2 (0x8000..0xBFFF)
- DSS имеет свой IM 2 handler — нужно chain'иться (иначе клавиатура / SYSTIME ломаются)
### Прочие крупные пункты для v2
- [ ] **FILE API rewrite — buffered streams** — текущая реализация в
`libc/stdio/file.c` это provisional unbuffered shim (каждый fputc/fgetc
= один read/write syscall). Нужна полноценная buffered семантика
как в Solid-C:
```c
typedef struct {
uint flags; // +0..1 file status flags
int level; // +2..3 empty/fill level of buffer
char *curp; // +4..5 current active pointer
int fd; // +6..7 underlying low-level fd
char *buffer; // +8..9 data transfer buffer
char hold; // +10 ungetc byte if no buffer
short token; // +11..12 reserved
char dummy; // +13 reserved
} FILE;
```
stdin/stdout/stderr — fd-маркеры `0 / -1 / -2`. Отрицательные для
stdout/stderr выбраны намеренно: ESTEX OPEN может вернуть positive
small fd (1, 2, …) для обычного файла → если бы stdout=1, реальный
fd=1 сталкивался бы с идентификатором. fd=0 для stdin безопасно
(ESTEX 0 не возвращает). Сами fd не передаются в syscall'ы —
диспетчеризация по флагам `_F_CONIN/_F_CONOUT`.
Принтер-потоки (stdaux/stdprn) НЕ реализуем — Sprinter принтерной
API не имеет.
Альтернатива — взять реализацию из third_party/solid-c (sources в
`SRC/CLIB/`); там есть готовый buffered FILE + fopen/fread/fwrite/
fseek/setvbuf и т.д. Адаптировать к нашим open/read/write/lseek.
При rewrite заодно решить deferred issues stdio-review:
- `fwrite` short-write должен ставить `_F_ERROR`
- `fgets(buf, 1, fp)` — стандарт говорит "empty string", мы вернули NULL
- `mode_to_flags` — break-out на '+' (cosmetic)
- [ ] **Audio API** — AY-3-8910 + COVOX через прерывания (требует IM2)
- [ ] **ISA-8 slot support** — ZX-Bus карты (sound, network, etc.) — требует IM2 + чтения portов
## Прочие задачи (v1 backlog, не блокирующие)
- [x] **#9: text I/O split (Turbo-C style)** — stdio (puts/printf/putchar) теперь fast no-attr через PCHARS/PUTCHAR. conio (cputs/cprintf/putch) применяет attr через textcolor/textbackground/textattr. KEEP_EXIST_ATTR → conio fallback на fast path. Verified в hello.exe. См. `memory/text_output_api_split.md`. Реализовано 2026-05-31.
- [x] **Mouse API полный** (резидентный driver, RST 30h) — все 14 функций обёрнуты (init/show/hide/refresh/read/goto/bounds/text_cursor/load_cursor/get_cursor/get/set_sensitivity/video_mode_changed). См. `memory/mouse_api.md`. Verified в MAME 2026-05-31. Sensitivity = divider (меньше = быстрее).
- [ ] Interrupt handlers — IM 2 vector table в HOME для user ISR'ов
- [ ] Поддержка `restore SP on EXIT` (паттерн из z88dk +pps) — проверить нужно ли
- [ ] CI: автоматически запускать MAME с `-aviwrite` для screenshot-сравнения, чтобы тесты примеров проходили без человека
## Идеи на потом
- Поддержка `<setjmp.h>` (есть в SDCC stdlib — нужно протестировать что наш crt0 совместим)
- `<time.h>` через ESTEX SYSTIME (`$21`) и CMOS BIOS-функции
- ZX Spectrum-совместимый режим как отдельный target (для портирования спектрумовских программ)
- Поддержка ZX-Bus карт (sound, network, etc.) — нужны драйверы
- Profile-guided optimization tools (hot/cold detection) для крупных программ
## Linker duplicate-symbol warnings (благоприятные, отфильтрованы)
Когда мы сознательно overrides'им SDCC z80.lib функции собственной версией в `sprinter.lib`, `sdldz80` пишет `?ASlink-Warning-Definition of public symbol '...' found more than once`. Линкер берёт первое найденное определение (наше), поэтому поведение корректное — warning только noise.
Текущие overrides:
- `_puts` — наша версия через PCHARS+\r\n vs SDCC posix puts
- `___sdcc_heap` — наш heap в W2 vs SDCC's стандартный
- `_asctime`, `_localtime` (и возможно другие из time) — наш `posix_time.c` через ESTEX SYSTIME vs SDCC's `time.rel` который зависит от `_RtcRead`
**Текущее решение:** `bin/sprinter-cc` отфильтровывает warning-блок (warning + 2 follow-up `Library:` строки) из вывода `sdcc`. Через `-v` (verbose) всё показывается. Реализовано через awk-pipe.
**Возможные улучшения:**
- Перейти на explicit `--nostdlib` + ручной список нужных модулей из z80.lib (string, math, stdlib без override'нутых) — убрать ИСТОЧНИК warning'ов, не маскировать
- Или: переименовать наши `_puts` → `_puts_sprinter` + alias через linker flag (не уверен что SDCC поддерживает)
- Или: оставить как сейчас (рабочее и benign) — приоритет низкий
## TODO: проверить на реальном железе
- [ ] **Port_Y banking trick** (`docs/part2/SprinterGraphics programming.txt`):
доку утверждает что после `OUT (0x89), Y` адреса 0xC000+0x400*N в окне W3
маппятся на строки Y..Y+15 (одно программирование → 16 строк).
Empirical 2026-06-01 в MAME 0.283 этот trick **не работает** — пиксели
по адресам выше 0xC000+row_width уходят в невидимую область. Канонический
`docs/samples/plasma2.asm` тоже не использует banking, переустанавливает
Port_Y per row.
План:
1. Получить доступ к реальному Sprinter
2. Запустить тест dual-write (`_gfx_putpixel_raw` + второй write в `0xD000+x`)
3. Если на железе видны двойные линии → бага MAME, открыть issue с
минимальным репро
4. Если на железе тоже одна линия → документ неверный, удалить упоминание
из доки и просто оставить текущую реализацию (Port_Y per pixel)
5. Если banking работает на железе → внедрить кэширование Port_Y в
`_gfx_putpixel_raw` (sentinel out-of-range, см. memory/gfx_port_y_banking.md)
Связанный выигрыш для Bresenham (60-pixel диагональ) — около 8× меньше
OUT (0x89) операций, для `gfx_fill_rect 320x256` — 16× меньше. Не блокирует
release v1.
## GFX: расширения по `docs/part2/accelerator_doc.txt`
После прочтения детального accelerator doc выявлены незакрытые направления.
Сейчас в коде используется только горизонтальный/вертикальный Fill mode.
### Quick wins для текущих primitives
- [ ] **Заменить SMC на `LD A, (var)` для block-size**. Документ явно
разрешает `LD A, (HL)`, `LD A, (BC)`, `LD A, (DE)` (но не `LD A, r`).
Это уберёт SMC complexity в `gfx_lines.c:hfill_chunk/vfill_chunk` и
`gfx_16.c:g16_hfill_chunk`. Запрещено только register-to-register.
- [ ] **Кэширование block-size**. Документ показывает что accel запоминает
block size между bursts (см. `Horizontal_Line_Fill`: устанавливают
size + `LD B,B` отключение, потом включают Fill mode и используют
сохранённый size). Для `gfx_fill_rect` с 100 одинаковыми
строками — установить size 1 раз, а не 100.
### Bank-prefix modes (port 0xE2 bits)
Документ показывает три варианта банка видеостраницы помимо стандартного 0x50:
| Bank byte | Effect |
|---|---|
| 0x50 | Normal write — пишется в shadow + видимый |
| 0x54 | "no copy in main shadow RAM" |
| 0x58 | **"FF is transparent"** — байт 0xFF при write оставляет background |
| 0x5C | both |
Bank 0x58 объясняет почему mouse cursor рисуется с 0xFF-прозрачностью.
Это путь к **sprite-blending через accel block copy**:
- [ ] **`gfx_set_bank_transparent(on)`** или флаг в `gfx_set_bank` для
выбора 0x50/0x58 при отрисовке sprite'ов
- [ ] Использовать в новом `gfx_blit()` чтобы по факту получать
transparent sprites через accel-копию
### Block copy mode (sprite blit'ы)
`LD L,L` (horizontal) и `LD A,A` (vertical) — режим копирования блока через
256-байтную accel memory. Это базис для blit'ов.
- [ ] **`gfx_blit(src_data, x, y, w, h)`** — копирование sprite'а
(произвольный размер, через accel)
- [ ] **`gfx_blit_transparent(src, x, y, w, h)`** — с использованием bank 0x58
См. `Draw_Restangle_Data` в accelerator_doc.txt как референс.
### AND / OR / XOR operations через accel
Документ показывает что accel поддерживает логические операции с блоками
данных. Применения:
- XOR — инверсия области (выделение selection в UI)
- OR / AND — masking, alpha-style blending
- См. пример в accelerator_doc.txt: "256 bytes block coding via XOR"
- [ ] **`gfx_xor_rect`** / **`gfx_or_rect`** / **`gfx_and_rect`** —
примитивы логических операций над прямоугольником
- [ ] **`gfx_invert_rect(x, y, w, h)`** — alias на xor с 0xFF
### Bitmap fonts разных размеров
Сейчас `gfx_text` / `gfx_putchar` хардкоженно работают с 8×8 шрифтом
(BIOS WIN_GET_ZG возвращает 256×8 байт). Для будущих UI / титульников
нужны:
- [ ] **`gfx_set_font_size(w, h)`** — переключить ширину/высоту glyph'а
- [ ] **`gfx_set_font_data(ptr, w, h, advance)`** — заменить указатель
на пользовательский шрифт + размеры
- [ ] Поддержка **proportional** (advance != w) шрифтов — добавить
array advance[256] на ширину каждого glyph'а
- [ ] **Big-font режимы**: 8×16, 16×16, 16×8 (для титульников)
- [ ] Возможно отдельный API `gfx_text_ex(x, y, str, font_id)` где
font_id выбирает один из загруженных шрифтов
- [ ] **Anti-alias 2-bit шрифты** (бит фон / бит граница / 2-бит alpha?)
— far future, для smooth UI
## Финальный этап оптимизаций (не сейчас)
- **`gfx_line` через accel для пологих диагоналей** — Bresenham для линии с |dy| << |dx| (или наоборот) выдаёт длинные runs одинакового Y (или X): пиксель, пиксель, пиксель, шаг Y, пиксель, пиксель... Каждый такой run — это готовый аргумент для `gfx_hline` (или `vline`).
План исследования: посчитать длину runs как функцию от наклона; решить минимальный run length, при котором выгоднее accel hline чем N×putpixel (overhead accel ~20µs, putpixel ~5µs — accel выгоднее при run ≥ 4-5 px); для крутых диагоналей (dx ≈ dy) оставить Bresenham, для пологих — run-length-based fill.
Сейчас `gfx_line` orthogonal cases уже через accel — оптимизировать только косые.
- **`gfx_fill_rect` с одним W3-swap на всю операцию** — сейчас каждый внутренний `gfx_hline`/`gfx_vline` делает свой DI/save-W3/restore-W3/EI. Можно сделать internal `_fill_rect_inner` который держит W3 замапленным и DI весь цикл; ~20µs × количество строк/столбцов экономии. Применимо ко всем композитным примитивам.
+360 -2040
View File
File diff suppressed because it is too large Load Diff
+279
View File
@@ -0,0 +1,279 @@
/*
* mdview2.h общие определения и интерфейс между модулями mdview2.
*
* Монолит mdview2.c расщеплён на ядро + модули: index (парсер), md/raw (виды),
* enc (кодировки), table, help. Здесь разделяемые константы экрана/атрибутов,
* словари флагов/стилей, тип cache_rec_t, extern-объявления разделяемого
* состояния ядра и API каждого модуля (сгруппированы по секциям ниже).
*/
#ifndef MDVIEW2_H
#define MDVIEW2_H
#include <stdint.h>
#include <palette.h> /* COLOR(), COLOR_* */
#include "mdview2_conf.h" /* WITH_RAW / WITH_HEX — выбор опциональных модулей */
/* ---- Геометрия экрана -------------------------------------------- */
#define SCREEN_W 80
#define SCREEN_H 32
#define VIEW_TOP_ROW 1
#define VIEW_H 30 /* видимая область: строки 1..30 включительно */
#define MENU_ROW 31
#define TAB_STOP 4
#define HPAN_STEP 8u /* шаг горизонтального сдвига (←/→) */
/* ---- Геометрия статус-бара (строка 0): фиксированные поля и разделители ---- */
#define SPINNER_COL 8 /* слот спиннера загрузки */
#define DIV1_X 45 /* разделитель │ перед числами диапазона строк */
#define DIV2_X 71 /* разделитель │ перед процентом */
/* Числа/метка пишутся с DIVn_X+2; область между разделителями — [DIV1_X+1 .. DIV2_X-1]. */
/* ---- Параметры файла и памяти ------------------------------------ */
#define PAGE_BITS 14u
#define PAGE_SIZE (1u << PAGE_BITS) /* размер EMM-страницы: 16 КБ */
#define PAGE_MASK ((uint16_t)(PAGE_SIZE - 1u))
#define MAX_PAGES 16 /* 16 страниц × 16 КБ = 256 КБ */
#define MAX_FILE ((uint32_t)MAX_PAGES * PAGE_SIZE)
#define FILE_BUF ((char *)0xC000) /* окно W3, куда мапится текущая EMM-страница */
/* ---- Палитра атрибутов ------------------------------------------- */
#define ATTR_RESET COLOR(COLOR_LIGHTGRAY, COLOR_BLACK)
#define ATTR_TEXT COLOR(COLOR_LIGHTGRAY, COLOR_BLUE)
#define ATTR_TEXT_TITLE1 COLOR(COLOR_YELLOW, COLOR_BLUE)
#define ATTR_TEXT_TITLE2 COLOR(COLOR_LIGHTBLUE, COLOR_BLUE)
#define ATTR_TEXT_TITLE3 COLOR(COLOR_LIGHTGREEN, COLOR_BLUE)
#define ATTR_TEXT_TITLE4 COLOR(COLOR_LIGHTGREEN, COLOR_BLUE)
#define ATTR_TEXT_BOLD COLOR(COLOR_LIGHTRED, COLOR_BLUE)
#define ATTR_TEXT_ITALIC COLOR(COLOR_LIGHTGREEN, COLOR_BLUE)
#define ATTR_TEXT_UNDERSORE COLOR(COLOR_LIGHTMAGENTA, COLOR_BLUE)
#define ATTR_TEXT_CODE COLOR(COLOR_WHITE, COLOR_BLUE)
#define ATTR_TEXT_STRIKE COLOR(COLOR_DARKGRAY, COLOR_BLUE)
#define ATTR_LIST_MARKER COLOR(COLOR_LIGHTCYAN, COLOR_BLUE)
#define ATTR_QUOTE_MARKER COLOR(COLOR_CYAN, COLOR_BLUE)
#define ATTR_HR COLOR(COLOR_CYAN, COLOR_BLUE)
#define ATTR_BOX COLOR(COLOR_CYAN, COLOR_BLUE)
#define ATTR_TRUNC COLOR(COLOR_YELLOW, COLOR_BLUE)
#define ATTR_BAR COLOR(COLOR_BLACK, COLOR_LIGHTCYAN)
#define ATTR_BAR_SPINNER COLOR(COLOR_WHITE, COLOR_LIGHTCYAN)
#define ATTR_MENU_T COLOR(COLOR_BLACK, COLOR_LIGHTCYAN)
#define ATTR_MENU_K COLOR(COLOR_YELLOW, COLOR_BLACK)
#define ATTR_WARN COLOR(COLOR_YELLOW, COLOR_RED) /* строка-обрыв (исчерпан кэш/лимит) */
/* ---- Атрибуты диалога справки ------------------------------------ */
#define ATTR_HELP_BG COLOR(COLOR_LIGHTGRAY, COLOR_BLACK)
#define ATTR_HELP_BDR COLOR(COLOR_WHITE, COLOR_BLACK)
#define ATTR_HELP_TIT COLOR(COLOR_YELLOW, COLOR_BLACK)
#define ATTR_HELP_HDR COLOR(COLOR_WHITE, COLOR_BLACK)
/* ---- Режимы просмотра -------------------------------------------- */
#define VIEW_MD 0 /* markdown с форматированием (по умолчанию) */
#define VIEW_RAW_WRAP 1 /* RAW: длинные строки переносятся кратно 80 */
#define VIEW_RAW_HSCROLL 2 /* RAW: длинные строки в одну, гориз. скролл */
/* ---- Кодировки --------------------------------------------------- */
/* 8-битные (CP866/CP1251/KOI8R) различаются только ремапом глифов [128-255]
* на отрисовке; UTF-8 отдельный декодированный набор. */
#define ENC_CP866 0
#define ENC_CP1251 1
#define ENC_KOI8R 2
#define ENC_UTF8 3
#define ENC_UNSUPPORTED -1 /* UTF16 / UTF32 */
#define CONV_MARGIN 4096u /* на сколько байт держать UTF-конвертацию впереди индексатора */
/* ---- Флаги сегмента индекса (IF_*) — общие для индексатора/кэша/вью ---- */
#define IF_CONT 0x01u /* сегмент является продолжением перенесённой строки */
#define IF_NOWRAP 0x02u /* строка не переносится (кодовый блок / HR / таблица) */
#define IF_BLANK 0x04u /* визуально пустая строка */
#define IF_CODE 0x08u /* тело fenced code-блока (verbatim-режим) */
#define IF_HSCROLL 0x10u /* блок горизонтально скроллируется ЦЕЛИКОМ (код/таблица),
* включая строки короче 80 двигаются как единый блок.
* НЕ ставится на HR и границы fence (им нечего прятать). */
#define IF_TRUNC_MSG 0x20u /* строка-обрыв: рисуется ВЖИВУЮ фикс. текстом (без контент-кэша);
* cache_rec.reserved несёт код причины (TRUNC_*). */
/* ---- Причина обрыва индексации (cache_rec.reserved при IF_TRUNC_MSG) ---- */
#define TRUNC_CONTENT 1 /* исчерпан контент-кэш (MAX_CACHE_CONTENT_PAGES) */
#define TRUNC_LINES 2 /* исчерпана ёмкость индекса (max_lines) */
#define TRUNC_FILE 3 /* файл больше MAX_FILE — прочитаны первые 256 КБ */
/* ---- Словарь сегментов/стилей (общий для индексатора и таблиц) ---- */
/* Тип сегмента-продолжения, передаваемый в emit_seg()/inline_scan(). */
#define CK_PLAIN 0
#define CK_QUOTE 1
#define CK_LIST 2
#define CK_OTHER 3
/* Начальный inline-стиль сегмента (INIT_STYLE_*). */
#define INIT_STYLE_PLAIN 0x0
#define INIT_STYLE_BOLD 0x1
#define INIT_STYLE_ITALIC 0x2
#define INIT_STYLE_UNDER 0x3
#define INIT_STYLE_CODE 0x4
#define INIT_STYLE_STRIKE 0x5
/* ---- Таблицы: CP866 light box-drawing (общий для индексатора и модуля) ---- */
#define TBL_MAX_COLS 16
#define TBL_ATTR ATTR_BOX
#define TBL_H 0xC4 /* ─ */
#define TBL_V 0xB3 /* │ */
#define TBL_TL 0xDA /* ┌ */
#define TBL_TM 0xC2 /* ┬ */
#define TBL_TR 0xBF /* ┐ */
#define TBL_ML 0xC3 /* ├ */
#define TBL_MM 0xC5 /* ┼ */
#define TBL_MR 0xB4 /* ┤ */
#define TBL_BL 0xC0 /* └ */
#define TBL_BM 0xC1 /* ┴ */
#define TBL_BR 0xD9 /* ┘ */
/* ================================================================== *
* Разделяемые символы mdview2.c, используемые модулем RAW.
* ================================================================== */
extern uint8_t g_view; /* активный режим просмотра (VIEW_*) */
extern uint32_t file_size; /* размер активного буфера документа */
extern const uint8_t *g_remap; /* таблица ремапа [128-255] (0 = нет) */
extern uint8_t g_scratch_phys; /* scratch EMM-страница для win_rest */
char fb(uint32_t p); /* байт активного буфера (W3-маппинг) */
void win_rest(uint8_t row, uint8_t col, uint8_t h, uint8_t w, uint8_t page, uint16_t off);
void fill_row(uint8_t y, uint8_t attr);
uint32_t seg_off(uint16_t idx); /* offset исходника для логической строки idx */
/* ================================================================== *
* 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_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) */
/* ================================================================== *
* Запись директории рендер-кэша (одна на видимую строку, РОВНО 8 байт
* cache_dir_get/put адресуют сдвигом idx<<3). Контент len пар (char,attr).
* ================================================================== */
typedef struct cache_rec_s {
uint8_t page; /* EMM-страница рендер-кэша (физический номер) */
uint16_t off; /* смещение в странице, 0..16383 (байты, не ячейки) */
uint8_t len; /* длина контента в ЯЧЕЙКАХ (char,attr пар), 0..255; 0 = пустая/HR */
uint8_t flags; /* копия IF_NOWRAP/IF_CODE/IF_BLANK на момент рендера */
uint8_t reserved; /* не используется */
uint8_t pad[2]; /* явный резерв, добивка до 8 байт */
} cache_rec_t;
void cache_dir_get(uint16_t idx, cache_rec_t *r); /* читатель директории кэша (ядро) */
void put_str_attr(uint8_t x, uint8_t y, const char *s, uint8_t attr); /* печать строки с атрибутом */
/* ================================================================== *
* Состояние навигации/представления (определено в mdview2.c).
* ================================================================== */
extern uint16_t n_lines; /* всего строк в индексе текущего набора */
extern uint16_t top_line; /* верхняя видимая строка */
extern uint8_t viewport_x; /* горизонтальный сдвиг (nowrap-строки) */
extern char filename[]; /* имя файла для статус-бара */
extern uint8_t g_loading; /* 1 во время index_lines() */
extern uint8_t g_ready; /* 1 когда первичный документ построен (F2 RAW) */
extern uint8_t g_f8_enabled; /* можно ли сейчас переключать кодировку */
/* ================================================================== *
* Статус-бар (строка 0), меню (строка 31), спиннер mdview2_status.c.
* Атомарные части: prerender (фикс. хром, один раз) / encoding (поле кодировки)
* / numbers (диапазон строк + %) / menu / spinner обновляются по отдельности.
* ================================================================== */
void prerender_status(void); /* фикс. часть: фон, разделители, MDVIEW, имя файла (1 раз) */
void status_encoding(void); /* только поле кодировки (col 37) */
void render_md_status_numbers(void); /* числа MD: диапазон строк + % */
void render_raw_status(void); /* RAW mode: метка режима */
void render_full_status(void); /* encoding + numbers (MD) */
void render_menu(void); /* строка меню (по смене режима/готовности) */
uint16_t drawable_lines(void); /* число строк, готовых к показу */
void spinner_tick(void); /* кадр спиннера загрузки */
void spinner_show(uint8_t on); /* вкл/выкл спиннер */
void render_percent_progress(uint8_t pct); /* показать процентный прогресс просмотра */
/* ================================================================== *
* Отрисовка области MD-документа и прокрутка (mdview2_md.c).
* ================================================================== */
void draw_viewport_from_cache(void); /* перерисовать область документа из кэша */
void clamp_top(void); /* привести top_line к диапазону */
void md_scroll_up(uint16_t n);
void md_scroll_down(uint16_t n);
void md_scroll_horizon(int8_t delta); /* горизонтальный сдвиг nowrap-блоков */
uint8_t md_key(uint8_t scan); /* навигация MD (после загрузки); 1 = обработано */
/* ================================================================== *
* API модуля справки (mdview2_help.c).
* ================================================================== */
void show_help(void); /* модальный диалог F1 */
/* ================================================================== *
* Парсер/индексатор (mdview2_index.c) и разделяемое с ним состояние.
* Хранилище индекса (index_blk/pages/phys, n_lines/max_lines) в ядре
* (аллокация/doc-slot), индексатор пишет в него; cache_*-писатели и
* progress_tick ядро экспортирует индексатору (spinner_tick из status).
* ================================================================== */
#define INDEX_RECS_PER_PAGE 2048u /* 16384 / 8; запись всегда в одной странице */
#define MAX_CACHE_LINE_LEN 255u /* кап длины контента строки в ячейках (буфер g_cells) */
extern uint16_t max_lines; /* ёмкость индекса: index_pages * 2048 */
extern uint8_t index_blk, index_pages; /* дескриптор EMM-блока индекса */
extern uint8_t index_phys[]; /* физ. страницы индекса */
extern uint8_t index_truncated; /* индекс упёрся в ёмкость */
extern uint8_t g_abort; /* F10/Esc во время загрузки → прервать индексацию */
extern uint8_t g_file_clamped; /* файл был обрезан до MAX_FILE при загрузке (>256 КБ) */
uint8_t cache_reserve(uint16_t nbytes, uint8_t *out_page, uint16_t *out_off);
void cache_commit(uint8_t page, uint16_t off, const void *buf, uint16_t len);
void cache_dir_put(uint16_t idx, const cache_rec_t *r);
void progress_tick(void); /* кооперативный шаг loading-loop (рисует/листает) */
void index_lines(void); /* построить индекс по загруженному файлу */
/* ================================================================== *
* Буфер ячеек рендера (mdview2.c) общий с модулем таблиц.
* Таблицы эмитят свои строки теми же примитивами, что и индексатор.
* ================================================================== */
extern uint8_t g_ncells; /* ячеек в текущем сегменте */
void gc_put(char ch, uint8_t attr); /* добавить ячейку в g_cells */
void gc_fill(char ch, uint8_t attr, uint8_t n); /* n одинаковых ячеек */
uint8_t inline_scan(uint32_t q, uint32_t q_end, uint8_t col,
uint8_t ckind, uint8_t line_style, uint8_t base_attr,
uint8_t nowrap);
/* ================================================================== *
* API модуля таблиц (mdview2_table.c) вызывается из index_lines().
* ================================================================== */
uint32_t row_end(uint32_t p); /* offset завершающего '\n' (или file_size) */
uint32_t table_first_cell(uint32_t row_start); /* контент первой ячейки строки */
uint8_t table_next_cell(uint32_t *pp, uint32_t lineend, uint32_t *cs, uint32_t *ce);
uint8_t table_is_sep_row(uint32_t row_start, uint32_t lineend);
void table_border(const uint8_t *widths, uint8_t ncols, char left, char mid, char right);
void table_data_row(uint32_t row_start, uint32_t lineend, const uint8_t *widths, uint8_t ncols);
/* ================================================================== *
* Кодировки и UTF-8 конвертация (mdview2_enc.c).
* Состояние кодека владеется модулем; build_doc()/index_lines() ядра
* оркестрируют его (привязка UTF-буфера к живому file_*, сборка наборов).
* ================================================================== */
extern uint8_t g_encoding; /* активная кодировка (ENC_*) */
extern uint8_t g_utf_building; /* 1 пока UTF-конвертация не дошла до конца исходника */
extern uint8_t utf_avail; /* 1 = страницы UTF-8 выделены */
extern uint8_t utf_blk, utf_pages; /* дескриптор EMM-буфера UTF-набора */
extern uint8_t utf_phys[]; /* физ. страницы UTF-буфера */
extern uint32_t utf_size; /* размер сконвертированного UTF-потока */
/* Ядро-owned состояние, читаемое кодеком (определено в mdview2.c). */
extern uint8_t cur_page; /* текущая страница файла в W3 (инвалидация маппинга) */
extern uint8_t orig_file_phys[]; /* физ. страницы снимка исходного файла */
extern uint8_t orig_file_pages;
extern uint32_t orig_file_size;
void set_encoding(uint8_t enc); /* активная кодировка + таблица ремапа */
const char *enc_name(uint8_t enc); /* короткое имя для статус-бара */
uint8_t detect_encoding(void); /* автоопределение по сэмплу байт */
void win_rest_remap(uint8_t row, uint8_t w, uint8_t page, uint16_t off); /* вывод среза с ремапом */
void utf_convert_more(uint32_t target); /* инкрементальная конвертация до target байт */
uint8_t utf_alloc(void); /* выделить EMM-страницы под UTF-набор; 0 = нет EMM */
#endif /* MDVIEW2_H */
+16
View File
@@ -0,0 +1,16 @@
/*
* mdview2_conf.h конфигурация сборки mdview2: какие опциональные модули
* просмотра включать. 1 = включить (код входит в бинарь), 0 = исключить
* полностью (нулевой расход кода/памяти).
*
* Модули обёрнуты в #if WITH_xxx целиком, поэтому файлы можно держать в
* сборке всегда (при 0 они компилируются в пустой объектник) менять
* нужно только эти define.
*/
#ifndef MDVIEW2_CONF_H
#define MDVIEW2_CONF_H
#define WITH_RAW 1 /* RAW-просмотр исходника (F2): mdview2_raw.c */
#define WITH_HEX 0 /* HEX-просмотр (зарезервировано): mdview2_hex.c */
#endif /* MDVIEW2_CONF_H */
+315
View File
@@ -0,0 +1,315 @@
/*
* mdview2_enc.c кодировки и конвертация UTF-8 CP866.
*
* Две связанные подсистемы:
* 1. 8-битные кодировки (CP866/CP1251/KOI8-R) различаются только ремапом
* старших байтов [128-255] на ОТРИСОВКЕ (g_remap, win_rest_remap).
* Автоопределение detect_encoding по частоте ходовых букв.
* 2. UTF-8 отдельный набор: исходник инкрементально декодируется в
* utf_phys[] как чистый CP866 (utf_convert_more), после чего весь
* конвейер индексации/кэша работает по нему без ремапа.
*
* Владеет состоянием кодека (conv-буфер, позиция конвертации, UTF-страницы);
* описание разделяемых символов и API в mdview2.h. Оркестрацией (привязка
* UTF-буфера к живому file_*, сборка наборов) занимается build_doc() ядра.
*/
#include <stdint.h>
#include <conio.h> /* COLOR()/COLOR_* для ATTR_* в attr_is_content */
#include <sprinter.h> /* sprinter_page_w3 */
#include <sprinter_mem.h> /* bank_read/bank_write/mem_alloc_pages/mem_get_page */
#include "mdview2.h"
/* ---- Состояние кодировки/UTF (владелец — этот модуль) ------------- */
uint8_t g_encoding = ENC_CP866; /* активная кодировка */
uint8_t utf_blk = 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 пока конвертация не дошла до конца исходника */
/* Приватное состояние конвертера (наружу не торчит — сброс через
* utf_conv_reset(), чтение оригинала через cv_read). */
static uint32_t utf_src = 0; /* позиция чтения ОРИГИНАЛА (utf-8) */
static uint8_t cv_page = 0xFF; /* orig-страница в W3 для cv_read (своя от cur_page) */
/* Таблицы ремапа СТАРШИХ байтов [128-255] в CP866. Индексируются (ch-0x80):
* младшие 128 (ASCII) identity, в ремапе не участвуют (win_rest_remap
* трогает только ch>=0x80), поэтому в таблицах их нет. Кириллица + ходовая
* пунктуация; неизвестное '?' (0x3F). Сгенерированы Python codecs. */
static const uint8_t cp1251_to_866[128] = {
/* 80 */ 0x3F, 0x3F, 0x27, 0x3F, 0x22, 0x2E, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3C, 0x3F, 0x3F, 0x3F, 0x3F,
/* 90 */ 0x3F, 0x27, 0x27, 0x22, 0x22, 0x07, 0x2D, 0x2D, 0x3F, 0x3F, 0x3F, 0x3E, 0x3F, 0x3F, 0x3F, 0x3F,
/* A0 */ 0xFF, 0xF6, 0xF7, 0x3F, 0xFD, 0x3F, 0x3F, 0x3F, 0xF0, 0x63, 0xF2, 0x3C, 0x3F, 0x3F, 0x72, 0xF4,
/* B0 */ 0xF8, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0xFA, 0xF1, 0xFC, 0xF3, 0x3E, 0x3F, 0x3F, 0x3F, 0xF5,
/* C0 */ 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F,
/* D0 */ 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F,
/* E0 */ 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF,
/* F0 */ 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF,
};
static const uint8_t koi8r_to_866[128] = {
/* 80 */ 0xC4, 0xB3, 0xDA, 0xBF, 0xC0, 0xD9, 0xC3, 0xB4, 0xC2, 0xC1, 0xC5, 0xDF, 0xDC, 0xDB, 0xDD, 0xDE,
/* 90 */ 0xB0, 0xB1, 0xB2, 0x3F, 0xFE, 0xF9, 0xFB, 0x3F, 0x3F, 0x3F, 0xFF, 0x3F, 0xF8, 0x3F, 0xFA, 0x3F,
/* A0 */ 0xCD, 0xBA, 0xD5, 0xF1, 0xD6, 0xC9, 0xB8, 0xB7, 0xBB, 0xD4, 0xD3, 0xC8, 0xBE, 0xBD, 0xBC, 0xC6,
/* B0 */ 0xC7, 0xCC, 0xB5, 0xF0, 0xB6, 0xB9, 0xD1, 0xD2, 0xCB, 0xCF, 0xD0, 0xCA, 0xD8, 0xD7, 0xCE, 0x63,
/* C0 */ 0xEE, 0xA0, 0xA1, 0xE6, 0xA4, 0xA5, 0xE4, 0xA3, 0xE5, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE,
/* D0 */ 0xAF, 0xEF, 0xE0, 0xE1, 0xE2, 0xE3, 0xA6, 0xA2, 0xEC, 0xEB, 0xA7, 0xE8, 0xED, 0xE9, 0xE7, 0xEA,
/* E0 */ 0x9E, 0x80, 0x81, 0x96, 0x84, 0x85, 0x94, 0x83, 0x95, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E,
/* F0 */ 0x8F, 0x9F, 0x90, 0x91, 0x92, 0x93, 0x86, 0x82, 0x9C, 0x9B, 0x87, 0x98, 0x9D, 0x99, 0x97, 0x9A,
};
/* Устанавливает активную кодировку и таблицу ремапа (для 8-битных). */
void set_encoding(uint8_t enc)
{
g_encoding = enc;
g_remap = (enc == ENC_CP1251) ? cp1251_to_866 :
(enc == ENC_KOI8R) ? koi8r_to_866 : (const uint8_t *)0;
}
/* Самые ходовые строчные русские буквы (о е а и н т с р в л) — их байты
* различают 8-битные кодировки по частоте. */
// static const uint8_t common866 [10] = {0xAE,0xA5,0xA0,0xA8,0xAD,0xE2,0xE1,0xE0,0xA2,0xAB};
// static const uint8_t common1251[10] = {0xEE,0xE5,0xE0,0xE8,0xED,0xF2,0xF1,0xF0,0xE2,0xEB};
// static const uint8_t commonkoi [10] = {0xCF,0xC5,0xC1,0xC9,0xCE,0xD4,0xD3,0xD2,0xD7,0xCC};
// static uint8_t in_set10(const uint8_t *s, uint8_t b)
// {
// for (uint8_t i = 0; i < 10; i++) if (s[i] == b) return 1;
// return 0;
// }
/* Проверяем только пять самых популярных символов (о е а и н) */
static const uint8_t common866 [5] = {0xAE,0xA5,0xA0,0xA8,0xAD};
static const uint8_t common1251[5] = {0xEE,0xE5,0xE0,0xE8,0xED};
static const uint8_t commonkoi [5] = {0xCF,0xC5,0xC1,0xC9,0xCE};
static uint8_t in_set10(const uint8_t *s, uint8_t b)
{
for (uint8_t i = 0; i < 5; i++) if (s[i] == b) return 1;
return 0;
}
/* Автоопределение кодировки дешёвым сканом байтов (до построения индекса).
* BOM UTF8; иначе валидность UTF-8 (структура multibyte); иначе 8-бит по
* частоте ходовых букв; фолбэк CP866. */
uint8_t detect_encoding(void)
{
if (file_size >= 3 && (uint8_t)fb(0) == 0xEF && (uint8_t)fb(1) == 0xBB && (uint8_t)fb(2) == 0xBF)
return ENC_UTF8;
uint32_t n = file_size;
if (n > 1024u) n = 1024u; /* сэмпл: первый 1 КБ — детекции хватает */
uint8_t utf_ok = 1, has_mb = 0, has_high = 0, cont = 0;
uint16_t s866 = 0, s1251 = 0, skoi = 0;
for (uint32_t p = 0; p < n; p++) {
uint8_t b = (uint8_t)fb(p);
if (b < 0x80) { if (cont) { utf_ok = 0; cont = 0; } continue; }
has_high = 1;
if (in_set10(common866, b)) s866++;
if (in_set10(common1251, b)) s1251++;
if (in_set10(commonkoi, b)) skoi++;
if (cont) {
if ((b & 0xC0) == 0x80) cont--;
else { utf_ok = 0; cont = 0; }
}
else if (b >= 0xC2 && b <= 0xDF) { cont = 1; has_mb = 1; }
else if (b >= 0xE0 && b <= 0xEF) { cont = 2; has_mb = 1; }
else if (b >= 0xF0 && b <= 0xF4) { cont = 3; has_mb = 1; }
else utf_ok = 0; /* битый лид/одиночный континюэйшн */
}
/* Незавершённая multibyte-последовательность на КОНЦЕ — нарушение только
* если это настоящий EOF; на границе сэмпла (n<file_size) это просто
* обрезка, не считаем за ошибку. */
if (cont && n == file_size) utf_ok = 0;
if (!has_high) return ENC_CP866; /* чистый ASCII */
if (utf_ok && has_mb) return ENC_UTF8;
if (s1251 >= s866 && s1251 >= skoi) return ENC_CP1251;
if (skoi >= s866) return ENC_KOI8R;
return ENC_CP866;
}
const char *enc_name(uint8_t enc)
{
return (enc == ENC_CP1251) ? "CP1251" :
(enc == ENC_KOI8R) ? "KOI8-R" :
(enc == ENC_UTF8) ? "UTF-8 " : "CP866 ";
}
/* Контентный глиф (ремапим при смене кодировки) vs структурный (рамка/HR/
* маркеры уже CP866, не трогаем). Различаем по attr. */
static uint8_t attr_is_content(uint8_t a)
{
return (uint8_t)(a != ATTR_BOX && a != ATTR_HR &&
a != ATTR_LIST_MARKER && a != ATTR_QUOTE_MARKER);
}
/* Вывод среза строки кэша с ремапом контентных глифов [128-255] через
* g_remap (CP1251/KOI8). Без активной таблицы прямой win_rest. */
void win_rest_remap(uint8_t row, uint8_t w, uint8_t page, uint16_t off)
{
if (!g_remap || w == 0) {
win_rest(row, 0, 1, w, page, off);
return;
}
uint8_t buf[SCREEN_W * 2];
bank_read(page, off, buf, (uint16_t)w * 2u);
for (uint8_t i = 0; i < w; i++) {
uint8_t ch = buf[(uint16_t)i * 2u];
if (ch >= 0x80 && attr_is_content(buf[(uint16_t)i * 2u + 1u]))
buf[(uint16_t)i * 2u] = g_remap[ch - 0x80]; /* таблицы хранят только старшие 128 */
}
bank_write(g_scratch_phys, 0, buf, (uint16_t)w * 2u);
win_rest(row, 0, 1, w, g_scratch_phys, 0);
}
/* ========================= UTF-8 → CP866 (Фаза 2) ======================== */
/* Кириллица U+0400..U+045F → CP866. Русский набор + Ё/ё и часть украинских
* (Є є Ї ї Ў ў), которые есть в CP866; пропуски '?' (0x3F). */
static const uint8_t utf_cyr_to_866[96] = {
/* 0400 */ 0x3F, 0xF0, 0x3F, 0x3F, 0xF2, 0x3F, 0x3F, 0xF4, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0xF6, 0x3F,
/* 0410 */ 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F,
/* 0420 */ 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F,
/* 0430 */ 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF,
/* 0440 */ 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF,
/* 0450 */ 0x3F, 0xF1, 0x3F, 0x3F, 0xF3, 0x3F, 0x3F, 0xF5, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0xF7, 0x3F,
};
/* Поток вывода конвертера: пишем в utf_phys чанками по странице, буфер в
* near-памяти, флаш через один bank_write (не побайтовый swap W3). */
static uint8_t conv_buf[256];
static uint16_t conv_n = 0; /* байт в буфере, ещё не сброшено */
static uint8_t conv_page = 0; /* индекс текущей страницы в utf_phys */
static uint16_t conv_off = 0; /* уже сброшенное смещение в этой странице */
static void conv_flush(void)
{
if (conv_n) {
bank_write(utf_phys[conv_page], conv_off, conv_buf, conv_n);
conv_off = (uint16_t)(conv_off + conv_n);
conv_n = 0;
}
}
/* Добавляет один CP866-байт в выходной поток. Запись никогда не пересекает
* границу страницы (256 делит 16384 нацело). */
static void conv_put(uint8_t b)
{
if ((uint16_t)(conv_off + conv_n) == PAGE_SIZE) { /* страница заполнена */
conv_flush();
conv_page++;
conv_off = 0;
}
conv_buf[conv_n++] = b;
if (conv_n == (uint16_t)sizeof(conv_buf)) conv_flush();
}
/* Подстановки одиночных не-кириллических символов UTF-8 → один CP866-байт.
* Таблица вместо switch (экономия кода); линейный поиск дёшев вызывается
* лишь при конвертации на редких символьных кодпойнтах. Чтобы добавить новый
* символ достаточно дописать одну строку {кодпойнт, байт-CP866}.
* (Символы с НЕ-1:1 заменой, напр. -> "...", обрабатываются отдельно ниже.) */
typedef struct { uint16_t utf8; uint8_t cp866; } utf_sym_t;
static const utf_sym_t utf_sym[] = {
{0x00A0, 0x20}, {0x00AB, 0x3C}, {0x00BB, 0x3E}, /* nbsp « » → space < > */
{0x00B0, 0xF8}, {0x00B7, 0xFA}, /* ° · */
{0x2013, 0x2D}, {0x2014, 0x2D}, /* — → '-' */
{0x2018, 0x27}, {0x2019, 0x27}, /* → '\'' */
{0x201C, 0x22}, {0x201D, 0x22}, /* “ ” → '"' */
{0x2022, 0xF9}, {0x2116, 0xFC}, /* • → ∙ № */
{0x2190, 0x1B}, {0x2191, 0x18}, {0x2192, 0x1A}, {0x2193, 0x19}, /* ← ↑ → ↓ */
{0x2500, 0xC4}, {0x2502, 0xB3}, {0x250C, 0xDA}, {0x2510, 0xBF}, /* ─ │ ┌ ┐ */
{0x2514, 0xC0}, {0x2518, 0xD9}, {0x251C, 0xC3}, {0x2524, 0xB4}, /* └ ┘ ├ ┤ */
{0x252C, 0xC2}, {0x2534, 0xC1}, {0x253C, 0xC5}, /* ┬ ┴ ┼ */
{0x2580, 0xDF}, {0x2584, 0xDC}, {0x2588, 0xDB}, /* ▀ ▄ █ */
{0x2591, 0xB0}, {0x2592, 0xB1}, {0x2593, 0xB2}, {0x25A0, 0xFE}, /* ░ ▒ ▓ ■ */
{0x2713, 0xFB}, {0x2714, 0xFB}, /* ✓ ✔ → галка */
{0x2715, 0x78}, {0x2717, 0x78}, {0x2718, 0x78}, /* ✕ ✗ ✘ → 'x' */
};
#define UTF_SYM_N (uint8_t)(sizeof(utf_sym) / sizeof(utf_sym[0]))
/* Кодпойнт → CP866-глиф(ы). Кириллица по таблице; спецслучаи (…, BOM) явно;
* остальные ходовые символы по таблице utf_sym[]; прочее '?'. */
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('?');
}
/* Последовательное чтение байта ОРИГИНАЛА (utf-8) конвертером. Оригинал лежит
* в orig_file_phys[]; мапим его страницу в W3 напрямую, со своей кэш-переменной
* cv_page (отдельной от cur_page индексатора, т.к. оба используют W3 и
* чередуются). На границе с fb()-чтением utf-буфера кэши взаимно сбрасываются. */
static uint8_t cv_read(uint32_t s)
{
uint8_t pg = (uint8_t)(s >> PAGE_BITS);
if (pg != cv_page) { sprinter_page_w3(orig_file_phys[pg]); cv_page = pg; }
return *((volatile uint8_t *)(0xC000u + (uint16_t)(s & PAGE_MASK)));
}
/* Возобновляемая конвертация: дописывает utf_phys из оригинала, пока
* сконвертированных (flushed) байт меньше target и не достигнут конец
* исходника. По выходу file_size = доступная (сконвертированная) часть; при
* достижении конца фиксирует utf_size и снимает g_utf_building. */
void utf_convert_more(uint32_t target)
{
cv_page = 0xFF; /* W3 был на utf (fb индексатора) — пере-смаппим orig */
uint32_t n = orig_file_size;
while (utf_src < n &&
(uint32_t)((uint32_t)conv_page * PAGE_SIZE + conv_off) < target) {
uint8_t b = (uint8_t)cv_read(utf_src++);
if (b < 0x80) {
conv_emit_cp(b);
continue;
}
uint32_t cp;
uint8_t need;
if ((b & 0xE0) == 0xC0) { cp = (uint32_t)(b & 0x1F); need = 1; }
else if ((b & 0xF0) == 0xE0) { cp = (uint32_t)(b & 0x0F); need = 2; }
else if ((b & 0xF8) == 0xF0) { cp = (uint32_t)(b & 0x07); need = 3; }
else { conv_put('?'); continue; }
uint8_t ok = 1;
for (uint8_t k = 0; k < need; k++) {
if (utf_src >= n) { ok = 0; break; }
uint8_t cb = (uint8_t)cv_read(utf_src);
if ((cb & 0xC0) != 0x80) { ok = 0; break; }
cp = (cp << 6) | (uint32_t)(cb & 0x3F);
utf_src++;
}
if (!ok || need == 3) { conv_put('?'); continue; }
conv_emit_cp(cp);
}
conv_flush();
file_size = (uint32_t)conv_page * PAGE_SIZE + conv_off; /* доступно индексатору */
if (utf_src >= n) { utf_size = file_size; g_utf_building = 0; }
cur_page = 0xFF; /* W3 был на orig — fb индексатора пере-смаппит utf */
}
/* Выделяет страницы под UTF-8 набор (конвертированный ≤ оригинала по размеру)
* и сбрасывает приватное состояние конвертера в начало (conv-буфер, utf_src,
* g_utf_building). Сама конвертация инкрементальная, через utf_convert_more().
* Возврат: 1 успех, 0 нет EMM. */
uint8_t utf_alloc(void)
{
utf_pages = orig_file_pages;
utf_blk = mem_alloc_pages(utf_pages);
if (utf_blk == 0) return 0;
for (uint8_t i = 0; i < utf_pages; i++)
utf_phys[i] = mem_get_page(utf_blk, i);
conv_n = 0; conv_page = 0; conv_off = 0; /* converter reset (вызывается один раз) */
utf_src = 0;
g_utf_building = 1;
return 1;
}
+106
View File
@@ -0,0 +1,106 @@
/*
* mdview2_help.c диалог справки (F1).
*
* Модальное окно поверх документа: рамка с заголовком, список горячих
* клавиш и поддерживаемых markdown-элементов. Блокирует до нажатия любой
* клавиши, после чего восстанавливает статус-бар, область документа и меню.
*
* Зависимости от ядра (mdview2.c): render_full_status / draw_viewport_from_cache
* / render_menu для восстановления экрана. Текст справки в CP866.
*/
#include <stdint.h>
#include <string.h>
#include <conio.h>
#include <bios/text.h>
#include "mdview2.h"
/* Геометрия диалога справки (в символьных координатах 80×32). */
#define HELP_X 8u /* левая граница рамки */
#define HELP_Y 3u /* верхняя граница рамки */
#define HELP_W 64u /* ширина рамки (включая │) */
#define HELP_H 26u /* высота рамки (включая ─) */
/* Заполняет одну внутреннюю строку диалога (r=0 — первая строка за рамкой).
* Строка s в кодировке CP866; остаток до края дополняется пробелами. */
static void help_line(uint8_t r, const char *s, uint8_t attr)
{
uint8_t x = HELP_X + 1u;
uint8_t y = HELP_Y + 1u + r;
uint8_t len = (uint8_t)strlen(s);
if (len > HELP_W - 2u)
len = HELP_W - 2u;
bios_set_place(y, x);
if (len > 0)
bios_writeattr(s, len, attr);
/* place уже продвинут bios_writeattr на len колонок (verified). */
if (len < HELP_W - 2u) {
bios_fillcharattr(' ', ATTR_HELP_BG, (uint8_t)(HELP_W - 2u - len));
}
}
void show_help(void)
{
/* " Помощь " в CP866 (8 байт) */
static const char title[] = " \x8F\xAE\xAC\xAE\xE9\xEC ";
uint8_t tlen = 8u;
uint8_t lft = (uint8_t)((HELP_W - 2u - tlen) / 2u); /* = 27 */
uint8_t rgt = (uint8_t)(HELP_W - 2u - tlen - lft);
/* Верхняя граница рамки с заголовком по центру.
* place продвигается каждым BIOS-вызовом (verified), поэтому
* достаточно одной установки места на всю строку рамки. */
wrchar(HELP_X, HELP_Y, 0xDA, ATTR_HELP_BDR); /* ┌ */
bios_set_place(HELP_Y, (uint8_t)(HELP_X + 1u));
if (lft > 0) bios_fillcharattr(0xC4, ATTR_HELP_BDR, lft);
if (tlen > 0) bios_writeattr(title, tlen, ATTR_HELP_TIT);
if (rgt > 0) bios_fillcharattr(0xC4, ATTR_HELP_BDR, rgt);
wrchar(HELP_X + HELP_W - 1u, HELP_Y, 0xBF, ATTR_HELP_BDR); /* ┐ */
/* Боковые границы (левый и правый │ для каждой строки тела) */
for (uint8_t r = 1u; r < HELP_H - 1u; r++) {
wrchar(HELP_X, HELP_Y + r, 0xB3, ATTR_HELP_BDR); /* │ */
wrchar(HELP_X + HELP_W - 1u, HELP_Y + r, 0xB3, ATTR_HELP_BDR); /* │ */
}
/* Нижняя граница рамки */
wrchar(HELP_X, HELP_Y + HELP_H - 1u, 0xC0, ATTR_HELP_BDR); /* └ */
bios_set_place((uint8_t)(HELP_Y + HELP_H - 1u), (uint8_t)(HELP_X + 1u));
bios_fillcharattr(0xC4, ATTR_HELP_BDR, (uint8_t)(HELP_W - 2u));
wrchar(HELP_X + HELP_W - 1u, HELP_Y + HELP_H - 1u, 0xD9, ATTR_HELP_BDR); /* ┘ */
/* Содержимое (20 внутренних строк) */
uint8_t r = 0;
help_line(r++, "", ATTR_HELP_BG);
help_line(r++, " MDView v1.0 (a3) -- Markdown Viewer for Sprinter", ATTR_HELP_HDR);
help_line(r++, " (c) 2026 \x8F\xA5\xE2\xE0\xAE\xA2 \x80\x2E\x83\x2E",
ATTR_HELP_BG);
help_line(r++, "", ATTR_HELP_BG);
help_line(r++, " Navigation:", ATTR_HELP_HDR);
help_line(r++, " \x18 \x19 Scroll one line up / down", ATTR_HELP_BG);
help_line(r++, " PgUp PgDn Scroll one page up / down", ATTR_HELP_BG);
help_line(r++, " Home End Jump to beginning / end of document", ATTR_HELP_BG);
help_line(r++, " \x1B \x1A Horizontal pan (code blocks/tables/unwrap)", ATTR_HELP_BG);
help_line(r++, " Esc F10 Exit", ATTR_HELP_BG);
help_line(r++, "", ATTR_HELP_BG);
help_line(r++, " Markdown elements:", ATTR_HELP_HDR);
help_line(r++, " # ## ### Headings H1-H6, **bold**, *italic*,", ATTR_HELP_BG);
help_line(r++, " `code`, ``` code ```, ~~strike~~, > quote", ATTR_HELP_BG);
help_line(r++, " - * + 1. 2. Ordered list, |----|----| Tables", ATTR_HELP_BG);
help_line(r++, "", ATTR_HELP_BG);
help_line(r++, " Encoding and View modes:", ATTR_HELP_HDR);
help_line(r++, " F2 RAW Mode", ATTR_HELP_BG);
help_line(r++, " F3 Wrap/Unwrap Mode for RAW View", ATTR_HELP_BG);
help_line(r++, " 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();
render_full_status();
draw_viewport_from_cache();
render_menu();
}
File diff suppressed because it is too large Load Diff
+227
View File
@@ -0,0 +1,227 @@
/*
* mdview2_md.c представление MD-документа (peer к mdview2_raw.c): отрисовка
* области из рендер-кэша + прокрутка (верт./гориз.) + навигация md_key().
* Статус-бар, меню и спиннер в mdview2_status.c (общие для MD/RAW).
*
* Работает поверх готового рендер-кэша (cache_dir_get cache_rec_t) и
* состояния навигации (top_line/viewport_x/n_lines, см. mdview2.h). Никакого
* парсинга/обращения к исходному файлу: всё рисуется из (char,attr)-ячеек кэша
* через win_rest_remap (ремап 8-битных кодировок в mdview2_enc.c).
*
* Пара видов: mdview2_md.c (MD, из кэша) mdview2_raw.c (RAW, из fb()).
*/
#include <stdint.h>
#include <conio.h> /* wrchar/scroll/COLOR */
#include <bios/text.h> /* bios_set_place/bios_fillcharattr */
#include "mdview2.h"
/* Фаза 4-5 — cache-only draw path: рисует одну строку ИЗ КЭША, без
* единого обращения к fb()/исходному файлу. Горизонтальный скролл для
* nowrap-строк (Фаза 5) это просто смещение начала среза на vx*2
* байт ВНУТРИ ТОГО ЖЕ кэш-буфера (cache_reserve гарантирует, что вся
* строка лежит в одной странице, так что off+vx*2 не пересекает
* границу страницы); не nowrap-строки игнорируют viewport_x, как и
* раньше в живом render_line() (там это называлось effective_vx). */
static void draw_line_from_cache(uint16_t line_idx, uint8_t row)
{
cache_rec_t rec;
cache_dir_get(line_idx, &rec);
/* Строка-обрыв (исчерпан контент-кэш / лимит строк): фикс. текст вживую,
* без обращения к контент-кэшу (см. index_lines финализацию). */
if (rec.flags & IF_TRUNC_MSG) {
const char *msg;
switch (rec.reserved) {
case TRUNC_LINES: msg = "*** Line limit reached - document truncated ***"; break;
case TRUNC_FILE: msg = "*** File too large - truncated at 256 KB ***"; break;
default: msg = "*** Content cache exhausted - document truncated ***"; break;
}
bios_set_place(row, 0);
bios_fillcharattr(' ', ATTR_WARN, SCREEN_W);
put_str_attr(2, row, msg, ATTR_WARN);
return;
}
bios_set_place(row, 0);
bios_fillcharattr(' ', ATTR_TEXT, SCREEN_W);
/* Горизонтальный сдвиг применяется ко ВСЕМ строкам hscroll-блока (код,
* таблицы) по ТИПУ, а не по длине: блок двигается целиком, включая
* строки короче 80 (короткая строка, ушедшая за левый край, просто
* показывает маркер '<' на пустом месте). HR и границы fence не имеют
* IF_HSCROLL и не сдвигаются. */
uint8_t vx = (rec.flags & IF_HSCROLL) ? viewport_x : 0;
if (rec.len > vx) {
uint16_t remaining = (uint16_t)(rec.len - vx);
uint8_t w = (remaining > SCREEN_W) ? SCREEN_W : (uint8_t)remaining;
uint16_t off = (uint16_t)(rec.off + (uint16_t)vx * 2u);
win_rest_remap(row, w, rec.page, off);
if (remaining > SCREEN_W) {
wrchar(SCREEN_W - 1, row, '>', ATTR_TRUNC);
}
}
if (vx > 0) {
wrchar(0, row, '<', ATTR_TRUNC);
}
}
/* Перерисовывает все VIEW_H строк окна из кэша — основной путь
* перерисовки видимой области после загрузки файла (Фаза 3-4). */
void draw_viewport_from_cache(void)
{
for (uint8_t i = 0; i < VIEW_H; i++) {
draw_line_from_cache((uint16_t)(top_line + i), (uint8_t)(VIEW_TOP_ROW + i));
}
}
/* ==================================================================
* Прокрутка
* ================================================================== */
/* Ограничивает top_line допустимым диапазоном с учётом высоты окна. */
void clamp_top(void)
{
if (n_lines <= VIEW_H) {
top_line = 0;
} else if (top_line > n_lines - VIEW_H) {
top_line = (uint16_t)(n_lines - VIEW_H);
}
}
/* Прокрутка вверх на n строк с частичной перерисовкой при n == 1. */
void md_scroll_up(uint16_t n)
{
uint16_t new_top_line = (top_line >= n) ? (uint16_t)(top_line - n) : 0;
if( new_top_line != top_line) {
top_line = new_top_line;
if (n == 1) {
scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 2, 0);
draw_line_from_cache((uint16_t)(top_line), (uint8_t)(VIEW_TOP_ROW));
} else {
draw_viewport_from_cache();
}
}
}
/* Прокрутка вниз на n строк с частичной перерисовкой при n == 1. */
void md_scroll_down(uint16_t n)
{
uint16_t new_top_line = (top_line + n < n_lines - VIEW_H) ?
(uint16_t)(top_line + n) : n_lines - VIEW_H;
if( new_top_line != top_line) {
top_line = new_top_line;
if (n == 1) {
scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 1, 0);
draw_line_from_cache((uint16_t)(top_line + VIEW_H - 1), (uint8_t)(VIEW_TOP_ROW + VIEW_H - 1));
} else {
clamp_top();
draw_viewport_from_cache();
}
}
}
/* Горизонтальный сдвиг (только если в окне есть nowrap-строки).
* Максимум сдвига ограничен самой широкой nowrap-строкой на экране.
* Фаза 5: ширина берётся прямо из директории кэша (rec.len уже
* посчитанная реальная длина в ячейках после рендера), без единого
* обращения к fb()/seg_off()/исходному файлу. */
void md_scroll_horizon(int8_t delta)
{
uint16_t maxw = 0;
for (uint8_t i = 0; i < VIEW_H; i++) {
uint16_t li = (uint16_t)(top_line + i);
if (li >= n_lines) break;
cache_rec_t rec;
cache_dir_get(li, &rec);
if (!(rec.flags & IF_HSCROLL)) continue; /* только код/таблицы */
if (rec.len > maxw) maxw = rec.len; /* самая широкая строка блока */
}
if (maxw == 0) return; /* нет строк шире экрана */
/* Максимальный сдвиг = ширина за пределами экрана, в границах uint8. */
uint16_t over = (maxw > SCREEN_W) ? (uint16_t)(maxw - SCREEN_W) : 0;
if (over > 248u) over = 248u;
uint8_t max_vx = (uint8_t)over;
int16_t nx = (int16_t)viewport_x + delta;
if (nx < 0) nx = 0;
if (nx > (int16_t)max_vx) nx = max_vx;
/* ПОРЯДОК ВАЖЕН: записываем viewport_x ДО сравнения, а сравниваем уже
* сохранённую копию old_vx с new_vx. Иначе SDCC генерирует для
* `if (new_vx != viewport_x) viewport_x = new_vx;` такой код:
* ld a, new_vx ; sub a,(viewport_x) ; jr Z,.. ; ld (viewport_x),a
* т.е. для записи переиспользует регистр A, испорченный вычитанием в
* сравнении, и кладёт в viewport_x не new_vx, а (new_vx - old_vx).
* (asm-дамп 2026-06-24 подтвердил; даёт 10-8=2, 10-2=8 ровно
* наблюдавшийся "прыжок" скролла.) Записав viewport_x первой, мы убираем
* портящий sub с пути записи; сравнение ниже только решает, перерисовывать
* ли экран. */
uint8_t new_vx = (uint8_t)nx;
uint8_t old_vx = viewport_x;
viewport_x = new_vx;
if (new_vx != old_vx)
draw_viewport_from_cache();
}
/* Процент прокрутки (0..100) относительно доступного диапазона. */
static uint8_t calc_md_pct(void)
{
uint16_t total = drawable_lines();
if (total <= VIEW_H) return 100;
return (uint8_t)((uint32_t)top_line * 100UL / (uint32_t)(total - VIEW_H));
}
/* ---- навигация MD после загрузки (peer к raw_key) ---------------- */
/* Обрабатывает навигационную клавишу в MD-режиме (вызывается из главного
* цикла). Сама перерисовывает область/статус. Возврат: 1 обработано,
* 0 не наша клавиша (F1/F8/F10 разбирает main). */
uint8_t md_key(uint8_t scan)
{
switch (scan) {
case KEY_UP:
md_scroll_up(1);
break;
case KEY_DOWN:
md_scroll_down(1);
break;
case KEY_LEFT:
md_scroll_horizon(-(int8_t)HPAN_STEP);
break;
case KEY_RIGHT:
md_scroll_horizon(+(int8_t)HPAN_STEP);
break;
case KEY_PGUP:
md_scroll_up(VIEW_H);
break;
case KEY_PGDN:
md_scroll_down(VIEW_H);
break;
case KEY_HOME:
if(top_line != 0 || viewport_x != 0 ) {
top_line = 0;
viewport_x = 0;
draw_viewport_from_cache();
}
break;
case KEY_END:
uint16_t new_top_line = (n_lines > VIEW_H) ? (uint16_t)(n_lines - VIEW_H) : 0;
if(top_line != new_top_line || viewport_x != 0 ) {
top_line = new_top_line;
viewport_x = 0;
draw_viewport_from_cache();
}
break;
default:
return 0; /* не наша клавиша */
}
render_percent_progress(calc_md_pct());
render_md_status_numbers();
return 1;
}
+299
View File
@@ -0,0 +1,299 @@
/*
* mdview2_raw.c RAW-просмотр исходного текста (без markdown-форматирования).
*
* Работает по АКТИВНОМУ буферу документа через fb() (8-бит = исходные байты,
* UTF-8 = декодированный в CP866 буфер), ремап 0x80 (CP1251/KOI8) на
* отрисовке. Индекс/кэш markdown не используются: позиция байт-offset,
* 1 байт = 1 ячейка, \t показываем пробелом. Два режима:
* VIEW_RAW_WRAP длинные строки переносятся кратно 80 (рвём слова);
* VIEW_RAW_HSCROLL строка в один ряд, горизонтальный скролл.
*
* Память: 0 доп. EMM; near-буфер строки + переиспользование g_scratch_phys.
*/
#include <stdint.h>
#include <stdio.h>
#include <conio.h>
#include <bios/text.h>
#include "mdview2.h"
#if WITH_RAW /* весь модуль — опционален (mdview_conf.h); при 0 пустой объектник */
static uint32_t raw_top = 0; /* байт-offset верхней строки экрана */
static uint16_t raw_hpan = 0; /* гориз. сдвиг (только HSCROLL) */
/* ---- примитивы навигации по физическим строкам / переносам -------- */
static uint32_t raw_line_start(uint32_t off)
{
while (off && fb(off - 1) != '\n') off--;
return off;
}
/* WRAP: начало следующего ряда от off (\n завершает строку; иначе +80). */
static uint32_t raw_next_wrap(uint32_t off)
{
uint32_t e = off, lim = off + SCREEN_W;
while (e < file_size) {
if (fb(e) == '\n') return e + 1;
e++;
if (e >= lim) { /* прошли 80 без \n */
if (e < file_size && fb(e) == '\n') return e + 1; /* ровно 80 + \n */
return e; /* длинная строка → продолжение */
}
}
return e; /* EOF */
}
static uint32_t raw_prev_wrap(uint32_t off)
{
if (off == 0) return 0;
uint32_t pls = raw_line_start(off - 1); /* off-1 — это \n пред. строки */
while (pls + SCREEN_W < (off - 1)) pls += SCREEN_W;
return pls;
}
/* HSCROLL: начало следующей/предыдущей физической строки. */
static uint32_t raw_next_line(uint32_t off)
{
while (off < file_size && fb(off) != '\n') off++;
return (off < file_size) ? off + 1 : off;
}
static uint32_t raw_prev_line(uint32_t off)
{
if (off == 0) return 0;
return raw_line_start(off - 1);
}
/* ---- отрисовка ряда (общий near-буфер → g_scratch_phys → win_rest) - */
static uint8_t raw_buf[SCREEN_W + 1]; /* только символы (атрибут единый, задаётся при выводе) */
/* В RAW атрибут единый (ATTR_TEXT): буфер — только 80 символов, без пар
* (char,attr). Кладём один байт-глиф (\tпробел, ремап 0x80). */
static void raw_cell(uint8_t i, uint8_t ch)
{
if (ch >= 0x80 && g_remap) ch = g_remap[ch - 0x80];
raw_buf[i] = ch;
}
/* Вывод готового 80-символьного буфера в строку экрана одним BIOS-вызовом
* (char-буфер + единый attr без WINREST/scratch-страницы). */
static void raw_flush(uint8_t row)
{
bios_set_place(row, 0);
// bios_writeattr((const char *)raw_buf, SCREEN_W, ATTR_TEXT);
// bios_writeattr_until((const char *)raw_buf, SCREEN_W, ATTR_TEXT, 0);
bios_write_until((const char *)raw_buf, SCREEN_W, 0);
}
static uint32_t raw_draw_wrap_row(uint32_t off, uint8_t row)
{
uint8_t i = 0;
uint32_t e = off, lim = off + SCREEN_W;
while (i < SCREEN_W && e < file_size) {
char c = fb(e);
if (c == '\n') {
raw_cell(i, 0);
break;
}
raw_cell(i, (uint8_t)(c == '\t' ? ' ' : c));
i++; e++;
}
raw_flush(row);
return raw_next_wrap(off);
}
static uint32_t raw_draw_hscroll_row(uint32_t off, uint8_t row)
{
uint32_t e = off;
while (e < file_size && fb(e) != '\n') e++; /* e = конец строки */
uint16_t len = e - off;
uint8_t i = 0;
uint32_t p = off + raw_hpan;
while (i < SCREEN_W && p < e) {
char c = fb(p);
raw_cell(i, (uint8_t)(c == '\t' ? ' ' : c));
i++; p++;
}
// raw_buf[i] = 0;
if (raw_hpan > 0)
raw_buf[0] = '<';
if (len > (uint16_t)raw_hpan + SCREEN_W)
raw_buf[SCREEN_W - 1] = '>';
if (i < SCREEN_W)
raw_buf[i] = 0;
raw_flush(row);
return (e < file_size) ? e + 1 : e;
}
/* Рисует один ряд от off (по режиму) и возвращает offset следующего ряда. */
static uint32_t raw_draw_one(uint32_t off, uint8_t row)
{
if (off >= file_size) { fill_row(row, ATTR_TEXT); return off; }
return (g_view == VIEW_RAW_WRAP) ? raw_draw_wrap_row(off, row)
: raw_draw_hscroll_row(off, row);
}
void raw_draw(void)
{
uint32_t off = raw_top;
for (uint8_t r = 0; r < VIEW_H; r++)
off = raw_draw_one(off, (uint8_t)(VIEW_TOP_ROW + r));
}
/* Прокрутка на одну строку: аппаратный scroll окна документа + отрисовка
* ОДНОЙ новой строки (как в MD-режиме). Смещение нижнего ряда вычисляется
* проходом VIEW_H рядов от raw_top без отдельного инкрементального счётчика,
* который рассинхронизировался, когда контент кончается в середине экрана
* (короче окна после wrapunwrap у конца файла). */
static uint32_t raw_next(uint32_t off)
{
return (g_view == VIEW_RAW_WRAP) ? raw_next_wrap(off) : raw_next_line(off);
}
static uint32_t raw_prev(uint32_t off)
{
return (g_view == VIEW_RAW_WRAP) ? raw_prev_wrap(off) : raw_prev_line(off);
}
/* ---- статус-бар: % по байтам (отрисовка — render_raw_status_numbers
* в mdview2_status.c, читает позицию через этот raw_pct) ---- */
/* Процент по байтам без 32-битного деления: масштабируем offset'ы (>>8),
* дальше 32-битная арифметика. 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);
}
/* ---- посев позиции / пересев при смене активного буфера ----------- */
void raw_seed_from(uint16_t md_top_line)
{
uint32_t off = seg_off(md_top_line);
if (off > file_size) off = 0;
raw_top = raw_line_start(off);
raw_hpan = 0;
}
/* Сброс в начало — при смене активного буфера (F8 в/из UTF-8: размеры/offset'ы
* разные, старый raw_top невалиден). Без percent-математики. */
void raw_home(void)
{
raw_top = 0;
raw_hpan = 0;
}
void raw_end(void)
{
raw_top = file_size; /* отмотать VIEW_H рядов назад */
for (uint8_t k = 0; k < VIEW_H; k++)
raw_top = raw_prev(raw_top);
raw_hpan = 0;
}
void raw_screen_init(void) {
bios_clearwin(1, 0, 30, 80, ATTR_TEXT);
}
/* Нормализация позиции при смене под-режима RAW (F3 Wrap/Unwrap): выровнять
* raw_top на начало физической строки и снять горизонтальный сдвиг. */
void raw_renorm(void)
{
raw_top = raw_line_start(raw_top);
raw_hpan = 0;
}
void raw_scroll_down(uint8_t delta) {
uint32_t off = raw_top;
for (uint8_t r = 0; r < VIEW_H; r++) {
if (off >= file_size)
return; /* экран не заполнен контентом */
off = raw_next(off);
}
if (off >= file_size)
return; /* контент ровно по экран, ниже пусто */
if(delta ==1) {
scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 1, 0); /* содержимое вверх */
off = raw_draw_one(off, (uint8_t)(VIEW_TOP_ROW + VIEW_H - 1)); /* новый нижний ряд */
raw_top = raw_next(raw_top);
} else {
for (uint8_t k = 0; k < delta; k++) {
uint32_t n2 = raw_next(raw_top);
if (n2 >= file_size)
break;
raw_top = n2;
}
raw_draw(); /* PgUp/PgDn/Home/End — полный экран */
}
}
void raw_scroll_up(uint8_t delta) {
if (raw_top == 0)
return;
if (delta == 1) {
raw_top = raw_prev(raw_top);
scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 2, 0); /* содержимое вниз */
(void)raw_draw_one(raw_top, VIEW_TOP_ROW);
} else {
for (uint8_t k = 0; k < delta; k++)
raw_top = raw_prev(raw_top);
raw_draw(); /* PgUp/PgDn/Home/End — полный экран */
}
}
/* ---- навигация (вызывается из главного цикла при g_view != VIEW_MD) - */
uint8_t raw_key(uint8_t scan)
{
switch (scan) {
case KEY_DOWN:
raw_scroll_down(1);
break;
case KEY_UP:
raw_scroll_up(1);
break;
case KEY_PGDN:
raw_scroll_down(VIEW_H);
break;
case KEY_PGUP:
raw_scroll_up(VIEW_H);
break;
case KEY_HOME:
raw_home();
raw_draw();
break;
case KEY_END:
raw_end();
raw_draw();
break;
case KEY_LEFT:
if (g_view == VIEW_RAW_HSCROLL && raw_hpan) {
raw_hpan = (raw_hpan >= HPAN_STEP) ? (uint16_t)(raw_hpan - HPAN_STEP) : 0;
raw_draw();
}
break;
case KEY_RIGHT:
if (g_view == VIEW_RAW_HSCROLL && raw_hpan < 248u) {
raw_hpan = (uint16_t)(raw_hpan + HPAN_STEP);
raw_draw();
}
break;
default:
return 0; /* не наша клавиша */
}
// render_raw_status_numbers();
render_percent_progress(calc_raw_pct());
return 1;
}
#endif /* WITH_RAW */
+176
View File
@@ -0,0 +1,176 @@
/*
* mdview2_status.c статус-бар (строка 0), нижнее меню (строка 31) и спиннер.
*
* Хром бара АТОМИЗИРОВАН обновляется по частям, без перерисовки всего бара:
* prerender_status() фон + разделители + "MDVIEW" + имя файла (1 раз);
* status_encoding() только поле кодировки (col 37), по F8;
* render_md_status_numbers() числа MD: диапазон строк + % (при скролле);
* render_raw_status_numbers() числа RAW: метка режима + % по байтам;
* render_full_status() encoding + numbers (MD), при смене набора/режима;
* render_menu() строка меню, по смене режима/готовности;
* spinner_tick/show индикатор занятости во время загрузки.
* Оба *_status_numbers() самоочищаются по геометрии разделителей (DIV1_X/DIV2_X
* в mdview2.h) фикс. хром не перерисовывается. RAW-позицию даёт raw_pct()
* из mdview2_raw.c (raw_top там приватен).
*
* Раскладка строки 0:
* 0 1..6 8 10.. 37..43 45 46..70 71 72..79
* . MDVIEW spinner filename encod. L a-b / total pct%
*/
#include <stdint.h>
#include <stdio.h> /* dec8/dec16, cputs */
#include <conio.h> /* textattr/gotoxy/wrchar/COLOR */
#include <limits.h>
#include <bios/text.h> /* bios_fillcharattr/bios_write_until */
#include "mdview2.h"
/* ---- Спиннер (слот SPINNER_COL в баре) --------------------------- */
static const char spinner_chars[4] = { '|', '/', '-', '\\' };
static uint8_t spinner_phase = 0;
static uint8_t spinner_active = 0;
/* Продвигает спиннер на один кадр (если включён). */
void spinner_tick(void)
{
if (!spinner_active)
return;
wrchar(SPINNER_COL, 0, spinner_chars[spinner_phase & 3], ATTR_BAR_SPINNER);
spinner_phase++;
}
/* Вкл/выкл спиннер; при выключении гасит его позицию. */
void spinner_show(uint8_t on)
{
spinner_active = on;
if (!on) wrchar(SPINNER_COL, 0, ' ', ATTR_BAR);
}
/* ---- Вычисления для чисел статус-бара ----------------------------- */
/* Число строк, готовых к показу. Во время загрузки последняя эмитированная
* строка ещё в g_cells (не зафлашена), поэтому доступны [0..n_lines-2]. */
uint16_t drawable_lines(void)
{
if (g_loading)
return (n_lines >= 1) ? (uint16_t)(n_lines - 1) : 0;
return n_lines;
}
/* ---- Атомарные части статус-бара ---------------------------------- */
/* Фиксированный хром бара — рисуется ОДИН раз при старте: фон, два
* разделителя , метка "MDVIEW" и имя файла (они не меняются за сессию). */
void prerender_status(void)
{
fill_row(0, ATTR_BAR);
wrchar(DIV1_X, 0, 0xB3, ATTR_BAR); /* │ */
wrchar(DIV2_X, 0, 0xB3, ATTR_BAR); /* │ */
put_str_attr(1, 0, "MDVIEW", ATTR_BAR);
put_str_attr(10, 0, filename, ATTR_BAR);
}
/* Поле текущей кодировки (col 37, 7 ячеек). Меняется только по F8. */
void status_encoding(void)
{
textattr(ATTR_BAR);
gotoxy(DIV1_X - 10, 0);
bios_write_until(enc_name(g_encoding), 8, 0);
}
/* Числовая часть (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)
last = total;
if(local_total != total || local_last != last || local_loading != g_loading) {
local_total = total; local_last = last; local_loading = g_loading;
textattr(ATTR_BAR);
gotoxy(DIV1_X + 2, 0);
bios_fillcharattr(' ', ATTR_BAR, DIV2_X - DIV1_X - 2); /* очистить [DIV1_X+1 .. DIV2_X-1] */
gotoxy(DIV1_X + 2, 0);
cputs("L ");
dec16(top_line + 1);
cputs("-");
dec16(last);
cputs(" / ");
dec16(total);
if (g_loading) cputs("..."); /* ещё грузится */
}
}
/* Полное обновление переменной части (MD): кодировка + числа. */
void render_full_status(void)
{
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);
bios_fillcharattr(' ', ATTR_BAR, SCREEN_W - DIV2_X - 2); /* очистить [DIV2_X+1 .. конец] */
gotoxy(DIV2_X + 2, 0);
dec8(pct);
cputs("%");
}
}
#if WITH_RAW
/* RAW-вариант числовой части бара (peer к render_md_status_numbers): метка
* режима между разделителями + процент справа. Сам очищает свои области (та же
* геометрия DIVn_X), атомарен не требует общего fill всего бара. Позиция
* берётся готовым числом из raw_pct() (raw_top приватен mdview2_raw.c). */
void render_raw_status(void)
{
// uint8_t pct = raw_pct();
textattr(ATTR_BAR);
gotoxy(DIV1_X + 1, 0);
bios_writeattr_until(g_view == VIEW_RAW_WRAP ? " RAW wrap" : " RAW pan",
DIV2_X - DIV1_X - 1, ATTR_BAR, 0); /* метка + пад до DIV2_X-1 */
}
#endif /* WITH_RAW */
/* ---- Нижнее меню (строка 31) -------------------------------------- */
void render_menu(void)
{
/* Блок i = колонка i*8 (8 шириной): 2 поз. НОМЕР клавиши без 'F' (стиль
* ATTR_MENU_K, ведущий «чёрный» пробел кроме F10) + 6 поз. ТЕКСТ-функция
* (ATTR_MENU_T) сразу за номером. Номера рисуем для ВСЕХ 10 клавиш; текст
* только у задействованных и доступных сейчас. */
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);
put_str_attr((uint8_t)(i * 8), MENU_ROW, num, ATTR_MENU_K);
}
put_str_attr(9 * 8, MENU_ROW, "10", ATTR_MENU_K); /* F10 */
put_str_attr(0 * 8 + 2, MENU_ROW, "Help", ATTR_MENU_T);
#if WITH_RAW
if (g_ready) /* RAW доступен после постройки документа */
put_str_attr(1 * 8 + 2, MENU_ROW, (g_view == VIEW_MD) ? "RAW" : "MD", ATTR_MENU_T);
if (g_view != VIEW_MD) /* Wrap/Unwrap — только в RAW */
put_str_attr(2 * 8 + 2, MENU_ROW,
(g_view == VIEW_RAW_WRAP) ? "UnWrap" : "Wrap", ATTR_MENU_T);
#endif
if (g_f8_enabled) /* смена кодировки сейчас возможна */
put_str_attr(7 * 8 + 2, MENU_ROW, "CodePg", ATTR_MENU_T);
put_str_attr(9 * 8 + 2, MENU_ROW, "Exit", ATTR_MENU_T);
}
+91
View File
@@ -0,0 +1,91 @@
/*
* mdview2_table.c выровненная отрисовка markdown-таблиц.
*
* Разбор |-разделённых ячеек и эмиссия рамок/строк в общий буфер ячеек
* рендера (gc_put/gc_fill, см. mdview2.h). Вызывается из index_lines()
* ядра при обнаружении таблицы. Геометрия рамок (TBL_n), словарь стилей
* (CK_n, INIT_STYLE_n) и cell-buffer API объявлены в mdview2.h.
*/
#include <stdint.h>
#include <conio.h> /* COLOR() / COLOR_* для атрибутов TBL_ATTR/ATTR_TEXT */
#include "mdview2.h"
uint32_t row_end(uint32_t p)
{
while (p < file_size && fb(p) != '\n') p++;
return p;
}
/* Начало контента первой ячейки строки (после ведущих пробелов и '|'). */
uint32_t table_first_cell(uint32_t row_start)
{
uint32_t p = row_start;
while (p < file_size && fb(p) == ' ') p++;
if (p < file_size && fb(p) == '|') p++;
return p;
}
/* Следующая ячейка: [*cs,*ce) — обрезанный диапазон; *pp продвигается за '|'.
* 0 ячеек больше нет (включая хвостовую пустоту после последнего '|'). */
uint8_t table_next_cell(uint32_t *pp, uint32_t lineend, uint32_t *cs, uint32_t *ce)
{
uint32_t p = *pp;
if (p >= lineend) return 0;
uint32_t a = p;
while (p < lineend && fb(p) != '|') p++;
uint32_t b = p;
uint8_t had_pipe = (uint8_t)(p < lineend && fb(p) == '|');
if (had_pipe) p++;
*pp = p;
while (a < b && fb(a) == ' ') a++;
while (b > a && fb(b - 1) == ' ') b--;
if (!had_pipe && a == b) return 0;
*cs = a; *ce = b;
return 1;
}
/* Строка-разделитель (|---|:-:|): каждая ячейка непустая и только '-'/':'. */
uint8_t table_is_sep_row(uint32_t row_start, uint32_t lineend)
{
uint32_t p = table_first_cell(row_start);
uint32_t cs, ce;
uint8_t n = 0;
while (table_next_cell(&p, lineend, &cs, &ce)) {
if (cs >= ce) return 0;
for (uint32_t q = cs; q < ce; q++) { char c = fb(q); if (c != '-' && c != ':') return 0; }
n++;
}
return (uint8_t)(n > 0);
}
/* Горизонтальная рамка в g_cells: left + (H×(w+2) + mid|right) по колонкам. */
void table_border(const uint8_t *widths, uint8_t ncols, char left, char mid, char right)
{
gc_put(left, TBL_ATTR);
for (uint8_t c = 0; c < ncols; c++) {
gc_fill(TBL_H, TBL_ATTR, (uint8_t)(widths[c] + 2));
gc_put((c == (uint8_t)(ncols - 1)) ? right : mid, TBL_ATTR);
}
}
/* Строка данных в g_cells: │ <ячейка, добитая до widths[c]> │ … */
void table_data_row(uint32_t row_start, uint32_t lineend, const uint8_t *widths, uint8_t ncols)
{
uint32_t p = table_first_cell(row_start);
uint32_t cs, ce;
gc_put(TBL_V, TBL_ATTR);
for (uint8_t c = 0; c < ncols; c++) {
uint8_t got = table_next_cell(&p, lineend, &cs, &ce);
gc_put(' ', TBL_ATTR);
uint8_t rendered = 0;
if (got && ce > cs) {
uint8_t before = g_ncells;
(void)inline_scan(cs, ce, 0, CK_OTHER, INIT_STYLE_PLAIN, ATTR_TEXT, 1);
rendered = (uint8_t)(g_ncells - before);
}
if (rendered < widths[c]) gc_fill(' ', ATTR_TEXT, (uint8_t)(widths[c] - rendered));
gc_put(' ', TBL_ATTR);
gc_put(TBL_V, TBL_ATTR);
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""Генератор тестовых файлов mdview2 для проверки лимита 256 КБ и путей обрыва.
Результат кладётся рядом со скриптом (examples/mdview2/testfiles/):
BIG.MD ~216 КБ обычной прозы (CP866): успешный рендер большого файла.
TABLES.MD таблично-тяжёлый (ASCII): пробивает кап контент-кэша (40 стр./набор),
т.к. неровные таблицы (короткие ячейки + один широкий столбец)
раздувают число (символ,атрибут)-ячеек кратно относительно источника.
LINES.MD ~22000 коротких строк (ASCII): пробивает лимит строк (18432).
Запуск: python3 gen_testfiles.py
"""
import os
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
def write(name, data):
path = os.path.join(HERE, name)
mode = 'wb' if isinstance(data, (bytes, bytearray)) else 'w'
with open(path, mode) as f:
f.write(data)
print(f"{name}: {os.path.getsize(path)} bytes")
# --- BIG.MD / HUGE.MD: склейка README + READMEBG, перекодировано в CP866 -----
# BIG (×2 ≈170 КБ) — успешный рендер большого файла (<256 КБ).
# HUGE (×4 ≈340 КБ) — БОЛЬШЕ 256 КБ: проверка клампа (читаются первые 256 КБ +
# строка-предупреждение TRUNC_FILE).
prose = bytearray()
for fn in ('README.MD', 'READMEBG.MD'):
with open(os.path.join(ROOT, fn), encoding='utf-8') as f:
prose += f.read().encode('cp866', errors='replace')
write('BIG.MD', bytes(prose * 2))
write('HUGE.MD', bytes(prose * 4))
# --- TABLES.MD: неровные таблицы -> исчерпание контент-кэша ------------------
# Один широкий столбец (72 симв.) на таблицу задаёт ширину; остальные ячейки
# короткие, но при рендере добиваются до этой ширины -> ~80 ячеек/строка из
# ~8 байт источника. ~300 таблиц × 30 строк дают ~840K ячеек (1.68 МБ контента)
# при ~108 КБ источника -> кап 640 КБ/набор пробивается, строк (~11K) < 18432,
# значит первой срабатывает причина TRUNC_CONTENT.
NT, ROWS = 300, 30
wide = 'x' * 72
out = ['# Tables test - content cache exhaustion trigger', '']
for t in range(NT):
out.append(f'## Table {t + 1}')
out.append('| Column A | B |')
out.append('|---|---|')
out.append(f'| {wide} | y |')
for r in range(ROWS):
out.append(f'| a{r} | b |')
out.append('')
write('TABLES.MD', '\n'.join(out) + '\n')
# --- LINES.MD: масса коротких строк -> исчерпание лимита строк ---------------
# ВАЖНО: вьювер склеивает подряд идущие непустые строки в один параграф
# (markdown soft-wrap), поэтому простые строки потекли бы в абзац. Нужен
# контент БЕЗ склейки -> кладём всё в fenced code block (verbatim, 1:1
# строка-источник = экранная строка). 22000 строк > 18432 -> TRUNC_LINES;
# контент мал (~9 ячеек/строка), кап контента не задевается.
N = 25000 # ~250 КБ -> 16 страниц -> max_lines = 18432; строк больше -> обрыв на 18432
out = ['# Short lines test - line limit trigger', '', '```']
out += [f'row {i:05d}' for i in range(1, N + 1)]
out.append('```')
write('LINES.MD', '\n'.join(out) + '\n')
+1
View File
@@ -60,6 +60,7 @@ LIBC_C := \
libc/stdio/getchar.c \ libc/stdio/getchar.c \
libc/stdio/putchar.c \ libc/stdio/putchar.c \
libc/stdio/puts.c \ libc/stdio/puts.c \
libc/stdlib/minmax.c \
libc/file/file.c \ libc/file/file.c \
libc/stdio/hex_print.c \ libc/stdio/hex_print.c \
libc/stdio/dec_print.c \ libc/stdio/dec_print.c \
+12
View File
@@ -0,0 +1,12 @@
#ifndef STDLIB_H
#define STDLIB_H
#include <stddef.h> /* size_t */
#include <stdint.h>
int16_t min(int16_t a, int16_t b);
int16_t max(int16_t a, int16_t b);
#endif
+42
View File
@@ -0,0 +1,42 @@
#include <stdlib.h>
int16_t min(int16_t a, int16_t b) __naked
{
(void)a; (void)b;
__asm
ld a, h
cp d
jr c, min_less
jr nz, min_greater
ld a, l
cp e
jr c, min_less
min_greater:
ret /* DE уже содержит b */
min_less:
ex de, hl
ret
__endasm;
}
int16_t max(int16_t a, int16_t b) __naked
{
(void)a; (void)b;
__asm
ld a, h
cp d
jr c, max_less
jr nz, max_greater
ld a, l
cp e
jr c, max_less
ret /* a >= b, return a in HL */
max_less:
/* a < b, return b in DE */
ret /* DE already contains b */
max_greater:
/* a > b, return a in HL */
ex de, hl
ret
__endasm;
}
+5
View File
@@ -23,6 +23,10 @@ C-toolchain'а, накопленные в процессе разработки.
* `RST 30h` — Mouse driver, номер функции в C * `RST 30h` — Mouse driver, номер функции в C
* **Видеорежимы:** 320×256×256, 640×256×16, 80×32 text, 40×32 text. * **Видеорежимы:** 320×256×256, 640×256×16, 80×32 text, 40×32 text.
* **Формат EXE:** SprintEXE (512-байтный header + образ HOME + опциональные банки). * **Формат EXE:** SprintEXE (512-байтный header + образ HOME + опциональные банки).
* **Fast RAM («КЭШ-ОЗУ»):** 64 КБ SRAM, процессор работает без wait-state. В
toolchain'е **не используется** — несовместима с акселератором, перекрывает
окно 0 (DSS), не переживает вызовы DSS/BIOS. Подробности и паттерн работы —
`docs/fast_ram.md`.
--- ---
@@ -372,3 +376,4 @@ cd mame/v306 && ./run_mame.sh
## История изменений ## История изменений
- 2026-06-01 — первый релиз v1.0 - 2026-06-01 — первый релиз v1.0
- 2026-06-26 — добавлен пункт о Fast RAM в cheat-sheet (детали — `docs/fast_ram.md`)