mdview2: add render-cache markdown viewer (Phases 0-4)
New examples/mdview2 — render-cache version of mdview: each logical line is rendered into an EMM (char,attr) buffer once during file load (interleaved with index_lines()), then scrolling draws straight from the cache via ESTEX WINREST, with no re-parsing or fb() access in the steady state. Horizontal scroll still uses the old live-render path (Phase 5, not yet migrated). Format and budget were derisked empirically first (tests/winrest): confirmed ESTEX WINCOPY/WINREST buffer layout and measured EMM free space, both folded into the implementation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,42 @@
|
|||||||
|
# Build mdview2.exe — Markdown viewer for Sprinter (render-cache version).
|
||||||
|
#
|
||||||
|
# Фаза 0: скаффолдинг — копия mdview.c как baseline, без изменений
|
||||||
|
# логики. План — docs/mdview2-plan.md.
|
||||||
|
#
|
||||||
|
# small memory mode: code in W1, data/stack/heap in W2 (32 KB total).
|
||||||
|
# W3 stays free for the file buffer (EMM-mapped).
|
||||||
|
|
||||||
|
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||||
|
EXAMPLE := mdview2
|
||||||
|
MEMORY := small
|
||||||
|
|
||||||
|
include $(PROJ_ROOT)/app.mk
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Образ дискеты: только mdview2.exe + README.MD (перекодированный
|
||||||
|
# из UTF-8 в CP866 — рабочую кодировку Sprinter).
|
||||||
|
#
|
||||||
|
# README.MD хранится в репозитории в UTF-8; iconv -c конвертирует
|
||||||
|
# его в CP866, отбрасывая символы без аналога в целевой кодировке.
|
||||||
|
# Результат кладётся в .disk_tmp/README.MD, чтобы make_disk.py
|
||||||
|
# использовал правильное имя файла на диске.
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
DISK_TMP := .disk_tmp
|
||||||
|
README_DISK := $(DISK_TMP)/README.MD
|
||||||
|
|
||||||
|
$(DISK_TMP):
|
||||||
|
mkdir -p $@
|
||||||
|
|
||||||
|
$(README_DISK): README.MD | $(DISK_TMP)
|
||||||
|
iconv -c -f UTF-8 -t CP866 README.MD > $@
|
||||||
|
|
||||||
|
floppy: $(EXAMPLE).exe $(README_DISK)
|
||||||
|
python3 $(MAKE_DISK) $(FLOPPY_IMG) $(EXAMPLE).exe $(README_DISK)
|
||||||
|
@echo
|
||||||
|
@echo "Floppy ready: $(FLOPPY_IMG)"
|
||||||
|
@echo "Run: cd $(MAME_DIR) && ./run_mame.sh"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf .sprinter-cc-* $(EXAMPLE).exe $(DISK_TMP)
|
||||||
|
|
||||||
|
.PHONY: all clean floppy run
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
# MDView — Просмотрщик Markdown для Sprinter
|
||||||
|
|
||||||
|
**MDView** — программа для просмотра документов в формате *Markdown* на компьютере
|
||||||
|
Sprinter (процессор Z80). Документ хранится в отдельном W3 окне и не занимает
|
||||||
|
основную RAM программы.
|
||||||
|
|
||||||
|
## Возможности
|
||||||
|
|
||||||
|
- Документы до **128 КБ** (8 страниц EMM по 16 КБ каждая)
|
||||||
|
- До **16 384** экранных строк в индексе
|
||||||
|
- Автоматический перенос слов по ширине экрана (80 столбцов)
|
||||||
|
- Горизонтальный сдвиг для широких строк (блоки кода, таблицы)
|
||||||
|
- Статус-бар: имя файла, диапазон строк, процент прокрутки
|
||||||
|
- Спиннер в строке состояния во время загрузки и индексации
|
||||||
|
- Поддержка «мягкого» склеивания строк в абзацах и цитатах
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```
|
||||||
|
mdview [имя_файла.md]
|
||||||
|
```
|
||||||
|
|
||||||
|
Если имя файла не задано, загружается `README.MD`.
|
||||||
|
|
||||||
|
## Управление
|
||||||
|
|
||||||
|
```
|
||||||
|
Клавиша Действие
|
||||||
|
───────────── ────────────────────────────────────────
|
||||||
|
Up Down Прокрутка на одну строку вверх / вниз
|
||||||
|
PgUp PgDn Прокрутка на страницу (30 строк)
|
||||||
|
Home Начало документа
|
||||||
|
End Конец документа
|
||||||
|
Left Right Горизонтальный сдвиг (только nowrap-строки)
|
||||||
|
F1 Окно справки
|
||||||
|
F10 / Esc Выход из программы
|
||||||
|
```
|
||||||
|
|
||||||
|
## Синтаксис Markdown
|
||||||
|
|
||||||
|
### Заголовки
|
||||||
|
|
||||||
|
Поддерживаются уровни H1–H4. Уровни H5 и H6 отображаются как H4.
|
||||||
|
|
||||||
|
# Заголовок первого уровня
|
||||||
|
## Заголовок второго уровня
|
||||||
|
### Заголовок третьего уровня
|
||||||
|
#### Заголовок четвёртого уровня
|
||||||
|
|
||||||
|
### Текстовое форматирование
|
||||||
|
|
||||||
|
**Жирный текст** выделяется двойными звёздочками: `**текст**`
|
||||||
|
|
||||||
|
*Курсив* выделяется одиночными звёздочками `*текст*` или знаком подчёркивания `_текст_`
|
||||||
|
|
||||||
|
`Встроенный код` обозначается обратными кавычками
|
||||||
|
|
||||||
|
~~Зачёркнутый текст~~ — двойные тильды: `~~текст~~`
|
||||||
|
|
||||||
|
### Ненумерованный список
|
||||||
|
|
||||||
|
Маркеры `-`, `*` или `+`:
|
||||||
|
|
||||||
|
- Первый пункт списка
|
||||||
|
- Второй пункт списка
|
||||||
|
- Третий пункт с достаточно длинным текстом, который при необходимости
|
||||||
|
будет перенесён на следующую строку с сохранением отступа
|
||||||
|
|
||||||
|
### Нумерованный список
|
||||||
|
|
||||||
|
1. Первый элемент
|
||||||
|
2. Второй элемент
|
||||||
|
3. Третий элемент
|
||||||
|
|
||||||
|
### Цитата
|
||||||
|
|
||||||
|
> Блок цитаты начинается с символа `>`. Несколько последовательных
|
||||||
|
> строк одной цитаты склеиваются в единый абзац с автоматическим
|
||||||
|
> переносом слов.
|
||||||
|
|
||||||
|
### Блок кода (verbatim)
|
||||||
|
|
||||||
|
Блок кода заключается в тройные обратные кавычки. Внутри блока
|
||||||
|
текст отображается «как есть» без разбора Markdown:
|
||||||
|
|
||||||
|
```
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <sprinter.h>
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
puts("Hello, Sprinter!");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Горизонтальная линия
|
||||||
|
|
||||||
|
Три или более символов `---`, `***` или `___` на отдельной строке:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Технические характеристики
|
||||||
|
|
||||||
|
- **Платформа:** Sprinter, процессор Z80 @ 21 МГц
|
||||||
|
- **Кодировка:** CP866 (DOS Cyrillic)
|
||||||
|
- **Максимальный размер файла:** 128 КБ
|
||||||
|
- **Максимальное число строк в индексе:** 16 384
|
||||||
|
- **Режим памяти:** small
|
||||||
|
- Код программы, cтек, данные, куча — окнa W1-W2 (32 КБ, адреса 0x4000–0xBFFF).
|
||||||
|
- Буфер файла — страницы EMM, отображаемые в W3 (0xC000–0xFFFF)
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
1. **Увеличение размера документов.** Снять лимит 128 КБ: Достаточно
|
||||||
|
разрешить работать с большим кол-вом страниц памяти, пока оттестированно
|
||||||
|
на работе с 8-мю страницами по 16Кб.
|
||||||
|
|
||||||
|
2. **Форматированные таблицы.** Разбирать строки вида `| ячейка | ячейка |`
|
||||||
|
с автоматическим выравниванием столбцов и отрисовкой разделительных
|
||||||
|
линий (строки `|---|---|`). На текущий момент таблицы отображаются
|
||||||
|
как обычные nowrap-строки без выравнивания.
|
||||||
|
|
||||||
|
3. **Поддержка кодировок CP1251 и UTF-8.** Автоопределение кодировки
|
||||||
|
по BOM, явное указание через аргумент командной строки (`--encoding cp1251`),
|
||||||
|
возможность переключения кодировки во время просмотра (`F8`).
|
||||||
|
Нужно прежде всего для документов на русском языке; CP866 кодировака уже поддерживается.
|
||||||
|
|
||||||
|
4. **Ускорение рендеринга.** Кэш строк экрана. Оптимизация цикла вывода
|
||||||
|
символов через BIOS WRCHAR (пакетный вывод, DMA).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*MDView v0.2 · (c) 2026 Петров А.Г.*
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
# mdview2 — план: рендер-кэш вместо живого парсинга на каждый скролл
|
||||||
|
|
||||||
|
Контекст и обоснование — см. обсуждение 2026-06-23 (после оптимизации mdview через
|
||||||
|
`<bios/text.h>`, см. `mdview-модель-документа-и-рендеринг.md`). Идея: вместо того
|
||||||
|
чтобы при каждом скролле повторно идти в файл (`fb()`, W3-банкинг) и гонять
|
||||||
|
markdown inline-парсер (`handle_inline_marker`/emphasis state machine), один раз
|
||||||
|
отрендерить каждую логическую строку в готовый байтовый буфер и дальше выводить
|
||||||
|
его на экран напрямую — без парсинга, без обращения к исходному файлу.
|
||||||
|
|
||||||
|
Ключевые подтверждённые факты (эмпирически в MAME, не из документации):
|
||||||
|
|
||||||
|
- **Формат буфера ESTEX `WINCOPY`(59h)/`WINREST`(5Ah)** — пара байт `(char, attr)`
|
||||||
|
на ячейку, по строкам, шаг строки = `width*2`, без паддинга. Можно генерировать
|
||||||
|
самим, не вызывая `WINCOPY`. `WINREST` копирует сразу `H` строк одним вызовом.
|
||||||
|
Детали ABI и регистров — см. `tests/winrest/winrest.c`.
|
||||||
|
- **Бюджет EMM**: 256 страниц (4 МБ) total, 215 (3440 КБ) free на старте программы.
|
||||||
|
Файл+индекс в худшем случае (128 КБ файл) съедают 16 страниц (256 КБ) — остаётся
|
||||||
|
≈199 страниц (3184 КБ). Кэш всего документа целиком (даже худший случай:
|
||||||
|
16384 строк × 160 байт = 2.6 МБ) укладывается без LRU/частичного кэша.
|
||||||
|
|
||||||
|
## Единый формат строки (пересмотрено 2026-06-23)
|
||||||
|
|
||||||
|
Исходно планировалось два типа строк (тип 1 — многоцветные, помещающиеся;
|
||||||
|
тип 2 — nowrap/code, один стиль, чистый текст для экономии памяти). От этого
|
||||||
|
деления отказались: строки таблиц (`IF_NOWRAP`, но не `IF_CODE`) всё равно
|
||||||
|
проходят inline-парсер и могут содержать несколько атрибутов (bold/italic в
|
||||||
|
ячейках) — предположение "один стиль" для них неверно. Бюджет EMM
|
||||||
|
([[sprinter_emm_budget]]) с большим запасом покрывает (char,attr)-формат для
|
||||||
|
ВСЕХ строк без исключения, поэтому усложнение не оправдано.
|
||||||
|
|
||||||
|
**Единый формат**: кэш-запись = `len` пар `(char,attr)` — реальная длина
|
||||||
|
контента в ячейках, без паддинга до 80, капается на `MAX_CACHE_LINE_LEN`=255.
|
||||||
|
Вывод: `bios_fillcharattr(' ', base_attr, SCREEN_W)` (очистить строку) →
|
||||||
|
`win_rest(row, left_margin, 1, len, page)` (контент). Для широких nowrap-строк
|
||||||
|
горизонтальный скролл (Фаза 5) — это просто смещение НАЧАЛА среза внутри ТОГО
|
||||||
|
ЖЕ (char,attr)-буфера на `hscroll*2` байт, тот же `win_rest`, без отдельного
|
||||||
|
плain-текстового формата и без необходимости на лету "разворачивать" текст+
|
||||||
|
атрибут в пары.
|
||||||
|
|
||||||
|
## Фазы реализации
|
||||||
|
|
||||||
|
### Фаза 0 — скаффолдинг `examples/mdview2/` [СДЕЛАНО 2026-06-23]
|
||||||
|
Новая директория со своим `Makefile`/`app.mk` (по аналогии с `mdview/`, не
|
||||||
|
модифицируем `mdview.c`). `mdview2.c` — копия `mdview.c` без изменений логики
|
||||||
|
(только usage-строка/заголовок комментария). Собирается чисто, дискета собрана.
|
||||||
|
|
||||||
|
### Фаза 1 — формат кэша и директория строк [СДЕЛАНО и ЗАКРЫТО 2026-06-23]
|
||||||
|
Реализовано в `mdview2.c`:
|
||||||
|
- `cache_rec_t` (РОВНО 8 байт: `page`, `off` (uint16_t), `len`, `flags`,
|
||||||
|
`reserved`, `pad[2]`; размер задаётся `CACHE_DIR_REC_SIZE = sizeof(cache_rec_t)`,
|
||||||
|
НЕ хардкодом — см. разобранный инцидент ниже) — **отдельная** директория
|
||||||
|
(`cache_dir_phys[]`/`cache_dir_get`/`cache_dir_put`), своя ёмкость =
|
||||||
|
`file_pages+1` страниц (как у индекса). **Важное уточнение к исходному тексту
|
||||||
|
плана ниже**: директория НЕ переиспользует слоты `idx_rec_t` in-place —
|
||||||
|
рендер-воркер (Фаза 2) при обработке строки N может заглядывать в idx-записи
|
||||||
|
СОСЕДНИХ строк (откат cont-сегментов, line_idx+1 для границы сегмента); если
|
||||||
|
бы строка N-1 была перезатёрта сразу после своего рендера, воркер строки N
|
||||||
|
прочитал бы уже не исходные off/flags, а указатель в кэш — поломав откат.
|
||||||
|
Бюджет EMM ([[sprinter_emm_budget]]) позволяет отдельный массив — он безопаснее.
|
||||||
|
- Пул контента — отдельные EMM-страницы (`cache_content_phys[]`), ленивый рост
|
||||||
|
по 1 странице в `cache_reserve()` (bump-allocator, запись никогда не
|
||||||
|
разбивается через границу страницы; длина капается на `MAX_CACHE_LINE_LEN`=255
|
||||||
|
ячеек).
|
||||||
|
- `cache_commit()` = один `bank_write()` на строку (буфер строки собирается
|
||||||
|
локально в W1/W2 заранее, не в W3 — иначе конфликт с `fb()` при чтении
|
||||||
|
исходника во время рендера).
|
||||||
|
- `win_rest()` (ESTEX WINREST 5Ah) — вывод готового буфера на экран.
|
||||||
|
- `phase1_selftest()` — самопроверка (резервирует/коммитит 4 тестовые ячейки,
|
||||||
|
кладёт запись в директорию по индексу 0, рисует через `win_rest` дважды
|
||||||
|
подряд с координатами через параметры функции) — убрать в Фазе 2.
|
||||||
|
|
||||||
|
**Подтверждено визуально в MAME (2026-06-23)**: оба квадрата 2×2 на месте, без
|
||||||
|
единой задержки, с координатами через переменные — Фаза 1 полностью закрыта.
|
||||||
|
|
||||||
|
**Разобранный инцидент (НЕ платформенный квирк)**: по пути долго казалось, что
|
||||||
|
`win_rest()` после "всплеска" обычных BIOS print-вызовов рисует буфер в
|
||||||
|
неправильном месте, причём воспроизводилось только когда `row`/`col` приходили
|
||||||
|
через переменные, а не как константы — это и было ключом. Настоящая причина:
|
||||||
|
`cache_rec_t` фактически занимал 7 байт (SDCC z80 не паддит структуры), а
|
||||||
|
`CACHE_DIR_REC_SIZE` был захардкожен как 8 — `bank_read`/`bank_write` копировали
|
||||||
|
8 байт в 7-байтный буфер на стеке, затирая соседнюю переменную (параметр `col`)
|
||||||
|
вызывающей функции. Фикс — `cache_rec_t` явно до 8 байт + размер через
|
||||||
|
`sizeof()`. Полная история и общий урок — [[sprinter_winrest_format]] и
|
||||||
|
[[defer_unexplained_quirks]].
|
||||||
|
|
||||||
|
### Фаза 2 — рендер-воркер (бывший `render_line`) [СДЕЛАНО 2026-06-23]
|
||||||
|
`render_line_to_cache(line_idx)` — адаптация `render_line()`: та же классификация
|
||||||
|
строк/префиксов/inline-парсинг (`handle_inline_marker`, emphasis state machine,
|
||||||
|
soft-wrap join) БЕЗ ИЗМЕНЕНИЙ, но вместо `bios_writeattr`/`flush_run`/`wrchar`/
|
||||||
|
`bios_fillcharattr` на экран — пишем `(char,attr)` пары через `cc_put`/`cc_fill`
|
||||||
|
в локальный `cellbuf` (без батчинга через `runbuf`/`flush_run` — тот паттерн был
|
||||||
|
нужен только чтобы минимизировать число BIOS-вызовов, при записи в локальный
|
||||||
|
буфер смысла нет, пишем посимвольно сразу по месту), затем ОДНИМ
|
||||||
|
`cache_reserve()`+`cache_commit()`+`cache_dir_put()` коммитим всю строку.
|
||||||
|
Кэшируется ПОЛНЫЙ контент строки до `MAX_CACHE_LINE_LEN`, без обрезки по
|
||||||
|
`SCREEN_W` и без среза по `viewport_x` (view/scroll-time понятия, Фаза 4-5).
|
||||||
|
Также найден и исправлен по ходу баг в `win_rest()`: `IX` всегда указывал на
|
||||||
|
начало страницы (`#0xC000`), полностью игнорируя `off` — работало только в
|
||||||
|
Фазе 1, где в кэше была ровно одна запись со смещением 0; как только Фаза 2
|
||||||
|
начала пакетировать много строк на одной странице с разными `off`, все строки
|
||||||
|
стали читаться с начала страницы. Фикс: `win_rest()` принимает `off`
|
||||||
|
(uint16_t), `IX = 0xC000 + off` (новая раскладка ABI сверена через `sdcc -S`
|
||||||
|
с непустым телом — у `__naked` с пустым телом SDCC не генерирует код доступа
|
||||||
|
к параметрам, нужен пробный non-naked враппер). Подтверждено визуально в MAME.
|
||||||
|
|
||||||
|
### Фаза 3 — фоновый пре-рендер с прогрессом [СДЕЛАНО 2026-06-23]
|
||||||
|
`emit_seg()` вызывает `render_line_to_cache()` **interleaved** с построением
|
||||||
|
индекса — но с отставанием на одну строку: рендер строки N требует уже
|
||||||
|
существующей idx-записи N+1 (источник `seg_end`), которой ещё нет в момент,
|
||||||
|
когда строка N только создана. Поэтому `emit_seg()` для новой строки рендерит
|
||||||
|
ПРЕДЫДУЩУЮ (`n_lines-2` после инкремента) — её флаги (`IF_NOWRAP`/`IF_CODE`/
|
||||||
|
`IF_BLANK`) к этому моменту уже дописаны вызовом `set_*_cur()` на предыдущей
|
||||||
|
итерации `index_lines()`. Последнюю строку файла (у которой "следующей" не
|
||||||
|
будет) дорендеривает сам `index_lines()` после выхода из цикла, как и
|
||||||
|
`render_line()` делал для последней строки при живом рендере (`seg_end =
|
||||||
|
file_size`). Требует, чтобы `cache_dir_phys[]` был выделен ДО `index_lines()` —
|
||||||
|
это уже так (`load_file()` выделяет директорию кэша, затем вызывается
|
||||||
|
`index_lines()`).
|
||||||
|
|
||||||
|
**Важное сужение скоупа относительно исходного текста плана ниже**: пункты
|
||||||
|
"спиннер крутится, пока `rendered_up_to < n_lines`" и "скролл ограничен
|
||||||
|
диапазоном `[0, rendered_up_to]`" **не реализованы и не нужны** в этой
|
||||||
|
архитектуре — рендеринг происходит СИНХРОННО внутри той же однопроходной
|
||||||
|
`index_lines()`, без событийного цикла во время загрузки; пользователь
|
||||||
|
физически не может начать скроллить, пока `index_lines()` не вернёт
|
||||||
|
управление, а к этому моменту весь документ уже полностью в кэше. Спиннер
|
||||||
|
из `index_lines()` (каждые 16 строк) сохранён как есть — он покрывает
|
||||||
|
индексацию+рендер вместе, отдельный прогресс-индикатор не нужен.
|
||||||
|
|
||||||
|
### Фаза 4 — cache-only draw path при скролле
|
||||||
|
Цикл перерисовки видимой области (после пре-рендера) идёт **только** по
|
||||||
|
директории строк → `win_rest` (тип 1) или срез текста+`bios_writeattr` (тип 2).
|
||||||
|
Никаких обращений к `fb()`/исходному файлу в steady-state скролле.
|
||||||
|
|
||||||
|
### Фаза 5 — горизонтальный скролл для широких nowrap-строк
|
||||||
|
Срез ТОГО ЖЕ (char,attr)-кэш-буфера по текущему `hscroll`-офсету (смещение
|
||||||
|
начала на `hscroll*2` байт внутри буфера строки), `win_rest(row, col, 1,
|
||||||
|
visible_len, page)` с новым `off`. Никакого отдельного плоско-текстового
|
||||||
|
формата не нужно (см. пересмотр "Единый формат строки" выше).
|
||||||
|
|
||||||
|
### Фаза 6 — тестирование в MAME
|
||||||
|
Тот же набор паттернов, что использовался для mdview1 (заголовки/списки/цитаты/
|
||||||
|
code-block/bold-italic/nowrap-обрезка), ПЛЮС: реальный замер EMM на большом
|
||||||
|
документе (не синтетика — проверить, что бюджет из [[sprinter_emm_budget]]
|
||||||
|
действительно держится на чём-то близком к 128 КБ); UX пре-рендера (спиннер +
|
||||||
|
ограничение скролла, отсутствие "дыр" в недорендеренной области); проверка
|
||||||
|
`win_rest` на РЕАЛЬНОМ отрендеренном контенте (не только синтетический A/B/C/D
|
||||||
|
тест из `tests/winrest`).
|
||||||
|
|
||||||
|
## Открытые вопросы (решить по ходу, не блокируют старт Фазы 0)
|
||||||
|
|
||||||
|
- ~~Хранение длины строки в директории~~ — решено в Фазе 1: поле `len` (uint8_t)
|
||||||
|
прямо в `cache_rec_t`.
|
||||||
|
- ~~Деление строк на тип 1/тип 2~~ — отказались (см. "Единый формат строки"
|
||||||
|
выше): единый (char,attr)-формат для всех строк, капается на
|
||||||
|
`MAX_CACHE_LINE_LEN`=255 ячеек (с тем же индикатором обрезки на этапе вывода,
|
||||||
|
что уже есть в рендере для nowrap-строк).
|
||||||
|
- Освобождать ли страницы исходного файла после того, как все его строки
|
||||||
|
отрендерены (вернуть EMM в пул) — даёт больше места про запас, но усложняет
|
||||||
|
(нужна гарантия, что назад к файлу обращаться больше не придётся — а это не
|
||||||
|
так, если позже добавится поиск по тексту). Не делать в v1.
|
||||||
|
- BIOS-вариант `WIN_COPY_WIN`/`WIN_RESTORE_WIN` (0B2h/0B3h, RST 8) не проверен
|
||||||
|
(см. [[sprinter_winrest_format]]) — ESTEX-варианта достаточно для v1, проверять
|
||||||
|
BIOS-вариант только если понадобится экономия на RST-диспетчеризации.
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
|||||||
|
# Build winrest.exe — probes ESTEX WINCOPY/WINREST buffer format, TINY mode.
|
||||||
|
|
||||||
|
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||||
|
EXAMPLE := winrest
|
||||||
|
include $(PROJ_ROOT)/app.mk
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
/*
|
||||||
|
* Probe for ESTEX WINCOPY (59h) / WINREST (5Ah) — "save/restore screen
|
||||||
|
* window" calls (docs/part2/DSS 1.60 rst 10.txt:1379-1417):
|
||||||
|
*
|
||||||
|
* D=row E=col H=height L=width B=buffer page IX=buffer address
|
||||||
|
*
|
||||||
|
* Goal: reverse-engineer the on-disk cell format (char/attr order,
|
||||||
|
* per-row stride/padding) by writing four DISTINCT (char,attr) cells,
|
||||||
|
* capturing them with WINCOPY, and hex-dumping the captured page.
|
||||||
|
* A second WINREST to a different screen location confirms the
|
||||||
|
* round-trip visually.
|
||||||
|
*
|
||||||
|
* Register layout verified via `sdcc -S` for win_copy(row,col,h,w,page):
|
||||||
|
* A=row, L=col, stack [SP+2]=h [SP+3]=w [SP+4]=page (sdcccall(1)).
|
||||||
|
*
|
||||||
|
* CONFIRMED in MAME (2026-06-23): buffer format is (char,attr) pairs,
|
||||||
|
* row-major, stride = width*2 bytes, no padding. See memory note
|
||||||
|
* sprinter_winrest_format.
|
||||||
|
*
|
||||||
|
* Also reports mem_info() (total/free 16 KB EMM pages) at program
|
||||||
|
* start, to size a future mdview2 pre-render cache budget.
|
||||||
|
*/
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <conio.h>
|
||||||
|
#include <sprinter.h>
|
||||||
|
#include <sprinter_mem.h>
|
||||||
|
|
||||||
|
#define BUF_ADDR 0xC000u
|
||||||
|
|
||||||
|
static void win_copy(uint8_t row, uint8_t col, uint8_t h, uint8_t w, uint8_t page) __naked
|
||||||
|
{
|
||||||
|
(void)row; (void)col; (void)h; (void)w; (void)page;
|
||||||
|
__asm
|
||||||
|
ld iy, #2
|
||||||
|
add iy, sp
|
||||||
|
ld d, a ; D = row
|
||||||
|
ld e, l ; E = col
|
||||||
|
ld h, 0 (iy) ; H = height
|
||||||
|
ld a, 1 (iy) ; width -> L
|
||||||
|
ld l, a
|
||||||
|
ld a, 2 (iy) ; page -> B
|
||||||
|
ld b, a
|
||||||
|
push ix
|
||||||
|
ld ix, #0xC000
|
||||||
|
ld c, #0x59 ; ESTEX WINCOPY
|
||||||
|
di
|
||||||
|
rst #0x10
|
||||||
|
ei
|
||||||
|
pop ix
|
||||||
|
pop hl ; return address
|
||||||
|
inc sp
|
||||||
|
inc sp
|
||||||
|
inc sp ; discard h,w,page
|
||||||
|
jp (hl)
|
||||||
|
__endasm;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void win_rest(uint8_t row, uint8_t col, uint8_t h, uint8_t w, uint8_t page) __naked
|
||||||
|
{
|
||||||
|
(void)row; (void)col; (void)h; (void)w; (void)page;
|
||||||
|
__asm
|
||||||
|
ld iy, #2
|
||||||
|
add iy, sp
|
||||||
|
ld d, a ; D = row
|
||||||
|
ld e, l ; E = col
|
||||||
|
ld h, 0 (iy) ; H = height
|
||||||
|
ld a, 1 (iy) ; width -> L
|
||||||
|
ld l, a
|
||||||
|
ld a, 2 (iy) ; page -> B
|
||||||
|
ld b, a
|
||||||
|
push ix
|
||||||
|
ld ix, #0xC000
|
||||||
|
ld c, #0x5A ; ESTEX WINREST
|
||||||
|
di
|
||||||
|
rst #0x10
|
||||||
|
ei
|
||||||
|
pop ix
|
||||||
|
pop hl
|
||||||
|
inc sp
|
||||||
|
inc sp
|
||||||
|
inc sp
|
||||||
|
jp (hl)
|
||||||
|
__endasm;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
clrscr();
|
||||||
|
|
||||||
|
/* --- mem_info() baseline, before any allocation by this program. */
|
||||||
|
uint16_t total_pages = 0, free_pages = 0;
|
||||||
|
mem_info(&total_pages, &free_pages);
|
||||||
|
|
||||||
|
textattr(0x0F);
|
||||||
|
gotoxy(0, 0);
|
||||||
|
printf("EMM pages: total=%u free=%u (each = 16 KB)\n", total_pages, free_pages);
|
||||||
|
printf(" => total %lu KB, free %lu KB\n",
|
||||||
|
(uint32_t)total_pages * 16UL, (uint32_t)free_pages * 16UL);
|
||||||
|
printf("Press a key to continue to WINCOPY/WINREST probe...\n");
|
||||||
|
(void)getchar();
|
||||||
|
|
||||||
|
/* Four distinguishable cells: char and attr both easy to recognise
|
||||||
|
* in a hex dump (A/B/C/D = 0x41..0x44, attrs 0x11/0x22/0x33/0x44). */
|
||||||
|
gotoxy(5, 10); textattr(0x11); putch('A');
|
||||||
|
gotoxy(6, 10); textattr(0x22); putch('B');
|
||||||
|
gotoxy(5, 11); textattr(0x33); putch('C');
|
||||||
|
gotoxy(6, 11); textattr(0x44); putch('D');
|
||||||
|
|
||||||
|
textattr(0x0F);
|
||||||
|
gotoxy(0, 13);
|
||||||
|
printf("Captured window drawn above (row 10-11, col 5-6).\n");
|
||||||
|
printf("Press a key to WINCOPY it...\n");
|
||||||
|
(void)getchar();
|
||||||
|
|
||||||
|
uint8_t blk = mem_alloc_pages(1);
|
||||||
|
uint8_t page = mem_get_page(blk, 0);
|
||||||
|
|
||||||
|
win_copy(10, 5, 2, 2, page);
|
||||||
|
|
||||||
|
/* Map the same physical page into our own W3 to inspect the bytes
|
||||||
|
* WINCOPY just wrote there. */
|
||||||
|
sprinter_page_w3(page);
|
||||||
|
{
|
||||||
|
volatile uint8_t *buf = (volatile uint8_t *)BUF_ADDR;
|
||||||
|
gotoxy(0, 15);
|
||||||
|
printf("Buffer dump (first 16 bytes of page %u):\n", page);
|
||||||
|
for (uint8_t i = 0; i < 16; i++) {
|
||||||
|
printf("%02X ", buf[i]);
|
||||||
|
}
|
||||||
|
printf("\n");
|
||||||
|
for (uint8_t i = 0; i < 16; i++) {
|
||||||
|
char c = (char)buf[i];
|
||||||
|
putchar((c >= 0x20 && c < 0x7F) ? c : '.');
|
||||||
|
}
|
||||||
|
printf("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("Press a key to WINREST it to row 20...\n");
|
||||||
|
(void)getchar();
|
||||||
|
|
||||||
|
win_rest(20, 5, 2, 2, page);
|
||||||
|
|
||||||
|
gotoxy(0, 23);
|
||||||
|
printf("Restored at row 20-21, col 5-6 -- compare colours/chars\n");
|
||||||
|
printf("with the original at row 10-11.\n");
|
||||||
|
printf("Press a key to exit...\n");
|
||||||
|
(void)getchar();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user