Replace per-character wrchar() loops in render_line/fill_row/help screens with batched BIOS calls (bios_writeattr/bios_fillcharattr), and cache the per-line index record (idx_get) instead of refetching it ~13 times per rendered line — each refetch cost two W3 bank switches via bank_read(). Also fixes a pre-existing indexer bug: when word-wrap pushes a token that starts with an emphasis/code marker (e.g. `_text_`) onto a new line, the marker got re-scanned a second time during the wrap continuation, flipping line_style back off and corrupting the rendered attribute of the next word. Fixed by snapshotting line_style at the last seen space and rolling back to that snapshot (both the saved continuation style and the live scan state) when a wrap is taken. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
21 KiB
mdview — модель документа и рендеринг (единый смешанный режим)
Дизайн-документ переработки mdview.c под новое ТЗ форматирования (examples/mdview/todo2). Описывает решение по хранению данных, разбор документа при загрузке, упрощение рендера и снятие лимита на число строк.
1. Контекст и главный вопрос
Мы отказываемся от двух режимов показа (Wrap/UnWrap-переключатель) и переходим к единому смешанному режиму: тип переноса задаётся типом блока (обычный текст/заголовки/списки/цитаты — Wrap; код/таблицы — UnWrap). Главный вопрос: нужно ли хранить оригинальный байтовый контент файла, или при загрузке сразу преобразовать его в «готовый к показу» текст (склеить строки параграфов, убрать маркеры, развернуть отступы и т.п.)? Ответ: оригинал храним; отдельный «готовый» текстовый/ячеечный буфер не строим. «Подготовка при загрузке» реализуется как построение компактного индекса метаданных, а не как преобразование содержимого.
2. Решение по архитектуре
2.1 Текущее состояние (база)
- Файл целиком лежит в EMM-страницах (до 8×16 КБ). В окно W3 (
0xC000) в каждый момент замаплена ровно одна страница; доступ к байту — черезfb()/map_page()(mdview.c:198-213). - Индекс — это параллельные массивы по экранным сегментам:
line_offset[](смещение в файле) + битфлагиcont/nowrap/blank/in_code, 2-битныйline_kind[],init_style[](mdview.c:122-135). index_lines()за один проход уже делает склейку параграфов обычного текста, перенос по словам на 80 колонок и перенос emphasis через soft/hard break (mdview.c:692-911).render_line()при каждой отрисовке заново читает байты из оригинала и заново парсит inline-разметку (mdview.c:918-1156). Вывод: «готовим при загрузке» мы уже делаем — но готовим индекс, а не текст.
2.2 Почему не материализуем содержимое
- Одно свободное окно W3. Режим памяти
small: код в W1, данные/стек/куча в W2, для банкуемых данных свободен только W3. Преобразование «оригинал → готовый буфер» требует одновременно держать замапленными исходную страницу (чтение) и страницу-приёмник (запись). При единственном окне это поток swap-ов на каждую границу. In-place преобразование тоже невозможно: склейка меняет длины, смещения «съезжают», параграф пересекает границу 16 КБ. - Удвоение памяти и срыв гарантии 128 КБ. Оригинал может занимать все 8 страниц; готовой копии нужны свои страницы — гарантировать, что влезут обе, нельзя.
- Готовая форма не обязательно меньше. Снятие маркеров экономит байты, но добавляются отступы-продолжения у переносов списков/цитат. В лучшем случае ≈ размер оригинала, в худшем — больше. Ячеечная модель (символ+атрибут) — это ×2 (до 256 КБ), невозможно.
- Покадровая стоимость и так мала. За кадр рисуются только 30 видимых строк (~30×80 чтений). Единственный дорогой проход —
index_lines()— неизбежен в любой архитектуре (нужен полный скан дляn_linesи процента прокрутки).
2.3 Что реально оптимизировать
Не текст, а повторную работу render_line():
- вызов
classify_line()на каждый кадр (mdview.c:984); - обратный проход к первому не-cont сегменту ради отступа продолжения (
mdview.c:944-970). Это снимается переносом результата классификации (kind, ширина отступа/контент-колонка) в сам индекс на этапеindex_lines().
3. Модель данных индекса
3.1 Запись сегмента
Единая запись на экранный сегмент полностью заменяет нынешние параллельные массивы (line_offset, cont_flag, in_code, nowrap_flag, blank_flag, line_kind, init_style); index_lines() переписывается с нуля под эту модель. Цель размера записи — 5–6 байт:
offset— 3 байта (24-битное смещение в файле, покрывает 128 КБ).flags— 1 байт: битыcont,nowrap,blank,in_code+ 2-битныйckind(тип продолжения: PLAIN/QUOTE/LIST/OTHER).style— 1 байт: стартовый стиль сегмента (init_style, теперь включая STRIKE) + при необходимости глубина вложенности.indent— 1 байт: предвычисленная контент-колонка/ширина префикса, чтобы рендер не вызывалclassify_line()и не делал обратный проход.
3.2 Размещение и снятие лимита MAX_LINES
Проблема: line_offset сейчас uint32_t[2048] = 8 КБ, init_style = 2 КБ; суммарно статический индекс ~11.6 КБ near-памяти (W2). Рост лимита в near невозможен — W2 переполнится.
Решение:
- Индекс храним в отдельном EMM-блоке (свои страницы, помимо файловых), доступ — через тот же W3.
- Near-кэш viewport: перед отрисовкой кадра разово вычитываем записи для
VIEW_H+1видимых сегментов в маленький near-массив (≈(VIEW_H+1)×6≈ 186 байт). Рендер работает по near-кэшу + читает только контент-страницы. Это устраняет per-byte thrashing между страницей индекса и страницей контента: переключений на кадр — единицы, а не тысячи. - Динамический размер: число страниц под индекс выделяем пропорционально размеру файла (число сегментов ∈ размеру).
MAX_LINESстановится функцией от выделенных страниц индекса. Это прямо ложится на заметку v2 («чем больше банков под файл, тем больше буферы»). - Без регресса скорости при индексе в EMM: чтобы вынос индекса в банки не замедлил сборку (W3 делится между чтением контента и записью индекса), записи копим в near-буфере батчами и сбрасываем в EMM-блок через
bank_write()(он сам сохраняет/восстанавливает маппинг W3). Переключений окна на всю сборку — единицы, а не на каждый сегмент. - Если индекс-страницы выделить не удалось — деградируем до текущего near-лимита и показываем явную диагностику обрезки (а не молчаливый обрыв в
emit_seg()—mdview.c:574).
4. index_lines() — разбор по новому ТЗ
Единый проход по файлу строит сегменты. Деление на параграфы — по пустым строкам; несколько пустых строк подряд схлопываются в одну.
4.0 Скорость подготовки — главный приоритет
Требование: подготовка максимально быстрая (сейчас ~25 КБ готовятся 6–10 с). Две структурные причины медленности и их устранение:
- Per-byte
fb(uint32_t). Каждый доступ к байту пересчитывает страницу 32-битнымиp >> 14иp & 0x3FFF(mdview.c:209-213). На Z80 32-битная арифметика — это программные подпрограммы на каждый символ. Замена: потоковый разбор — мапим страницу один раз, идём по окнуchar */16-битным индексом, страницу переключаем только на границе 16 КБ. 32-битным остаётся лишь сохраняемый в индексoffset. - Многократные пере-сканы. На каждом
\nвнутри параграфа вызываютсяis_fence_raw(),is_hr_raw(),classify_line(),is_line_blank()(mdview.c:800-809) — каждая заново сканирует следующую строку, аclassify_line()ещё и повторяет цикл детекции HR. Для параграфа из N строк — O(N×длина) лишней работы. Замена: один проход — каждую строку классифицируем ровно один раз в момент её начала, lookahead — минимальный (несколько первых байт). - 32-битные сравнения. Курсор скана — (страница:8 бит, смещение:16 бит); сравнение с концом — сначала по странице, потом 16-битно.
Ожидаемый эффект: подготовка — по сути один линейный проход с 16-битными операциями, кратное ускорение относительно текущего multi-pass + 32-bit. (
render_line()может остаться наfb()— там только ~30×80 байт за кадр.)
4.1 Обычный текст (Wrap)
- Soft break (одиночный
\n): склейка, следующая строка продолжается через пробел. - Wide break: 2+ пробелов перед
\nили символ\перед\n→ принудительный перенос внутри параграфа, стиль сохраняется. (Текущий код ловит только 2 пробела —mdview.c:813-818; добавить ветку для\.) - Hard break (пустая строка): новый параграф, отделяется ОДНОЙ пустой строкой независимо от числа пустых строк в оригинале.
- Модификаторы bold/italic/strike/code действуют через soft/wide break внутри параграфа и сбрасываются на границе параграфа. (Добавить STRIKE
~~…~~— сейчас его нет вINIT_STYLE_*/ATTR_*.)
4.2 Заголовки (Wrap)
Один оригинальный абзац-строка; стартовый стиль по уровню. Во входе распознаём H1–H6, но H4/H5/H6 далее обрабатываются одинаково как H4 (сливаются в один стиль; classify_line() уже сворачивает lvl>4 → LK_H4, mdview.c:488). Внутри допустимы bold/italic/code/strike. После заголовка всегда пустая строка.
4.3 Горизонтальный разделитель HR
Всегда одна строка, после неё всегда пустая строка (новый абзац).
4.4 Списки (Wrap) — НОВОЕ: многострочная склейка
- Пункт может занимать несколько оригинальных строк; soft break внутри пункта склеивается через пробел (как обычный текст). Сейчас списки эмитятся построчно (
mdview.c:762-772) — переписать на paragraph-модель. - Новая строка с префиксом пункта → новый пункт.
- Пустая строка завершает пункт. Следующая непустая НЕ-пункт строка не является продолжением.
- Группировка: если после ОДНОЙ пустой строки идёт снова пункт — это тот же список, пустая строка в показе подавляется (пункты идут вплотную). Только ДВЕ+ пустые строки между пунктами разрывают на разные списки (в показе — одна пустая строка между ними).
- Перенос продолжения пункта печатается с отступом до контент-колонки (для уровня 1 — 2 пробела).
- Незакрытые модификаторы НЕ переносятся на следующий пункт (каждый пункт — свой параграф).
- Вложенные списки поддерживаются (отступ растёт с ведущими пробелами).
- Отдельные стили: префикс маркера и текст списка.
Пример соответствует разделу «Списки» в
todo2(строки 1–6).
4.5 Цитаты (Wrap)
- Отдельный параграф; перед текстом — префикс цитаты, перенесённые строки тоже предваряются префиксом.
- Многострочная склейка как у текста; пустая строка-цитата (
>) показывается как пустая строка с префиксом. - Вложенность (
> >→ двойной префикс). Отдельные стили: префикс и текст цитаты.
4.6 Блок кода ``` (UnWrap)
Весь блок одним стилем кода, без inline-модификаторов. После блока обязательна пустая строка. Строки не переносятся (truncate + горизонтальный скролл).
4.7 Таблицы (UnWrap)
Пока as-is, без переноса. Выравнивание столбцов — v2.
5. render_line() — упрощение
- Убрать ветку truncate-режима и
wrap_mode(уже частично снято;toggle_wrap()— мёртвая заглушкаmdview.c:1286-1291, удалить вместе с упоминаниями F2). - Не вызывать
classify_line()и не делать обратный проход: использоватьkind/indent/styleиз индекса. - Для cont-сегментов списков/цитат — печать отступа/префикса по
kind+indentиз записи сегмента. - Inline-парсинг emphasis выполняется только в пределах видимого сегмента (дёшево); для кода/таблиц — отключён.
6. Горизонтальный скроллинг (UnWrap)
- Скроллится только UnWrap-текст (код/таблицы). Грануляция 8 символов (
HPAN_STEP). - Правый край: индикатор
>своим стилем, если есть скрытый контент справа (есть —mdview.c:1135-1155). - Добавить левый индикатор
<в первой колонке, когдаviewport_x > 0. - Границы скролла по факту: текущий кламп жёстко до 240 (
mdview.c:1275). Заменить на вычисление максимального переполнения среди UnWrap-строк в текущем viewport, чтобы вправо нельзя было уйти за самую длинную строку, а влево — до колонки 0.
7. Чеклист расхождений с текущим кодом
- [индекс] Перейти на запись-на-сегмент в EMM-банке + near-кэш viewport; снять
MAX_LINES=2048. - [скорость] Потоковый разбор: один проход, 16-битный курсор в окне (без per-byte
fb()с 32-битной арифметикой), классификация строки один раз, минимальный lookahead; батч-флеш индекса. - [текст] Wide break по символу
\. - [текст] Модификатор strikethrough
~~…~~(+ стиль). - [списки] Многострочная склейка пунктов и правило группировки по одной/двум пустым строкам.
- [цитаты] Многострочная склейка и повтор префикса (в т.ч. вложенные) на переносах.
- [заголовки] Читать H1–H6; H4/H5/H6 трактовать как H4 (частично уже есть —
mdview.c:488). - [скролл] Индикатор
<и корректные границы по фактическому переполнению. - [рендер] Снять per-кадровый
classify_line()и обратный проход (данные — из индекса). - [очистка] Удалить
toggle_wrap()и упоминания F2 (mdview.c:1286-1291,1311).
8. Память: бюджет
- near (W2): текущий статический индекс ~11.6 КБ — у предела окна. После переноса
line_offset/init_styleв EMM в near остаётся near-кэш viewport (~0.2 КБ) + мелкие флаги → запас под стек/кучу растёт. - EMM: файл до 8 страниц + индекс ~1–2 страницы (при 5–6 байт/сегмент и нескольких тысячах сегментов). Перед выделением проверять
mem_info()на доступность страниц.
9. Заметки для v2
- Прогрессивный показ: отрисовать первую страницу (первые ~30 сегментов) ДО завершения полной подготовки; остальное доиндексировать дальше или по мере прокрутки. Процент и
Endпоказывать как «вычисляется», пока не готов полныйn_lines. (Синергия с потоковым разбором §4.0: первый экран готов после разбора лишь нескольких КБ.) - Разделители переноса Wrap: точка/запятая/
!/?/дефис; правило «новая строка не начинается с разделителя», серия разделителей остаётся на первой строке. - Таблицы: вычисление ширины столбцов и выравнивание.
- Подсветка синтаксиса внутри блоков кода.
- URL/Images и прочие типы строк.