diff --git a/docs/fast_ram.md b/docs/fast_ram.md new file mode 100644 index 0000000..a79b10b --- /dev/null +++ b/docs/fast_ram.md @@ -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 вокруг всех +точек входа в систему. diff --git a/docs/turboc.txt b/docs/turboc.txt new file mode 100644 index 0000000..5f3227b --- /dev/null +++ b/docs/turboc.txt @@ -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, с которой ассоциирован +некоторый открытый файл. При необходимости несколько +потоков могут ссылаться на один и тот же файл. + + diff --git a/examples/mdview/mdview.c b/examples/mdview/mdview.c index b6d8219..ab472f3 100644 --- a/examples/mdview/mdview.c +++ b/examples/mdview/mdview.c @@ -1758,7 +1758,7 @@ static void clamp_top(void) } /* Прокрутка вверх на 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; if( new_top_line != top_line) { @@ -1773,7 +1773,7 @@ static void scroll_up(uint16_t n) } /* Прокрутка вниз на 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)(top_line + n) : n_lines - VIEW_H; @@ -1791,7 +1791,7 @@ static void scroll_down(uint16_t n) /* Горизонтальный сдвиг (только если в окне есть nowrap-строки). * Максимум сдвига ограничен самой широкой nowrap-строкой на экране. */ -static void scroll_h(int8_t delta) +static void md_scroll_horizon(int8_t delta) { uint16_t maxw = 0; for (uint8_t i = 0; i < VIEW_H; i++) { @@ -1999,12 +1999,12 @@ int main(int argc, char **argv) switch (scan) { case KEY_F1: show_help(); break; case KEY_F10: goto exit_loop; - case KEY_UP: scroll_up(1); break; - case KEY_DOWN: scroll_down(1); break; - case KEY_LEFT: scroll_h(-(int8_t)HPAN_STEP); break; - case KEY_RIGHT: scroll_h(+(int8_t)HPAN_STEP); break; - case KEY_PGUP: scroll_up(VIEW_H); break; - case KEY_PGDN: scroll_down(VIEW_H); break; + 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: top_line = 0; viewport_x = 0; render_viewport(); break; diff --git a/examples/mdview2/Makefile b/examples/mdview2/Makefile index d55b503..83e4bc8 100644 --- a/examples/mdview2/Makefile +++ b/examples/mdview2/Makefile @@ -8,7 +8,7 @@ PROJ_ROOT := $(abspath $(CURDIR)/../..) EXAMPLE := mdview2 -EXTRA_SRCS := mdview2_raw.c mdview2_help.c mdview2_table.c mdview2_enc.c mdview2_md.c mdview2_index.c +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 include $(PROJ_ROOT)/app.mk diff --git a/examples/mdview2/README.MD b/examples/mdview2/README.MD index 4952b65..e4b414e 100644 --- a/examples/mdview2/README.MD +++ b/examples/mdview2/README.MD @@ -112,10 +112,51 @@ int main(void) { - **Кодировки:** CP866 / CP1251 / KOI8-R / UTF-8 (автоопределение, `F8`) - **Максимальный размер файла:** 128 КБ - **Максимальное число строк в индексе:** 16 384 +- **Максимальная длина строки в рендер-кэше:** 255 ячеек (см. ограничение ниже) - **Режим памяти:** small - Код программы, cтек, данные, куча — окнa W1-W2 (32 КБ, адреса 0x4000–0xBFFF). - Буфер файла — страницы EMM, отображаемые в W3 (0xC000–0xFFFF) +## Известные ограничения + +### Длина строки в MD-режиме — 255 ячеек + +Рендер-кэш хранит каждую логическую строку как **не более 255 пар (символ, атрибут)** +— константа `MAX_CACHE_LINE_LEN`. Лимит задан типами: `g_ncells` и +`cache_rec_t.len` — `uint8_t`. Касается всех строк, но заметнее всего на +**горизонтально скроллируемых** строках (блоки кода и строки таблиц, флаг +`IF_HSCROLL`), которые в MD-режиме можно листать вправо. + +**Что происходит с более длинной строкой:** при индексации `gc_put()` молча +отбрасывает каждую ячейку после 255-й (`if (g_ncells < MAX_CACHE_LINE_LEN)`). +В кэш попадают только первые 255 ячеек, остаток **теряется** — до него нельзя +доскроллить и **нет маркера обрезки** на 255-й позиции (маркер `>` означает лишь +«есть ещё в пределах кэша»). Переполнения буфера нет — `gc_put` проверяет границу. + +Важно: «255 ячеек» — это **отрендеренная ширина**, не байты исходника. Табы в +коде разворачиваются в пробелы (до `TAB_STOP`), а ячейки таблицы добиваются +пробелами до ширины колонки + рамки `│` — поэтому кап достигается раньше, чем +255 «полезных» символов. + +> **RAW-режим (`F2`) этого лимита не имеет** — он рисует прямо из файла +> побайтово, длинные строки видны целиком (через wrap `F3` или гориз. скролл). + +**Идея снятия лимита** (оценка, не реализовано) — расширить длину до `uint16_t`: +- `cache_rec_t.len` `uint8_t→uint16_t` — структура остаётся **ровно 8 байт** + (len съедает один pad-байт), адресация `idx<<3` не меняется. Бесплатно. +- `g_ncells` / `g_ncells_at_space` → `uint16_t` — главная цена по **коду/скорости**: + 16-битная арифметика на Z80 в горячем `gc_put` (вызов на каждую ячейку) и в + scan-циклах. Ориентир: **+0.2…0.4 КБ кода** + замедление индексации. +- Буфер `g_cells[MAX_CACHE_LINE_LEN*2]` в near-RAM (W2) — главная цена по + **памяти**: `2 × кап` байт. Сейчас 510 Б; кап 512 → +0.5 КБ, 1024 → +1.5 КБ. + EMM-кэш контента (1 МБ) длинные строки тянет легко — узкое место именно near. +- `viewport_x` (и копия в `docset_t`) + потолок `max_vx` (сейчас 248) → `uint16_t`, + иначе хранить >255 можно, а доскроллить нельзя. Плюс `widths[]`/`ccx` в таблицах/коде. + + Реалистичный компромисс — кап 512–1023: хватит почти всем листингам/таблицам, + цена ~+0.3 КБ кода и +0.5…1.5 КБ near-RAM. **Дешёвая полумера без `uint16_t`** — + ставить честный маркер обрезки на 255-й позиции, чтобы потеря была видна. + ## TODO 1. **Увеличение размера документов.** Снять лимит 128 КБ: Достаточно diff --git a/examples/mdview2/READMEBG.MD b/examples/mdview2/READMEBG.MD new file mode 100644 index 0000000..4952b65 --- /dev/null +++ b/examples/mdview2/READMEBG.MD @@ -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 +#include + +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 КБ, адреса 0x4000–0xBFFF). + - Буфер файла — страницы EMM, отображаемые в W3 (0xC000–0xFFFF) + +## 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 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 < +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: +* `` — puts / printf / FILE\* + Sprinter-specific dec/hex helpers +* `` — atoi / atof / malloc / qsort / ... (from SDCC z80.lib) +* `` / `` / `` — from SDCC z80.lib +* `` — read / write / close / lseek / unlink +* `` — open / creat + O\_RDONLY / O\_CREAT / ... +* `` — errno + error names + strerror +* `` — stat / fstat +* `` / `` — from SDCC + +Sprinter-specific: +* `` — putch / cputs / cprintf, textcolor / textbackground / textattr, kbhit / getch, clrscr, gotoxy, wherex/y +* `` — gfx_init/done, palette, putpixel, hline/vline/rect/fill_rect/line, text — for both 320 and 640 modes (gfx_\*16 variants) +* `` — full 14-function driver wrapper + mouse_cursor_t with bitmap support +* `` — chdir / getcwd / mkdir / rmdir / ffirst / fnext / ffblk +* `` — getdatetime / setdatetime + POSIX time / localtime / etc. +* `` — raw ports, ESTEX/BIOS function numbers, env API +* `` — exit / \_exit / atexit +* `` — mem\_alloc\_pages / mem\_free\_block / bank\_read / bank\_write +* `` — 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` +- Полный `` (memchr/cmp/set/cpy, strcat/cmp/cpy/len/chr/spn/etc.) +- `` (toupper/tolower) +- `` (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 ``** (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-сравнения, чтобы тесты примеров проходили без человека + +## Идеи на потом + +- Поддержка `` (есть в SDCC stdlib — нужно протестировать что наш crt0 совместим) +- `` через 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 × количество строк/столбцов экономии. Применимо ко всем композитным примитивам. diff --git a/examples/mdview2/mdview2.c b/examples/mdview2/mdview2.c index 415339e..e43a10a 100644 --- a/examples/mdview2/mdview2.c +++ b/examples/mdview2/mdview2.c @@ -35,6 +35,7 @@ #include #include #include +#include #include #include "mdview2.h" /* геометрия, атрибуты, общий интерфейс модулей */ @@ -94,7 +95,7 @@ static const uint8_t md_pallete[16 * 4] = { /* cache_rec_t (РОВНО 8 байт, см. sprinter_winrest_format) — в mdview2.h. */ #define CACHE_DIR_REC_SIZE ((uint16_t)sizeof(cache_rec_t)) -#define CACHE_DIR_RECS_PER_PAGE 2048u +#define CACHE_DIR_RECS_PER_PAGE (16384 / CACHE_DIR_REC_SIZE) /* 2048 */ #define MAX_CACHE_DIR_PAGES 8u /* MAX_CACHE_LINE_LEN — в mdview2.h (буфер ячеек g_cells живёт в mdview2_index.c). */ #define MAX_CACHE_CONTENT_PAGES 64u /* запас: до 1 МБ кэша контента */ @@ -113,14 +114,14 @@ static uint16_t cache_cur_off = 0; /* текущее смещ void cache_dir_get(uint16_t idx, cache_rec_t *r) { uint8_t page = (uint8_t)(idx >> 11); - uint16_t off = (uint16_t)((idx & 2047u) << 3); + uint16_t off = (uint16_t)((idx & 0x07FF) << 3); /* 2047 */ bank_read(cache_dir_phys[page], off, r, CACHE_DIR_REC_SIZE); } void cache_dir_put(uint16_t idx, const cache_rec_t *r) { uint8_t page = (uint8_t)(idx >> 11); - uint16_t off = (uint16_t)((idx & 2047u) << 3); + uint16_t off = (uint16_t)((idx & 0x07FF) << 3); /* 2047 */ bank_write(cache_dir_phys[page], off, r, CACHE_DIR_REC_SIZE); } @@ -140,9 +141,11 @@ uint8_t cache_reserve(uint16_t nbytes, uint8_t *out_page, uint16_t *out_off) * не вмещает запись целиком. */ uint8_t next_idx = (cache_content_n_pages == 0) ? 0 : (uint8_t)(cache_cur_page_idx + 1); if (next_idx >= cache_content_n_pages) { - if (cache_content_n_pages >= MAX_CACHE_CONTENT_PAGES) return 0; + if (cache_content_n_pages >= MAX_CACHE_CONTENT_PAGES) + return 0; uint8_t blk = mem_alloc_pages(1); - if (blk == 0) return 0; + if (blk == 0) + return 0; cache_content_blk[cache_content_n_pages] = blk; cache_content_phys[cache_content_n_pages] = mem_get_page(blk, 0); cache_content_n_pages++; @@ -171,40 +174,40 @@ void cache_commit(uint8_t page, uint16_t off, const void *buf, uint16_t len) /* emph_to_attr определена ниже (рядом с inline-логикой), нужна в inline_scan. */ /* emph_to_attr — в mdview2_index.c. */ -uint16_t n_lines = 0; /* extern: вью читает */ -uint16_t max_lines = 0; /* ёмкость индекса: index_pages * 2048 */ -uint16_t top_line = 0; /* extern: вью/скролл */ -uint32_t file_size = 0; +uint16_t n_lines = 0; /* extern: всего строк в индексе текущего набора */ +uint16_t max_lines = 0; /* extern: ёмкость индекса: index_pages * 2048 */ +uint16_t top_line = 0; /* extern: верхняя видимая строка */ +uint32_t file_size = 0; /* extern: размер активного буфера документа */ static uint8_t file_blk = 0; static uint8_t file_pages = 0; static uint8_t file_phys[MAX_PAGES] = {0}; -uint8_t index_blk = 0; -uint8_t index_pages = 0; -uint8_t index_phys[MAX_INDEX_PAGES] = {0}; -uint8_t index_truncated = 0; /* выставляется при исчерпании ёмкости индекса */ +uint8_t index_blk = 0; /* extern: дескриптор EMM-блока индекса */ +uint8_t index_pages = 0; /* */ +uint8_t index_phys[MAX_INDEX_PAGES] = {0}; /* extern: физ. страницы индекса */ +uint8_t index_truncated = 0; /* выставляется при исчерпании ёмкости индекса */ + /* cur_seg_off/cur_rec (near-состояние индексатора) — в mdview2_index.c. */ -uint8_t cur_page = 0xFF; /* 0xFF = в W3 ещё не показана ни одна страница (extern: кодек инвалидирует) */ -uint8_t viewport_x = 0; /* горизонтальный сдвиг для nowrap-строк (extern: вью) */ -char filename[64] = {0}; /* extern: статус-бар */ +uint8_t cur_page = 0xFF; /* extern: 0xFF = в W3 ещё не показана ни одна страница (extern: кодек инвалидирует) */ +uint8_t viewport_x = 0; /* extern: горизонтальный сдвиг для nowrap-строк */ +char filename[64] = {0}; /* extern: статус-бар */ /* Прогрессивная загрузка: рисуем первый экран и пускаем скролл, пока * index_lines() ещё досканирует файл (кооперативно, без прерываний). */ +uint8_t g_view = VIEW_MD; /* extern: активный режим просмотра (существует даже когда доступен только MD режим) */ #if WITH_RAW -uint8_t g_view = VIEW_MD; /* режим просмотра (общий с mdview2_raw.c) */ -uint8_t g_ready = 0; /* 1 когда первичный документ построен — F2 (RAW) рабочий (extern: вью) */ -static uint8_t g_raw_last = VIEW_RAW_WRAP; /* последний под-режим RAW (возврат по F2) */ +uint8_t g_ready = 0; /* extern: 1 когда первичный документ построен — F2 (RAW) рабочий */ +static uint8_t g_raw_last = VIEW_RAW_WRAP; /* последний под-режим RAW (возврат по F2) */ #endif -uint8_t g_loading = 0; /* 1 во время index_lines() (extern: вью) */ -static uint8_t g_first_drawn = 0; /* первый экран уже нарисован */ -uint8_t g_f8_enabled = 0; /* можно ли сейчас переключать кодировку (extern: вью-меню) */ -static uint8_t g_utf_failed = 0; /* UTF-8 набор не удалось построить (нет EMM) — пропускаем UTF-8 в цикле F8 */ -uint8_t g_abort = 0; /* F10/Esc во время загрузки → прервать индексацию */ -static uint16_t g_last_prog = 0; /* n_lines на последнем обновлении статуса */ -/* progress_tick — объявлена в mdview2.h (определена ниже, зовётся из index_lines). */ +uint8_t g_loading = 0; /* extern: 1 во время index_lines() */ +static uint8_t g_first_drawn = 0; /* первый экран уже нарисован */ +uint8_t g_f8_enabled = 0; /* extern: можно ли сейчас переключать кодировку */ +static uint8_t g_utf_failed = 0; /* UTF-8 набор не удалось построить (нет EMM) — пропускаем UTF-8 в цикле F8 */ +uint8_t g_abort = 0; /* extern: F10/Esc во время загрузки → прервать индексацию */ +static uint16_t g_last_prog = 0; /* n_lines на последнем обновлении статуса */ /* Кодировки (ENC_*) и g_encoding — в mdview2.h / mdview2_enc.c. */ -const uint8_t *g_remap = 0; /* активная таблица ремапа (CP1251/KOI8), 0 = без ремапа */ -uint8_t g_scratch_phys = 0; /* EMM-страница под ремап-буфер строки для win_rest */ +const uint8_t *g_remap = 0; /* extern: активная таблица ремапа (CP1251/KOI8), 0 = без ремапа (CP866)*/ +uint8_t g_scratch_phys = 0; /* extern: EMM-страница под ремап-буфер строки для win_rest */ /* UTF-8 набор (utf_blk/utf_pages/utf_phys/utf_size/utf_avail, конвертер и его * состояние) — в mdview2_enc.c; разделяемые символы и API — в mdview2.h. @@ -214,11 +217,11 @@ uint8_t g_scratch_phys = 0; /* EMM-страница под ремап /* Снимок исходного (8-битного) файлового буфера — набор DOC_8BIT читает его * как есть, набор DOC_UTF8 читает utf_phys. */ static uint8_t orig_file_blk = 0; -uint8_t orig_file_pages = 0; /* extern: читает кодек (utf_alloc) */ -uint32_t orig_file_size = 0; /* extern: читает кодек (utf_convert_more) */ -uint8_t orig_file_phys[MAX_PAGES] = {0}; /* extern: читает кодек (cv_read) */ +uint8_t orig_file_pages = 0; /* extern: читает кодек (utf_alloc) */ +uint32_t orig_file_size = 0; /* extern: читает кодек (utf_convert_more) */ +uint8_t orig_file_phys[MAX_PAGES] = {0}; /* extern: читает кодек (cv_read) */ -/* ---------- Два сосуществующих набора индекс/кэш (Фазы 4-5) ---------- +/* ----------------- Два сосуществующих набора индекс/кэш ----------------- * DOC_8BIT — исходные байты (CP866/CP1251/KOI8R, различаются ремапом на * отрисовке); DOC_UTF8 — UTF-8, декодированный в CP866. Оба строятся eager * при старте (кооперативно), поэтому F8 переключает активный набор мгновенно. @@ -242,6 +245,7 @@ typedef struct { uint16_t top_line; uint8_t viewport_x; } docset_t; + #define DOC_8BIT 0 #define DOC_UTF8 1 static docset_t g_doc[2] = {{0}, {0}}; /* ОБЯЗАТЕЛЬНО инициализировать: built=0 для обоих @@ -252,13 +256,7 @@ static uint8_t g_disp_doc = DOC_8BIT; /* какой набор показа static void doc_switch(uint8_t target); /* forward (нужна в progress_tick) */ -/* Спиннер в статус-баре (col=8,row=0). - * Включается во время загрузки/индексации, чтобы показать, - * что программа занята и не зависла. */ -#define SPINNER_COL 8 -static const char spinner_chars[4] = { '|', '/', '-', '\\' }; -static uint8_t spinner_phase = 0; -static uint8_t spinner_active = 0; +/* Статус-бар, меню и спиннер — в mdview2_status.c. */ /* ================================================================== * Примитивы экрана, доступ к EMM/окну W3 (fb/win_rest), палитра @@ -270,50 +268,32 @@ static uint8_t spinner_active = 0; * можно передать в BIOS LP_PRINT_LN напрямую, без промежуточной копии. */ void put_str_attr(uint8_t x, uint8_t y, const char *s, uint8_t attr) { - uint8_t maxlen = (x < SCREEN_W) ? (uint8_t)(SCREEN_W - x) : 0; - uint8_t len = (uint8_t)strlen(s); - if (len > maxlen) len = maxlen; + uint8_t len = (x < SCREEN_W) ? (uint8_t)(SCREEN_W - x) : 0; if (len == 0) return; bios_set_place(y, x); - bios_writeattr(s, len, attr); + bios_writeattr_until(s, len, attr, 0); } /* Полностью очищает указанную строку экрана символами пробела с attr. */ void fill_row(uint8_t y, uint8_t attr) { - // bios_set_place(y, 0); - // bios_fillcharattr(' ', attr, SCREEN_W); bios_clearwin(y, 0, 1, SCREEN_W, attr); } -/* Продвигает спиннер на один кадр. - * Если спиннер выключен — функция ничего не делает. */ -void spinner_tick(void) -{ - if (!spinner_active) return; - wrchar(SPINNER_COL, 0, spinner_chars[spinner_phase & 3], ATTR_BAR_SPINNER); - spinner_phase++; -} - -/* Включает/выключает спиннер. - * При выключении очищает его позицию пробелом в статус-баре. */ -static void spinner_show(uint8_t on) -{ - spinner_active = on; - if (!on) wrchar(SPINNER_COL, 0, ' ', ATTR_BAR); -} /* Отображает указанную EMM-страницу файла в окно W3. * Если эта страница уже активна, переключение не выполняется. */ -static inline void map_page(uint8_t page) -{ - if (page != cur_page) { - /* file_phys[] позволяет не вызывать mem_get_page() на каждом свопе. */ - sprinter_page_w3(file_phys[page]); - cur_page = page; - } -} +// static inline void map_page(uint8_t page) +// { +// if (page != cur_page) { +// /* file_phys[] позволяет не вызывать mem_get_page() на каждом свопе. */ +// sprinter_page_w3(file_phys[page]); +// cur_page = page; +// } +// } + + /* Читает один байт логического файла по смещению p. * При переходе между страницами автоматически переключает W3. * Проверка границ выполняется вызывающей стороной. @@ -458,13 +438,15 @@ static void set_pallete(void) { static int load_file(const char *path) { int fd = open(path, O_RDONLY); - if (fd < 0) return -1; + if (fd < 0) + return -1; long sz = lseek(fd, 0, SEEK_END); if (sz < 0 || (uint32_t)sz > MAX_FILE) { close(fd); return -2; } + file_size = (uint32_t)sz; lseek(fd, 0, SEEK_SET); @@ -506,40 +488,25 @@ static int load_file(const char *path) * Возврат: 0 — успех, -5/-6 — нехватка EMM. */ static int alloc_set_storage(void) { - /* Блок индекса (8 байт на сегмент, 2048 записей/страница). */ - { - uint8_t want = (uint8_t)(file_pages + 1u); - if (want > MAX_INDEX_PAGES) want = MAX_INDEX_PAGES; - index_blk = 0; - while (want >= 1u) { - index_blk = mem_alloc_pages(want); - if (index_blk) break; - want--; - } - if (index_blk == 0) return -5; - index_pages = want; - for (uint8_t i = 0; i < index_pages; i++) - index_phys[i] = mem_get_page(index_blk, i); - max_lines = (uint16_t)index_pages * INDEX_RECS_PER_PAGE; - } + uint8_t want; - /* Директория рендер-кэша — та же ёмкость, что у индекса. */ - { - uint8_t want = (uint8_t)(file_pages + 1u); - if (want > MAX_CACHE_DIR_PAGES) want = MAX_CACHE_DIR_PAGES; - cache_dir_blk = 0; - while (want >= 1u) { - cache_dir_blk = mem_alloc_pages(want); - if (cache_dir_blk) break; - want--; - } - if (cache_dir_blk == 0) return -6; - cache_dir_pages = want; - for (uint8_t i = 0; i < cache_dir_pages; i++) - cache_dir_phys[i] = mem_get_page(cache_dir_blk, i); - } + want = (uint8_t)(file_pages >> 1 + 1u); + if (want > MAX_INDEX_PAGES) want = MAX_INDEX_PAGES; + index_blk = mem_alloc_pages(want); + if (index_blk == 0) return -5; + index_pages = want; + for (uint8_t i = 0; i < index_pages; i++) + index_phys[i] = mem_get_page(index_blk, i); + max_lines = (uint16_t)index_pages * INDEX_RECS_PER_PAGE; + + want = (uint8_t)(file_pages >> 1 + 1u); + if (want > MAX_CACHE_DIR_PAGES) want = MAX_CACHE_DIR_PAGES; + cache_dir_blk = mem_alloc_pages(want); + if (cache_dir_blk == 0) return -6; + cache_dir_pages = want; + for (uint8_t i = 0; i < cache_dir_pages; i++) + cache_dir_phys[i] = mem_get_page(cache_dir_blk, i); - /* Свежий пул контента под этот набор. */ cache_content_n_pages = 0; cache_cur_page_idx = 0; cache_cur_off = 0; @@ -617,7 +584,8 @@ static void doc_load_live(uint8_t i) static void doc_switch(uint8_t target) { - if (target == g_active_doc) return; + if (target == g_active_doc) + return; doc_save_live(g_active_doc); doc_load_live(target); g_active_doc = target; @@ -628,18 +596,6 @@ static void doc_switch(uint8_t target) /* Отрисовка области/статус-бара/меню и прокрутка — в модуле mdview2_md.c. */ -/* ================================================================== - * Вывод ошибки - * ================================================================== */ - -/* Печатает сообщение об ошибке и ждёт нажатия клавиши. */ -static void die(const char *msg) -{ - clrscr_attr(ATTR_TEXT); - puts(msg); - puts("Press any key to exit..."); - (void)getkey(); -} /* ================================================================== * Прогрессивная загрузка и диспетчер клавиш (MD) @@ -668,10 +624,10 @@ static void load_key(uint16_t k) case KEY_DOWN: nt = top_line + 1; break; case KEY_PGUP: nt = (top_line >= VIEW_H) ? (uint16_t)(top_line - VIEW_H) : 0; break; case KEY_PGDN: nt = top_line + VIEW_H; break; - case KEY_HOME: top_line = 0; viewport_x = 0; draw_viewport_from_cache(); render_status_numbers(); return; - case KEY_END: top_line = maxtop; viewport_x = 0; draw_viewport_from_cache(); render_status_numbers(); return; - case KEY_LEFT: scroll_h(-(int8_t)HPAN_STEP); render_status_numbers(); return; - case KEY_RIGHT: scroll_h(+(int8_t)HPAN_STEP); render_status_numbers(); return; + case KEY_HOME: top_line = 0; viewport_x = 0; draw_viewport_from_cache(); render_md_status_numbers(); return; + case KEY_END: top_line = maxtop; viewport_x = 0; draw_viewport_from_cache(); render_md_status_numbers(); return; + case KEY_LEFT: md_scroll_horizon(-(int8_t)HPAN_STEP); render_md_status_numbers(); return; + case KEY_RIGHT: md_scroll_horizon(+(int8_t)HPAN_STEP); render_md_status_numbers(); return; case KEY_F1: show_help(); return; case KEY_F8: /* во время сборки крутим ТОЛЬКО 8-бит и только если набор 8-битный * (ремап на отрисовке; индексация не зависит от кодировки) */ @@ -689,7 +645,7 @@ static void load_key(uint16_t k) if (nt != top_line) { top_line = nt; draw_viewport_from_cache(); - render_status_numbers(); + render_md_status_numbers(); } } @@ -704,7 +660,7 @@ void progress_tick(void) if (g_view != VIEW_MD) { if (!g_first_drawn) { raw_draw(); - raw_status_numbers(); + // render_raw_status(); g_first_drawn = 1; cur_page = 0xFF; /* raw_draw сбил W3-маппинг, на который опирается fb() */ } @@ -716,14 +672,14 @@ void progress_tick(void) uint8_t touched = 0; if (!g_first_drawn && drawable_lines() >= VIEW_H) { draw_viewport_from_cache(); - render_status_numbers(); + render_md_status_numbers(); g_first_drawn = 1; touched = 1; } if (n_lines >= (uint16_t)(g_last_prog + 20)) { g_last_prog = n_lines; spinner_tick(); - if (g_first_drawn) render_status_numbers(); + if (g_first_drawn) render_md_status_numbers(); touched = 1; } if (g_first_drawn && kbhit()) { @@ -833,6 +789,15 @@ static void switch_encoding(uint8_t next) clamp_top(); /* привести top_line к новому набору (для MD) */ } +/* ================================================================== + * Вывод ошибки + * ================================================================== */ + +static void on_exit(void) { + unload_file(); + pal_reset(PAL_CGA); +} + int main(int argc, char **argv) { const char *path; @@ -844,16 +809,20 @@ int main(int argc, char **argv) { int chk = open(path, O_RDONLY); if (chk < 0) { - cputs("MDView v0.2 -- Markdown Viewer for Sprinter\r\n"); - cputs("(c) 2026 Petrov A.G.\r\n\r\n"); - cputs("Usage: mdview2 \r\n"); - cputs(" Displays a Markdown document (CP866, up to 128 KB).\r\n"); - return 1; + // settextmode(TEXT_MODE_80x32); + clrscr_attr(ATTR_TEXT); + cputs("MDView v0.3 -- Markdown Viewer for Sprinter\r\n" \ + "(c) 2026 Petrov A.G.\r\n\r\n" \ + "Usage: mdview2 \r\n" \ + " Displays a Markdown document (CP866/CP1251/KOI8R/UTF8, up to 128 KB).\r\n"); + exit(-1); } close(chk); } } + atexit(on_exit); + /* Копируем путь в static filename[] для отображения в статус-баре. */ { uint8_t i = 0; @@ -869,8 +838,10 @@ int main(int argc, char **argv) settextmode(TEXT_MODE_80x32); clrscr_attr(ATTR_TEXT); set_pallete(); + prerender_status(); render_menu(); render_full_status(); + render_percent_progress(0); spinner_show(1); int rc = load_file(path); @@ -890,7 +861,7 @@ int main(int argc, char **argv) case -6: cputs("render-cache dir alloc failed"); break; } cputs("\n\r\n\r"); - return 1; + exit(-1); } /* Scratch-страница под ремап-буфер строки (win_rest_remap). */ g_scratch_phys = mem_get_page(mem_alloc_pages(1), 0); @@ -928,13 +899,15 @@ int main(int argc, char **argv) clrscr_attr(ATTR_RESET); textattr(ATTR_RESET); cputs("MDView: index/cache alloc failed\n\r\n\r"); - return 1; + exit(-1); } - if (g_abort) goto exit_loop; /* F10/Esc во время загрузки — корректный выход */ + if (g_abort) + exit(0); /* F10/Esc во время загрузки — корректный выход */ if (n_lines == 0) { - die("mdview: empty file"); - unload_file(); - return 1; + clrscr_attr(ATTR_RESET); + textattr(ATTR_RESET); + cputs("mdview: empty file"); + exit(-1); } /* Первичный готов — гасим спиннер, включаем F8 (второй набор дособерётся @@ -956,11 +929,17 @@ int main(int argc, char **argv) uint8_t scan = (uint8_t)((k >> 8) & 0x7F); if (ascii) { - if (ascii == 0x1B) break; /* клавиша Esc */ + if (ascii == 0x1B) + break; /* клавиша Esc */ continue; } - -#if WITH_RAW + if (scan == KEY_F10) { + break; + } + if (scan == KEY_F1) { + show_help(); + continue; + } /* show_help сам перерисует */ /* F2 — тумблер RAW ↔ MD (рабочий только после постройки документа). */ if (scan == KEY_F2) { if (!g_ready) continue; @@ -968,8 +947,10 @@ int main(int argc, char **argv) g_view = g_raw_last; /* → RAW (последний под-режим) */ raw_seed_from(top_line); /* позиция из MD */ raw_screen_init(); - render_full_status(); render_menu(); - raw_status_numbers(); raw_draw(); + status_encoding(); + render_menu(); + render_raw_status(); + raw_draw(); } else { g_raw_last = g_view; /* запомнить под-режим RAW */ g_view = VIEW_MD; @@ -979,64 +960,48 @@ int main(int argc, char **argv) } continue; } - - /* RAW-режим: F3 Wrap/Unwrap, своя навигация; F8 — кодировка; F1/F10. */ - if (g_view != VIEW_MD) { - if (scan == KEY_F10) goto exit_loop; - if (scan == KEY_F1) { - show_help(); - raw_screen_init(); - render_full_status(); render_menu(); - raw_status_numbers(); raw_draw(); - continue; - } - if (scan == KEY_F3) { /* Wrap ↔ Unwrap */ - g_view = (g_view == VIEW_RAW_WRAP) ? VIEW_RAW_HSCROLL : VIEW_RAW_WRAP; - g_raw_last = g_view; - raw_renorm(); - raw_screen_init(); - render_menu(); - raw_status_numbers(); raw_draw(); - continue; - } - if (scan == KEY_F8) { - uint8_t next = (g_encoding == ENC_CP866) ? ENC_CP1251 : - (g_encoding == ENC_CP1251) ? ENC_KOI8R : - (g_encoding == ENC_KOI8R) ? ENC_UTF8 : ENC_CP866; - if (next == ENC_UTF8 && g_utf_failed) next = ENC_CP866; - uint8_t target = (next == ENC_UTF8) ? DOC_UTF8 : DOC_8BIT; - /* другой буфер (в/из UTF-8) → в начало ДО сборки, чтобы прогрессивный - * raw_draw в progress_tick показывал верх нового набора. */ - if (target != g_active_doc) raw_home(); - switch_encoding(next); /* меняет набор/ремап */ - if (g_abort) goto exit_loop; - render_full_status(); raw_status_numbers(); raw_draw(); - continue; - } - raw_key(scan); - continue; - } -#endif /* WITH_RAW */ - - /* MD-режим: F1/F8/F10 здесь, навигация — в md_key() (peer к raw_key). */ - if (scan == KEY_F10) goto exit_loop; - if (scan == KEY_F1) { show_help(); continue; } /* show_help сам перерисует */ - if (scan == KEY_F8) { /* цикл CP866→CP1251→KOI8R→UTF8→CP866; набор дособерётся лениво */ + /* цикл CP866→CP1251→KOI8R→UTF8→CP866; набор дособерётся лениво */ + if (scan == KEY_F8) { uint8_t next = (g_encoding == ENC_CP866) ? ENC_CP1251 : - (g_encoding == ENC_CP1251) ? ENC_KOI8R : - (g_encoding == ENC_KOI8R) ? ENC_UTF8 : ENC_CP866; - if (next == ENC_UTF8 && g_utf_failed) next = ENC_CP866; /* UTF-8 недоступен */ + (g_encoding == ENC_CP1251) ? ENC_KOI8R : + (g_encoding == ENC_KOI8R) ? ENC_UTF8 : ENC_CP866; + if (next == ENC_UTF8 && g_utf_failed) + next = ENC_CP866; /* UTF-8 недоступен */ switch_encoding(next); - if (g_abort) goto exit_loop; /* Esc/F10 во время ленивой досборки */ + if (g_abort) + exit(0); /* Esc/F10 во время ленивой досборки */ render_full_status(); /* перерисовка — в MD-режиме */ draw_viewport_from_cache(); continue; } - md_key(scan); /* навигация MD (сама рисует+статус); незнакомые игнорит */ + switch(g_view) { + case VIEW_MD: + /* MD-режим: F1/F8/F10 здесь, навигация — в md_key() (peer к raw_key). */ + md_key(scan); /* навигация MD (сама рисует+статус); незнакомые игнорит */ + continue; +#if WITH_RAW + case VIEW_RAW_WRAP: + case VIEW_RAW_HSCROLL: + /* RAW-режим: F3 Wrap/Unwrap, своя навигация; F8 — кодировка; F1/F10. */ + /* Wrap ↔ Unwrap */ + if (scan == KEY_F3) { + g_view = (g_view == VIEW_RAW_WRAP) ? VIEW_RAW_HSCROLL : VIEW_RAW_WRAP; + g_raw_last = g_view; + raw_renorm(); + raw_screen_init(); + render_menu(); + render_raw_status(); + raw_draw(); + continue; + } + raw_key(scan); + continue; +#endif /* WITH_RAW */ + default: + break; + } } -exit_loop: - unload_file(); - pal_reset(PAL_CGA); clrscr_attr(ATTR_TEXT); + exit(0); return 0; } diff --git a/examples/mdview2/mdview2.h b/examples/mdview2/mdview2.h index f6a99cc..ab2ef80 100644 --- a/examples/mdview2/mdview2.h +++ b/examples/mdview2/mdview2.h @@ -22,6 +22,12 @@ #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 КБ */ @@ -130,7 +136,7 @@ uint32_t seg_off(uint16_t idx); /* offset исходника для ло void raw_seed_from(uint16_t md_top_line); /* вход в RAW: позиция по top_line */ void raw_home(void); /* сброс в начало (смена буфера при F8 в/из UTF-8) */ void raw_draw(void); /* перерисовать область документа в RAW */ -void raw_status_numbers(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) */ @@ -163,18 +169,29 @@ extern uint8_t g_ready; /* 1 когда первичный до extern uint8_t g_f8_enabled; /* можно ли сейчас переключать кодировку */ /* ================================================================== * - * Отрисовка области/статус-бара/меню и прокрутка (mdview2_md.c). + * Статус-бар (строка 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); /* перерисовать область документа из кэша */ -uint16_t drawable_lines(void); /* число строк, готовых к показу */ -void render_status_numbers(void); /* числовая часть статус-бара */ -void render_full_status(void); /* полная перерисовка статус-бара */ -void render_updated_status(void); /* обновить только числа статус-бара */ -void render_menu(void); /* перерисовать строку меню */ void clamp_top(void); /* привести top_line к диапазону */ -void scroll_up(uint16_t n); -void scroll_down(uint16_t n); -void scroll_h(int8_t delta); /* горизонтальный сдвиг nowrap-блоков */ +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 = обработано */ /* ================================================================== * @@ -186,7 +203,7 @@ void show_help(void); /* модальный диалог F1 */ * Парсер/индексатор (mdview2_index.c) и разделяемое с ним состояние. * Хранилище индекса (index_blk/pages/phys, n_lines/max_lines) — в ядре * (аллокация/doc-slot), индексатор пишет в него; cache_*-писатели и - * progress_tick/spinner_tick ядро экспортирует индексатору. + * progress_tick ядро экспортирует индексатору (spinner_tick — из status). * ================================================================== */ #define INDEX_RECS_PER_PAGE 2048u /* 16384 / 8; запись всегда в одной странице */ #define MAX_CACHE_LINE_LEN 255u /* кап длины контента строки в ячейках (буфер g_cells) */ @@ -200,7 +217,6 @@ extern uint8_t g_abort; /* F10/Esc во время загруз 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 spinner_tick(void); /* анимация индикатора загрузки */ void progress_tick(void); /* кооперативный шаг loading-loop (рисует/листает) */ void index_lines(void); /* построить индекс по загруженному файлу */ diff --git a/examples/mdview2/mdview2_md.c b/examples/mdview2/mdview2_md.c index e184e93..9c10ce7 100644 --- a/examples/mdview2/mdview2_md.c +++ b/examples/mdview2/mdview2_md.c @@ -1,6 +1,7 @@ /* * 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). Никакого @@ -11,8 +12,7 @@ */ #include -#include /* dec8/dec16 */ -#include /* wrchar/gotoxy/cputs/textattr/scroll/COLOR */ +#include /* wrchar/scroll/COLOR */ #include /* bios_set_place/bios_fillcharattr */ #include "mdview2.h" @@ -61,101 +61,6 @@ void draw_viewport_from_cache(void) } } -/* Число строк, готовых к показу. Во время загрузки последняя эмитированная - * строка ещё в 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; -} - -/* Возвращает процент прокрутки (0..100) относительно доступного диапазона. */ -static uint8_t calc_pct(void) -{ - uint16_t total = drawable_lines(); - uint8_t pct; - if (total <= VIEW_H) - pct = 100; - else - pct = (uint8_t)((uint32_t)top_line * 100UL / (uint32_t)(total - VIEW_H)); - return pct; -} - -/* Рисует динамическую часть статус-бара без printf. - * Полностью перезаписывает колонки 45..79, чтобы не оставлять «хвосты». */ -void render_status_numbers(void) -{ - uint16_t total = drawable_lines(); - uint16_t last = top_line + VIEW_H; - if (last > total) last = total; - uint8_t pct = calc_pct(); - - textattr(ATTR_BAR); - bios_set_place(0, 45); - bios_fillcharattr(' ', ATTR_BAR, (uint8_t)(SCREEN_W - 45)); - - wrchar(45, 0, 0xB3, ATTR_BAR); /* вертикальный разделитель */ - gotoxy(47, 0); - cputs("L "); - dec16(top_line + 1); - cputs("-"); - dec16(last); - cputs(" / "); - dec16(total); - if (g_loading) cputs("..."); /* ещё грузится */ - - wrchar(70, 0, 0xB3, ATTR_BAR); /* вертикальный разделитель */ - gotoxy( 73, 0); - dec8(pct); - cputs("%"); -} - -/* Полностью перерисовывает статус-бар (заголовок + числа). */ -void render_full_status(void) -{ - - fill_row(0, ATTR_BAR); - /* Размещение в шапке: "MDVIEW", затем слот спиннера и имя файла. */ - put_str_attr(1, 0, "MDVIEW", ATTR_BAR); - put_str_attr(10, 0, filename, ATTR_BAR); - put_str_attr(37, 0, enc_name(g_encoding), ATTR_BAR); /* текущая кодировка */ - render_status_numbers(); -} - -/* Обновляет только изменяемую числовую часть статус-бара. */ -void render_updated_status(void) -{ - render_status_numbers(); -} - -/* Отрисовывает нижнюю строку меню. */ -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); -} /* ================================================================== * Прокрутка @@ -172,7 +77,7 @@ void clamp_top(void) } /* Прокрутка вверх на n строк с частичной перерисовкой при n == 1. */ -void scroll_up(uint16_t n) +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) { @@ -187,7 +92,7 @@ void scroll_up(uint16_t n) } /* Прокрутка вниз на n строк с частичной перерисовкой при n == 1. */ -void scroll_down(uint16_t n) +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; @@ -208,7 +113,7 @@ void scroll_down(uint16_t n) * Фаза 5: ширина берётся прямо из директории кэша (rec.len — уже * посчитанная реальная длина в ячейках после рендера), без единого * обращения к fb()/seg_off()/исходному файлу. */ -void scroll_h(int8_t delta) +void md_scroll_horizon(int8_t delta) { uint16_t maxw = 0; for (uint8_t i = 0; i < VIEW_H; i++) { @@ -247,6 +152,14 @@ void scroll_h(int8_t delta) 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-режиме (вызывается из главного @@ -255,19 +168,40 @@ void scroll_h(int8_t delta) uint8_t md_key(uint8_t scan) { switch (scan) { - case KEY_UP: scroll_up(1); break; - case KEY_DOWN: scroll_down(1); break; - case KEY_LEFT: scroll_h(-(int8_t)HPAN_STEP); break; - case KEY_RIGHT: scroll_h(+(int8_t)HPAN_STEP); break; - case KEY_PGUP: scroll_up(VIEW_H); break; - case KEY_PGDN: scroll_down(VIEW_H); break; - case KEY_HOME: top_line = 0; viewport_x = 0; - draw_viewport_from_cache(); break; - case KEY_END: top_line = (n_lines > VIEW_H) ? (uint16_t)(n_lines - VIEW_H) : 0; - viewport_x = 0; - draw_viewport_from_cache(); break; - default: return 0; /* не наша клавиша */ + 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: + top_line = 0; + viewport_x = 0; + draw_viewport_from_cache(); + break; + case KEY_END: + top_line = (n_lines > VIEW_H) ? (uint16_t)(n_lines - VIEW_H) : 0; + viewport_x = 0; + draw_viewport_from_cache(); + break; + + default: + return 0; /* не наша клавиша */ } - render_updated_status(); + + render_percent_progress(calc_md_pct()); + render_md_status_numbers(); return 1; } diff --git a/examples/mdview2/mdview2_raw.c b/examples/mdview2/mdview2_raw.c index d151f85..d94865b 100644 --- a/examples/mdview2/mdview2_raw.c +++ b/examples/mdview2/mdview2_raw.c @@ -45,21 +45,6 @@ static uint32_t raw_next_wrap(uint32_t off) return e; /* EOF */ } -/* WRAP: начало предыдущего ряда. */ -// static uint32_t raw_prev_wrap(uint32_t off) -// { -// if (off == 0) return 0; -// uint32_t ls = raw_line_start(off); -// if (off > ls) /* тот же абзац: на 80 назад */ -// return off - SCREEN_W; -// /* off == начало строки: последний ряд предыдущей физической строки. -// * Последнюю 80-границу ищем циклом (без 32-битного деления). */ -// uint32_t pls = raw_line_start(off - 1); /* off-1 — это \n пред. строки */ -// uint32_t last = pls; -// while (last + SCREEN_W < (off - 1)) last += SCREEN_W; -// return last; -// } - static uint32_t raw_prev_wrap(uint32_t off) { if (off == 0) return 0; @@ -74,15 +59,6 @@ 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; -// uint32_t p = off - 1; /* \n, завершивший пред. строку */ -// if (p == 0) return 0; -// p--; -// while (p != 0 && fb(p) != '\n') p--; -// return (fb(p) == '\n') ? p + 1 : 0; -// } static uint32_t raw_prev_line(uint32_t off) { @@ -128,27 +104,6 @@ static uint32_t raw_draw_wrap_row(uint32_t off, uint8_t 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++; -// } -// for (; i < SCREEN_W; i++) raw_buf[i] = ' '; - -// if (raw_hpan > 0) raw_buf[0] = '<'; -// if (len > (uint16_t)raw_hpan + SCREEN_W) raw_buf[SCREEN_W - 1] = '>'; -// raw_flush(row); -// return (e < file_size) ? e + 1 : e; -// } - static uint32_t raw_draw_hscroll_row(uint32_t off, uint8_t row) { uint32_t e = off; @@ -203,54 +158,18 @@ static uint32_t raw_prev(uint32_t off) return (g_view == VIEW_RAW_WRAP) ? raw_prev_wrap(off) : raw_prev_line(off); } -static void raw_scroll_down1(void) -{ - /* Скроллим вниз ТОЛЬКО если контент уходит за нижний край. Проходим VIEW_H - * рядов от raw_top: если контент кончился раньше (экран не полон) — внизу - * была бы пустая строка, не скроллим; если ровно по нижний ряд (off==EOF) — - * ниже пусто, тоже не скроллим. Иначе off = ряд сразу под экраном. */ - 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; /* контент ровно по экран, ниже пусто */ +/* ---- статус-бар: % по байтам (отрисовка — render_raw_status_numbers + * в mdview2_status.c, читает позицию через этот raw_pct) ---- */ - scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 1, 0); /* содержимое вверх */ - raw_draw_one(off, (uint8_t)(VIEW_TOP_ROW + VIEW_H - 1)); /* новый нижний ряд */ - raw_top = raw_next(raw_top); -} -static void raw_scroll_up1(void) +/* Процент по байтам без 32-битного деления: масштабируем offset'ы (>>8), + * дальше 32-битная арифметика. raw_top приватен модулю, поэтому % считаем + * здесь и отдаём готовым числом в статус-модуль. */ +uint8_t calc_raw_pct(void) { - if (raw_top == 0) return; - uint32_t nt = raw_prev(raw_top); - scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 2, 0); /* содержимое вниз */ - (void)raw_draw_one(nt, VIEW_TOP_ROW); - raw_top = nt; -} - -/* ---- статус-бар (без подсчёта строк: процент по байтам + режим) ---- */ - -/* Процент по байтам без 32-битного деления: масштабируем offset'ы в единицы - * по 1 КБ (>>10), дальше 16-битная арифметика (a*100 ≤ 12800). */ -static uint8_t raw_pct(void) -{ - uint16_t a = (uint16_t)(raw_top >> 10); - uint16_t b = (uint16_t)(file_size >> 10); + 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 * 100u / b); -} - -void raw_status_numbers(void) -{ - uint8_t pct = raw_pct(); - textattr(ATTR_BAR); - gotoxy(47, 0); - bios_writeattr_until(g_view == VIEW_RAW_WRAP ? "RAW wrap" : "RAW pan", - 20, ATTR_BAR, 0); - gotoxy(73, 0); - dec8(pct); - cputs("% "); + return (uint8_t)(a * 100UL / b); } /* ---- посев позиции / пересев при смене активного буфера ----------- */ @@ -271,6 +190,14 @@ void raw_home(void) 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); } @@ -283,58 +210,89 @@ void raw_renorm(void) 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) { - uint32_t nt; - switch (scan) { - case KEY_DOWN: raw_scroll_down1(); raw_status_numbers(); return 1; - case KEY_UP: raw_scroll_up1(); raw_status_numbers(); return 1; - + case KEY_DOWN: + raw_scroll_down(1); + break; + case KEY_UP: + raw_scroll_up(1); + break; case KEY_PGDN: - nt = raw_top; - for (uint8_t k = 0; k < VIEW_H; k++) { - uint32_t n2 = raw_next(nt); - if (n2 >= file_size) break; - nt = n2; - } + raw_scroll_down(VIEW_H); break; case KEY_PGUP: - nt = raw_top; - for (uint8_t k = 0; k < VIEW_H; k++) nt = raw_prev(nt); + raw_scroll_up(VIEW_H); break; case KEY_HOME: - nt = 0; raw_hpan = 0; + raw_home(); + raw_draw(); break; case KEY_END: - nt = file_size; /* отмотать VIEW_H рядов назад */ - for (uint8_t k = 0; k < VIEW_H; k++) nt = raw_prev(nt); - raw_hpan = 0; + 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(); - raw_status_numbers(); } - return 1; + break; case KEY_RIGHT: - if (g_view == VIEW_RAW_HSCROLL) { - if (raw_hpan < 248u) raw_hpan = (uint16_t)(raw_hpan + HPAN_STEP); + if (g_view == VIEW_RAW_HSCROLL && raw_hpan < 248u) { + raw_hpan = (uint16_t)(raw_hpan + HPAN_STEP); raw_draw(); - raw_status_numbers(); } - return 1; + break; default: return 0; /* не наша клавиша */ } - raw_top = nt; - raw_draw(); /* PgUp/PgDn/Home/End — полный экран */ - raw_status_numbers(); + // render_raw_status_numbers(); + render_percent_progress(calc_raw_pct()); return 1; } diff --git a/examples/mdview2/mdview2_status.c b/examples/mdview2/mdview2_status.c new file mode 100644 index 0000000..f911339 --- /dev/null +++ b/examples/mdview2/mdview2_status.c @@ -0,0 +1,162 @@ +/* + * 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 +#include /* dec8/dec16, cputs */ +#include /* textattr/gotoxy/wrchar/COLOR */ +#include /* 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(37, 0); + bios_write_until(enc_name(g_encoding), 7, 0); +} + +/* Числовая часть (MD): "L a-b / total" между разделителями + "pct%" справа. + * Перезаписывает ТОЛЬКО области между/после разделителей, сами │ и фикс. + * хром не трогает. */ +void render_md_status_numbers(void) +{ + uint16_t total = drawable_lines(); + uint16_t last = top_line + VIEW_H; + if (last > total) + last = total; + + textattr(ATTR_BAR); + gotoxy(DIV1_X + 1, 0); + bios_fillcharattr(' ', ATTR_BAR, DIV2_X - DIV1_X - 1); /* очистить [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) { + gotoxy(DIV2_X + 1, 0); + bios_fillcharattr(' ', ATTR_BAR, SCREEN_W - DIV2_X - 1); /* очистить [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); +} diff --git a/release_docs/ru/platform_reference.md b/release_docs/ru/platform_reference.md index b4d73d9..26416ed 100644 --- a/release_docs/ru/platform_reference.md +++ b/release_docs/ru/platform_reference.md @@ -23,6 +23,10 @@ C-toolchain'а, накопленные в процессе разработки. * `RST 30h` — Mouse driver, номер функции в C * **Видеорежимы:** 320×256×256, 640×256×16, 80×32 text, 40×32 text. * **Формат 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-26 — добавлен пункт о Fast RAM в cheat-sheet (детали — `docs/fast_ram.md`)