Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a14f19b657 | |||
| a6e0aacc80 | |||
| 13ef8d2fa5 | |||
| 68201dca44 | |||
| 50596e6a2a | |||
| de11882d16 | |||
| 92c0a9a4a9 | |||
| 2f4eaf5b25 | |||
| 138e783e72 | |||
| eab5a2d6ac | |||
| 1b39a60ff0 | |||
| 8cfdcf307a | |||
| 49a87b4d02 | |||
| 5e5e70d0c8 | |||
| 016fedd94c | |||
| c9d5ea35a4 | |||
| bd116d7361 | |||
| 858e5755ad | |||
| f87b52bb7f | |||
| 07c4004bd9 | |||
| 6992c1436e |
@@ -24,6 +24,9 @@ examples/*/*.cdb
|
||||
examples/*/*.mem
|
||||
examples/*/*.rst
|
||||
|
||||
# Temporary build directory for floppy disk image preparation
|
||||
examples/*/.disk_tmp/
|
||||
|
||||
tests/*/*.exe
|
||||
tests/*/*.asm
|
||||
tests/*/*.lst
|
||||
|
||||
@@ -52,7 +52,7 @@ pages by program size — small programs get only one page. Pick a memory mode
|
||||
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 | |
|
||||
@@ -217,7 +217,6 @@ This repository contains:
|
||||
|
||||
* **Sprinter / Peters Plus** — Иван Мак, Дмитрий Паринов and the original team
|
||||
* **SDCC** — for the underlying Z80 compiler
|
||||
* **z88dk +pps** — Дмитрий M. for paving the way with the first Sprinter target
|
||||
* **MAME** — for the Sprinter Sp2000 emulation
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
# [z80] `global = local` after `if (local != global)` stores `local - global` (A clobbered by comparison)
|
||||
|
||||
## Summary
|
||||
|
||||
On the z80 backend, the sequence
|
||||
|
||||
```c
|
||||
if (n != g) { g = n; }
|
||||
```
|
||||
|
||||
where `n` is in register `A` and `g` is a global, miscompiles. The compiler
|
||||
evaluates the `!=` comparison with `SUB A,(HL)`, which destroys `A`, and then
|
||||
emits the assignment `g = n` by storing `A` — which now holds `n - g`, not `n`.
|
||||
As a result the global ends up holding `(unsigned char)(n - old_g)` instead of `n`.
|
||||
|
||||
It should either use `CP (HL)` (which leaves `A` intact) for the comparison, or
|
||||
reload `n` before the store.
|
||||
|
||||
## Version
|
||||
|
||||
SDCC 4.5.0 #15242 (Mac OS X x86_64). Default options; also reproduces with
|
||||
`--opt-code-speed` and with `--no-peep` (so this is a code-generator bug, not a
|
||||
peephole-optimizer bug).
|
||||
|
||||
## Minimal reproducer
|
||||
|
||||
```c
|
||||
unsigned char vx;
|
||||
|
||||
void update(unsigned char n)
|
||||
{
|
||||
if (n != vx) {
|
||||
vx = n;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Build:
|
||||
|
||||
```
|
||||
sdcc -mz80 -S repro.c
|
||||
```
|
||||
|
||||
## Generated assembly (wrong)
|
||||
|
||||
```asm
|
||||
_update::
|
||||
;repro.c: if (n != vx) {
|
||||
ld hl, #_vx
|
||||
sub a, (hl) ; A (= n) is destroyed: A = n - vx
|
||||
ret Z
|
||||
;repro.c: vx = n;
|
||||
ld (_vx+0), a ; stores (n - vx) instead of n
|
||||
;repro.c: }
|
||||
ret
|
||||
```
|
||||
|
||||
With `--no-peep` the same defect is present (only the branch shape differs):
|
||||
|
||||
```asm
|
||||
_update::
|
||||
ld iy, #_vx
|
||||
sub a, 0 (iy) ; A (= n) destroyed
|
||||
jp NZ, 00112$
|
||||
jp 00103$
|
||||
00112$:
|
||||
ld (_vx+0), a ; stores (n - vx)
|
||||
00103$:
|
||||
ret
|
||||
```
|
||||
|
||||
## Why it happens
|
||||
|
||||
`n` arrives in `A` (sdcccall). The code generator picks `SUB A,(HL)` to evaluate
|
||||
the relational `n != vx`. `SUB` overwrites `A` with the difference. The generator
|
||||
then treats the still-live value `n` as if it were still in `A` and emits a bare
|
||||
store `LD (_vx),A` for the assignment, without reloading `n` first. Because the
|
||||
defect survives `--no-peep`, it is in code generation (register/lifetime tracking
|
||||
across the comparison), not in the peephole optimizer.
|
||||
|
||||
The correct lowering for the comparison is `CP (HL)`, which sets the flags exactly
|
||||
like `SUB` but preserves `A`, so the subsequent store would be correct with no
|
||||
extra instructions.
|
||||
|
||||
## Variants that also reproduce
|
||||
|
||||
- `if (n == vx) return; vx = n;` (early-return form)
|
||||
- `n` coming from a function call result instead of a parameter
|
||||
- both `-mz80` default and `--opt-code-speed`
|
||||
|
||||
## Workaround
|
||||
|
||||
Store into the global *before* the comparison, so the destructive `SUB` is no
|
||||
longer on the store path; compare a saved copy instead:
|
||||
|
||||
```c
|
||||
void update(unsigned char n)
|
||||
{
|
||||
unsigned char old = vx;
|
||||
vx = n; /* store first, A still holds n */
|
||||
if (n != old) {
|
||||
/* side effect */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
generates the correct:
|
||||
|
||||
```asm
|
||||
_update::
|
||||
ld (_vx+0), a
|
||||
ret
|
||||
```
|
||||
|
||||
## Files in this directory
|
||||
|
||||
- `repro.c` — minimal reproducer
|
||||
- `repro.asm` — generated output, default options (defect visible)
|
||||
- `repro.nopeep.asm` — generated output with `--no-peep` (defect still present)
|
||||
- `workaround.c` / `workaround.asm` — store-before-compare workaround (correct)
|
||||
|
||||
## Tracker search
|
||||
|
||||
A search of the SDCC bug tracker did not turn up an exact duplicate. The closest
|
||||
version-matching report, #3834 "[Z80][SDCC 4.5] Compiler bug", is a *different*
|
||||
defect (`genPointerSet`, swapped push/pop order), not this comparison-clobbers-A
|
||||
case.
|
||||
@@ -0,0 +1,127 @@
|
||||
# [z80] `global = local` после `if (local != global)` пишет `local - global` (A затирается сравнением)
|
||||
|
||||
## Кратко
|
||||
|
||||
На бэкенде z80 последовательность
|
||||
|
||||
```c
|
||||
if (n != g) { g = n; }
|
||||
```
|
||||
|
||||
где `n` лежит в регистре `A`, а `g` — глобальная переменная, компилируется
|
||||
неверно. Сравнение `!=` вычисляется через `SUB A,(HL)`, который **разрушает `A`**,
|
||||
после чего присваивание `g = n` выполняется записью `A` — а там уже `n - g`, а не
|
||||
`n`. В итоге в глобальную переменную попадает `(unsigned char)(n - старое_g)`
|
||||
вместо `n`.
|
||||
|
||||
Корректно было бы использовать для сравнения `CP (HL)` (он не меняет `A`) либо
|
||||
перезагрузить `n` перед записью.
|
||||
|
||||
## Версия
|
||||
|
||||
SDCC 4.5.0 #15242 (Mac OS X x86_64). Опции по умолчанию; также воспроизводится с
|
||||
`--opt-code-speed` и с `--no-peep` (то есть это баг кодогенератора, а не
|
||||
peephole-оптимизатора).
|
||||
|
||||
## Минимальный пример
|
||||
|
||||
```c
|
||||
unsigned char vx;
|
||||
|
||||
void update(unsigned char n)
|
||||
{
|
||||
if (n != vx) {
|
||||
vx = n;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Сборка:
|
||||
|
||||
```
|
||||
sdcc -mz80 -S repro.c
|
||||
```
|
||||
|
||||
## Сгенерированный ассемблер (неверный)
|
||||
|
||||
```asm
|
||||
_update::
|
||||
;repro.c: if (n != vx) {
|
||||
ld hl, #_vx
|
||||
sub a, (hl) ; A (= n) разрушается: A = n - vx
|
||||
ret Z
|
||||
;repro.c: vx = n;
|
||||
ld (_vx+0), a ; пишет (n - vx) вместо n
|
||||
;repro.c: }
|
||||
ret
|
||||
```
|
||||
|
||||
С `--no-peep` дефект тот же (отличается лишь форма ветвления):
|
||||
|
||||
```asm
|
||||
_update::
|
||||
ld iy, #_vx
|
||||
sub a, 0 (iy) ; A (= n) разрушен
|
||||
jp NZ, 00112$
|
||||
jp 00103$
|
||||
00112$:
|
||||
ld (_vx+0), a ; пишет (n - vx)
|
||||
00103$:
|
||||
ret
|
||||
```
|
||||
|
||||
## Почему так происходит
|
||||
|
||||
`n` приходит в `A` (sdcccall). Кодогенератор выбирает `SUB A,(HL)` для вычисления
|
||||
отношения `n != vx`. `SUB` перезаписывает `A` разностью. Далее генератор считает,
|
||||
что ещё «живое» значение `n` по-прежнему в `A`, и для присваивания выдаёт голую
|
||||
запись `LD (_vx),A`, не перезагрузив `n`. Поскольку дефект сохраняется при
|
||||
`--no-peep`, он находится в кодогенерации (учёт регистров/времён жизни через
|
||||
сравнение), а не в peephole-оптимизаторе.
|
||||
|
||||
Правильное преобразование сравнения — `CP (HL)`: он выставляет флаги ровно как
|
||||
`SUB`, но сохраняет `A`, поэтому последующая запись была бы корректной без единой
|
||||
лишней инструкции.
|
||||
|
||||
## Варианты, которые тоже воспроизводят
|
||||
|
||||
- `if (n == vx) return; vx = n;` (форма с ранним выходом)
|
||||
- `n` как результат вызова функции, а не как параметр
|
||||
- и `-mz80` по умолчанию, и `--opt-code-speed`
|
||||
|
||||
## Обходной путь (workaround)
|
||||
|
||||
Записывать в глобальную переменную **до** сравнения, чтобы разрушающий `SUB` не
|
||||
оказался на пути записи; сравнивать сохранённую копию:
|
||||
|
||||
```c
|
||||
void update(unsigned char n)
|
||||
{
|
||||
unsigned char old = vx;
|
||||
vx = n; /* запись первой, A ещё держит n */
|
||||
if (n != old) {
|
||||
/* побочный эффект */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
даёт корректное:
|
||||
|
||||
```asm
|
||||
_update::
|
||||
ld (_vx+0), a
|
||||
ret
|
||||
```
|
||||
|
||||
## Файлы в этом каталоге
|
||||
|
||||
- `repro.c` — минимальный пример
|
||||
- `repro.asm` — вывод с опциями по умолчанию (дефект виден)
|
||||
- `repro.nopeep.asm` — вывод с `--no-peep` (дефект сохраняется)
|
||||
- `workaround.c` / `workaround.asm` — обход «запись до сравнения» (корректно)
|
||||
|
||||
## Поиск по трекеру
|
||||
|
||||
Поиск по баг-трекеру SDCC точного дубликата не нашёл. Ближайший по версии отчёт
|
||||
#3834 «[Z80][SDCC 4.5] Compiler bug» — это **другой** дефект (`genPointerSet`,
|
||||
переставленный порядок push/pop), а не данный случай «сравнение затирает A».
|
||||
@@ -0,0 +1,61 @@
|
||||
;--------------------------------------------------------
|
||||
; File Created by SDCC : free open source ISO C Compiler
|
||||
; Version 4.5.0 #15242 (Mac OS X x86_64)
|
||||
;--------------------------------------------------------
|
||||
.module repro
|
||||
|
||||
.optsdcc -mz80 sdcccall(1)
|
||||
;--------------------------------------------------------
|
||||
; Public variables in this module
|
||||
;--------------------------------------------------------
|
||||
.globl _update
|
||||
.globl _vx
|
||||
;--------------------------------------------------------
|
||||
; special function registers
|
||||
;--------------------------------------------------------
|
||||
;--------------------------------------------------------
|
||||
; ram data
|
||||
;--------------------------------------------------------
|
||||
.area _DATA
|
||||
_vx::
|
||||
.ds 1
|
||||
;--------------------------------------------------------
|
||||
; ram data
|
||||
;--------------------------------------------------------
|
||||
.area _INITIALIZED
|
||||
;--------------------------------------------------------
|
||||
; absolute external ram data
|
||||
;--------------------------------------------------------
|
||||
.area _DABS (ABS)
|
||||
;--------------------------------------------------------
|
||||
; global & static initialisations
|
||||
;--------------------------------------------------------
|
||||
.area _HOME
|
||||
.area _GSINIT
|
||||
.area _GSFINAL
|
||||
.area _GSINIT
|
||||
;--------------------------------------------------------
|
||||
; Home
|
||||
;--------------------------------------------------------
|
||||
.area _HOME
|
||||
.area _HOME
|
||||
;--------------------------------------------------------
|
||||
; code
|
||||
;--------------------------------------------------------
|
||||
.area _CODE
|
||||
;repro.c:20: void update(unsigned char n)
|
||||
; ---------------------------------
|
||||
; Function update
|
||||
; ---------------------------------
|
||||
_update::
|
||||
;repro.c:22: if (n != vx) {
|
||||
ld hl, #_vx
|
||||
sub a, (hl)
|
||||
ret Z
|
||||
;repro.c:23: vx = n;
|
||||
ld (_vx+0), a
|
||||
;repro.c:25: }
|
||||
ret
|
||||
.area _CODE
|
||||
.area _INITIALIZER
|
||||
.area _CABS (ABS)
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Minimal reproducer — SDCC z80 codegen bug.
|
||||
*
|
||||
* "global = local" right after "if (local != global)" stores a WRONG value:
|
||||
* the SUB used to evaluate the comparison clobbers register A (which still
|
||||
* holds `n`), and the store reuses that clobbered A.
|
||||
*
|
||||
* Build (stock SDCC, no extra flags, no custom toolchain):
|
||||
* sdcc -mz80 -S repro.c
|
||||
*
|
||||
* Tested with: SDCC 4.5.0 #15242 (Mac OS X x86_64), default options.
|
||||
* Also reproduces with --opt-code-speed and with --no-peep
|
||||
* (so it is a code-generator bug, not a peephole-optimizer bug).
|
||||
*
|
||||
* Expected semantics: after update(n), vx == n whenever n != vx.
|
||||
* Actual result: vx == (unsigned char)(n - old_vx).
|
||||
*/
|
||||
unsigned char vx;
|
||||
|
||||
void update(unsigned char n)
|
||||
{
|
||||
if (n != vx) {
|
||||
vx = n;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
;--------------------------------------------------------
|
||||
; File Created by SDCC : free open source ISO C Compiler
|
||||
; Version 4.5.0 #15242 (Mac OS X x86_64)
|
||||
;--------------------------------------------------------
|
||||
.module repro
|
||||
|
||||
.optsdcc -mz80 sdcccall(1)
|
||||
;--------------------------------------------------------
|
||||
; Public variables in this module
|
||||
;--------------------------------------------------------
|
||||
.globl _update
|
||||
.globl _vx
|
||||
;--------------------------------------------------------
|
||||
; special function registers
|
||||
;--------------------------------------------------------
|
||||
;--------------------------------------------------------
|
||||
; ram data
|
||||
;--------------------------------------------------------
|
||||
.area _DATA
|
||||
_vx::
|
||||
.ds 1
|
||||
;--------------------------------------------------------
|
||||
; ram data
|
||||
;--------------------------------------------------------
|
||||
.area _INITIALIZED
|
||||
;--------------------------------------------------------
|
||||
; absolute external ram data
|
||||
;--------------------------------------------------------
|
||||
.area _DABS (ABS)
|
||||
;--------------------------------------------------------
|
||||
; global & static initialisations
|
||||
;--------------------------------------------------------
|
||||
.area _HOME
|
||||
.area _GSINIT
|
||||
.area _GSFINAL
|
||||
.area _GSINIT
|
||||
;--------------------------------------------------------
|
||||
; Home
|
||||
;--------------------------------------------------------
|
||||
.area _HOME
|
||||
.area _HOME
|
||||
;--------------------------------------------------------
|
||||
; code
|
||||
;--------------------------------------------------------
|
||||
.area _CODE
|
||||
;repro.c:20: void update(unsigned char n)
|
||||
; ---------------------------------
|
||||
; Function update
|
||||
; ---------------------------------
|
||||
_update::
|
||||
;repro.c:22: if (n != vx) {
|
||||
ld iy, #_vx
|
||||
sub a, 0 (iy)
|
||||
jp NZ, 00112$
|
||||
jp 00103$
|
||||
00112$:
|
||||
;repro.c:23: vx = n;
|
||||
ld (_vx+0), a
|
||||
00103$:
|
||||
;repro.c:25: }
|
||||
ret
|
||||
.area _CODE
|
||||
.area _INITIALIZER
|
||||
.area _CABS (ABS)
|
||||
@@ -0,0 +1,57 @@
|
||||
;--------------------------------------------------------
|
||||
; File Created by SDCC : free open source ISO C Compiler
|
||||
; Version 4.5.0 #15242 (Mac OS X x86_64)
|
||||
;--------------------------------------------------------
|
||||
.module workaround
|
||||
|
||||
.optsdcc -mz80 sdcccall(1)
|
||||
;--------------------------------------------------------
|
||||
; Public variables in this module
|
||||
;--------------------------------------------------------
|
||||
.globl _update
|
||||
.globl _vx
|
||||
;--------------------------------------------------------
|
||||
; special function registers
|
||||
;--------------------------------------------------------
|
||||
;--------------------------------------------------------
|
||||
; ram data
|
||||
;--------------------------------------------------------
|
||||
.area _DATA
|
||||
_vx::
|
||||
.ds 1
|
||||
;--------------------------------------------------------
|
||||
; ram data
|
||||
;--------------------------------------------------------
|
||||
.area _INITIALIZED
|
||||
;--------------------------------------------------------
|
||||
; absolute external ram data
|
||||
;--------------------------------------------------------
|
||||
.area _DABS (ABS)
|
||||
;--------------------------------------------------------
|
||||
; global & static initialisations
|
||||
;--------------------------------------------------------
|
||||
.area _HOME
|
||||
.area _GSINIT
|
||||
.area _GSFINAL
|
||||
.area _GSINIT
|
||||
;--------------------------------------------------------
|
||||
; Home
|
||||
;--------------------------------------------------------
|
||||
.area _HOME
|
||||
.area _HOME
|
||||
;--------------------------------------------------------
|
||||
; code
|
||||
;--------------------------------------------------------
|
||||
.area _CODE
|
||||
;workaround.c:4: void update(unsigned char n)
|
||||
; ---------------------------------
|
||||
; Function update
|
||||
; ---------------------------------
|
||||
_update::
|
||||
ld (_vx+0), a
|
||||
;workaround.c:8: if (n != old) {
|
||||
;workaround.c:11: }
|
||||
ret
|
||||
.area _CODE
|
||||
.area _INITIALIZER
|
||||
.area _CABS (ABS)
|
||||
@@ -0,0 +1,11 @@
|
||||
/* Workaround: store BEFORE the comparison so the SUB that evaluates the
|
||||
* comparison no longer sits on the store path. Generates correct code. */
|
||||
unsigned char vx;
|
||||
void update(unsigned char n)
|
||||
{
|
||||
unsigned char old = vx;
|
||||
vx = n; /* store first, with A still holding n */
|
||||
if (n != old) {
|
||||
/* redraw / side effect */
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
# MDView: описание работы и тестовые паттерны
|
||||
## Назначение
|
||||
`mdview` — консольный просмотрщик Markdown для текстового режима 80x32.
|
||||
Программа загружает `.MD`-файл, индексирует его на экранные сегменты и рендерит содержимое с учётом переносов, стилей и специальных блоков.
|
||||
|
||||
## Как работает программа
|
||||
### 1) Загрузка файла
|
||||
- На старте вызывается `load_file(path)`.
|
||||
- Если путь не передан, используется `SAMPLE.MD`.
|
||||
- При ошибках загрузки программа показывает причину и завершает работу после нажатия клавиши.
|
||||
|
||||
### 2) Индексация (`index_lines`)
|
||||
- Файл разбирается в массив сегментов для быстрого рендера.
|
||||
- Поддерживаются ключевые типы строк:
|
||||
- заголовки `#..####`,
|
||||
- маркированные/нумерованные списки,
|
||||
- цитаты `>`,
|
||||
- горизонтальные разделители,
|
||||
- fenced code-блоки,
|
||||
- таблицы (nowrap-режим),
|
||||
- обычные параграфы.
|
||||
- Для list/quote/plain используется общая логика сканирования с режимами склейки строк.
|
||||
|
||||
### 3) Рендер (`render_line`, `render_viewport`)
|
||||
- Для каждой видимой строки рисуется префикс (маркер списка, цитаты и т.д.) и текст.
|
||||
- Поддерживаются inline-стили:
|
||||
- `**жирный**`,
|
||||
- `*курсив*`,
|
||||
- `_подчёркнутый_`,
|
||||
- `~~зачёркнутый~~`,
|
||||
- `` `code` ``.
|
||||
- Табы расширяются до фиксированного шага.
|
||||
- Для длинных nowrap-строк применяется горизонтальный сдвиг и индикаторы `<`/`>`.
|
||||
|
||||
### 4) Навигация
|
||||
- `Up/Down` — прокрутка на 1 строку.
|
||||
- `PgUp/PgDn` — прокрутка на экран.
|
||||
- `Home/End` — начало/конец документа.
|
||||
- `Left/Right` — горизонтальная прокрутка nowrap-контента.
|
||||
- `Esc` или `F10` — выход.
|
||||
|
||||
## Сборка и запуск
|
||||
```bash
|
||||
make -C examples/mdview
|
||||
```
|
||||
|
||||
Запуск на целевой системе:
|
||||
```bash
|
||||
mdview.exe <путь_к_файлу.md>
|
||||
```
|
||||
|
||||
Если путь не указан, открывается `SAMPLE.MD`.
|
||||
|
||||
## Тестовые паттерны
|
||||
Ниже набор паттернов для ручной регрессии.
|
||||
|
||||
### P01 — Базовый smoke-тест открытия
|
||||
Вход:
|
||||
- валидный markdown-файл среднего размера.
|
||||
|
||||
Ожидается:
|
||||
- файл открывается без ошибок;
|
||||
- статус-бар и меню отображаются корректно;
|
||||
- прокрутка работает.
|
||||
|
||||
### P02 — Ошибка открытия файла
|
||||
Вход:
|
||||
- несуществующий путь к файлу.
|
||||
|
||||
Ожидается:
|
||||
- сообщение `mdview: cannot load file`;
|
||||
- корректный код причины (`open failed` и т.п.);
|
||||
- ожидание клавиши перед выходом.
|
||||
|
||||
### P03 — Пустой файл
|
||||
Вход:
|
||||
- пустой `.md`.
|
||||
|
||||
Ожидается:
|
||||
- сообщение `mdview: empty file`;
|
||||
- корректное завершение после нажатия клавиши.
|
||||
|
||||
### P04 — Обычный параграф и переносы
|
||||
Шаблон:
|
||||
```md
|
||||
Это длинный абзац для проверки переноса строк в обычном тексте.
|
||||
Вторая строка должна мягко склеиться с первой.
|
||||
```
|
||||
|
||||
Ожидается:
|
||||
- строки склеиваются как единый параграф;
|
||||
- переносы происходят по ширине экрана без потери символов.
|
||||
|
||||
### P05 — Hard break в параграфе
|
||||
Шаблон:
|
||||
```md
|
||||
Первая строка с двумя пробелами в конце.
|
||||
Вторая строка после hard break.
|
||||
```
|
||||
|
||||
Ожидается:
|
||||
- между строками сохраняется принудительный разрыв;
|
||||
- inline-стиль не ломается.
|
||||
|
||||
### P06 — Многострочный список с lazy continuation
|
||||
Шаблон:
|
||||
```md
|
||||
- Пункт списка, который продолжается
|
||||
на следующей строке с отступом.
|
||||
```
|
||||
|
||||
Ожидается:
|
||||
- continuation-строка склеивается с пунктом через один пробел;
|
||||
- лишние ведущие отступы continuation не попадают в итоговый текст;
|
||||
- переносы не ломают маркер списка.
|
||||
|
||||
### P07 — Многострочная цитата
|
||||
Шаблон:
|
||||
```md
|
||||
> Первая строка цитаты
|
||||
> Вторая строка цитаты
|
||||
> Третья строка цитаты
|
||||
```
|
||||
|
||||
Ожидается:
|
||||
- для продолжений корректно повторяется quote-префикс;
|
||||
- сырой символ `>` из исходника не «просачивается» в середину текста.
|
||||
|
||||
### P08 — Пустая quoted-строка как разделитель
|
||||
Шаблон:
|
||||
```md
|
||||
> Первый параграф цитаты
|
||||
>
|
||||
> Второй параграф цитаты
|
||||
```
|
||||
|
||||
Ожидается:
|
||||
- пустая quoted-строка разделяет два параграфа;
|
||||
- рендер не объединяет их в один поток.
|
||||
|
||||
### P09 — Таблица в nowrap-режиме
|
||||
Шаблон:
|
||||
```md
|
||||
| col1 | col2 | col3 |
|
||||
|------|------|------|
|
||||
| очень длинное значение | очень длинное значение | очень длинное значение |
|
||||
```
|
||||
|
||||
Ожидается:
|
||||
- строка таблицы не переносится по словам;
|
||||
- работает горизонтальный сдвиг `Left/Right`;
|
||||
- индикаторы `<`/`>` показывают скрытый контент.
|
||||
|
||||
### P10 — Fenced code-block
|
||||
Шаблон:
|
||||
~~~md
|
||||
```text
|
||||
**это не жирный**
|
||||
_это не подчёркнутый_
|
||||
```
|
||||
~~~
|
||||
|
||||
Ожидается:
|
||||
- внутри code-блока inline-разметка не применяется;
|
||||
- текст отображается как литерал.
|
||||
|
||||
### P11 — Inline-стили на обычном тексте
|
||||
Шаблон:
|
||||
```md
|
||||
Текст с **жирным**, *курсивом*, _подчёркнутым_, ~~зачёркнутым~~ и `code`.
|
||||
```
|
||||
|
||||
Ожидается:
|
||||
- все перечисленные стили рендерятся корректно;
|
||||
- литеральные случаи вроде `2 * 3` не превращаются в стиль.
|
||||
|
||||
### P12 — Статус-бар и границы прокрутки
|
||||
Вход:
|
||||
- документ на несколько экранов.
|
||||
|
||||
Ожидается:
|
||||
- корректные `L x-y / total` и `%`;
|
||||
- `Home/End` приводят к ожидаемым позициям;
|
||||
- при выходе за границы документа top-line корректно clamp-ится.
|
||||
|
||||
## Рекомендация по регрессии
|
||||
После любых изменений в логике индексации/рендера прогонять минимум:
|
||||
- `P04`, `P05`, `P06`, `P07`, `P09`, `P10`, `P11`, `P12`.
|
||||
@@ -1,10 +1,44 @@
|
||||
# Build mdview.exe — Markdown viewer for Sprinter.
|
||||
#
|
||||
# small memory mode: code in W1, data/stack/heap in W2 (32 KB total).
|
||||
# W3 stays free for the file buffer (EMM-mapped) and Phase 3+ render cache.
|
||||
# W3 stays free for the file buffer (EMM-mapped).
|
||||
|
||||
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||
EXAMPLE := mdview
|
||||
MEMORY := small
|
||||
EXTRA_DATA := SAMPLE.MD PLAN_866.md SAMPLEF.MD
|
||||
|
||||
include $(PROJ_ROOT)/app.mk
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Образ дискеты: только mdview.exe + README.MD (перекодированный
|
||||
# из UTF-8 в CP866 — рабочую кодировку Sprinter).
|
||||
#
|
||||
# README.MD хранится в репозитории в UTF-8; iconv -c конвертирует
|
||||
# его в CP866, отбрасывая символы без аналога в целевой кодировке.
|
||||
# Результат кладётся в .disk_tmp/README.MD, чтобы make_disk.py
|
||||
# использовал правильное имя файла на диске.
|
||||
#
|
||||
# iconv -c возвращает ненулевой код, если хоть один символ отброшен
|
||||
# (даже с -c) — это ОЖИДАЕМО при потере символов без аналога в CP866,
|
||||
# не ошибка конвертации; вывод при этом всё равно корректно записан.
|
||||
# Поэтому код возврата iconv игнорируется (|| true).
|
||||
# ------------------------------------------------------------------
|
||||
DISK_TMP := .disk_tmp
|
||||
README_DISK := $(DISK_TMP)/README.MD
|
||||
|
||||
$(DISK_TMP):
|
||||
mkdir -p $@
|
||||
|
||||
$(README_DISK): README.MD | $(DISK_TMP)
|
||||
iconv -c -f UTF-8 -t CP866 README.MD > $@ || true
|
||||
|
||||
floppy: $(EXAMPLE).exe $(README_DISK)
|
||||
python3 $(MAKE_DISK) $(FLOPPY_IMG) $(EXAMPLE).exe $(README_DISK)
|
||||
@echo
|
||||
@echo "Floppy ready: $(FLOPPY_IMG)"
|
||||
@echo "Run: cd $(MAME_DIR) && ./run_mame.sh"
|
||||
|
||||
clean:
|
||||
rm -rf .sprinter-cc-* $(EXAMPLE).exe $(DISK_TMP)
|
||||
|
||||
.PHONY: all clean floppy run
|
||||
|
||||
@@ -1,570 +0,0 @@
|
||||
# План: текстовый Markdown viewer для Sprinter (`examples/mdview`)
|
||||
|
||||
## Context
|
||||
|
||||
Тестовая крупная задача — проверить нашу libc на нетривиальном interactive-приложении (полноэкранный UI, файловый I/O, парсер). Параллельно даст хороший showcase платформы и поможет вытащить недоделки в conio/io. Конечная цель: viewer для `.md` файлов с подсветкой синтаксиса, навигацией по тексту и постраничным скроллингом.
|
||||
|
||||
**Ограничения v1 (зафиксированы пользователем):**
|
||||
- POSIX file API (open/read/lseek/close); FILE pointer / fread / fgets — не использовать
|
||||
- Подсветка через цвет: **размер заголовка** → цвет шрифта; **bold/italic** → цвет фона (моноширинный фонт без жирного/курсивного начертания)
|
||||
|
||||
**Расширения, реализованные после плана:**
|
||||
- Файлы до 128 KB (1–8 EMM-страниц, lazy map в W3 через `fb()/map_page()`) — изначально было в v2
|
||||
- Анимированный spinner и предварительная отрисовка UI при старте — UX
|
||||
- Inline emphasis: `_`/`*`/`**` подчиняются XOR flanking-правилу (whitespace ровно с одной стороны), `COLOR_YELLOW` и `2 * 3` остаются литералами
|
||||
|
||||
---
|
||||
|
||||
## Текущий статус (2026-06-05)
|
||||
|
||||
| Phase | Статус | Комментарий |
|
||||
|---|---|---|
|
||||
| 1 Plain text + nav | ✓ | загрузка, индексация, status/menu, ↑↓/PgUp/PgDn/Home/End/F1/F10/Esc |
|
||||
| 2 Headers + HR | ✓ | H1..H4, `---`/`***`/`___` (с ≥3 marker'ов) |
|
||||
| 3 Inline emphasis | ✓ | `**` / `*` / `_` / `` ` ``; XOR flanking (см. выше) |
|
||||
| 4 Lists / quote / fenced code | ✓ | `- / * / +`, `N. / N)`, `> `, ``` ``` ```; light nested lists |
|
||||
| 4-tables | ✗ | таблицы отложены вместе с Phase 6 |
|
||||
| 5 Wrap / Unwrap (F2) | ✓ | wrap-by-default; soft wrap; F2 переключает; hpan ←/→ в truncate-режиме |
|
||||
| 6 Полный layout таблиц | ✗ | deferred |
|
||||
| 7 Links + search | ✗ | deferred |
|
||||
| 8 F8 Raw toggle | ✗ | deferred |
|
||||
| Cache рендеренных строк | ✗ | не нужно по скорости |
|
||||
|
||||
**UX-поправки (отдельно от phase-плана, 2026-06-05):**
|
||||
- UI (menu + title bar) отрисовывается ДО `load_file`/`index_lines` — пользователь сразу видит интерфейс, а не чёрный экран
|
||||
- Title bar: `MDVIEW <spinner> <filename>` (3 пробела между MDVIEW и filename, slot спиннера — col 8)
|
||||
- Spinner крутится во время `load_file` (по странице) и `index_lines` (раз в 32 логических строки), включая `toggle_wrap`
|
||||
|
||||
---
|
||||
|
||||
## Архитектура
|
||||
|
||||
### Раскладка экрана (80×32, текст mode 0x03)
|
||||
|
||||
```
|
||||
Row 0: ┃ MDVIEW │ mdview.md │ L 1-30 / 142 │ 21% ┃ ← status bar (BG=blue, FG=white)
|
||||
▲ ▲
|
||||
│ └── filename @ col 10
|
||||
└────────── spinner slot @ col 8 (anim. while busy)
|
||||
Row 1: ┃ ┃
|
||||
... ┃ document viewport (30 rows) ┃
|
||||
Row 30: ┃ ┃
|
||||
Row 31: ┃ F1 Help F2 Wrap F10 Exit ┃ ← menu bar (BG=blue, FG=cyan)
|
||||
```
|
||||
|
||||
- Viewport = 30 строк × 80 столбцов.
|
||||
- Status / menu рендерятся через `wrchar()` (без авто-скролла), viewport — через `LOCATE` + посимвольный `wrchar()` (тоже без авто-скролла, ставит и char и attr за один call).
|
||||
|
||||
### Память
|
||||
|
||||
Memory mode: **`small`** — DSS отводит под наш образ **два банка (W1 + W2)** = 32 KB суммарно (CODE в W1, DATA + STACK + HEAP в W2). Этого должно хватить чтобы НЕ заводить `__banked` функции. W3 остаётся полностью свободным для маппинга больших буферов:
|
||||
|
||||
```
|
||||
W0 (0x0000-0x3FFF): ESTEX (system, untouchable)
|
||||
W1 (0x4000-0x7FFF): CODE (small mode page 1)
|
||||
W2 (0x8000-0xBFFF): DATA + STACK + HEAP (small mode page 2)
|
||||
W3 (0xC000-0xFFFF): paged window — лениво переключается между EMM-страницами
|
||||
файла (до 8 страниц = 128 KB). `cur_page` кэширует
|
||||
текущую mapping, `fb(p)` маппит нужную страницу при
|
||||
первом обращении.
|
||||
```
|
||||
|
||||
Почему small + W3:
|
||||
- 32 KB на код+данные с большим запасом → нет банкинга
|
||||
- W3 — стандартный paged window, под него у нас уже есть `bank_io_w3` API
|
||||
- Файлы до 128 KB поддерживаются нативно: `mem_alloc_pages(pages_needed)` под весь файл; `map_page()` через `sprinter_page_w3()`; `fb(p)` — единая точка доступа из индексатора и рендера.
|
||||
|
||||
Статики (в W2):
|
||||
- `line_offset[MAX_LINES]` — uint16_t смещение каждой строки в файловой странице (4 KB на 2048 строк)
|
||||
- `cache_tag[CACHE_N]` — uint16_t тег слота (200 байт на 100 слотов, появляется в Phase 3)
|
||||
- `filename[64]`, `top_line`, `total_lines`, `file_size`, `file_blk`, `cache_blk` — единицы байт
|
||||
|
||||
**FILE_BUF**: `((char*)0xC000)` — фиксированная адресация в W3 после маппинга нужной страницы.
|
||||
|
||||
### Поток данных
|
||||
|
||||
```
|
||||
main → open() → read() chunks 1KB → write to W1 (mapped EMM page) → close()
|
||||
→ index_lines() (одно сканирование, заполняет line_offset[])
|
||||
→ render_viewport() + main loop { getkey(); handle(); render_status(); render_viewport() }
|
||||
```
|
||||
|
||||
**Тонкость с `read()`**: ESTEX READ записывает по dst-указателю в адресном пространстве вызывающего. Поскольку мы маппим EMM-страницу в W3 (0xC000) ДО вызова read(), указатель 0xC000+offset валиден. Если выяснится что BIOS трогает W3 во время read (графический видеобуфер по умолчанию в W3 при графических режимах, но в текстовом — должен быть свободен) — fallback: читать в 1 KB буфер в W2 и копировать в W3 через `bank_write_w3()`.
|
||||
|
||||
---
|
||||
|
||||
## Реализация — поэтапная
|
||||
|
||||
### Phase 1 — Plain text viewer (MVP) ✓
|
||||
|
||||
**Что работает:**
|
||||
- Загрузка файла (`open/read/close`) в W1-страницу
|
||||
- Индексация строк (LF / CRLF разделители)
|
||||
- Status bar: имя файла, L N-M / Total, процент скроллинга
|
||||
- Menu bar: `F1 Help F10 Exit`
|
||||
- Навигация: ↑/↓ (1 строка), PgUp/PgDn (30 строк), Home/End (начало/конец), Esc/F10 (выход), F1 (help screen)
|
||||
- Обрезка строк длиннее 80 символов (без word-wrap)
|
||||
- Цвета: текст белый на чёрном; status/menu — белый на синем
|
||||
|
||||
**Критичные файлы:**
|
||||
- `examples/mdview/mdview.c` — main, key loop, rendering, indexing (one-file MVP)
|
||||
- `examples/mdview/Makefile`
|
||||
- `examples/mdview/SAMPLE.MD` — тестовый markdown файл
|
||||
|
||||
### Phase 2 — Headers и горизонтальная линия ✓
|
||||
|
||||
**MD фичи:**
|
||||
- `# H1` → ярко-жёлтый (COLOR_YELLOW = 14) на чёрном
|
||||
- `## H2` → ярко-голубой (COLOR_LBLUE = 11)
|
||||
- `### H3` → ярко-зелёный (COLOR_LGREEN = 10)
|
||||
- `#### H4+` → серый (COLOR_GREY = 8)
|
||||
- `---` / `***` на отдельной строке → линия 0xC4 (горизонтальная рамка ASCII) во всю ширину
|
||||
|
||||
### Phase 3 — Inline emphasis ✓
|
||||
|
||||
**Парсер inline (per-line, runs в одну строку):**
|
||||
- `**bold**` → ATTR_TEXT_BOLD
|
||||
- `*italic*` → ATTR_TEXT_ITALIC
|
||||
- `_underscore_` → ATTR_TEXT_UNDERSORE
|
||||
- `` `code` `` → ATTR_TEXT_CODE
|
||||
- Маркеры `**`/`*`/`_`/`` ` `` НЕ рендерятся (съедаются)
|
||||
|
||||
State machine: один активный стиль одновременно (без вложенности); конфликтующий маркер при чужом активном стиле всё равно консьюмится (zero-width) для синхронизации ширины с index_lines. Состояние сбрасывается на каждой строке.
|
||||
|
||||
**Flanking-правило (CommonMark intraword, реализовано после изначального плана):**
|
||||
- `*` / `**` / `_` считаются markdown-маркером только если whitespace/EOL
|
||||
ровно с ОДНОЙ стороны (XOR).
|
||||
- Случаи "оба whitespace" (`2 * 3`, `2 ** 3`) → литералы (арифметика).
|
||||
- Случаи "ни одного whitespace" (`COLOR_YELLOW`, `FILE*/fread`, `foo*bar*baz`)
|
||||
→ литералы (intraword).
|
||||
- Backtick (`` ` ``) flanking НЕ требует — `` `code` `` работает без пробелов.
|
||||
- Правило применено симметрично в 4 местах (`index_lines`, cont-render,
|
||||
основной inline-парсер, truncation peek), иначе wrap-индексатор и
|
||||
рендер разъедутся по ширине.
|
||||
|
||||
> **Кэш отформатированных строк** — отложен в самый конец, см. "Phase ∞: оптимизации".
|
||||
> Скорости текущего наивного рендера хватает на 80×30 = 2400 wrchar / кадр; PgUp/PgDn визуально мгновенен.
|
||||
|
||||
### Phase 4 — Block elements ✓ (без таблиц)
|
||||
|
||||
- Маркированные списки: `- foo`, `* foo`, `+ foo` → префикс `•` (0x07) + пробел; цвет маркера ярче основного
|
||||
- Нумерованные списки: `1. foo`, `2. foo` → как есть (число оставляем)
|
||||
- Blockquote: строки с `> ` → префикс `│` (0xB3) серого цвета, остальной текст слегка приглушённый
|
||||
- Fenced code blocks: `` ``` `` открывает/закрывает блок; все строки между — bg=серый, моноширинно (без inline-парсинга)
|
||||
- Indented code blocks (4+ пробелов): аналогично fenced, но без явного маркера
|
||||
|
||||
**Light nested lists (v1 — реализовано):**
|
||||
- `classify_line()` пропускает leading spaces перед ulist/olist/quote маркером,
|
||||
возвращает `content_off` после маркера → `content_off - p_start` = indent + marker
|
||||
бит в visible col.
|
||||
- `render_line()` рисует leading-spaces в `ATTR_TEXT`, потом маркер на сдвинутой
|
||||
позиции (col = indent). Marker всё ещё фиксирован при горизонтальном pan'е.
|
||||
- HR / header / fence delim остаются строго col-0 (CommonMark разрешает до 3
|
||||
ведущих пробелов для них — упростили).
|
||||
- Tab-indent → не распознаётся как nesting (только spaces).
|
||||
|
||||
**Phase 4-full — полная поддержка вложенности (deferred):**
|
||||
- **Tab-indent**: считать tab = 4 пробела для определения уровня.
|
||||
- **Quote nesting** (`> > foo`): каждый `>` подряд = +1 уровень, каждый рисуется
|
||||
отдельным `│` в `ATTR_QUOTE_MARKER` (визуальная "лестница" слева).
|
||||
- **Hanging indent в wrap-continuation**: когда `- some very long bullet text
|
||||
that wraps...` — continuation seg должен начинаться от content-col (после
|
||||
маркера), а не от col 0. Сейчас continuation идёт от col 0 (v1 simplification).
|
||||
Требует хранить `marker_width` per логическая строка (8 бит) или re-classify
|
||||
first seg при рендере continuation.
|
||||
- **Lazy continuation**: строки без маркера, но с правильным indent под
|
||||
предыдущим bullet'ом, должны считаться продолжением того bullet'а
|
||||
(визуально — общий attr).
|
||||
- **Strict CommonMark indent rules**: вложенный пункт должен быть на indent
|
||||
≥ content_col родителя, иначе считается breakout. Нужен мини-stack
|
||||
активных списков при индексации.
|
||||
|
||||
### Phase 5 — Wrap / Unwrap длинных строк ✓
|
||||
|
||||
Дефолт: **wrap on**. F2 переключает; в меню-баре подпись отражает действие
|
||||
("Unwrap" когда wrap включён, "Wrap" когда выключен). Во время реиндексации
|
||||
крутится спиннер на title bar.
|
||||
|
||||
**v1 — реализовано:**
|
||||
- Один массив `line_offset[2048]` хранит ВИДИМЫЕ сегменты (а не логические
|
||||
строки); биты 0..13 — байтовое смещение, бит 15 — CONT-флаг continuation.
|
||||
- Wrap-режим: soft wrap на последнем пробеле ≤ 80; hard fallback если
|
||||
пробела нет.
|
||||
- Маркеры эмфазиса (`**`/`*`/`_`/`` ` ``) и header-префиксы (`#`/`##`/...) не
|
||||
учитываются в visible-col при поиске точки переноса.
|
||||
- "Специальные" логические строки не wrap'аются вообще (одна seg-запись на
|
||||
логическую строку): fence delim, table row (header/separator/body), HR.
|
||||
- F2 toggle сохраняет визуальную позицию через `top_offset` в FILE_BUF.
|
||||
- Bitmaps (`in_code`/`in_table`/`is_tab_hdr`) перестраиваются вместе с
|
||||
сегментами, индексируются seg-индексом, биты ставятся только на первом
|
||||
seg'е логической строки.
|
||||
- Continuation-сегменты рендерятся в стиле "v1: плоско" — plain text,
|
||||
никаких markdown-классификаций; padding до конца строки `ATTR_TEXT`.
|
||||
|
||||
**v1.5 — отложено для полной картины wrap:**
|
||||
- **Hanging indent**: continuation от ulist/olist/quote должен выравниваться
|
||||
под content, а не от col 0. Требует хранить marker_width per логическая
|
||||
строка ИЛИ re-classify первого seg'а при рендере continuation.
|
||||
- **Наследование base_attr**: continuation от header'а должен сохранять
|
||||
цвет; continuation от code body — фон ATTR_TEXT_CODE. Требует хранить
|
||||
1 байт `base_attr` per seg ИЛИ lookup первого seg'а.
|
||||
- **Inline emphasis через границу**: эмфазис, открытый в первом seg'е и не
|
||||
закрытый, должен продолжаться во втором. Требует хранить emph state per
|
||||
seg (3 бита).
|
||||
- Compact way: добавить параллельный массив `seg_meta[MAX_SEGS]` по 1 байту
|
||||
— пакует marker_width (4 бита) + emph_state (3 бита) + base_attr_idx
|
||||
(4 бита из таблицы → нужен 2-байтовый seg_meta).
|
||||
- **Hpan для длинных строк**: если wrap выключен, добавить ←/→ для
|
||||
горизонтального скролла >80 cols. Общий механизм с tables (Phase 6).
|
||||
**РЕАЛИЗОВАНО** (light) — `viewport_x` + полный re-render на каждое ←/→.
|
||||
- **Ускорение hpan через ESTEX WINCOPY/WINREST** (deferred): сейчас pan
|
||||
делает полный `render_viewport()` = 30 строк × 80 wrchar. Можно
|
||||
скопировать существующее содержимое viewport'а на N cols влево/вправо
|
||||
через win-copy, потом рендерить только узкую полосу справа/слева
|
||||
(HPAN_STEP cols × 30 rows ≈ 240 wrchar вместо 2400). ESTEX SCROLL
|
||||
горизонталь не поддерживает — нужна именно WINCOPY-операция или
|
||||
rdchar/wrchar loop. Активировать когда ощутится тормоз; сейчас на
|
||||
типовом markdown'е не заметно.
|
||||
|
||||
**v2 — отдельная фича, мимо wrap:**
|
||||
- Toggle подсветки целиком (F3?)
|
||||
- Search по тексту (Ctrl+F / F4)
|
||||
- Links `[text](url)` → синий подчёркнутый text, url прячется
|
||||
- Images `` → `[IMG: alt]`
|
||||
|
||||
### Phase 7 — Links и поиск (post-v1)
|
||||
|
||||
- `[text](url)` → отрисовать только `text` с ярко-синим FG (визуально подчёркнутое)
|
||||
- `` → `[IMG: alt]` в скобках
|
||||
- Search по тексту (F3 / Ctrl+F): инкрементальный, подсветка совпадений
|
||||
|
||||
### Phase 8 — F8 Raw / Render toggle
|
||||
|
||||
Переключатель режима отображения: при включённом Raw показывается исходный
|
||||
текст файла как есть — все markdown-маркеры (`#`, `**`, `_`, `` ` ``, `|`,
|
||||
`>`, `-`, etc.) рендерятся литералами с `ATTR_TEXT`, без классификации.
|
||||
Полезно когда:
|
||||
- нужно увидеть точную разметку (отлаживание .md, скриншоты, копирование)
|
||||
- markdown-классификатор ошибся и хочется увидеть оригинал
|
||||
- хочется быстро сравнить "до/после" рендера
|
||||
|
||||
**Поведение:**
|
||||
- F8 переключает `render_mode` (1=render, 0=raw); меню показывает обратное
|
||||
действие ("Raw" когда сейчас render, "Render" когда сейчас raw) — той же
|
||||
логикой что F2/Wrap/Unwrap.
|
||||
- В Raw режиме: `render_line()` идёт по короткому пути — никакого
|
||||
`classify_line`, `is_fence_delim`, `is_code_body`, inline-эмфазиса; просто
|
||||
байтовый дамп FILE_BUF от seg-offset до next-seg/EOL с tab-expansion и
|
||||
ATTR_TEXT.
|
||||
- Раздельно от F2: оба режима независимы (можно Raw+Wrap, Raw+Truncate,
|
||||
Render+Wrap, Render+Truncate). Wrap-логика в `index_lines` работает в
|
||||
обоих случаях одинаково (опирается на визуальные колонки независимо от
|
||||
раскраски).
|
||||
- Статус-бар: добавить индикатор `[R]` / `[V]` (Raw / View) или текстом
|
||||
`RAW` рядом с именем файла.
|
||||
|
||||
**Минимальная реализация:**
|
||||
- Один новый static `uint8_t render_mode = 1;`
|
||||
- В `render_line()`: на самом верху `if (!render_mode) { … raw render … return; }`
|
||||
- В `render_menu()`: добавить ярлык F8 рядом с F2.
|
||||
- В главном цикле: `case KEY_F8: toggle_render(); break;`
|
||||
- `toggle_render()` отличается от `toggle_wrap()` тем, что НЕ перестраивает
|
||||
`line_offset[]` (wrap-сегментация не меняется), только перерендерит экран.
|
||||
|
||||
---
|
||||
|
||||
## API-новинки в libc (минимальные)
|
||||
|
||||
### `getkey()` — extended key reader (в libc)
|
||||
|
||||
Текущий `getch()` теряет scan code расширенных клавиш (возвращает только E=ASCII). Добавляем **сразу в `libc/conio/conio.c`** новую функцию рядом с `getch()`:
|
||||
|
||||
```c
|
||||
// Returns scan in high byte, ASCII in low byte.
|
||||
// Extended keys (arrows, F-keys, PgUp/PgDn, Home/End): ASCII=0, scan code в high byte.
|
||||
// Plain keys: ASCII в low byte; high byte содержит positional scan (бит 7 = Ctrl/Alt/Shift modifier).
|
||||
uint16_t getkey(void) __naked {
|
||||
__asm
|
||||
ld c, #0x30 ; ESTEX WAITKEY
|
||||
rst #0x10 ; A=ASCII, D=scan, E=ASCII
|
||||
ld e, a ; ensure E=ASCII even if E clobbered
|
||||
ret ; SDCC __sdcccall(1): возврат uint16_t в DE (D=scan, E=ASCII)
|
||||
__endasm;
|
||||
}
|
||||
```
|
||||
|
||||
Дополнительно — **в `libc/include/conio.h`** прописать прототип и константы scan-кодов:
|
||||
|
||||
```c
|
||||
uint16_t getkey(void);
|
||||
|
||||
/* Scan codes for getkey() high byte when ASCII=0 (extended keys). */
|
||||
#define KEY_F1 0x0E
|
||||
#define KEY_F2 0x0F
|
||||
#define KEY_F3 0x10
|
||||
#define KEY_F4 0x11
|
||||
#define KEY_F5 0x12
|
||||
#define KEY_F6 0x13
|
||||
#define KEY_F7 0x14
|
||||
#define KEY_F8 0x15
|
||||
#define KEY_F9 0x16
|
||||
#define KEY_F10 0x17
|
||||
#define KEY_F11 0x18
|
||||
#define KEY_F12 0x19
|
||||
#define KEY_END 0x24
|
||||
#define KEY_DOWN 0x25
|
||||
#define KEY_PGDN 0x26
|
||||
#define KEY_LEFT 0x27
|
||||
#define KEY_RIGHT 0x29
|
||||
#define KEY_HOME 0x2A
|
||||
#define KEY_UP 0x2B
|
||||
#define KEY_PGUP 0x2C
|
||||
#define KEY_INS 0x23
|
||||
#define KEY_DEL 0x22
|
||||
```
|
||||
|
||||
**Скан-коды (из docs/converted/ProgrammerManual.txt:2143-2323):**
|
||||
| Клавиша | scan | Клавиша | scan |
|
||||
|---|---|---|---|
|
||||
| F1 | 0x0E | Up | 0x2B |
|
||||
| F10 | 0x17 | Down | 0x25 |
|
||||
| F11 | 0x18 | Left | 0x27 |
|
||||
| F12 | 0x19 | Right | 0x29 |
|
||||
| | | PgUp | 0x2C |
|
||||
| | | PgDn | 0x26 |
|
||||
| | | Home | 0x2A |
|
||||
| | | End | 0x24 |
|
||||
|
||||
### Что **переиспользуем** из существующей libc
|
||||
|
||||
- `open/read/lseek/close` — `libc/io/{open,read,lseek}.c` (POSIX wrappers)
|
||||
- `mem_alloc_pages/mem_free_block/mem_get_page` — `libc/mem/mem_alloc.c`
|
||||
- `sprinter_page_w3()` — inline `__sfr` write в `libc/include/sprinter.h:113`
|
||||
- `bank_read_w3/bank_write_w3` — `libc/mem/bank_io_w3.c` (для fallback или v2 multi-page)
|
||||
- `wrchar(x, y, ch, attr)` — `libc/conio/conio.c:476` (без auto-scroll, идеально для viewport)
|
||||
- `clrscr_attr(attr)` — `libc/conio/conio.c:395`
|
||||
- `gotoxy/wherex/wherey` — `libc/conio/conio.c:412-462` (если нужно)
|
||||
- `kbhit()` — `libc/conio/conio.c:22` (для non-blocking опроса, опционально)
|
||||
- `dec16/dec8` — `libc/stdio/dec_print.c` (для status bar: текущая строка / total / %)
|
||||
- `COLOR(fg, bg)` макрос — `libc/include/conio.h:152`
|
||||
- Цветовые константы `COLOR_*` — `libc/include/conio.h:145`
|
||||
- `strlen/memcpy/memset` — z80.lib (НЕ переписывать)
|
||||
|
||||
---
|
||||
|
||||
## Структура исходников
|
||||
|
||||
```
|
||||
examples/mdview/
|
||||
├── Makefile # стандартный pattern (см. examples/cat/Makefile)
|
||||
├── mdview.c # Phase 1: всё в одном файле (main, keys, indexing, render)
|
||||
├── SAMPLE.MD # тестовый markdown
|
||||
└── README.md # описание и controls
|
||||
```
|
||||
|
||||
После Phase 3 раскидать по модулям (если суммарный размер > ~6KB):
|
||||
```
|
||||
mdview.c — main loop, status/menu bars, key dispatch
|
||||
mdrender.c — line rendering with MD inline parser
|
||||
mdindex.c — file load + line indexing
|
||||
```
|
||||
|
||||
### Сборка
|
||||
|
||||
```makefile
|
||||
PROJ ?= ../..
|
||||
SPRINTER_CC := $(PROJ)/bin/sprinter-cc
|
||||
mdview.exe: mdview.c
|
||||
$(SPRINTER_CC) --memory small -o $@ mdview.c
|
||||
```
|
||||
|
||||
`--memory small`: код+данные в W2 (DSS даёт нужное число страниц); файл — отдельная EMM-страница в W3.
|
||||
|
||||
---
|
||||
|
||||
## Структура mdview.c (Phase 1, эскиз)
|
||||
|
||||
```c
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <conio.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sprinter.h>
|
||||
#include <sprinter_mem.h>
|
||||
|
||||
#define VIEW_TOP 1
|
||||
#define VIEW_BOT 30 // inclusive
|
||||
#define VIEW_H 30
|
||||
#define SCREEN_W 80
|
||||
#define MAX_LINES 2048
|
||||
#define FILE_BUF ((char*)0xC000) /* W3 — EMM page mapped here */
|
||||
#define TAB_STOP 4
|
||||
|
||||
#define ATTR_TEXT COLOR(COLOR_WHITE, COLOR_BLACK)
|
||||
#define ATTR_BAR COLOR(COLOR_WHITE, COLOR_BLUE)
|
||||
#define ATTR_MENU_K COLOR(COLOR_YELLOW, COLOR_BLUE)
|
||||
#define ATTR_MENU_T COLOR(COLOR_WHITE, COLOR_BLUE)
|
||||
|
||||
static uint16_t line_off[MAX_LINES];
|
||||
static uint16_t n_lines;
|
||||
static uint16_t top_line;
|
||||
static uint16_t file_size;
|
||||
static uint8_t file_blk;
|
||||
static char filename[64];
|
||||
|
||||
static int load_file(const char *path); // open, alloc EMM page, map W3, read, close
|
||||
static void index_lines(void); // scan FILE_BUF, fill line_off[]
|
||||
static void render_status(void); // row 0
|
||||
static void render_menu(void); // row 31
|
||||
static void render_line(uint16_t idx, uint8_t row); // one line @ row
|
||||
static void render_viewport(void); // VIEW_H lines starting from top_line
|
||||
static void scroll_up(uint16_t n);
|
||||
static void scroll_down(uint16_t n);
|
||||
static void help_screen(void); // F1
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc < 2) { puts("Usage: mdview <file.md>"); return 1; }
|
||||
if (load_file(argv[1]) < 0) { puts("load error"); return 1; }
|
||||
index_lines();
|
||||
clrscr_attr(ATTR_TEXT);
|
||||
render_menu();
|
||||
render_status();
|
||||
render_viewport();
|
||||
for (;;) {
|
||||
uint16_t k = getkey();
|
||||
uint8_t ascii = k & 0xFF;
|
||||
uint8_t scan = (k >> 8) & 0x7F; // strip mod bit
|
||||
if (ascii) {
|
||||
if (ascii == 0x1B) break; // Esc → exit
|
||||
continue;
|
||||
}
|
||||
switch (scan) {
|
||||
case KEY_F10: goto exit;
|
||||
case KEY_F1: help_screen(); break;
|
||||
case KEY_UP: scroll_up(1); break;
|
||||
case KEY_DOWN: scroll_down(1); break;
|
||||
case KEY_PGUP: scroll_up(VIEW_H); break;
|
||||
case KEY_PGDN: scroll_down(VIEW_H); break;
|
||||
case KEY_HOME: top_line = 0;
|
||||
render_viewport(); break;
|
||||
case KEY_END: /* clamp to last viewport */ break;
|
||||
}
|
||||
render_status();
|
||||
}
|
||||
exit:
|
||||
mem_free_block(file_blk);
|
||||
clrscr_attr(ATTR_TEXT);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
### Phase 1
|
||||
1. Подготовить `SAMPLE.MD` ~5 KB (заголовки, абзацы, списки) — рендериться будет plain.
|
||||
2. `cd examples/mdview && make`
|
||||
3. `python make_disk.py mdview.exe SAMPLE.MD → mc.img && ./run_mame.sh`
|
||||
4. Проверить:
|
||||
- Status bar показывает `SAMPLE.MD L 1-30 / N X%`
|
||||
- Menu bar внизу
|
||||
- ↑/↓: 1 строка
|
||||
- PgUp/PgDn: 30 строк, корректное clamp на границах
|
||||
- Home: top_line=0
|
||||
- End: top_line = total_lines - VIEW_H
|
||||
- F1: показывает help, любая клавиша возвращает
|
||||
- F10 / Esc: выход, экран очищен
|
||||
5. Edge cases: пустой файл, файл из одной строки, файл с очень длинной строкой (>80), CRLF и LF mixed.
|
||||
|
||||
### Phase 2-4
|
||||
Расширять `SAMPLE.MD` с фичами по мере добавления, визуально верифицировать в MAME. Скриншот-сравнение опционально.
|
||||
|
||||
### Регрессии
|
||||
- Никаких изменений в libc на Phase 1 кроме (потенциально) добавления `getkey()` в `libc/conio/conio.c` — если так, прогнать `examples/conio2` и `examples/filetest` чтобы убедиться что ничего не сломалось.
|
||||
|
||||
---
|
||||
|
||||
## Решения по неоднозначностям
|
||||
|
||||
1. **Word-wrap vs truncate**: v1 = truncate (просто). v2 — F2 toggle wrap.
|
||||
2. **Горизонтальный скроллинг**: v1 — нет; v2 — `←/→` сдвиг viewport по столбцам.
|
||||
3. **Tab handling**: преобразование при рендере, **tabstop = 4** (стандарт MD). Оригинал в W3 не трогаем.
|
||||
4. **UTF-8**: рендерим байты как есть. Если файл в CP866 — отрисуется кириллицей через системный фонт. UTF-8 — пока не поддерживаем (визуально будет каша на не-ASCII символах; детект и предупреждение — в v2).
|
||||
|
||||
---
|
||||
|
||||
### Phase 6 — Полный layout таблиц (deferred)
|
||||
|
||||
Сейчас (Phase 4-tables) таблицы рисуются "декоративно" — пайпы и тире
|
||||
заменяются на box-drawing chars, но ширины колонок берутся как есть из
|
||||
исходника. Цель Phase 6 — пересчитать таблицу в нормализованный вид:
|
||||
|
||||
- **Pre-scan таблицы**: пройти все строки одного table-блока, найти
|
||||
максимальную ширину каждой колонки (с учётом съеденных inline-маркеров —
|
||||
визуальный размер, не байтовый).
|
||||
- **Re-emit в буфере**: при загрузке файла (или при первой встрече таблицы)
|
||||
переписать строки в FILE_BUF так, чтобы все ячейки одной колонки имели
|
||||
одинаковую ширину; добавить top/bottom рамки (`┌─┬─┐` / `└─┴─┘`) как
|
||||
синтетические строки. Это позволит сохранить 1:1 соответствие "логическая
|
||||
строка → одна viewport row" без специальной логики при рендере.
|
||||
- **Память**: re-emit может УВЕЛИЧИТЬ файл за счёт padding и доп.рамок.
|
||||
Если буфер близок к 16KB — отрезать таблицу и пометить её overflow'ом.
|
||||
- **Горизонтальный скроллинг**: если итоговая ширина таблицы (или любой
|
||||
строки) > SCREEN_W = 80 — добавить ←/→ для horizontal pan. Это будет
|
||||
общий механизм для длинных строк (см. также wrap mode), не только таблиц.
|
||||
- **Выравнивание из separator-row**: `:-` → left, `-:` → right, `:-:` →
|
||||
center; учитывать при padding'е содержимого ячейки.
|
||||
- **Шаги реализации**:
|
||||
1. Walking pass по фенсам/таблицам прямо в `index_lines()` — собрать
|
||||
extents всех таблиц.
|
||||
2. Для каждой таблицы — определить ширины колонок.
|
||||
3. Решение: rewrite-in-buffer (проще для рендера, но мутирует исходник)
|
||||
vs render-time layout (cleaner, но требует отдельной структуры
|
||||
описания layout'а на каждую таблицу).
|
||||
4. Hpan: общий `viewport_x_offset` для всего экрана, или отдельный
|
||||
"широкий режим" только внутри таблиц.
|
||||
|
||||
> Не блокирующая фича. Запускать когда станет понятен типовой источник
|
||||
> markdown-файлов (узкие читалки → достаточно текущего декоратора;
|
||||
> широкие README с большими таблицами → нужен полный layout).
|
||||
|
||||
---
|
||||
|
||||
### Phase ∞ — Кэш рендеренных строк (low priority)
|
||||
|
||||
Отложено: текущая скорость более чем достаточна. Активировать если появится
|
||||
сценарий, где видна задержка PgUp/PgDn (например, при тяжёлом inline-парсере
|
||||
v2 с UTF-8 / linkifier / таблицами).
|
||||
|
||||
**Кэш отформатированных строк** (W3, отдельная EMM-страница):
|
||||
|
||||
```
|
||||
Cache layout (16 KB EMM page, всего 16000 байт используется):
|
||||
slot 0: 80 chars + 80 attrs = 160 bytes @ offset 0
|
||||
...
|
||||
slot 99: 80 chars + 80 attrs = 160 bytes @ offset 15840
|
||||
|
||||
Cache tags (W2 static): uint16_t cache_tag[100] = 200 bytes
|
||||
cache_tag[i] = line_id, или 0xFFFF = invalid
|
||||
```
|
||||
|
||||
Стратегия — **direct map (no LRU)**: `slot = line_id % 100`. Коллизия → вытеснение.
|
||||
|
||||
**Batched viewport render**: 2 page-swap'а на ВЕСЬ viewport (cache → file → cache),
|
||||
не 60 как при наивной реализации.
|
||||
|
||||
При сборке: `cache_blk = mem_alloc_pages(1)` после `file_blk`; `mem_free_block` на exit.
|
||||
|
||||
---
|
||||
|
||||
## Что отложено в v2
|
||||
|
||||
- ~~Файлы >16 KB~~ — **сделано в v1.5** (до 128 KB через 1–8 EMM-страниц + lazy map в W3).
|
||||
- ~~Word wrap~~ — **сделано** (Phase 5, F2 toggle).
|
||||
- Search (Find / Find next) — F3 / F4.
|
||||
- F8 Raw / Render toggle — спецификация в Phase 8.
|
||||
- Links `[text](url)` + image alt — Phase 7.
|
||||
- Tables — Phase 6 (полный layout).
|
||||
- Toggle highlight on/off — частный случай F8 Raw.
|
||||
@@ -1,570 +0,0 @@
|
||||
# �« : ⥪áâ®¢ë© Markdown viewer ¤«ï Sprinter (`examples/mdview`)
|
||||
|
||||
## Context
|
||||
|
||||
’¥á⮢ ï ªàã¯ ï § ¤ ç - ¯à®¢¥à¨âì èã libc ¥âਢ¨ «ì®¬ interactive-¯à¨«®¦¥¨¨ (¯®«®íªà ë© UI, ä ©«®¢ë© I/O, ¯ àá¥à). � à ««¥«ì® ¤ áâ å®à®è¨© showcase ¯« âä®à¬ë ¨ ¯®¬®¦¥â ¢ëâ é¨âì ¥¤®¤¥«ª¨ ¢ conio/io. Š®¥ç ï æ¥«ì: viewer ¤«ï `.md` ä ©«®¢ á ¯®¤á¢¥âª®© á¨â ªá¨á , ¢¨£ 樥© ¯® ⥪áâã ¨ ¯®áâà ¨çë¬ áªà®««¨£®¬.
|
||||
|
||||
**Ž£à ¨ç¥¨ï v1 (§ 䨪á¨à®¢ ë ¯®«ì§®¢ ⥫¥¬):**
|
||||
- POSIX file API (open/read/lseek/close); FILE pointer / fread / fgets - ¥ ¨á¯®«ì§®¢ âì
|
||||
- �®¤á¢¥âª ç¥à¥§ 梥â: **à §¬¥à § £®«®¢ª ** -> 梥â èà¨äâ ; **bold/italic** -> 梥â ä® (¬®®è¨à¨ë© ä®â ¡¥§ ¦¨à®£®/ªãàᨢ®£® ç¥àâ ¨ï)
|
||||
|
||||
**� áè¨à¥¨ï, ॠ«¨§®¢ ë¥ ¯®á«¥ ¯« :**
|
||||
- ” ©«ë ¤® 128 KB (1-8 EMM-áâà ¨æ, lazy map ¢ W3 ç¥à¥§ `fb()/map_page()`) - ¨§ ç «ì® ¡ë«® ¢ v2
|
||||
- €¨¬¨à®¢ ë© spinner ¨ ¯à¥¤¢ à¨â¥«ì ï ®âà¨á®¢ª UI ¯à¨ áâ à⥠- UX
|
||||
- Inline emphasis: `_`/`*`/`**` ¯®¤ç¨ïîâáï XOR flanking-¯à ¢¨«ã (whitespace ஢® á ®¤®© áâ®à®ë), `COLOR_YELLOW` ¨ `2 * 3` ®áâ îâáï «¨â¥à « ¬¨
|
||||
|
||||
---
|
||||
|
||||
## ’¥ªã騩 áâ âãá (2026-06-05)
|
||||
|
||||
| Phase | ‘â âãá | Š®¬¬¥â ਩ |
|
||||
|---|---|---|
|
||||
| 1 Plain text + nav | | § £à㧪 , ¨¤¥ªá æ¨ï, status/menu, ^V/PgUp/PgDn/Home/End/F1/F10/Esc |
|
||||
| 2 Headers + HR | | H1..H4, `---`/`***`/`___` (á >=3 marker'®¢) |
|
||||
| 3 Inline emphasis | | `**` / `*` / `_` / `` ` ``; XOR flanking (á¬. ¢ëè¥) |
|
||||
| 4 Lists / quote / fenced code | | `- / * / +`, `N. / N)`, `> `, ``` ``` ```; light nested lists |
|
||||
| 4-tables | | â ¡«¨æë ®â«®¦¥ë ¢¬¥á⥠á Phase 6 |
|
||||
| 5 Wrap / Unwrap (F2) | | wrap-by-default; soft wrap; F2 ¯¥à¥ª«îç ¥â; hpan <-/-> ¢ truncate-०¨¬¥ |
|
||||
| 6 �®«ë© layout â ¡«¨æ | | deferred |
|
||||
| 7 Links + search | | deferred |
|
||||
| 8 F8 Raw toggle | | deferred |
|
||||
| Cache ।¥à¥ëå áâப | | ¥ 㦮 ¯® ᪮à®á⨠|
|
||||
|
||||
**UX-¯®¯à ¢ª¨ (®â¤¥«ì® ®â phase-¯« , 2026-06-05):**
|
||||
- UI (menu + title bar) ®âà¨á®¢ë¢ ¥âáï „Ž `load_file`/`index_lines` - ¯®«ì§®¢ ⥫ì áà §ã ¢¨¤¨â ¨â¥à䥩á, ¥ çñàë© íªà
|
||||
- Title bar: `MDVIEW <spinner> <filename>` (3 ¯à®¡¥« ¬¥¦¤ã MDVIEW ¨ filename, slot ᯨ¥à - col 8)
|
||||
- Spinner ªàãâ¨âáï ¢® ¢à¥¬ï `load_file` (¯® áâà ¨æ¥) ¨ `index_lines` (à § ¢ 32 «®£¨ç¥áª¨å áâப¨), ¢ª«îç ï `toggle_wrap`
|
||||
|
||||
---
|
||||
|
||||
## €àå¨â¥ªâãà
|
||||
|
||||
### � ᪫ ¤ª íªà (80x32, ⥪áâ mode 0x03)
|
||||
|
||||
```
|
||||
Row 0: MDVIEW ³ mdview.md ³ L 1-30 / 142 ³ 21% <- status bar (BG=blue, FG=white)
|
||||
|
||||
³ ÀÄÄ filename @ col 10
|
||||
ÀÄÄÄÄÄÄÄÄÄÄ spinner slot @ col 8 (anim. while busy)
|
||||
Row 1:
|
||||
... document viewport (30 rows)
|
||||
Row 30:
|
||||
Row 31: F1 Help F2 Wrap F10 Exit <- menu bar (BG=blue, FG=cyan)
|
||||
```
|
||||
|
||||
- Viewport = 30 áâப x 80 á⮫¡æ®¢.
|
||||
- Status / menu ।¥àïâáï ç¥à¥§ `wrchar()` (¡¥§ ¢â®-áªà®«« ), viewport - ç¥à¥§ `LOCATE` + ¯®á¨¬¢®«ìë© `wrchar()` (⮦¥ ¡¥§ ¢â®-áªà®«« , áâ ¢¨â ¨ char ¨ attr § ®¤¨ call).
|
||||
|
||||
### � ¬ïâì
|
||||
|
||||
Memory mode: **`small`** - DSS ®â¢®¤¨â ¯®¤ è ®¡à § **¤¢ ¡ ª (W1 + W2)** = 32 KB á㬬 à® (CODE ¢ W1, DATA + STACK + HEAP ¢ W2). �⮣® ¤®«¦® å¢ â¨âì ç⮡ë �… § ¢®¤¨âì `__banked` äãªæ¨¨. W3 ®áâ ñâáï ¯®«®áâìî ᢮¡®¤ë¬ ¤«ï ¬ ¯¯¨£ ¡®«ìè¨å ¡ãä¥à®¢:
|
||||
|
||||
```
|
||||
W0 (0x0000-0x3FFF): ESTEX (system, untouchable)
|
||||
W1 (0x4000-0x7FFF): CODE (small mode page 1)
|
||||
W2 (0x8000-0xBFFF): DATA + STACK + HEAP (small mode page 2)
|
||||
W3 (0xC000-0xFFFF): paged window - «¥¨¢® ¯¥à¥ª«îç ¥âáï ¬¥¦¤ã EMM-áâà ¨æ ¬¨
|
||||
ä ©« (¤® 8 áâà ¨æ = 128 KB). `cur_page` ªíè¨àã¥â
|
||||
⥪ãéãî mapping, `fb(p)` ¬ ¯¯¨â ã¦ãî áâà ¨æã ¯à¨
|
||||
¯¥à¢®¬ ®¡à 饨¨.
|
||||
```
|
||||
|
||||
�®ç¥¬ã small + W3:
|
||||
- 32 KB ª®¤+¤ ë¥ á ¡®«ì訬 § ¯ ᮬ -> ¥â ¡ ª¨£
|
||||
- W3 - áâ ¤ àâë© paged window, ¯®¤ ¥£® ã á 㦥 ¥áâì `bank_io_w3` API
|
||||
- ” ©«ë ¤® 128 KB ¯®¤¤¥à¦¨¢ îâáï ⨢®: `mem_alloc_pages(pages_needed)` ¯®¤ ¢¥áì ä ©«; `map_page()` ç¥à¥§ `sprinter_page_w3()`; `fb(p)` - ¥¤¨ ï â®çª ¤®áâ㯠¨§ ¨¤¥ªá â®à ¨ ।¥à .
|
||||
|
||||
‘â ⨪¨ (¢ W2):
|
||||
- `line_offset[MAX_LINES]` - uint16_t ᬥ饨¥ ª ¦¤®© áâப¨ ¢ ä ©«®¢®© áâà ¨æ¥ (4 KB 2048 áâப)
|
||||
- `cache_tag[CACHE_N]` - uint16_t ⥣ á«®â (200 ¡ ©â 100 ᫮⮢, ¯®ï¢«ï¥âáï ¢ Phase 3)
|
||||
- `filename[64]`, `top_line`, `total_lines`, `file_size`, `file_blk`, `cache_blk` - ¥¤¨¨æë ¡ ©â
|
||||
|
||||
**FILE_BUF**: `((char*)0xC000)` - 䨪á¨à®¢ ï ¤à¥á æ¨ï ¢ W3 ¯®á«¥ ¬ ¯¯¨£ 㦮© áâà ¨æë.
|
||||
|
||||
### �®â®ª ¤ ëå
|
||||
|
||||
```
|
||||
main -> open() -> read() chunks 1KB -> write to W1 (mapped EMM page) -> close()
|
||||
-> index_lines() (®¤® ᪠¨à®¢ ¨¥, § ¯®«ï¥â line_offset[])
|
||||
-> render_viewport() + main loop { getkey(); handle(); render_status(); render_viewport() }
|
||||
```
|
||||
|
||||
**’®ª®áâì á `read()`**: ESTEX READ § ¯¨áë¢ ¥â ¯® dst-㪠§ â¥«î ¢ ¤à¥á®¬ ¯à®áâà á⢥ ¢ë§ë¢ î饣®. �®áª®«ìªã ¬ë ¬ ¯¯¨¬ EMM-áâà ¨æã ¢ W3 (0xC000) „Ž ¢ë§®¢ read(), 㪠§ ⥫ì 0xC000+offset ¢ «¨¤¥. …᫨ ¢ëïá¨âáï çâ® BIOS âண ¥â W3 ¢® ¢à¥¬ï read (£à ä¨ç¥áª¨© ¢¨¤¥®¡ãä¥à ¯® 㬮«ç ¨î ¢ W3 ¯à¨ £à ä¨ç¥áª¨å ०¨¬ å, ® ¢ ⥪á⮢®¬ - ¤®«¦¥ ¡ëâì ᢮¡®¤¥) - fallback: ç¨â âì ¢ 1 KB ¡ãä¥à ¢ W2 ¨ ª®¯¨à®¢ âì ¢ W3 ç¥à¥§ `bank_write_w3()`.
|
||||
|
||||
---
|
||||
|
||||
## �¥ «¨§ æ¨ï - ¯®íâ ¯ ï
|
||||
|
||||
### Phase 1 - Plain text viewer (MVP)
|
||||
|
||||
**—â® à ¡®â ¥â:**
|
||||
- ‡ £à㧪 ä ©« (`open/read/close`) ¢ W1-áâà ¨æã
|
||||
- ˆ¤¥ªá æ¨ï áâப (LF / CRLF à §¤¥«¨â¥«¨)
|
||||
- Status bar: ¨¬ï ä ©« , L N-M / Total, ¯à®æ¥â áªà®««¨£
|
||||
- Menu bar: `F1 Help F10 Exit`
|
||||
- � ¢¨£ æ¨ï: ^/V (1 áâப ), PgUp/PgDn (30 áâப), Home/End ( ç «®/ª®¥æ), Esc/F10 (¢ë室), F1 (help screen)
|
||||
- Ž¡à¥§ª áâப ¤«¨¥¥ 80 ᨬ¢®«®¢ (¡¥§ word-wrap)
|
||||
- –¢¥â : ⥪áâ ¡¥«ë© çñ஬; status/menu - ¡¥«ë© ᨥ¬
|
||||
|
||||
**Šà¨â¨çë¥ ä ©«ë:**
|
||||
- `examples/mdview/mdview.c` - main, key loop, rendering, indexing (one-file MVP)
|
||||
- `examples/mdview/Makefile`
|
||||
- `examples/mdview/SAMPLE.MD` - â¥áâ®¢ë© markdown ä ©«
|
||||
|
||||
### Phase 2 - Headers ¨ £®à¨§®â «ì ï «¨¨ï
|
||||
|
||||
**MD ä¨ç¨:**
|
||||
- `# H1` -> ïમ-¦ñ«âë© (COLOR_YELLOW = 14) çñ஬
|
||||
- `## H2` -> ïમ-£®«ã¡®© (COLOR_LBLUE = 11)
|
||||
- `### H3` -> ïમ-§¥«ñë© (COLOR_LGREEN = 10)
|
||||
- `#### H4+` -> á¥àë© (COLOR_GREY = 8)
|
||||
- `---` / `***` ®â¤¥«ì®© áâப¥ -> «¨¨ï 0xC4 (£®à¨§®â «ì ï à ¬ª ASCII) ¢® ¢áî è¨à¨ã
|
||||
|
||||
### Phase 3 - Inline emphasis
|
||||
|
||||
**� àá¥à inline (per-line, runs ¢ ®¤ã áâபã):**
|
||||
- `**bold**` -> ATTR_TEXT_BOLD
|
||||
- `*italic*` -> ATTR_TEXT_ITALIC
|
||||
- `_underscore_` -> ATTR_TEXT_UNDERSORE
|
||||
- `` `code` `` -> ATTR_TEXT_CODE
|
||||
- Œ થàë `**`/`*`/`_`/`` ` `` �… ।¥àïâáï (áꥤ îâáï)
|
||||
|
||||
State machine: ®¤¨ ªâ¨¢ë© áâ¨«ì ®¤®¢à¥¬¥® (¡¥§ ¢«®¦¥®áâ¨); ª®ä«¨ªâãî騩 ¬ àª¥à ¯à¨ ç㦮¬ ªâ¨¢®¬ á⨫¥ ¢áñ à ¢® ª®áìâáï (zero-width) ¤«ï á¨åந§ 樨 è¨à¨ë á index_lines. ‘®áâ®ï¨¥ á¡à áë¢ ¥âáï ª ¦¤®© áâப¥.
|
||||
|
||||
**Flanking-¯à ¢¨«® (CommonMark intraword, ॠ«¨§®¢ ® ¯®á«¥ ¨§ ç «ì®£® ¯« ):**
|
||||
- `*` / `**` / `_` áç¨â îâáï markdown-¬ થ஬ ⮫쪮 ¥á«¨ whitespace/EOL
|
||||
஢® á Ž„�މ áâ®à®ë (XOR).
|
||||
- ‘«ãç ¨ "®¡ whitespace" (`2 * 3`, `2 ** 3`) -> «¨â¥à «ë ( à¨ä¬¥â¨ª ).
|
||||
- ‘«ãç ¨ "¨ ®¤®£® whitespace" (`COLOR_YELLOW`, `FILE*/fread`, `foo*bar*baz`)
|
||||
-> «¨â¥à «ë (intraword).
|
||||
- Backtick (`` ` ``) flanking �… âॡã¥â - `` `code` `` à ¡®â ¥â ¡¥§ ¯à®¡¥«®¢.
|
||||
- �à ¢¨«® ¯à¨¬¥¥® ᨬ¬¥âà¨ç® ¢ 4 ¬¥áâ å (`index_lines`, cont-render,
|
||||
®á®¢®© inline-¯ àá¥à, truncation peek), ¨ ç¥ wrap-¨¤¥ªá â®à ¨
|
||||
।¥à à §ê¥¤ãâáï ¯® è¨à¨¥.
|
||||
|
||||
> **Šíè ®âä®à¬ â¨à®¢ ëå áâப** - ®â«®¦¥ ¢ á ¬ë© ª®¥æ, á¬. "Phase : ®¯â¨¬¨§ 樨".
|
||||
> ‘ª®à®á⨠⥪ã饣® ¨¢®£® ।¥à å¢ â ¥â 80x30 = 2400 wrchar / ª ¤à; PgUp/PgDn ¢¨§ã «ì® ¬£®¢¥¥.
|
||||
|
||||
### Phase 4 - Block elements (¡¥§ â ¡«¨æ)
|
||||
|
||||
- Œ ન஢ ë¥ á¯¨áª¨: `- foo`, `* foo`, `+ foo` -> ¯à¥ä¨ªá `o` (0x07) + ¯à®¡¥«; 梥⠬ થà ïàç¥ ®á®¢®£®
|
||||
- �㬥஢ ë¥ á¯¨áª¨: `1. foo`, `2. foo` -> ª ª ¥áâì (ç¨á«® ®áâ ¢«ï¥¬)
|
||||
- Blockquote: áâப¨ á `> ` -> ¯à¥ä¨ªá `³` (0xB3) á¥à®£® 梥â , ®á⠫쮩 ⥪áâ á«¥£ª ¯à¨£«ãèñë©
|
||||
- Fenced code blocks: `` ``` `` ®âªàë¢ ¥â/§ ªàë¢ ¥â ¡«®ª; ¢á¥ áâப¨ ¬¥¦¤ã - bg=á¥àë©, ¬®®è¨à¨® (¡¥§ inline-¯ àᨣ )
|
||||
- Indented code blocks (4+ ¯à®¡¥«®¢): «®£¨ç® fenced, ® ¡¥§ £® ¬ થà
|
||||
|
||||
**Light nested lists (v1 - ॠ«¨§®¢ ®):**
|
||||
- `classify_line()` ¯à®¯ã᪠¥â leading spaces ¯¥à¥¤ ulist/olist/quote ¬ થ஬,
|
||||
¢®§¢à é ¥â `content_off` ¯®á«¥ ¬ થà -> `content_off - p_start` = indent + marker
|
||||
¡¨â ¢ visible col.
|
||||
- `render_line()` à¨áã¥â leading-spaces ¢ `ATTR_TEXT`, ¯®â®¬ ¬ થà ᤢ¨ã⮩
|
||||
¯®§¨æ¨¨ (col = indent). Marker ¢áñ ¥éñ 䨪á¨à®¢ ¯à¨ £®à¨§®â «ì®¬ pan'¥.
|
||||
- HR / header / fence delim ®áâ îâáï áâண® col-0 (CommonMark à §à¥è ¥â ¤® 3
|
||||
¢¥¤ãé¨å ¯à®¡¥«®¢ ¤«ï ¨å - ã¯à®á⨫¨).
|
||||
- Tab-indent -> ¥ à ᯮ§ ñâáï ª ª nesting (⮫쪮 spaces).
|
||||
|
||||
**Phase 4-full - ¯®« ï ¯®¤¤¥à¦ª ¢«®¦¥®á⨠(deferred):**
|
||||
- **Tab-indent**: áç¨â âì tab = 4 ¯à®¡¥« ¤«ï ®¯à¥¤¥«¥¨ï ã஢ï.
|
||||
- **Quote nesting** (`> > foo`): ª ¦¤ë© `>` ¯®¤àï¤ = +1 ã஢¥ì, ª ¦¤ë© à¨áã¥âáï
|
||||
®â¤¥«ìë¬ `³` ¢ `ATTR_QUOTE_MARKER` (¢¨§ã «ì ï "«¥áâ¨æ " á«¥¢ ).
|
||||
- **Hanging indent ¢ wrap-continuation**: ª®£¤ `- some very long bullet text
|
||||
that wraps...` - continuation seg ¤®«¦¥ ç¨ âìáï ®â content-col (¯®á«¥
|
||||
¬ થà ), ¥ ®â col 0. ‘¥©ç á continuation ¨¤ñâ ®â col 0 (v1 simplification).
|
||||
’ॡã¥â åà ¨âì `marker_width` per «®£¨ç¥áª ï áâப (8 ¡¨â) ¨«¨ re-classify
|
||||
first seg ¯à¨ ।¥à¥ continuation.
|
||||
- **Lazy continuation**: áâப¨ ¡¥§ ¬ થà , ® á ¯à ¢¨«ìë¬ indent ¯®¤
|
||||
¯à¥¤ë¤ã騬 bullet'®¬, ¤®«¦ë áç¨â âìáï ¯à®¤®«¦¥¨¥¬ ⮣® bullet'
|
||||
(¢¨§ã «ì® - ®¡é¨© attr).
|
||||
- **Strict CommonMark indent rules**: ¢«®¦¥ë© ¯ãªâ ¤®«¦¥ ¡ëâì indent
|
||||
>= content_col த¨â¥«ï, ¨ ç¥ áç¨â ¥âáï breakout. �㦥 ¬¨¨-stack
|
||||
ªâ¨¢ëå ᯨ᪮¢ ¯à¨ ¨¤¥ªá 樨.
|
||||
|
||||
### Phase 5 - Wrap / Unwrap ¤«¨ëå áâப
|
||||
|
||||
„¥ä®«â: **wrap on**. F2 ¯¥à¥ª«îç ¥â; ¢ ¬¥î-¡ ॠ¯®¤¯¨áì ®âà ¦ ¥â ¤¥©á⢨¥
|
||||
("Unwrap" ª®£¤ wrap ¢ª«îçñ, "Wrap" ª®£¤ ¢ëª«îç¥). ‚® ¢à¥¬ï २¤¥ªá 樨
|
||||
ªàãâ¨âáï ᯨ¥à title bar.
|
||||
|
||||
**v1 - ॠ«¨§®¢ ®:**
|
||||
- ޤ¨ ¬ áᨢ `line_offset[2048]` åà ¨â ‚ˆ„ˆŒ›… ᥣ¬¥âë ( ¥ «®£¨ç¥áª¨¥
|
||||
áâப¨); ¡¨âë 0..13 - ¡ ©â®¢®¥ ᬥ饨¥, ¡¨â 15 - CONT-ä« £ continuation.
|
||||
- Wrap-०¨¬: soft wrap ¯®á«¥¤¥¬ ¯à®¡¥«¥ <= 80; hard fallback ¥á«¨
|
||||
¯à®¡¥« ¥â.
|
||||
- Œ થàë í¬ä §¨á (`**`/`*`/`_`/`` ` ``) ¨ header-¯à¥ä¨ªáë (`#`/`##`/...) ¥
|
||||
ãç¨âë¢ îâáï ¢ visible-col ¯à¨ ¯®¨áª¥ â®çª¨ ¯¥à¥®á .
|
||||
- "‘¯¥æ¨ «ìë¥" «®£¨ç¥áª¨¥ áâப¨ ¥ wrap' îâáï ¢®®¡é¥ (®¤ seg-§ ¯¨áì
|
||||
«®£¨ç¥áªãî áâபã): fence delim, table row (header/separator/body), HR.
|
||||
- F2 toggle á®åà ï¥â ¢¨§ã «ìãî ¯®§¨æ¨î ç¥à¥§ `top_offset` ¢ FILE_BUF.
|
||||
- Bitmaps (`in_code`/`in_table`/`is_tab_hdr`) ¯¥à¥áâà ¨¢ îâáï ¢¬¥á⥠á
|
||||
ᥣ¬¥â ¬¨, ¨¤¥ªá¨àãîâáï seg-¨¤¥ªá®¬, ¡¨âë áâ ¢ïâáï ⮫쪮 ¯¥à¢®¬
|
||||
seg'¥ «®£¨ç¥áª®© áâப¨.
|
||||
- Continuation-ᥣ¬¥âë ।¥àïâáï ¢ á⨫¥ "v1: ¯«®áª®" - plain text,
|
||||
¨ª ª¨å markdown-ª« áá¨ä¨ª 権; padding ¤® ª®æ áâப¨ `ATTR_TEXT`.
|
||||
|
||||
**v1.5 - ®â«®¦¥® ¤«ï ¯®«®© ª àâ¨ë wrap:**
|
||||
- **Hanging indent**: continuation ®â ulist/olist/quote ¤®«¦¥ ¢ëà ¢¨¢ âìáï
|
||||
¯®¤ content, ¥ ®â col 0. ’ॡã¥â åà ¨âì marker_width per «®£¨ç¥áª ï
|
||||
áâப ˆ‹ˆ re-classify ¯¥à¢®£® seg' ¯à¨ ।¥à¥ continuation.
|
||||
- **� á«¥¤®¢ ¨¥ base_attr**: continuation ®â header' ¤®«¦¥ á®åà ïâì
|
||||
梥â; continuation ®â code body - ä® ATTR_TEXT_CODE. ’ॡã¥â åà ¨âì
|
||||
1 ¡ ©â `base_attr` per seg ˆ‹ˆ lookup ¯¥à¢®£® seg' .
|
||||
- **Inline emphasis ç¥à¥§ £à ¨æã**: í¬ä §¨á, ®âªàëâë© ¢ ¯¥à¢®¬ seg'¥ ¨ ¥
|
||||
§ ªàëâë©, ¤®«¦¥ ¯à®¤®«¦ âìáï ¢® ¢â®à®¬. ’ॡã¥â åà ¨âì emph state per
|
||||
seg (3 ¡¨â ).
|
||||
- Compact way: ¤®¡ ¢¨âì ¯ à ««¥«ìë© ¬ áᨢ `seg_meta[MAX_SEGS]` ¯® 1 ¡ ©âã
|
||||
- ¯ ªã¥â marker_width (4 ¡¨â ) + emph_state (3 ¡¨â ) + base_attr_idx
|
||||
(4 ¡¨â ¨§ â ¡«¨æë -> 㦥 2-¡ ©â®¢ë© seg_meta).
|
||||
- **Hpan ¤«ï ¤«¨ëå áâப**: ¥á«¨ wrap ¢ëª«îç¥, ¤®¡ ¢¨âì <-/-> ¤«ï
|
||||
£®à¨§®â «ì®£® áªà®«« >80 cols. ޡ鍩 ¬¥å ¨§¬ á tables (Phase 6).
|
||||
**�…€‹ˆ‡Ž‚€�Ž** (light) - `viewport_x` + ¯®«ë© re-render ª ¦¤®¥ <-/->.
|
||||
- **“᪮२¥ hpan ç¥à¥§ ESTEX WINCOPY/WINREST** (deferred): ᥩç á pan
|
||||
¤¥« ¥â ¯®«ë© `render_viewport()` = 30 áâப x 80 wrchar. Œ®¦®
|
||||
᪮¯¨à®¢ âì áãé¥áâ¢ãî饥 ᮤ¥à¦¨¬®¥ viewport' N cols ¢«¥¢®/¢¯à ¢®
|
||||
ç¥à¥§ win-copy, ¯®â®¬ ।¥à¨âì ⮫쪮 㧪ãî ¯®«®áã á¯à ¢ /á«¥¢
|
||||
(HPAN_STEP cols x 30 rows 240 wrchar ¢¬¥áâ® 2400). ESTEX SCROLL
|
||||
£®à¨§®â «ì ¥ ¯®¤¤¥à¦¨¢ ¥â - 㦠¨¬¥® WINCOPY-®¯¥à æ¨ï ¨«¨
|
||||
rdchar/wrchar loop. €ªâ¨¢¨à®¢ âì ª®£¤ ®éãâ¨âáï â®à¬®§; ᥩç á
|
||||
⨯®¢®¬ markdown'¥ ¥ § ¬¥â®.
|
||||
|
||||
**v2 - ®â¤¥«ì ï ä¨ç , ¬¨¬® wrap:**
|
||||
- Toggle ¯®¤á¢¥âª¨ 楫¨ª®¬ (F3?)
|
||||
- Search ¯® ⥪áâã (Ctrl+F / F4)
|
||||
- Links `[text](url)` -> ᨨ© ¯®¤çñàªãâë© text, url ¯àïç¥âáï
|
||||
- Images `` -> `[IMG: alt]`
|
||||
|
||||
### Phase 7 - Links ¨ ¯®¨áª (post-v1)
|
||||
|
||||
- `[text](url)` -> ®âà¨á®¢ âì ⮫쪮 `text` á ïમ-ᨨ¬ FG (¢¨§ã «ì® ¯®¤çñàªã⮥)
|
||||
- `` -> `[IMG: alt]` ¢ ᪮¡ª å
|
||||
- Search ¯® ⥪áâã (F3 / Ctrl+F): ¨ªà¥¬¥â «ìë©, ¯®¤á¢¥âª ᮢ¯ ¤¥¨©
|
||||
|
||||
### Phase 8 - F8 Raw / Render toggle
|
||||
|
||||
�¥à¥ª«îç ⥫ì ०¨¬ ®â®¡à ¦¥¨ï: ¯à¨ ¢ª«îçñ®¬ Raw ¯®ª §ë¢ ¥âáï ¨á室ë©
|
||||
⥪áâ ä ©« ª ª ¥áâì - ¢á¥ markdown-¬ થàë (`#`, `**`, `_`, `` ` ``, `|`,
|
||||
`>`, `-`, etc.) ।¥àïâáï «¨â¥à « ¬¨ á `ATTR_TEXT`, ¡¥§ ª« áá¨ä¨ª 樨.
|
||||
�®«¥§® ª®£¤ :
|
||||
- 㦮 㢨¤¥âì â®çãî à §¬¥âªã (®â« ¦¨¢ ¨¥ .md, áªà¨è®âë, ª®¯¨à®¢ ¨¥)
|
||||
- markdown-ª« áá¨ä¨ª â®à ®è¨¡áï ¨ å®ç¥âáï 㢨¤¥âì ®à¨£¨ «
|
||||
- å®ç¥âáï ¡ëáâà® áà ¢¨âì "¤®/¯®á«¥" ।¥à
|
||||
|
||||
**�®¢¥¤¥¨¥:**
|
||||
- F8 ¯¥à¥ª«îç ¥â `render_mode` (1=render, 0=raw); ¬¥î ¯®ª §ë¢ ¥â ®¡à ⮥
|
||||
¤¥©á⢨¥ ("Raw" ª®£¤ ᥩç á render, "Render" ª®£¤ ᥩç á raw) - ⮩ ¦¥
|
||||
«®£¨ª®© çâ® F2/Wrap/Unwrap.
|
||||
- ‚ Raw ०¨¬¥: `render_line()` ¨¤ñâ ¯® ª®à®âª®¬ã ¯ã⨠- ¨ª ª®£®
|
||||
`classify_line`, `is_fence_delim`, `is_code_body`, inline-í¬ä §¨á ; ¯à®áâ®
|
||||
¡ ©â®¢ë© ¤ ¬¯ FILE_BUF ®â seg-offset ¤® next-seg/EOL á tab-expansion ¨
|
||||
ATTR_TEXT.
|
||||
- � §¤¥«ì® ®â F2: ®¡ ०¨¬ ¥§ ¢¨á¨¬ë (¬®¦® Raw+Wrap, Raw+Truncate,
|
||||
Render+Wrap, Render+Truncate). Wrap-«®£¨ª ¢ `index_lines` à ¡®â ¥â ¢
|
||||
®¡®¨å á«ãç ïå ®¤¨ ª®¢® (®¯¨à ¥âáï ¢¨§ã «ìë¥ ª®«®ª¨ ¥§ ¢¨á¨¬® ®â
|
||||
à áªà ᪨).
|
||||
- ‘â âãá-¡ à: ¤®¡ ¢¨âì ¨¤¨ª â®à `[R]` / `[V]` (Raw / View) ¨«¨ ⥪á⮬
|
||||
`RAW` à冷¬ á ¨¬¥¥¬ ä ©« .
|
||||
|
||||
**Œ¨¨¬ «ì ï ॠ«¨§ æ¨ï:**
|
||||
- ޤ¨ ®¢ë© static `uint8_t render_mode = 1;`
|
||||
- ‚ `render_line()`: á ¬®¬ ¢¥àåã `if (!render_mode) { ... raw render ... return; }`
|
||||
- ‚ `render_menu()`: ¤®¡ ¢¨âì ïà«ëª F8 à冷¬ á F2.
|
||||
- ‚ £« ¢®¬ 横«¥: `case KEY_F8: toggle_render(); break;`
|
||||
- `toggle_render()` ®â«¨ç ¥âáï ®â `toggle_wrap()` ⥬, çâ® �… ¯¥à¥áâà ¨¢ ¥â
|
||||
`line_offset[]` (wrap-ᥣ¬¥â æ¨ï ¥ ¬¥ï¥âáï), ⮫쪮 ¯¥à¥à¥¤¥à¨â íªà .
|
||||
|
||||
---
|
||||
|
||||
## API-®¢¨ª¨ ¢ libc (¬¨¨¬ «ìë¥)
|
||||
|
||||
### `getkey()` - extended key reader (¢ libc)
|
||||
|
||||
’¥ªã騩 `getch()` â¥àï¥â scan code à áè¨à¥ëå ª« ¢¨è (¢®§¢à é ¥â ⮫쪮 E=ASCII). „®¡ ¢«ï¥¬ **áà §ã ¢ `libc/conio/conio.c`** ®¢ãî äãªæ¨î à冷¬ á `getch()`:
|
||||
|
||||
```c
|
||||
// Returns scan in high byte, ASCII in low byte.
|
||||
// Extended keys (arrows, F-keys, PgUp/PgDn, Home/End): ASCII=0, scan code ¢ high byte.
|
||||
// Plain keys: ASCII ¢ low byte; high byte ᮤ¥à¦¨â positional scan (¡¨â 7 = Ctrl/Alt/Shift modifier).
|
||||
uint16_t getkey(void) __naked {
|
||||
__asm
|
||||
ld c, #0x30 ; ESTEX WAITKEY
|
||||
rst #0x10 ; A=ASCII, D=scan, E=ASCII
|
||||
ld e, a ; ensure E=ASCII even if E clobbered
|
||||
ret ; SDCC __sdcccall(1): ¢®§¢à â uint16_t ¢ DE (D=scan, E=ASCII)
|
||||
__endasm;
|
||||
}
|
||||
```
|
||||
|
||||
„®¯®«¨â¥«ì® - **¢ `libc/include/conio.h`** ¯à®¯¨á âì ¯à®â®â¨¯ ¨ ª®áâ âë scan-ª®¤®¢:
|
||||
|
||||
```c
|
||||
uint16_t getkey(void);
|
||||
|
||||
/* Scan codes for getkey() high byte when ASCII=0 (extended keys). */
|
||||
#define KEY_F1 0x0E
|
||||
#define KEY_F2 0x0F
|
||||
#define KEY_F3 0x10
|
||||
#define KEY_F4 0x11
|
||||
#define KEY_F5 0x12
|
||||
#define KEY_F6 0x13
|
||||
#define KEY_F7 0x14
|
||||
#define KEY_F8 0x15
|
||||
#define KEY_F9 0x16
|
||||
#define KEY_F10 0x17
|
||||
#define KEY_F11 0x18
|
||||
#define KEY_F12 0x19
|
||||
#define KEY_END 0x24
|
||||
#define KEY_DOWN 0x25
|
||||
#define KEY_PGDN 0x26
|
||||
#define KEY_LEFT 0x27
|
||||
#define KEY_RIGHT 0x29
|
||||
#define KEY_HOME 0x2A
|
||||
#define KEY_UP 0x2B
|
||||
#define KEY_PGUP 0x2C
|
||||
#define KEY_INS 0x23
|
||||
#define KEY_DEL 0x22
|
||||
```
|
||||
|
||||
**‘ª -ª®¤ë (¨§ docs/converted/ProgrammerManual.txt:2143-2323):**
|
||||
| Š« ¢¨è | scan | Š« ¢¨è | scan |
|
||||
|---|---|---|---|
|
||||
| F1 | 0x0E | Up | 0x2B |
|
||||
| F10 | 0x17 | Down | 0x25 |
|
||||
| F11 | 0x18 | Left | 0x27 |
|
||||
| F12 | 0x19 | Right | 0x29 |
|
||||
| | | PgUp | 0x2C |
|
||||
| | | PgDn | 0x26 |
|
||||
| | | Home | 0x2A |
|
||||
| | | End | 0x24 |
|
||||
|
||||
### —â® **¯¥à¥¨á¯®«ì§ã¥¬** ¨§ áãé¥áâ¢ãî饩 libc
|
||||
|
||||
- `open/read/lseek/close` - `libc/io/{open,read,lseek}.c` (POSIX wrappers)
|
||||
- `mem_alloc_pages/mem_free_block/mem_get_page` - `libc/mem/mem_alloc.c`
|
||||
- `sprinter_page_w3()` - inline `__sfr` write ¢ `libc/include/sprinter.h:113`
|
||||
- `bank_read_w3/bank_write_w3` - `libc/mem/bank_io_w3.c` (¤«ï fallback ¨«¨ v2 multi-page)
|
||||
- `wrchar(x, y, ch, attr)` - `libc/conio/conio.c:476` (¡¥§ auto-scroll, ¨¤¥ «ì® ¤«ï viewport)
|
||||
- `clrscr_attr(attr)` - `libc/conio/conio.c:395`
|
||||
- `gotoxy/wherex/wherey` - `libc/conio/conio.c:412-462` (¥á«¨ 㦮)
|
||||
- `kbhit()` - `libc/conio/conio.c:22` (¤«ï non-blocking ®¯à®á , ®¯æ¨® «ì®)
|
||||
- `dec16/dec8` - `libc/stdio/dec_print.c` (¤«ï status bar: ⥪ãé ï áâப / total / %)
|
||||
- `COLOR(fg, bg)` ¬ ªà®á - `libc/include/conio.h:152`
|
||||
- –¢¥â®¢ë¥ ª®áâ âë `COLOR_*` - `libc/include/conio.h:145`
|
||||
- `strlen/memcpy/memset` - z80.lib (�… ¯¥à¥¯¨áë¢ âì)
|
||||
|
||||
---
|
||||
|
||||
## ‘âàãªâãà ¨á室¨ª®¢
|
||||
|
||||
```
|
||||
examples/mdview/
|
||||
ÃÄÄ Makefile # áâ ¤ àâë© pattern (á¬. examples/cat/Makefile)
|
||||
ÃÄÄ mdview.c # Phase 1: ¢áñ ¢ ®¤®¬ ä ©«¥ (main, keys, indexing, render)
|
||||
ÃÄÄ SAMPLE.MD # â¥áâ®¢ë© markdown
|
||||
ÀÄÄ README.md # ®¯¨á ¨¥ ¨ controls
|
||||
```
|
||||
|
||||
�®á«¥ Phase 3 à ᪨¤ âì ¯® ¬®¤ã«ï¬ (¥á«¨ á㬬 àë© à §¬¥à > ~6KB):
|
||||
```
|
||||
mdview.c - main loop, status/menu bars, key dispatch
|
||||
mdrender.c - line rendering with MD inline parser
|
||||
mdindex.c - file load + line indexing
|
||||
```
|
||||
|
||||
### ‘¡®àª
|
||||
|
||||
```makefile
|
||||
PROJ ?= ../..
|
||||
SPRINTER_CC := $(PROJ)/bin/sprinter-cc
|
||||
mdview.exe: mdview.c
|
||||
$(SPRINTER_CC) --memory small -o $@ mdview.c
|
||||
```
|
||||
|
||||
`--memory small`: ª®¤+¤ ë¥ ¢ W2 (DSS ¤ ñâ 㦮¥ ç¨á«® áâà ¨æ); ä ©« - ®â¤¥«ì ï EMM-áâà ¨æ ¢ W3.
|
||||
|
||||
---
|
||||
|
||||
## ‘âàãªâãà mdview.c (Phase 1, í᪨§)
|
||||
|
||||
```c
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <conio.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sprinter.h>
|
||||
#include <sprinter_mem.h>
|
||||
|
||||
#define VIEW_TOP 1
|
||||
#define VIEW_BOT 30 // inclusive
|
||||
#define VIEW_H 30
|
||||
#define SCREEN_W 80
|
||||
#define MAX_LINES 2048
|
||||
#define FILE_BUF ((char*)0xC000) /* W3 - EMM page mapped here */
|
||||
#define TAB_STOP 4
|
||||
|
||||
#define ATTR_TEXT COLOR(COLOR_WHITE, COLOR_BLACK)
|
||||
#define ATTR_BAR COLOR(COLOR_WHITE, COLOR_BLUE)
|
||||
#define ATTR_MENU_K COLOR(COLOR_YELLOW, COLOR_BLUE)
|
||||
#define ATTR_MENU_T COLOR(COLOR_WHITE, COLOR_BLUE)
|
||||
|
||||
static uint16_t line_off[MAX_LINES];
|
||||
static uint16_t n_lines;
|
||||
static uint16_t top_line;
|
||||
static uint16_t file_size;
|
||||
static uint8_t file_blk;
|
||||
static char filename[64];
|
||||
|
||||
static int load_file(const char *path); // open, alloc EMM page, map W3, read, close
|
||||
static void index_lines(void); // scan FILE_BUF, fill line_off[]
|
||||
static void render_status(void); // row 0
|
||||
static void render_menu(void); // row 31
|
||||
static void render_line(uint16_t idx, uint8_t row); // one line @ row
|
||||
static void render_viewport(void); // VIEW_H lines starting from top_line
|
||||
static void scroll_up(uint16_t n);
|
||||
static void scroll_down(uint16_t n);
|
||||
static void help_screen(void); // F1
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc < 2) { puts("Usage: mdview <file.md>"); return 1; }
|
||||
if (load_file(argv[1]) < 0) { puts("load error"); return 1; }
|
||||
index_lines();
|
||||
clrscr_attr(ATTR_TEXT);
|
||||
render_menu();
|
||||
render_status();
|
||||
render_viewport();
|
||||
for (;;) {
|
||||
uint16_t k = getkey();
|
||||
uint8_t ascii = k & 0xFF;
|
||||
uint8_t scan = (k >> 8) & 0x7F; // strip mod bit
|
||||
if (ascii) {
|
||||
if (ascii == 0x1B) break; // Esc -> exit
|
||||
continue;
|
||||
}
|
||||
switch (scan) {
|
||||
case KEY_F10: goto exit;
|
||||
case KEY_F1: help_screen(); break;
|
||||
case KEY_UP: scroll_up(1); break;
|
||||
case KEY_DOWN: scroll_down(1); break;
|
||||
case KEY_PGUP: scroll_up(VIEW_H); break;
|
||||
case KEY_PGDN: scroll_down(VIEW_H); break;
|
||||
case KEY_HOME: top_line = 0;
|
||||
render_viewport(); break;
|
||||
case KEY_END: /* clamp to last viewport */ break;
|
||||
}
|
||||
render_status();
|
||||
}
|
||||
exit:
|
||||
mem_free_block(file_blk);
|
||||
clrscr_attr(ATTR_TEXT);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
### Phase 1
|
||||
1. �®¤£®â®¢¨âì `SAMPLE.MD` ~5 KB (§ £®«®¢ª¨, ¡§ æë, ᯨ᪨) - ।¥à¨âìáï ¡ã¤¥â plain.
|
||||
2. `cd examples/mdview && make`
|
||||
3. `python make_disk.py mdview.exe SAMPLE.MD -> mc.img && ./run_mame.sh`
|
||||
4. �஢¥à¨âì:
|
||||
- Status bar ¯®ª §ë¢ ¥â `SAMPLE.MD L 1-30 / N X%`
|
||||
- Menu bar ¢¨§ã
|
||||
- ^/V: 1 áâப
|
||||
- PgUp/PgDn: 30 áâப, ª®à४⮥ clamp £à ¨æ å
|
||||
- Home: top_line=0
|
||||
- End: top_line = total_lines - VIEW_H
|
||||
- F1: ¯®ª §ë¢ ¥â help, «î¡ ï ª« ¢¨è ¢®§¢à é ¥â
|
||||
- F10 / Esc: ¢ë室, íªà ®ç¨é¥
|
||||
5. Edge cases: ¯ãá⮩ ä ©«, ä ©« ¨§ ®¤®© áâப¨, ä ©« á ®ç¥ì ¤«¨®© áâப®© (>80), CRLF ¨ LF mixed.
|
||||
|
||||
### Phase 2-4
|
||||
� áè¨àïâì `SAMPLE.MD` á ä¨ç ¬¨ ¯® ¬¥à¥ ¤®¡ ¢«¥¨ï, ¢¨§ã «ì® ¢¥à¨ä¨æ¨à®¢ âì ¢ MAME. ‘ªà¨è®â-áà ¢¥¨¥ ®¯æ¨® «ì®.
|
||||
|
||||
### �¥£à¥áᨨ
|
||||
- �¨ª ª¨å ¨§¬¥¥¨© ¢ libc Phase 1 ªà®¬¥ (¯®â¥æ¨ «ì®) ¤®¡ ¢«¥¨ï `getkey()` ¢ `libc/conio/conio.c` - ¥á«¨ â ª, ¯à®£ âì `examples/conio2` ¨ `examples/filetest` ç⮡ë ã¡¥¤¨âìáï çâ® ¨ç¥£® ¥ á«®¬ «®áì.
|
||||
|
||||
---
|
||||
|
||||
## �¥è¥¨ï ¯® ¥®¤®§ ç®áâï¬
|
||||
|
||||
1. **Word-wrap vs truncate**: v1 = truncate (¯à®áâ®). v2 - F2 toggle wrap.
|
||||
2. **ƒ®à¨§®â «ìë© áªà®««¨£**: v1 - ¥â; v2 - `<-/->` ᤢ¨£ viewport ¯® á⮫¡æ ¬.
|
||||
3. **Tab handling**: ¯à¥®¡à §®¢ ¨¥ ¯à¨ ।¥à¥, **tabstop = 4** (áâ ¤ àâ MD). Žà¨£¨ « ¢ W3 ¥ âண ¥¬.
|
||||
4. **UTF-8**: ।¥à¨¬ ¡ ©âë ª ª ¥áâì. …᫨ ä ©« ¢ CP866 - ®âà¨áã¥âáï ª¨à¨««¨æ¥© ç¥à¥§ á¨áâ¥¬ë© ä®â. UTF-8 - ¯®ª ¥ ¯®¤¤¥à¦¨¢ ¥¬ (¢¨§ã «ì® ¡ã¤¥â ª è ¥-ASCII ᨬ¢®« å; ¤¥â¥ªâ ¨ ¯à¥¤ã¯à¥¦¤¥¨¥ - ¢ v2).
|
||||
|
||||
---
|
||||
|
||||
### Phase 6 - �®«ë© layout â ¡«¨æ (deferred)
|
||||
|
||||
‘¥©ç á (Phase 4-tables) â ¡«¨æë à¨áãîâáï "¤¥ª®à ⨢®" - ¯ ©¯ë ¨ â¨à¥
|
||||
§ ¬¥ïîâáï box-drawing chars, ® è¨à¨ë ª®«®®ª ¡¥àãâáï ª ª ¥áâì ¨§
|
||||
¨á室¨ª . –¥«ì Phase 6 - ¯¥à¥áç¨â âì â ¡«¨æã ¢ ®à¬ «¨§®¢ ë© ¢¨¤:
|
||||
|
||||
- **Pre-scan â ¡«¨æë**: ¯à®©â¨ ¢á¥ áâப¨ ®¤®£® table-¡«®ª , ©â¨
|
||||
¬ ªá¨¬ «ìãî è¨à¨ã ª ¦¤®© ª®«®ª¨ (á ãçñ⮬ áꥤ¥ëå inline-¬ થ஢ -
|
||||
¢¨§ã «ìë© à §¬¥à, ¥ ¡ ©â®¢ë©).
|
||||
- **Re-emit ¢ ¡ãä¥à¥**: ¯à¨ § £à㧪¥ ä ©« (¨«¨ ¯à¨ ¯¥à¢®© ¢áâà¥ç¥ â ¡«¨æë)
|
||||
¯¥à¥¯¨á âì áâப¨ ¢ FILE_BUF â ª, çâ®¡ë ¢á¥ ï祩ª¨ ®¤®© ª®«®ª¨ ¨¬¥«¨
|
||||
®¤¨ ª®¢ãî è¨à¨ã; ¤®¡ ¢¨âì top/bottom à ¬ª¨ (`ÚÄÂÄ¿` / `ÀÄÁÄÙ`) ª ª
|
||||
á¨â¥â¨ç¥áª¨¥ áâப¨. �â® ¯®§¢®«¨â á®åà ¨âì 1:1 ᮮ⢥âá⢨¥ "«®£¨ç¥áª ï
|
||||
áâப -> ®¤ viewport row" ¡¥§ á¯¥æ¨ «ì®© «®£¨ª¨ ¯à¨ ।¥à¥.
|
||||
- **� ¬ïâì**: re-emit ¬®¦¥â “‚…‹ˆ—ˆ’œ ä ©« § áçñâ padding ¨ ¤®¯.à ¬®ª.
|
||||
…᫨ ¡ãä¥à ¡«¨§®ª ª 16KB - ®â१ âì â ¡«¨æã ¨ ¯®¬¥â¨âì ¥ñ overflow'®¬.
|
||||
- **ƒ®à¨§®â «ìë© áªà®««¨£**: ¥á«¨ ¨â®£®¢ ï è¨à¨ â ¡«¨æë (¨«¨ «î¡®©
|
||||
áâப¨) > SCREEN_W = 80 - ¤®¡ ¢¨âì <-/-> ¤«ï horizontal pan. �â® ¡ã¤¥â
|
||||
®¡é¨© ¬¥å ¨§¬ ¤«ï ¤«¨ëå áâப (á¬. â ª¦¥ wrap mode), ¥ ⮫쪮 â ¡«¨æ.
|
||||
- **‚ëà ¢¨¢ ¨¥ ¨§ separator-row**: `:-` -> left, `-:` -> right, `:-:` ->
|
||||
center; ãç¨âë¢ âì ¯à¨ padding'¥ ᮤ¥à¦¨¬®£® ï祩ª¨.
|
||||
- **˜ £¨ ॠ«¨§ 樨**:
|
||||
1. Walking pass ¯® ä¥á ¬/â ¡«¨æ ¬ ¯àאַ ¢ `index_lines()` - ᮡà âì
|
||||
extents ¢á¥å â ¡«¨æ.
|
||||
2. „«ï ª ¦¤®© â ¡«¨æë - ®¯à¥¤¥«¨âì è¨à¨ë ª®«®®ª.
|
||||
3. �¥è¥¨¥: rewrite-in-buffer (¯à®é¥ ¤«ï ।¥à , ® ¬ãâ¨àã¥â ¨á室¨ª)
|
||||
vs render-time layout (cleaner, ® âॡã¥â ®â¤¥«ì®© áâàãªâãàë
|
||||
®¯¨á ¨ï layout' ª ¦¤ãî â ¡«¨æã).
|
||||
4. Hpan: ®¡é¨© `viewport_x_offset` ¤«ï ¢á¥£® íªà , ¨«¨ ®â¤¥«ìë©
|
||||
"è¨à®ª¨© ०¨¬" ⮫쪮 ¢ãâਠ⠡«¨æ.
|
||||
|
||||
> �¥ ¡«®ª¨àãîé ï ä¨ç . ‡ ¯ã᪠âì ª®£¤ á⠥⠯®ï⥠⨯®¢®© ¨áâ®ç¨ª
|
||||
> markdown-ä ©«®¢ (㧪¨¥ ç¨â «ª¨ -> ¤®áâ â®ç® ⥪ã饣® ¤¥ª®à â®à ;
|
||||
> è¨à®ª¨¥ README á ¡®«ì訬¨ â ¡«¨æ ¬¨ -> 㦥 ¯®«ë© layout).
|
||||
|
||||
---
|
||||
|
||||
### Phase - Šíè ।¥à¥ëå áâப (low priority)
|
||||
|
||||
Žâ«®¦¥®: ⥪ãé ï ᪮à®áâì ¡®«¥¥ 祬 ¤®áâ â®ç . €ªâ¨¢¨à®¢ âì ¥á«¨ ¯®ï¢¨âáï
|
||||
áæ¥ ਩, £¤¥ ¢¨¤ § ¤¥à¦ª PgUp/PgDn ( ¯à¨¬¥à, ¯à¨ âï¦ñ«®¬ inline-¯ àá¥à¥
|
||||
v2 á UTF-8 / linkifier / â ¡«¨æ ¬¨).
|
||||
|
||||
**Šíè ®âä®à¬ â¨à®¢ ëå áâப** (W3, ®â¤¥«ì ï EMM-áâà ¨æ ):
|
||||
|
||||
```
|
||||
Cache layout (16 KB EMM page, ¢á¥£® 16000 ¡ ©â ¨á¯®«ì§ã¥âáï):
|
||||
slot 0: 80 chars + 80 attrs = 160 bytes @ offset 0
|
||||
...
|
||||
slot 99: 80 chars + 80 attrs = 160 bytes @ offset 15840
|
||||
|
||||
Cache tags (W2 static): uint16_t cache_tag[100] = 200 bytes
|
||||
cache_tag[i] = line_id, ¨«¨ 0xFFFF = invalid
|
||||
```
|
||||
|
||||
‘âà ⥣¨ï - **direct map (no LRU)**: `slot = line_id % 100`. Š®««¨§¨ï -> ¢ëâ¥á¥¨¥.
|
||||
|
||||
**Batched viewport render**: 2 page-swap' ‚…‘œ viewport (cache -> file -> cache),
|
||||
¥ 60 ª ª ¯à¨ ¨¢®© ॠ«¨§ 樨.
|
||||
|
||||
�ਠᡮથ: `cache_blk = mem_alloc_pages(1)` ¯®á«¥ `file_blk`; `mem_free_block` exit.
|
||||
|
||||
---
|
||||
|
||||
## —â® ®â«®¦¥® ¢ v2
|
||||
|
||||
- ~~” ©«ë >16 KB~~ - **ᤥ« ® ¢ v1.5** (¤® 128 KB ç¥à¥§ 1-8 EMM-áâà ¨æ + lazy map ¢ W3).
|
||||
- ~~Word wrap~~ - **ᤥ« ®** (Phase 5, F2 toggle).
|
||||
- Search (Find / Find next) - F3 / F4.
|
||||
- F8 Raw / Render toggle - ᯥæ¨ä¨ª æ¨ï ¢ Phase 8.
|
||||
- Links `[text](url)` + image alt - Phase 7.
|
||||
- Tables - Phase 6 (¯®«ë© layout).
|
||||
- Toggle highlight on/off - ç áâë© á«ãç © F8 Raw.
|
||||
@@ -0,0 +1,133 @@
|
||||
# MDView — Просмотрщик Markdown для Sprinter
|
||||
|
||||
**MDView** — программа для просмотра документов в формате *Markdown* на компьютере
|
||||
Sprinter (процессор Z80). Документ хранится в отдельном W3 окне и не занимает
|
||||
основную RAM программы.
|
||||
|
||||
## Возможности
|
||||
|
||||
- Документы до **128 КБ** (8 страниц EMM по 16 КБ каждая)
|
||||
- До **16 384** экранных строк в индексе
|
||||
- Автоматический перенос слов по ширине экрана (80 столбцов)
|
||||
- Горизонтальный сдвиг для широких строк (блоки кода, таблицы)
|
||||
- Статус-бар: имя файла, диапазон строк, процент прокрутки
|
||||
- Спиннер в строке состояния во время загрузки и индексации
|
||||
- Поддержка «мягкого» склеивания строк в абзацах и цитатах
|
||||
|
||||
## Запуск
|
||||
|
||||
```
|
||||
mdview [имя_файла.md]
|
||||
```
|
||||
|
||||
Если имя файла не задано, загружается `README.MD`.
|
||||
|
||||
## Управление
|
||||
|
||||
```
|
||||
Клавиша Действие
|
||||
───────────── ────────────────────────────────────────
|
||||
Up Down Прокрутка на одну строку вверх / вниз
|
||||
PgUp PgDn Прокрутка на страницу (30 строк)
|
||||
Home Начало документа
|
||||
End Конец документа
|
||||
Left Right Горизонтальный сдвиг (только nowrap-строки)
|
||||
F1 Окно справки
|
||||
F10 / Esc Выход из программы
|
||||
```
|
||||
|
||||
## Синтаксис Markdown
|
||||
|
||||
### Заголовки
|
||||
|
||||
Поддерживаются уровни H1–H4. Уровни H5 и H6 отображаются как H4.
|
||||
|
||||
# Заголовок первого уровня
|
||||
## Заголовок второго уровня
|
||||
### Заголовок третьего уровня
|
||||
#### Заголовок четвёртого уровня
|
||||
|
||||
### Текстовое форматирование
|
||||
|
||||
**Жирный текст** выделяется двойными звёздочками: `**текст**`
|
||||
|
||||
*Курсив* выделяется одиночными звёздочками `*текст*` или знаком подчёркивания `_текст_`
|
||||
|
||||
`Встроенный код` обозначается обратными кавычками
|
||||
|
||||
~~Зачёркнутый текст~~ — двойные тильды: `~~текст~~`
|
||||
|
||||
### Ненумерованный список
|
||||
|
||||
Маркеры `-`, `*` или `+`:
|
||||
|
||||
- Первый пункт списка
|
||||
- Второй пункт списка
|
||||
- Третий пункт с достаточно длинным текстом, который при необходимости
|
||||
будет перенесён на следующую строку с сохранением отступа
|
||||
|
||||
### Нумерованный список
|
||||
|
||||
1. Первый элемент
|
||||
2. Второй элемент
|
||||
3. Третий элемент
|
||||
|
||||
### Цитата
|
||||
|
||||
> Блок цитаты начинается с символа `>`. Несколько последовательных
|
||||
> строк одной цитаты склеиваются в единый абзац с автоматическим
|
||||
> переносом слов.
|
||||
|
||||
### Блок кода (verbatim)
|
||||
|
||||
Блок кода заключается в тройные обратные кавычки. Внутри блока
|
||||
текст отображается «как есть» без разбора Markdown:
|
||||
|
||||
```
|
||||
#include <stdio.h>
|
||||
#include <sprinter.h>
|
||||
|
||||
int main(void) {
|
||||
puts("Hello, Sprinter!");
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Горизонтальная линия
|
||||
|
||||
Три или более символов `---`, `***` или `___` на отдельной строке:
|
||||
|
||||
---
|
||||
|
||||
## Технические характеристики
|
||||
|
||||
- **Платформа:** Sprinter, процессор Z80 @ 21 МГц
|
||||
- **Кодировка:** CP866 (DOS Cyrillic)
|
||||
- **Максимальный размер файла:** 128 КБ
|
||||
- **Максимальное число строк в индексе:** 16 384
|
||||
- **Режим памяти:** small
|
||||
- Код программы, cтек, данные, куча — окнa W1-W2 (32 КБ, адреса 0x4000–0xBFFF).
|
||||
- Буфер файла — страницы EMM, отображаемые в W3 (0xC000–0xFFFF)
|
||||
|
||||
## TODO
|
||||
|
||||
1. **Увеличение размера документов.** Снять лимит 128 КБ: Достаточно
|
||||
разрешить работать с большим кол-вом страниц памяти, пока оттестированно
|
||||
на работе с 8-мю страницами по 16Кб.
|
||||
|
||||
2. **Форматированные таблицы.** Разбирать строки вида `| ячейка | ячейка |`
|
||||
с автоматическим выравниванием столбцов и отрисовкой разделительных
|
||||
линий (строки `|---|---|`). На текущий момент таблицы отображаются
|
||||
как обычные nowrap-строки без выравнивания.
|
||||
|
||||
3. **Поддержка кодировок CP1251 и UTF-8.** Автоопределение кодировки
|
||||
по BOM, явное указание через аргумент командной строки (`--encoding cp1251`),
|
||||
возможность переключения кодировки во время просмотра (`F8`).
|
||||
Нужно прежде всего для документов на русском языке; CP866 кодировака уже поддерживается.
|
||||
|
||||
4. **Ускорение рендеринга.** Кэш строк экрана. Оптимизация цикла вывода
|
||||
символов через BIOS WRCHAR (пакетный вывод, DMA).
|
||||
|
||||
---
|
||||
|
||||
*MDView v0.2 · (c) 2026 Петров А.Г.*
|
||||
@@ -1,5 +0,0 @@
|
||||
MDVIEW Sample Document
|
||||
|
||||
This is a sample Markdown file for testing the Sprinter *mdview text
|
||||
viewer*. Phase 3 adds inline emphasis: bold, italic and underscore
|
||||
runs render with distinct background colours.
|
||||
@@ -0,0 +1,103 @@
|
||||
# mdview — модель документа и рендеринг (единый смешанный режим)
|
||||
Дизайн-документ переработки `mdview.c` под новое ТЗ форматирования (`examples/mdview/todo2`). Описывает решение по хранению данных, разбор документа при загрузке, упрощение рендера и снятие лимита на число строк.
|
||||
## 1. Контекст и главный вопрос
|
||||
Мы отказываемся от двух режимов показа (Wrap/UnWrap-переключатель) и переходим к единому смешанному режиму: тип переноса задаётся типом блока (обычный текст/заголовки/списки/цитаты — Wrap; код/таблицы — UnWrap).
|
||||
Главный вопрос: нужно ли хранить оригинальный байтовый контент файла, или при загрузке сразу преобразовать его в «готовый к показу» текст (склеить строки параграфов, убрать маркеры, развернуть отступы и т.п.)?
|
||||
Ответ: **оригинал храним; отдельный «готовый» текстовый/ячеечный буфер не строим.** «Подготовка при загрузке» реализуется как построение компактного *индекса метаданных*, а не как преобразование *содержимого*.
|
||||
## 2. Решение по архитектуре
|
||||
### 2.1 Текущее состояние (база)
|
||||
* Файл целиком лежит в EMM-страницах (до 8×16 КБ). В окно W3 (`0xC000`) в каждый момент замаплена ровно одна страница; доступ к байту — через `fb()`/`map_page()` (`mdview.c:198-213`).
|
||||
* Индекс — это параллельные массивы по экранным сегментам: `line_offset[]` (смещение в файле) + битфлаги `cont/nowrap/blank/in_code`, 2-битный `line_kind[]`, `init_style[]` (`mdview.c:122-135`).
|
||||
* `index_lines()` за один проход уже делает склейку параграфов обычного текста, перенос по словам на 80 колонок и перенос emphasis через soft/hard break (`mdview.c:692-911`).
|
||||
* `render_line()` при каждой отрисовке заново читает байты из оригинала и заново парсит inline-разметку (`mdview.c:918-1156`).
|
||||
Вывод: «готовим при загрузке» мы уже делаем — но готовим **индекс**, а не текст.
|
||||
### 2.2 Почему не материализуем содержимое
|
||||
* **Одно свободное окно W3.** Режим памяти `small`: код в W1, данные/стек/куча в W2, для банкуемых данных свободен только W3. Преобразование «оригинал → готовый буфер» требует одновременно держать замапленными исходную страницу (чтение) и страницу-приёмник (запись). При единственном окне это поток swap-ов на каждую границу. In-place преобразование тоже невозможно: склейка меняет длины, смещения «съезжают», параграф пересекает границу 16 КБ.
|
||||
* **Удвоение памяти и срыв гарантии 128 КБ.** Оригинал может занимать все 8 страниц; готовой копии нужны свои страницы — гарантировать, что влезут обе, нельзя.
|
||||
* **Готовая форма не обязательно меньше.** Снятие маркеров экономит байты, но добавляются отступы-продолжения у переносов списков/цитат. В лучшем случае ≈ размер оригинала, в худшем — больше. Ячеечная модель (символ+атрибут) — это ×2 (до 256 КБ), невозможно.
|
||||
* **Покадровая стоимость и так мала.** За кадр рисуются только 30 видимых строк (~30×80 чтений). Единственный дорогой проход — `index_lines()` — неизбежен в любой архитектуре (нужен полный скан для `n_lines` и процента прокрутки).
|
||||
### 2.3 Что реально оптимизировать
|
||||
Не текст, а повторную работу `render_line()`:
|
||||
* вызов `classify_line()` на каждый кадр (`mdview.c:984`);
|
||||
* обратный проход к первому не-cont сегменту ради отступа продолжения (`mdview.c:944-970`).
|
||||
Это снимается переносом результата классификации (`kind`, ширина отступа/контент-колонка) в сам индекс на этапе `index_lines()`.
|
||||
## 3. Модель данных индекса
|
||||
### 3.1 Запись сегмента
|
||||
Единая запись на экранный сегмент полностью заменяет нынешние параллельные массивы (`line_offset`, `cont_flag`, `in_code`, `nowrap_flag`, `blank_flag`, `line_kind`, `init_style`); `index_lines()` переписывается с нуля под эту модель. Цель размера записи — 5–6 байт:
|
||||
* `offset` — 3 байта (24-битное смещение в файле, покрывает 128 КБ).
|
||||
* `flags` — 1 байт: биты `cont`, `nowrap`, `blank`, `in_code` + 2-битный `ckind` (тип продолжения: PLAIN/QUOTE/LIST/OTHER).
|
||||
* `style` — 1 байт: стартовый стиль сегмента (`init_style`, теперь включая STRIKE) + при необходимости глубина вложенности.
|
||||
* `indent` — 1 байт: предвычисленная контент-колонка/ширина префикса, чтобы рендер не вызывал `classify_line()` и не делал обратный проход.
|
||||
### 3.2 Размещение и снятие лимита `MAX_LINES`
|
||||
Проблема: `line_offset` сейчас `uint32_t[2048]` = 8 КБ, `init_style` = 2 КБ; суммарно статический индекс ~11.6 КБ near-памяти (W2). Рост лимита в near невозможен — W2 переполнится.
|
||||
Решение:
|
||||
* **Индекс храним в отдельном EMM-блоке** (свои страницы, помимо файловых), доступ — через тот же W3.
|
||||
* **Near-кэш viewport**: перед отрисовкой кадра разово вычитываем записи для `VIEW_H+1` видимых сегментов в маленький near-массив (≈ `(VIEW_H+1)×6` ≈ 186 байт). Рендер работает по near-кэшу + читает только контент-страницы. Это устраняет per-byte thrashing между страницей индекса и страницей контента: переключений на кадр — единицы, а не тысячи.
|
||||
* **Динамический размер**: число страниц под индекс выделяем пропорционально размеру файла (число сегментов ∈ размеру). `MAX_LINES` становится функцией от выделенных страниц индекса. Это прямо ложится на заметку v2 («чем больше банков под файл, тем больше буферы»).
|
||||
* **Без регресса скорости при индексе в EMM**: чтобы вынос индекса в банки не замедлил сборку (W3 делится между чтением контента и записью индекса), записи копим в near-буфере батчами и сбрасываем в EMM-блок через `bank_write()` (он сам сохраняет/восстанавливает маппинг W3). Переключений окна на всю сборку — единицы, а не на каждый сегмент.
|
||||
* Если индекс-страницы выделить не удалось — деградируем до текущего near-лимита и показываем явную диагностику обрезки (а не молчаливый обрыв в `emit_seg()` — `mdview.c:574`).
|
||||
## 4. `index_lines()` — разбор по новому ТЗ
|
||||
Единый проход по файлу строит сегменты. Деление на параграфы — по пустым строкам; несколько пустых строк подряд схлопываются в одну.
|
||||
### 4.0 Скорость подготовки — главный приоритет
|
||||
Требование: подготовка максимально быстрая (сейчас ~25 КБ готовятся 6–10 с). Две структурные причины медленности и их устранение:
|
||||
* **Per-byte `fb(uint32_t)`.** Каждый доступ к байту пересчитывает страницу 32-битными `p >> 14` и `p & 0x3FFF` (`mdview.c:209-213`). На Z80 32-битная арифметика — это программные подпрограммы на каждый символ. Замена: **потоковый разбор** — мапим страницу один раз, идём по окну `char *`/16-битным индексом, страницу переключаем только на границе 16 КБ. 32-битным остаётся лишь сохраняемый в индекс `offset`.
|
||||
* **Многократные пере-сканы.** На каждом `\n` внутри параграфа вызываются `is_fence_raw()`, `is_hr_raw()`, `classify_line()`, `is_line_blank()` (`mdview.c:800-809`) — каждая заново сканирует следующую строку, а `classify_line()` ещё и повторяет цикл детекции HR. Для параграфа из N строк — O(N×длина) лишней работы. Замена: **один проход** — каждую строку классифицируем ровно один раз в момент её начала, lookahead — минимальный (несколько первых байт).
|
||||
* **32-битные сравнения.** Курсор скана — (страница:8 бит, смещение:16 бит); сравнение с концом — сначала по странице, потом 16-битно.
|
||||
Ожидаемый эффект: подготовка — по сути один линейный проход с 16-битными операциями, кратное ускорение относительно текущего multi-pass + 32-bit. (`render_line()` может остаться на `fb()` — там только ~30×80 байт за кадр.)
|
||||
### 4.1 Обычный текст (Wrap)
|
||||
* **Soft break** (одиночный `\n`): склейка, следующая строка продолжается через пробел.
|
||||
* **Wide break**: 2+ пробелов перед `\n` **или** символ `\` перед `\n` → принудительный перенос внутри параграфа, стиль сохраняется. (Текущий код ловит только 2 пробела — `mdview.c:813-818`; добавить ветку для `\`.)
|
||||
* **Hard break** (пустая строка): новый параграф, отделяется ОДНОЙ пустой строкой независимо от числа пустых строк в оригинале.
|
||||
* **Модификаторы** bold/italic/strike/code действуют через soft/wide break внутри параграфа и сбрасываются на границе параграфа. (Добавить STRIKE `~~…~~` — сейчас его нет в `INIT_STYLE_*`/`ATTR_*`.)
|
||||
### 4.2 Заголовки (Wrap)
|
||||
Один оригинальный абзац-строка; стартовый стиль по уровню. Во входе распознаём H1–H6, но H4/H5/H6 далее обрабатываются одинаково как H4 (сливаются в один стиль; `classify_line()` уже сворачивает `lvl>4` → `LK_H4`, `mdview.c:488`). Внутри допустимы bold/italic/code/strike. После заголовка всегда пустая строка.
|
||||
### 4.3 Горизонтальный разделитель HR
|
||||
Всегда одна строка, после неё всегда пустая строка (новый абзац).
|
||||
### 4.4 Списки (Wrap) — НОВОЕ: многострочная склейка
|
||||
* Пункт может занимать несколько оригинальных строк; soft break внутри пункта склеивается через пробел (как обычный текст). Сейчас списки эмитятся построчно (`mdview.c:762-772`) — переписать на paragraph-модель.
|
||||
* Новая строка с префиксом пункта → новый пункт.
|
||||
* Пустая строка завершает пункт. Следующая непустая НЕ-пункт строка не является продолжением.
|
||||
* **Группировка**: если после ОДНОЙ пустой строки идёт снова пункт — это тот же список, пустая строка в показе подавляется (пункты идут вплотную). Только ДВЕ+ пустые строки между пунктами разрывают на разные списки (в показе — одна пустая строка между ними).
|
||||
* Перенос продолжения пункта печатается с отступом до контент-колонки (для уровня 1 — 2 пробела).
|
||||
* Незакрытые модификаторы НЕ переносятся на следующий пункт (каждый пункт — свой параграф).
|
||||
* Вложенные списки поддерживаются (отступ растёт с ведущими пробелами).
|
||||
* Отдельные стили: префикс маркера и текст списка.
|
||||
Пример соответствует разделу «Списки» в `todo2` (строки 1–6).
|
||||
### 4.5 Цитаты (Wrap)
|
||||
* Отдельный параграф; перед текстом — префикс цитаты, перенесённые строки тоже предваряются префиксом.
|
||||
* Многострочная склейка как у текста; пустая строка-цитата (`>`) показывается как пустая строка с префиксом.
|
||||
* Вложенность (`> >` → двойной префикс). Отдельные стили: префикс и текст цитаты.
|
||||
### 4.6 Блок кода ``` ``` ``` (UnWrap)
|
||||
Весь блок одним стилем кода, без inline-модификаторов. После блока обязательна пустая строка. Строки не переносятся (truncate + горизонтальный скролл).
|
||||
### 4.7 Таблицы (UnWrap)
|
||||
Пока as-is, без переноса. Выравнивание столбцов — v2.
|
||||
## 5. `render_line()` — упрощение
|
||||
* Убрать ветку truncate-режима и `wrap_mode` (уже частично снято; `toggle_wrap()` — мёртвая заглушка `mdview.c:1286-1291`, удалить вместе с упоминаниями F2).
|
||||
* Не вызывать `classify_line()` и не делать обратный проход: использовать `kind`/`indent`/`style` из индекса.
|
||||
* Для cont-сегментов списков/цитат — печать отступа/префикса по `kind`+`indent` из записи сегмента.
|
||||
* Inline-парсинг emphasis выполняется только в пределах видимого сегмента (дёшево); для кода/таблиц — отключён.
|
||||
## 6. Горизонтальный скроллинг (UnWrap)
|
||||
* Скроллится только UnWrap-текст (код/таблицы). Грануляция 8 символов (`HPAN_STEP`).
|
||||
* Правый край: индикатор `>` своим стилем, если есть скрытый контент справа (есть — `mdview.c:1135-1155`).
|
||||
* **Добавить** левый индикатор `<` в первой колонке, когда `viewport_x > 0`.
|
||||
* **Границы скролла по факту**: текущий кламп жёстко до 240 (`mdview.c:1275`). Заменить на вычисление максимального переполнения среди UnWrap-строк в текущем viewport, чтобы вправо нельзя было уйти за самую длинную строку, а влево — до колонки 0.
|
||||
## 7. Чеклист расхождений с текущим кодом
|
||||
* [индекс] Перейти на запись-на-сегмент в EMM-банке + near-кэш viewport; снять `MAX_LINES=2048`.
|
||||
* [скорость] Потоковый разбор: один проход, 16-битный курсор в окне (без per-byte `fb()` с 32-битной арифметикой), классификация строки один раз, минимальный lookahead; батч-флеш индекса.
|
||||
* [текст] Wide break по символу `\`.
|
||||
* [текст] Модификатор strikethrough `~~…~~` (+ стиль).
|
||||
* [списки] Многострочная склейка пунктов и правило группировки по одной/двум пустым строкам.
|
||||
* [цитаты] Многострочная склейка и повтор префикса (в т.ч. вложенные) на переносах.
|
||||
* [заголовки] Читать H1–H6; H4/H5/H6 трактовать как H4 (частично уже есть — `mdview.c:488`).
|
||||
* [скролл] Индикатор `<` и корректные границы по фактическому переполнению.
|
||||
* [рендер] Снять per-кадровый `classify_line()` и обратный проход (данные — из индекса).
|
||||
* [очистка] Удалить `toggle_wrap()` и упоминания F2 (`mdview.c:1286-1291`, `1311`).
|
||||
## 8. Память: бюджет
|
||||
* near (W2): текущий статический индекс ~11.6 КБ — у предела окна. После переноса `line_offset`/`init_style` в EMM в near остаётся near-кэш viewport (~0.2 КБ) + мелкие флаги → запас под стек/кучу растёт.
|
||||
* EMM: файл до 8 страниц + индекс ~1–2 страницы (при 5–6 байт/сегмент и нескольких тысячах сегментов). Перед выделением проверять `mem_info()` на доступность страниц.
|
||||
## 9. Заметки для v2
|
||||
* **Прогрессивный показ**: отрисовать первую страницу (первые ~30 сегментов) ДО завершения полной подготовки; остальное доиндексировать дальше или по мере прокрутки. Процент и `End` показывать как «вычисляется», пока не готов полный `n_lines`. (Синергия с потоковым разбором §4.0: первый экран готов после разбора лишь нескольких КБ.)
|
||||
* Разделители переноса Wrap: точка/запятая/`!`/`?`/дефис; правило «новая строка не начинается с разделителя», серия разделителей остаётся на первой строке.
|
||||
* Таблицы: вычисление ширины столбцов и выравнивание.
|
||||
* Подсветка синтаксиса внутри блоков кода.
|
||||
* URL/Images и прочие типы строк.
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
# mdview — унифицированный рендеринг (Unified Wrap + Paragraph Model)
|
||||
|
||||
|
||||
## Цель
|
||||
Отказаться от двух режимов Wrap/Unwrap. Ввести единую модель отображения Markdown-документов, соответствующую стандарту CommonMark:
|
||||
* Обычный текст: параграфы склеиваются по soft breaks (`\n` → пробел), перенос по словам на 80 колонок.
|
||||
* Hard break (` \n`): принудительный перенос внутри параграфа.
|
||||
* Paragraph break (`\n\n`): новый абзац (пустая строка между блоками).
|
||||
* Fenced code block и таблицы (будущее): не переносятся, работают как truncate + горизонтальный скроллинг + индикатор `>`.
|
||||
* Горизонтальный скроллинг активен только когда в текущем viewport есть nowrap-строки.
|
||||
|
||||
## Текущее состояние
|
||||
* `mdview.c` использует `wrap_mode` (0 = truncate, 1 = wrap) и `toggle_wrap()` (F2).
|
||||
* `index_lines()` делает два разных прохода: truncate (1 строка файла = 1 строка экрана) и wrap (сегментация по SCREEN_W).
|
||||
* `render_line()` имеет ветвление по `wrap_mode` и `cont`.
|
||||
* `line_offset[]`, `cont_flag[]`, `in_code[]`, `init_style[]` — существующие структуры.
|
||||
|
||||
## Предлагаемые изменения
|
||||
|
||||
### 1. Новые структуры данных
|
||||
* Добавить `nowrap_flag[]` (битмап, аналогично `cont_flag[]`): строка не должна переноситься (code block, HR, таблица).
|
||||
* Добавить `line_kind[]` (2 бита на строку, 512 байт для 2048 строк): хранит классификацию (PLAIN, H1-H4, HR, ULIST, OLIST, QUOTE, CODE, TABLE). Нужен для continuation-сегментов, чтобы render_line знал, какой префикс/отступ повторять.
|
||||
* Убрать `wrap_mode` и `toggle_wrap()`.
|
||||
* Убрать truncate-ветку из `index_lines()` и `render_line()`.
|
||||
* `viewport_x` остаётся глобальным, но применяется только к `nowrap` строкам.
|
||||
|
||||
### 2. Новый `index_lines()` — paragraph scanner
|
||||
Единый проход по файлу, без двух режимов.
|
||||
|
||||
#### 2.1 Сканирование «сущностей»
|
||||
Walk по файлу от `p = 0` до `file_size`:
|
||||
1. Если `fb(p) == '`' × 3 → Fenced code block. Все строки до закрывающего ` ``` ` — `nowrap_flag = 1`, `in_code = 1` (для body), `line_kind = CODE`. Длинные строки не wrap'аются.
|
||||
2. Если строка начинается с `|...|` (будущее) → Table. `nowrap_flag = 1`, `line_kind = TABLE`.
|
||||
3. Если строка — HR (`---`/`***`/`___`) → `nowrap_flag = 1`, `line_kind = HR`. 1 строка экрана.
|
||||
4. Если строка — Header (`#...`), List (`- ` / `* ` / `+ ` / `N. ` / `N) `), Quote (`> `) → начало **Normal paragraph / block**. Обрабатывается как единый параграф до `\n\n` или EOF или code block.
|
||||
5. Иначе — Normal paragraph (plain text).
|
||||
|
||||
#### 2.2 Normal paragraph — обработка
|
||||
Внутри параграфа символы читаются как единый поток (не прерываясь на `\n`):
|
||||
* `\n\n` → конец параграфа. Emit текущую сегментную строку (если есть). Добавить пустую строку (1 строка экрана с нулевой длиной, или offset указывающий на второй `\n`).
|
||||
* ` \n` (два пробела или таб + пробел перед `\n`) → **Hard break**. Emit текущую сегментную строку. Начать новую сегментную строку с того же абзаца, **сохранив `line_style`**. `prev_ch = ' '`.
|
||||
* Обычный `\n` → **Soft break**. Считаем как 1 пробел: `visible col++`, `prev_ch = ' '`. Продолжаем текущую сегментную строку.
|
||||
* Wrap at `SCREEN_W` — так же как сейчас, по `last_space`. При создании continuation-сегмента: `set_cont()`, `set_init_style_raw()` (carry emphasis), `set_line_kind()` (carry list/quote kind для отступа/префикса).
|
||||
* Emphasis tracking (`line_style`) работает через весь параграф, через soft/hard breaks.
|
||||
|
||||
#### 2.3 Fenced code block — обработка
|
||||
* Delimiter строка (` `): `line_kind = CODE`, `nowrap_flag = 1`, рендерится как пустая строка (или строка с ` ` — но сейчас пустая).
|
||||
* Body строки: каждая физическая строка = 1 строка экрана. `nowrap_flag = 1`, `in_code = 1`, `line_kind = CODE`. Нет inline parsing, нет wrap.
|
||||
* Если строка длиннее `SCREEN_W` — truncation (не wrap). `render_line()` рисует `>` в последней колонке, если есть content за пределами `SCREEN_W + viewport_x`.
|
||||
|
||||
#### 2.4 List continuation
|
||||
* Первая строка list item: `line_kind = ULIST/OLIST`, `marker_visible_col` определяет отступ. Текст начинается после маркера.
|
||||
* Wrap внутри list item: `set_line_kind()` = тот же `ULIST`/`OLIST`. `render_line()` для continuation-сегмента не рисует маркер, но добавляет отступ = `marker_visible_col` (т.е. количество пробелов до текста первой строки).
|
||||
* Для вложенных списков отступ растёт, потому что `marker_visible_col` учитывает ведущие пробелы.
|
||||
|
||||
#### 2.5 Quote continuation
|
||||
* Первая строка quote: `line_kind = QUOTE`, `marker_visible_col` = отступ + 2 (для `│ `).
|
||||
* Wrap внутри quote: `set_line_kind()` = `QUOTE`. `render_line()` для continuation рисует префикс `│ ` (или пробелы, если символ не нужен на всех кроме первой строки).
|
||||
|
||||
### 3. Изменения в `render_line()`
|
||||
* Убрать ветку `if (!wrap_mode)` (truncate mode больше не существует).
|
||||
* Добавить `if (is_nowrap(line_idx))`: применяется `viewport_x`, отображается `>` в колонке 79 если контент выходит за пределы.
|
||||
* Для `cont` сегментов (wrap continuation):
|
||||
* Если `line_kind` == `QUOTE`: повторить префикс отступа + `│ ` (или только отступ, если решено показывать `│` только на первой строке).
|
||||
* Если `line_kind` == `ULIST`/`OLIST`: повторить отступ = `marker_visible_col` (без маркера/цифр). Текст начинается с той же колонки, что и на первой строке item.
|
||||
* Для plain text: просто продолжить текст без префиксов.
|
||||
* Для `nowrap` строк: применять `viewport_x` как смещение. Текст сдвигается влево, скрытые символы не рисуются. Если за пределами видимой зоны есть ещё контент — `>` в колонке 79.
|
||||
|
||||
### 4. Горизонтальный скроллинг
|
||||
* `scroll_h()` проверяет: есть ли в текущем viewport (top_line .. top_line+VIEW_H-1) хотя бы одна строка с `nowrap_flag == 1`. Если нет — return (no-op).
|
||||
* Для `nowrap` строк `render_line()` использует `viewport_x` при отображении: символы с `cc < viewport_x` пропускаются, `cc >= viewport_x` рисуются.
|
||||
* Для wrap-строк `viewport_x` игнорируется (эффективно = 0).
|
||||
* При `Home`/`End`/`PgUp`/`PgDn`/`Up`/`Down` — `viewport_x` не сбрасывается (пользователь может скроллить по вертикали, оставаясь на горизонтальном смещении для code block).
|
||||
|
||||
### 5. Очистка и навигация
|
||||
* Убрать `wrap_mode`, `toggle_wrap()`.
|
||||
* Убрать F2 из меню и help.
|
||||
* `scroll_up`/`scroll_down`/`scroll_h`/`clamp_top` — обновить без ссылок на `wrap_mode`.
|
||||
* `calc_pct` и статусная строка — обновить, убрать ссылку на wrap/unwrap.
|
||||
|
||||
### 6. Порядок реализации (по файлу mdview.c)
|
||||
1. **Структуры данных**: добавить `nowrap_flag[]`, `line_kind[]`, убрать `wrap_mode`.
|
||||
2. **`index_lines()`**: полностью переписать в paragraph scanner. Это самый сложный и объёмный блок.
|
||||
3. **`render_line()`**: убрать truncate, добавить nowrap/quote/list continuation, добавить `viewport_x` для nowrap.
|
||||
4. **`scroll_h()`**: добавить проверку наличия nowrap в viewport.
|
||||
5. **Очистка**: убрать toggle_wrap, обновить меню/help, обновить main() (убрать F2).
|
||||
6. **Тестирование**: soft breaks, hard breaks, paragraph breaks, emphasis через soft breaks, fenced code blocks, list wrap с отступом, quote wrap с префиксом, горизонтальный скролл только для code.
|
||||
|
||||
## Orchestration
|
||||
План реализуется одним агентом (последовательно в одном файле), параллелизм не требуется. Дочерние агенты не используются.
|
||||
+753
-711
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,180 @@
|
||||
Новое требование к первоначальной подготовке документа.
|
||||
|
||||
Правила форматирования и показа -
|
||||
1) Деление на параграфы. Параграфы разделяются пустыми строками.
|
||||
2) Заголовки H1-H6 - отдельные параграфы (после них всегда пустые строки)
|
||||
3) Разделители (---) - тоже - после них всегда пустые строки.
|
||||
4) Таблица/код (через ```)/списки - это единый параграф.
|
||||
|
||||
Типы текста (строк) -
|
||||
|
||||
Обычный текст
|
||||
|
||||
Может в оригинале находиться на нескольких строках.
|
||||
Строки могут разделяться
|
||||
- обычный break - это когда на строке после текста идет перевод строки -
|
||||
такой текст просто объединяется (в нашем случае если возможно то следующая строка продолжает
|
||||
выводиться на той же строке что и предыдущая, только отделяется от нее уже не переводом строки
|
||||
а через пробел.
|
||||
- широкий break - это когда перед переводом строки есть два или более пробелов или перед переводом
|
||||
строки находится символ обратный слэш '\' - в этом случае и когда мы выводим текст на экран следующая
|
||||
строка начинается с новой строки.
|
||||
- жесткий break - это когда две строки обычного текста разделяются двумя или более переводами
|
||||
стрки (то есть между ними как минимум есть одна пустая строка) - в этом случае вторая строка
|
||||
будет считаться новым параграфом и отделяться от предыдущей строки ОДНОЙ пустой строкой (вне
|
||||
зависимости от того сколько пустых строк в оригинале.
|
||||
Должно сохраняться действие модификатора bold/italic/strike начатое на одной строке на следующие
|
||||
если они так же находятся в этом параграфе. Новый параграф (после пустой строки) теряет воздействие
|
||||
незакрытого модификатора из предыдущего параграфа.
|
||||
Тип переноса строк - Wrap.
|
||||
|
||||
Заголовки -
|
||||
|
||||
абзацы из одной оригинальной строки. Начальный стиль зависит от типа заголовка (в нем могут встречаться
|
||||
модификаторы bold/italic/code/strike.
|
||||
Тип переноса строк - Wrap.
|
||||
|
||||
Разделитель (horizontal rules) -
|
||||
|
||||
Всегда одна строка. После нее выводим пустую строку всегда (следующий текст - новый абзац)
|
||||
|
||||
Списки
|
||||
|
||||
как и в обычном тексте - один пункт списка может находиться в оригинале на нескольких строках.
|
||||
если появляется новая строка с префиксом пункта списка - это означает что с этой строки начинается
|
||||
новый пункт списка. если появляется пустая строка - это означает что следующая за пустой строкой
|
||||
непустая строка уже не является продолжением пункта списка.
|
||||
Если следующая за пустой строкой строка так же является пунктом списка то такая строка считается
|
||||
продолжением текущего списка. Только если две строки списков разделены ДВУМЯ и более пустыми
|
||||
строками то вторая строка с пунктом списка будет считаться началом нового списка -
|
||||
Пример -
|
||||
|
||||
- строка 1
|
||||
- строка 2
|
||||
строка 3
|
||||
- строка 4
|
||||
|
||||
- строка 5
|
||||
|
||||
|
||||
- строка 6
|
||||
|
||||
Должно отображаться так -
|
||||
|
||||
- строка 1
|
||||
- строка 2 строка 3
|
||||
- строка 4
|
||||
- строка 5
|
||||
|
||||
- строка 6
|
||||
|
||||
Тип переноса строк - Wrap.
|
||||
Замечание - перенесенная строка продолжение пункта списка должна начинаться с отступа в
|
||||
несколько пробелов (для списка первого уровня - два пробела) - то есть с той же позиции
|
||||
что и начальный текст строки -
|
||||
Пример -
|
||||
|
||||
- длинная строка которая не может поместиться и будет перенесена по слову 'будет'
|
||||
|
||||
Должно отображаться так -
|
||||
- длинная строка которая не может поместиться и
|
||||
будет перенесена по слову 'будет'
|
||||
|
||||
Незакрытые модификаторы типа (bold/etc.) не переносят свою модификацию не последующие пункты списка.
|
||||
Действуют только в пределах одного пункта.
|
||||
(то есть фактически - пункт списка - это отдельный параграф но следующий пункт списка (тоже отдельный
|
||||
параграф) не отделяется от него пустой строкой.
|
||||
Вложенные списки - поддерживаются.
|
||||
Для отображения префиксов пунктов списка используется свой стиль. Так же для текста списков используется
|
||||
отдельный (от обычного текста) стиль.
|
||||
|
||||
Блок Кода -
|
||||
|
||||
Весь блок кода отображается только одним стилем - стилем Кода. В нем не действуют модификаторы
|
||||
bold/italic/etc (возможно в дальнейшей использование парсера языка для кода что бы отобразить
|
||||
этот код с подсветкой синтаксиса этого языка, но сейчас весь блок рисуется только одним стилем)
|
||||
После блока кода - обязательна пустая строка.
|
||||
Тип переноса строк - UnWrap. - То есть строки НЕ ПЕРЕНОСЯТСЯ.
|
||||
|
||||
Quoted -
|
||||
|
||||
Отображается как отдельный параграф. Перед отображением текста отображается символ префикса Цитирования
|
||||
Способы переноса текста аналогичны обычному тексту, за исключение того что перенесенные строки так же
|
||||
предваряются префиксом Цитирования
|
||||
Для отображения префиксов Цитирования используется свой стиль. Так же для текста цитирования используется
|
||||
отдельный (от обычного текста) стиль.
|
||||
Тип переноса строк - Wrap.
|
||||
Пример форматирования -
|
||||
|
||||
> Первый параграф
|
||||
>
|
||||
> Второй параграф
|
||||
> > Вложенный параграф
|
||||
>
|
||||
> Продолжение основной цитаты - длинная строка (переносится по слову 'строка')
|
||||
|
||||
Будет отображаться так -
|
||||
| Первый параграф
|
||||
|
|
||||
| Второй параграф
|
||||
| | Вложенный параграф
|
||||
|
|
||||
| Продолжение основной цитаты - длинная
|
||||
| строка (переносится по слову 'строка')
|
||||
|
||||
|
||||
Таблицы -
|
||||
|
||||
Пока отображаются as is.
|
||||
Дальше возможно предусмотрим вариант вычисления ширины столбцов и форматирование
|
||||
вывода что бы все ячейки столбца имели одинаковую ширину.
|
||||
Тип переноса строк - UnWrap.
|
||||
|
||||
Типы переноса строк -
|
||||
|
||||
Wrap -
|
||||
|
||||
происходит перенос текста с одной строки на другую по разделителям пробелам (предусмотреть
|
||||
во второй версии возможность использовать разделителем знаков точка, запятая, восклицательный
|
||||
знак, вопросительный знак, дефис. Замечание - новая строка не может начинаться со знаков
|
||||
разделителей - то есть если у нас идет многоточие (три точки) то нельзя что бы одна точка
|
||||
была в конце первой строки а остальные две на другой - если идут несколько разделителей подряд
|
||||
то они считаются как один и должны оставаться на первой строке).
|
||||
|
||||
UnWrap -
|
||||
|
||||
Текст НЕ ПЕРЕНОСИТСЯ.
|
||||
Если строка не помещается на экране - то в конец строки на экране выводим символ-знак наличия
|
||||
продолжения строки справа за краем экрана (свой стиль для этого символа).
|
||||
Символ '>'.
|
||||
Если есть хотя бы одна строка которая не помещается на экране - то разрешаем горизонтальный
|
||||
скроллинг.
|
||||
ЗАМЕЧАНИЕ - скроллируется только текст выводящийся в режиме UnWrap.
|
||||
Если произведен скроллинг влево (строки UnWrap начинаются показываться не с первой позиции)
|
||||
то на первой позиции отображем другой символ '<' сообщающий пользователю что есть текст
|
||||
за левым краем экрана.
|
||||
Скроллирование не бесконечно - если на экране нет строк UnWrap для которых есть скрытый текст
|
||||
за правым краем экрана то скроллирование влево больше не возможно, и наоборот - если при
|
||||
скроллировании вправо дошли до показа строк UnWrap с первой позиции то дальше скроллинг в этом
|
||||
направлении так же невозможен. Скроллинг идет с грануляцией по 8 символов.
|
||||
(сейчас UnWrap текст это только блоки кода и таблицы).
|
||||
|
||||
|
||||
Проанализируй данную постановку задачи.
|
||||
|
||||
Что требуется -
|
||||
|
||||
Мы более не поддерживаем два режима показа - Wrap/UnWrap - только один (смешанный).
|
||||
Потому вопрос - надо ли нам сохранять оригинальный контент считанного файла с диска ?
|
||||
Или при подготовке данных считываемых с диска можно сразу преобразовывать его в формат
|
||||
готовый для показа (объединять строки в одном параграфе и так далее) ?
|
||||
Ограничения - не забывать о том что это все работает на компьютере с 8-ми битным процессором.
|
||||
Потому очень экономно относимся к памяти и лишней работе процессора - все должно быть весьма
|
||||
быстрым и компактным.
|
||||
Требования для работы с файлами 128 Кб сохраняется.
|
||||
|
||||
Для версии 2 -
|
||||
Подумай о возможности использования банков памяти не только для содержимого файла а например
|
||||
для буферов типа line_offset/init_style и прочих (тогда их размер может так же быть динамическим
|
||||
и чем больше банков памяти будет использовано для чтения файла тем больше станут размеры этих
|
||||
буферов.
|
||||
@@ -0,0 +1,49 @@
|
||||
# Build mdview2.exe — Markdown viewer for Sprinter (render-cache version).
|
||||
#
|
||||
# Фаза 0: скаффолдинг — копия mdview.c как baseline, без изменений
|
||||
# логики. План — docs/mdview2-plan.md.
|
||||
#
|
||||
# small memory mode: code in W1, data/stack/heap in W2 (32 KB total).
|
||||
# W3 stays free for the file buffer (EMM-mapped).
|
||||
|
||||
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||
EXAMPLE := mdview2
|
||||
MEMORY := small
|
||||
|
||||
include $(PROJ_ROOT)/app.mk
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Образ дискеты: только mdview2.exe + README.MD (перекодированный
|
||||
# из UTF-8 в CP866 — рабочую кодировку Sprinter).
|
||||
#
|
||||
# README.MD хранится в репозитории в UTF-8; iconv -c конвертирует
|
||||
# его в CP866, отбрасывая символы без аналога в целевой кодировке.
|
||||
# Результат кладётся в .disk_tmp/README.MD, чтобы make_disk.py
|
||||
# использовал правильное имя файла на диске.
|
||||
#
|
||||
# iconv -c возвращает ненулевой код, если хоть один символ отброшен
|
||||
# (даже с -c) — это ОЖИДАЕМО при потере символов без аналога в CP866,
|
||||
# не ошибка конвертации; вывод при этом всё равно корректно записан.
|
||||
# Поэтому код возврата iconv игнорируется (|| true).
|
||||
# ------------------------------------------------------------------
|
||||
DISK_TMP := .disk_tmp
|
||||
README_DISK := $(DISK_TMP)/README.MD
|
||||
|
||||
$(DISK_TMP):
|
||||
mkdir -p $@
|
||||
|
||||
$(README_DISK): README.MD | $(DISK_TMP)
|
||||
iconv -c -f UTF-8 -t CP866 README.MD > $@ || true
|
||||
|
||||
# UTF8TEST.MD кладётся на диск КАК ЕСТЬ (в UTF-8, без перекодировки) —
|
||||
# это тестовый вход для проверки UTF-8 рендеринга (Фаза 2 кодировок).
|
||||
floppy: $(EXAMPLE).exe $(README_DISK)
|
||||
python3 $(MAKE_DISK) $(FLOPPY_IMG) $(EXAMPLE).exe $(README_DISK) UTF8TEST.MD
|
||||
@echo
|
||||
@echo "Floppy ready: $(FLOPPY_IMG)"
|
||||
@echo "Run: cd $(MAME_DIR) && ./run_mame.sh"
|
||||
|
||||
clean:
|
||||
rm -rf .sprinter-cc-* $(EXAMPLE).exe $(DISK_TMP)
|
||||
|
||||
.PHONY: all clean floppy run
|
||||
@@ -0,0 +1,799 @@
|
||||
# MDView — Просмотрщик Markdown для Sprinter
|
||||
|
||||
**MDView** — программа для просмотра документов в формате *Markdown* на компьютере
|
||||
Sprinter (процессор Z80). Документ хранится в отдельном W3 окне и не занимает
|
||||
основную RAM программы.
|
||||
|
||||
## Возможности
|
||||
|
||||
- Документы до **128 КБ** (8 страниц EMM по 16 КБ каждая)
|
||||
- До **16 384** экранных строк в индексе
|
||||
- Автоматический перенос слов по ширине экрана (80 столбцов)
|
||||
- Горизонтальный сдвиг для широких строк (блоки кода, таблицы)
|
||||
- Статус-бар: имя файла, диапазон строк, процент прокрутки
|
||||
- Спиннер в строке состояния во время загрузки и индексации
|
||||
- Поддержка «мягкого» склеивания строк в абзацах и цитатах
|
||||
|
||||
## Запуск
|
||||
|
||||
```
|
||||
mdview [имя_файла.md]
|
||||
```
|
||||
|
||||
Если имя файла не задано, загружается `README.MD`.
|
||||
|
||||
## Управление
|
||||
|
||||
```
|
||||
Клавиша Действие
|
||||
───────────── ────────────────────────────────────────
|
||||
Up Down Прокрутка на одну строку вверх / вниз
|
||||
PgUp PgDn Прокрутка на страницу (30 строк)
|
||||
Home Начало документа
|
||||
End Конец документа
|
||||
Left Right Горизонтальный сдвиг (только nowrap-строки)
|
||||
F1 Окно справки
|
||||
F8 Кодировка: CP866 → CP1251 → KOI8-R → UTF-8 → ...
|
||||
F10 / Esc Выход из программы
|
||||
```
|
||||
|
||||
Кодировка определяется автоматически при открытии (BOM + эвристика по
|
||||
первым 4 КБ); `F8` переключает её вручную, если детекция ошиблась.
|
||||
8-битные кодировки (CP866/CP1251/KOI8-R) переключаются мгновенно (ремап на
|
||||
отрисовке). Второй набор индекс/кэша (например UTF-8) строится лениво — при
|
||||
первом переключении в него (короткая пауза со спиннером), дальше мгновенно.
|
||||
|
||||
## Синтаксис Markdown
|
||||
|
||||
### Заголовки
|
||||
|
||||
Поддерживаются уровни H1–H4. Уровни H5 и H6 отображаются как H4.
|
||||
|
||||
# Заголовок первого уровня
|
||||
## Заголовок второго уровня
|
||||
### Заголовок третьего уровня
|
||||
#### Заголовок четвёртого уровня
|
||||
|
||||
### Текстовое форматирование
|
||||
|
||||
**Жирный текст** выделяется двойными звёздочками: `**текст**`
|
||||
|
||||
*Курсив* выделяется одиночными звёздочками `*текст*` или знаком подчёркивания `_текст_`
|
||||
|
||||
`Встроенный код` обозначается обратными кавычками
|
||||
|
||||
~~Зачёркнутый текст~~ — двойные тильды: `~~текст~~`
|
||||
|
||||
### Ненумерованный список
|
||||
|
||||
Маркеры `-`, `*` или `+`:
|
||||
|
||||
- Первый пункт списка
|
||||
- Второй пункт списка
|
||||
- Третий пункт с достаточно длинным текстом, который при необходимости
|
||||
будет перенесён на следующую строку с сохранением отступа
|
||||
|
||||
### Нумерованный список
|
||||
|
||||
1. Первый элемент
|
||||
2. Второй элемент
|
||||
3. Третий элемент
|
||||
|
||||
### Цитата
|
||||
|
||||
> Блок цитаты начинается с символа `>`. Несколько последовательных
|
||||
> строк одной цитаты склеиваются в единый абзац с автоматическим
|
||||
> переносом слов.
|
||||
|
||||
### Блок кода (verbatim)
|
||||
|
||||
Блок кода заключается в тройные обратные кавычки. Внутри блока
|
||||
текст отображается «как есть» без разбора Markdown:
|
||||
|
||||
```
|
||||
#include <stdio.h>
|
||||
#include <sprinter.h>
|
||||
|
||||
int main(void) {
|
||||
puts("Hello, Sprinter!");
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### Горизонтальная линия
|
||||
|
||||
Три или более символов `---`, `***` или `___` на отдельной строке:
|
||||
|
||||
---
|
||||
|
||||
## Технические характеристики
|
||||
|
||||
- **Платформа:** Sprinter, процессор Z80 @ 21 МГц
|
||||
- **Кодировки:** CP866 / CP1251 / KOI8-R / UTF-8 (автоопределение, `F8`)
|
||||
- **Максимальный размер файла:** 128 КБ
|
||||
- **Максимальное число строк в индексе:** 16 384
|
||||
- **Режим памяти:** small
|
||||
- Код программы, cтек, данные, куча — окнa W1-W2 (32 КБ, адреса 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 <this repo> sprinter-c
|
||||
cd sprinter-c
|
||||
make sdcc # one-time: fetch SDCC 4.5 binary (~25 MB)
|
||||
make all # build mkexe + libsprinter.lib + 27 examples
|
||||
make floppy # pack everything into mame/v306/IMG/mc.img
|
||||
cd mame/v306 && ./run_mame.sh # boot Sprinter in MAME
|
||||
```
|
||||
|
||||
Compile a single program:
|
||||
|
||||
```sh
|
||||
cat > hello.c <<EOF
|
||||
#include <stdio.h>
|
||||
int main(void) { puts("Hello, Sprinter!"); return 0; }
|
||||
EOF
|
||||
|
||||
bin/sprinter-cc -o hello.exe hello.c
|
||||
```
|
||||
|
||||
That's it — `hello.exe` is now a valid SprintEXE you can `RUN HELLO` from the ESTEX shell.
|
||||
|
||||
## Memory modes
|
||||
|
||||
Sprinter's address space is four 16 KB windows (W0 / W1 / W2 / W3). DSS allocates
|
||||
pages by program size — small programs get only one page. Pick a memory mode based
|
||||
on what your program needs:
|
||||
|
||||
| Mode | Code lives in | Banking | Use when | Note |
|
||||
|---|---|---|---|---|
|
||||
| `tiny` (default) | W2 (0x8100+) | no | code+data < 14 KB | |
|
||||
| `small` | W1-W2 (0x4100+) | no | code+data < 30 KB | |
|
||||
| `big` | W2 + W1 banking | yes (W1) | tiny + extra code modules | |
|
||||
| `huge` | W1-W2 + W3 banking | yes (W3) | small + extra code modules | |
|
||||
| `manual` | user-specified | optional | special layouts | Not implemented |
|
||||
|
||||
```sh
|
||||
sprinter-cc --memory small -o big.exe bigprog.c
|
||||
sprinter-cc --memory huge -o app.exe main.c --bank 1=engine.c --bank 2=ai.c
|
||||
```
|
||||
|
||||
Banked functions are declared with `__banked`:
|
||||
|
||||
```c
|
||||
void engine_tick(int dt) __banked; // lives in BANK1, automatically swapped
|
||||
```
|
||||
|
||||
## Examples (27 total)
|
||||
|
||||
| Example | What it demonstrates |
|
||||
|---|---|
|
||||
| `hello` | Hello world with stdio + conio Turbo-C-style colors |
|
||||
| `argv` | argv parsing in crt0 |
|
||||
| `cat` | File I/O — read & print TEST.TXT |
|
||||
| `seek` | 32-bit lseek over a 100 KB file |
|
||||
| `ls` | Directory listing via ffirst/fnext |
|
||||
| `filetest` | FILE\* streams (fopen/fread/...) |
|
||||
| `stattest` | `stat`/`fstat` on files and directories |
|
||||
| `errno` | errno / strerror / perror |
|
||||
| `mem_test` | Page allocator + bank\_read/bank\_write |
|
||||
| `malloc` | Heap stress test (200+ allocations) |
|
||||
| `banked` | Banked code in W3 (huge mode) |
|
||||
| `bankedbg` | Banked code in W1 (big mode) |
|
||||
| `banklocl` | Bank-local static data and BSS |
|
||||
| `mouse` | Mouse driver in text mode |
|
||||
| `gfx_mous` | Mouse with custom bitmap cursor in graphics mode |
|
||||
| `gfx_demo` | 320×256×256 lines / rects / accelerator |
|
||||
| `gfx_d16` | 640×256×16 same primitives |
|
||||
| `gfx_text` | Bitmap-font text on graphics screen |
|
||||
| `timedir` | Date/time + directory listing |
|
||||
| `ptime` | POSIX time API |
|
||||
| `openenv` | open() flags + environment vars |
|
||||
| `conio` | conio API smoke test |
|
||||
| `attrprob` | Probe Sprinter text attribute byte layout |
|
||||
| `strtest` | string.h test (from SDCC's z80.lib) |
|
||||
| `stdlib` | stdlib.h test (qsort / rand / strtol / etc.) |
|
||||
| `assrtest` | assert() |
|
||||
| `rt_test` | Runtime helpers (sleep, setjmp, atexit) |
|
||||
|
||||
## Headers
|
||||
|
||||
Standard:
|
||||
* `<stdio.h>` — puts / printf / FILE\* + Sprinter-specific dec/hex helpers
|
||||
* `<stdlib.h>` — atoi / atof / malloc / qsort / ... (from SDCC z80.lib)
|
||||
* `<string.h>` / `<ctype.h>` / `<math.h>` — from SDCC z80.lib
|
||||
* `<unistd.h>` — read / write / close / lseek / unlink
|
||||
* `<fcntl.h>` — open / creat + O\_RDONLY / O\_CREAT / ...
|
||||
* `<errno.h>` — errno + error names + strerror
|
||||
* `<sys/stat.h>` — stat / fstat
|
||||
* `<setjmp.h>` / `<assert.h>` — from SDCC
|
||||
|
||||
Sprinter-specific:
|
||||
* `<conio.h>` — putch / cputs / cprintf, textcolor / textbackground / textattr, kbhit / getch, clrscr, gotoxy, wherex/y
|
||||
* `<gfx.h>` — gfx_init/done, palette, putpixel, hline/vline/rect/fill_rect/line, text — for both 320 and 640 modes (gfx_\*16 variants)
|
||||
* `<mouse.h>` — full 14-function driver wrapper + mouse_cursor_t with bitmap support
|
||||
* `<dir.h>` — chdir / getcwd / mkdir / rmdir / ffirst / fnext / ffblk
|
||||
* `<time.h>` — getdatetime / setdatetime + POSIX time / localtime / etc.
|
||||
* `<sprinter.h>` — raw ports, ESTEX/BIOS function numbers, env API
|
||||
* `<sprinter_exit.h>` — exit / \_exit / atexit
|
||||
* `<sprinter_mem.h>` — mem\_alloc\_pages / mem\_free\_block / bank\_read / bank\_write
|
||||
* `<sprinter_compat.h>` — Solid-C compatibility layer (aliases + BOOL/WORD/uint types)
|
||||
|
||||
## Toolchain commands
|
||||
|
||||
```sh
|
||||
make all # build mkexe + lib + every example
|
||||
make floppy # repack mame/v306/IMG/mc.img with all .exe files
|
||||
make check # 17 mkexe unit-tests
|
||||
make clean # remove all build artefacts
|
||||
make sdcc # one-time: fetch SDCC 4.5 binary
|
||||
```
|
||||
|
||||
### sprinter-cc options
|
||||
|
||||
```
|
||||
sprinter-cc -o foo.exe foo.c [more.c ...] [options]
|
||||
|
||||
--memory MODE tiny | small | big | huge | manual (default: tiny)
|
||||
--memory-manual SPEC explicit placement (CODE=W1|W2,DATA=W1|W2|SAME,BANKED=W1|W3)
|
||||
--stack-size N bytes reserved for the stack (default ~1278)
|
||||
--crt0=TYPE default | minimal | banked | small
|
||||
--bank N=FILE.c compile FILE.c into bank N (repeatable, max 15)
|
||||
--debug enable runtime diagnostics (defines DEBUG_RT)
|
||||
-I PATH extra include path
|
||||
-L 0xADDR / -E / -S override load / entry / stack addresses
|
||||
-Wl FLAG pass FLAG to sdldz80
|
||||
--mkexe FLAG pass FLAG to mkexe (e.g. --mkexe -p --mkexe 0 for bank padding)
|
||||
-v verbose
|
||||
```
|
||||
|
||||
## Status
|
||||
|
||||
What works in v1.0:
|
||||
* Compile / link / pack to SprintEXE — verified on all 27 examples
|
||||
* Four memory modes (tiny / small / big / huge)
|
||||
* Graphics (both modes) with accelerator
|
||||
* Mouse (text + graphics cursor)
|
||||
* File I/O, directories, environment, time
|
||||
* All headers listed above
|
||||
|
||||
Deferred to v2.0 (see `docs/TODO.md`):
|
||||
* **Turbo-C-style BGI graphics API** — `initgraph` / `setcolor` / `circle` /
|
||||
`getimage` / `putimage` / etc. on top of our `gfx_*` primitives
|
||||
* Remaining Solid-C compatibility gaps (Phase 2/3) — see `docs/solid_c_compatibility.md`
|
||||
* Manual memory mode
|
||||
* Rewrite FILE\* stream API (current implementation is very primitive and doesn't use buffers)
|
||||
|
||||
Deferred to v3.0:
|
||||
* **IM2 interrupt handlers** — research complete (`docs/im2_isr_design.md`),
|
||||
implementation scheduled for v3
|
||||
* **Audio API** (AY-3-8910 + COVOX) — requires IM2
|
||||
* **ISA-8 slot drivers** — requires IM2 (???)
|
||||
|
||||
## Documentation
|
||||
|
||||
* `docs/TODO.md` — roadmap and open work items
|
||||
* `docs/solid_c_compatibility.md` — gap analysis vs Solid-C 2004
|
||||
* `docs/im2_isr_design.md` — interrupt handler design (v2)
|
||||
* `docs/converted/` — source documentation (ESTEX, BIOS, architecture)
|
||||
converted to plain text for `grep`
|
||||
* `docs/reference/`, `docs/samples/`, `docs/memory management/` — original
|
||||
Russian docs and code samples from Peters Plus
|
||||
|
||||
## Repository layout
|
||||
|
||||
```
|
||||
bin/sprinter-cc one-line compiler driver (bash)
|
||||
toolchain/mkexe/ host-side tool: .ihx -> .exe SprintEXE
|
||||
toolchain/check_banks.py post-link bank size enforcer
|
||||
runtime/ crt0 variants (default, minimal, small, banked)
|
||||
bank trampolines, heap, heap_top
|
||||
libc/include/ headers
|
||||
libc/io|stdio|mem|gfx/ C and asm sources for libsprinter.lib
|
||||
lib/ Makefile that archives libsprinter.lib via sdar
|
||||
examples/ 27 example programs
|
||||
mame/v306/ MAME binary + Sprinter ROM/HDD images + floppy script
|
||||
third_party/sdcc/ vendored SDCC 4.5 (fetched via `make sdcc`)
|
||||
third_party/solid-c/ reference: original Sprinter native C (for compat target)
|
||||
docs/ documentation
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This repository contains:
|
||||
* Original code in `bin/`, `toolchain/`, `runtime/`, `libc/`, `lib/`, `examples/` —
|
||||
MIT-licensed.
|
||||
* `third_party/sdcc/` — SDCC 4.5 under GPLv2 with linking exception
|
||||
(see `third_party/sdcc/COPYING.txt`)
|
||||
* `third_party/solid-c/` — original Sprinter Solid C, used only as a reference
|
||||
|
||||
## Credits
|
||||
|
||||
* **Sprinter / Peters Plus** — Иван Мак, Дмитрий Паринов and the original team
|
||||
* **SDCC** — for the underlying Z80 compiler
|
||||
* **MAME** — for the Sprinter Sp2000 emulation
|
||||
|
||||
---
|
||||
|
||||
For questions / patches: see CONTRIBUTING.md (TBD) or open an issue.
|
||||
|
||||
---
|
||||
|
||||
# TODO / Roadmap
|
||||
|
||||
Открытые задачи в порядке убывания приоритета. По мере появления реальных программ — приоритеты будут смещаться.
|
||||
|
||||
## Этап 5 — malloc / free + banking-aware page allocator ✅ ГОТОВО
|
||||
|
||||
- [x] SDCC's `malloc`/`free` + наш `runtime/heap.s` (полностью заменяет library heap.rel, 14000-байтный heap в окне 2)
|
||||
- [x] `libc/mem/mem_alloc.c` — page allocator: `mem_alloc_pages`/`mem_free_block`/`mem_get_page`/`mem_info` через ESTEX `$3C/$3D/$3E` + BIOS `$C4`
|
||||
- [x] `libc/mem/bank_io.c` — HOME-резидентные `bank_read`/`bank_write`/`bank_load_byte`/`bank_store_byte` со свопом W3 внутри
|
||||
- [x] `examples/malloc_test/` — проверка SDCC's malloc (~210 64-байтных allocations через всю heap)
|
||||
- [x] `examples/mem_test/` — проверка page allocator: 3 страницы, разные паттерны через bank_write, верификация через bank_read
|
||||
|
||||
## Этап 6 — argv parsing + sprinter-cc wrapper ✅ ГОТОВО
|
||||
|
||||
- [x] crt0 парсит ESTEX command-line из IX-prefix (inline asm в `runtime/crt0.s`)
|
||||
- [x] Strip leading CP/M-style space (DSS quirk)
|
||||
- [x] Передача `argc`/`argv` в main() через HL/DE (SDCC __sdcccall(1) ABI)
|
||||
- [x] argv[0] = basename .EXE через ESTEX APPINFO ($47 subfn 2)
|
||||
- [x] `runtime/crt0_minimal.s` — opt-out для очень маленьких программ
|
||||
- [x] `runtime/crt0_banked.s` — теперь тоже парсит argv (parse_argv + get_progname скопированы из crt0.s; будет factored в argv.s когда возьмёмся за libsprinter.lib)
|
||||
- [x] Bash-обёртка `bin/sprinter-cc`: `sprinter-cc -o foo.exe foo.c` одной строкой
|
||||
- [x] Поддержка опций: `--memory`, `--memory-manual`, `--stack-size`, `--crt0=`, `--bank N=FILE.c`, `--debug`, `-I`, `-L`/`-E`/`-S`, `-Wl`, `--mkexe`
|
||||
|
||||
## Этап 8 — графика (320×256×256 + 640×256×16 + accel + bitmap font) ✅ ГОТОВО
|
||||
|
||||
- [x] **8a** Graphics core: `gfx_init`/`gfx_done`/`gfx_clear`/`gfx_putpixel`/`gfx_pal_load`/`gfx_pal_set` (libc/gfx/gfx_core.c). Палитра через BIOS PIC_SET_PAL ($A4). Verified 320×256×256.
|
||||
- [x] **8b** Линии/прямоугольники/fill через accelerator (libc/gfx/gfx_lines.c): `gfx_hline`/`gfx_vline` через accel Fill (LD C,C / LD E,E + SMC block-size), `gfx_rect`/`gfx_fill_rect` с heuristic выбором ориентации (h/v bursts count), `gfx_line` с Bresenham для диагоналей. `gfx_clear` тоже переписан на column-major accel (~4× быстрее).
|
||||
- [x] **8c** 640×256×16 mode (libc/gfx/gfx_16.c): `gfx_*16` API, HIGH nibble = LEFT pixel (документация misleading), per-row RMW для vline (один байт = 2 горизонтальных пикселя).
|
||||
- [x] **8d** Bitmap font + gfx_text (libc/gfx/gfx_text.c): шрифт через BIOS WIN_GET_ZG ($B8), interleaved layout `font[row*256+char]`, `gfx_text`/`gfx_putchar` для 320 mode, `gfx_text16`/`gfx_putchar16` для 640 mode с pair-table lookup.
|
||||
|
||||
См. memory/sprinter_graphics.md, sprinter_accelerator.md, sprinter_graphics_16.md, sprinter_font_format.md.
|
||||
|
||||
Открытые мелочи (не блокируют):
|
||||
- [ ] Шрифт-quad для 640: per-cell палитра (mode 0x82 разрешает 1 из 4 палитр per 16×8 cell) — через прямой доступ к area-описания экрана 0x0300..0x039F
|
||||
|
||||
## Auto-banking (см. `memory/banking_roadmap.md` для деталей)
|
||||
|
||||
Phase 1 — file-level bin-packing — реализовывать когда проект перерастёт ~30 KB кода.
|
||||
|
||||
- [ ] `toolchain/auto_bank.py`:
|
||||
- Парсит размеры из `.rel`-файлов (или из .map после dry-run link'а)
|
||||
- First-fit-decreasing bin-packing
|
||||
- Уважает `#pragma codeseg BANKn` как manual override
|
||||
- Перелинковывает с новыми `-Wl-b_BANKn=...` параметрами
|
||||
- Печатает план распределения
|
||||
|
||||
Phase 2-5: incremental rebalance, declarative `banks.toml`, function-level, call-graph-aware. Только если/когда понадобится.
|
||||
|
||||
## Bank-local static data (mutable data в том же банке что и код) — ✅ ГОТОВО
|
||||
|
||||
- [x] Пример `examples/bank_local_data/` — функция в BANK1 со своим writable BSS array + const table + malloc-тест
|
||||
- [x] `mkexe -p 0` для нулевого padding банков (BSS-storage обнуляется при загрузке)
|
||||
- [x] Канонический рецепт: `--codeseg BANK1 --constseg BANK1 --dataseg BANK1` для bank1.c + `-Wl-b_BANK1=0x1C000` для линковки. **`--dataseg BANK1` РАБОТАЕТ** — раньше казалось обратное из-за trampoline bug который маскировал результат.
|
||||
- [x] **Критичный фикс trampoline'a в runtime/bank.s** — старый `pop af; out (n), a` клобберил A → все banked-функции возвращающие uint8_t тихо возвращали мусор. Новый `pop bc; out (c), b` сохраняет A.
|
||||
- [x] **malloc из banked-функции работает прозрачно** — heap живёт в W2 (HOME), W2 никогда не свапается trampoline'ом, pointer валиден из любого контекста. См. memory/bank_local_data_pattern.md.
|
||||
- [x] Документация в memory: `memory/bank_local_data_pattern.md` (полный рецепт + malloc + nuances), `memory/sdcc_banking.md` (trampoline fix)
|
||||
- [ ] Опционально — расширить `check_banks.py` чтобы показывать разбивку size = code + const + bss per bank (cosmetic)
|
||||
|
||||
Зачем: для модулей с большим private state (level loader, audio engine, scene data). Экономит W2 heap для динамики, а статика остаётся в бэке.
|
||||
|
||||
## Подсказки из solid-c (нативный Sprinter C — `third_party/solid-c/`)
|
||||
|
||||
После анализа solid-c'овской libc (см. `memory/solid_c_findings.md`) выявлены готовые паттерны для следующих недостающих функций. Приоритет от **высокого** к низкому:
|
||||
|
||||
### High-priority gaps (легко портировать, большая польза)
|
||||
- [x] **`errno` + `strerror`/`perror`** — табличка 32 ошибок (libc/io/errno.c)
|
||||
- [x] **Расширенный `open()`** для O_CREAT/O_TRUNC/O_APPEND/O_EXCL state machine
|
||||
- [x] **`atexit`** — 8-callback LIFO + `exit()` + `_exit()` (libc/io/atexit.c)
|
||||
- [x] **`setjmp`/`longjmp`** — 6-байт jmp_buf={sp,ix,pc} (libc/io/setjmp.c)
|
||||
- [x] **`sleep(seconds)`** — 50Hz halt-loop (libc/io/sleep.c)
|
||||
- [x] **ESTEX ENV API** ($46, getenv/putenv) — libc/io/env.c. Учли doc-bug: реально A=0 это NOT FOUND
|
||||
|
||||
### Medium-priority (нужно для shell-like утилит)
|
||||
- [ ] **Mouse driver** — `rst $30h`, 17 функций. **Сначала тест что работает в MAME**.
|
||||
- [x] **`ffirst`/`fnext` + ffblk_t struct** для directory listing — реализовано, demo: ls.exe
|
||||
- [x] **`getdatetime`/`setdatetime`** через ESTEX $21/$22 — libc/io/time.c, demo: time_dir_test
|
||||
- [x] **`chdir`/`getcwd`/`mkdir`/`rmdir`** — wrappers для ESTEX $1B-$1E — libc/io/fsdir.c
|
||||
- [x] **conio: `kbhit`/`getch`/`getche`/`cputs`/`clrscr`/`gotoxy`** — реализовано
|
||||
- [x] **conio extras**: `wherex`/`wherey` ($53), `wrchar`/`rdchar` ($58/$57), `textmode_get/set` ($50/$51), `clrscr_attr` ($56) + COLOR macros
|
||||
|
||||
### Low-priority — ✅ FILE* stack ГОТОВО
|
||||
|
||||
- [x] **Минимальный unbuffered FILE\*** — `libc/stdio/file.c` + `libc/include/stdio.h`. fopen/fclose/fputs/fgets/fread/fwrite/fseek/ftell/rewind/feof/ferror/clearerr/fflush + stdin/stdout/stderr как pseudo-streams. См. `memory/file_star_design.md` и `examples/filetest`.
|
||||
- [ ] fprintf / fscanf — нужна printf-через-callback machinery. Пока пользователь может `sprintf(buf, ...) + fputs(buf, fp)`.
|
||||
- [ ] Опциональный buffered mode (setvbuf, line/block buffering) — если когда-то понадобится.
|
||||
|
||||
### POSIX time API — ✅ ГОТОВО
|
||||
- [x] `libc/io/posix_time.c` — time/localtime/gmtime/mktime/asctime/ctime поверх getdatetime. SDCC's time.rel избегаем (нельзя override _RtcRead). См. `examples/ptime`.
|
||||
|
||||
### sys/stat — ✅ ГОТОВО
|
||||
- [x] `libc/io/stat.c` — POSIX stat/fstat. Гибрид open+fstat для файлов, ffirst+iter для папок (включая "."/".."). См. `examples/stattest` и `memory/estex_ffirst_dotdot.md`.
|
||||
|
||||
### assert — ✅ ГОТОВО (используем SDCC's __assert через fallback include path)
|
||||
|
||||
## libc/stdlib — ✅ не нужно делать (см. memory/sdcc_stdlib_works.md)
|
||||
|
||||
Проверено через `examples/stdlib_test/`: SDCC z80.lib содержит работающие реализации:
|
||||
- `atoi/atol/atof, strtol/strtoul, rand/srand, qsort/bsearch, abs/labs, div/ldiv`
|
||||
- Полный `<string.h>` (memchr/cmp/set/cpy, strcat/cmp/cpy/len/chr/spn/etc.)
|
||||
- `<ctype.h>` (toupper/tolower)
|
||||
- `<math.h>` (sinf/cosf/sqrtf/etc.)
|
||||
|
||||
Линкер автоматически тянет из z80.lib когда нужно. **НЕ переписывать**.
|
||||
|
||||
Наши Sprinter-specific обязательные модули остаются: atexit, env, errno, setjmp, putchar/puts/getchar, conio, fsdir, time, mouse, open/read/lseek/close.
|
||||
|
||||
## Build-system: libsprinter.lib + sprinter-cc — ✅ ГОТОВО
|
||||
|
||||
- [x] `lib/Makefile` — собирает каждый libc/*.c в `.rel`, архивирует через sdar в `lib/sprinter.lib`
|
||||
- [x] Включает runtime/bank.s и runtime/heap.s (auto-pulled при __banked/malloc)
|
||||
- [x] `bin/sprinter-cc` — bash-wrapper: `sprinter-cc -o foo.exe foo.c` одной строкой
|
||||
- [x] Поддержка опций `--crt0=default|minimal|banked`, `--bank N=FILE.c`, `-I`, `-L`/`-E`/`-S`, `-Wl`, `--mkexe`
|
||||
- [x] `examples/hello_sccc/` — демо: `hello.c` собирается за один shell-вызов, размер совпадает с ручным Makefile (925 байт)
|
||||
- [x] Split `putchar.c` → `putchar.c` + `puts.c` для per-function granularity (puts override SDCC's z80.lib version)
|
||||
- [x] Включён в `make all` (зависимость `lib` перед `examples`)
|
||||
|
||||
Возможные улучшения (опционально):
|
||||
- [ ] Мигрировать остальные examples на sprinter-cc вместо ручных Makefile (косметика)
|
||||
- [ ] Дальнейшая декомпозиция libc/*.c per-function (но текущая granularity уже даёт нужный размер — линкер пакетует .rel целиком, и для большинства файлов это одна функция)
|
||||
|
||||
## Этап 9 — memory modes для sprinter-cc
|
||||
|
||||
DSS выделяет страницы памяти по размеру приложения: < 16 KB → одна страница, в остальные окна подключается «страница #FF» (read=0xFF, write игнорится). Из-за этого CODE-в-W1 + DATA-в-W2 для маленькой программы молча ломается. См. [memory/sprinter_memory_modes.md](../../.claude/projects/-Volumes-SAM8-Projects-DIY-Z80-Sprinter-C-Compiler/memory/sprinter_memory_modes.md).
|
||||
|
||||
- [x] **`tiny`**: всё (CODE+DATA+стек) в W2. Default. Verified hello/argv/conio/malloc/file/etc.
|
||||
- [x] **`--memory MODE` флаг в sprinter-cc**: parser + per-mode дефолты CODE_LOC/DATA_LOC, override через явные `--code-loc`/`--data-loc`. tiny работает; small/big/huge компилируются с warning'ом (runtime не готов). Реализовано 2026-05-30.
|
||||
- [x] **`--memory-manual SPEC`**: парсит `CODE=W1|W2,DATA=W1|W2|SAME,BANKED=W1|W3`. Реализовано 2026-05-30.
|
||||
- [x] **`small` runtime**: `runtime/crt0_small.s` использует ESTEX `$3D GETMEM` + `$3A SETWIN2` чтобы выделить и замапить W2-страницу ДО gsinit. **НЕ** BIOS `$C4` — стек на этом этапе в W1 (boot_stack в HOME), а BIOS требует стек в W2. После маппинга SP переключается на 0xBFFE, дальше стандартный flow. Реализовано 2026-05-30, verified hello.exe.
|
||||
- [x] **`small` auto-detect для >16 KB программ**: `crt0_small.s` читает порт `0xC2` (текущая страница в W2 — не `0xA2`! это W1). Если 0xFF — выделяет page; иначе DSS уже сделала это (программа сама вылезла в W2). Один crt0 покрывает 0..30 KB. mkexe также разрешает HOME span W1+W2 (0x4000..0xBFFF). Verified hello: small (5 KB файл, SETWIN2 path) + 32 KB файл (auto-skip). Реализовано 2026-05-30.
|
||||
- [x] **`big` runtime** (tiny + banked code в W1): параметризовали `crt0_banked.s` + `bank.s` через `.ifdef BANK_W1` — другой banking port (0xA2 vs 0xE2), другой load-addr (0x4000 vs 0xC000). sprinter-cc prepend'ит `BANK_W1 = 1` при `--memory big`, передаёт `mkexe -B 0x4000`. Пример `examples/banked_big/`. Реализовано 2026-05-30.
|
||||
- [x] **`huge` runtime** (small + banked code в W3): merge W2-detect логики из `crt0_small.s` в `crt0_banked.s`. Существующий пример `examples/banked/` теперь использует MEMORY=huge. Реализовано 2026-05-30.
|
||||
- [x] **`--debug` флаг**: prepend `DEBUG_RT = 1` в crt0 + `-DDEBUG_RT` в sdcc. Открывает symbol `_w2_self_allocated` (uint8_t) — runtime diagnostic кто аллоцировал W2. Реализовано 2026-05-30.
|
||||
|
||||
### Дизайн-решения по libc и crt0
|
||||
|
||||
**Одна `sprinter.lib`** работает для всех memory mode — `.rel`-члены relocatable, SDLD делает dead-code elimination per-member (без графики не подтягивает `gfx_core.rel` и т.д.). Verified hello vs malloc_test через map-файлы.
|
||||
|
||||
**`gfx.lib` отдельно — НЕ нужен**: dead-code elimination уже работает.
|
||||
|
||||
**`libc_banked` (libc в bank вместо HOME)** — идея на потом, когда HOME (16 KB) забит user-кодом + libc в `huge` mode. Реализуется через `--codeseg BANK0` при компиляции libc; trade-off: trampoline ~30 циклов на каждый libc-вызов. Триггер: реальная программа упрётся в HOME budget.
|
||||
|
||||
**HW-зависимые модули — `sprinter_home.lib` отдельно.** Часть libc физически не может быть забанкована в W3, потому что она РАБОТАЕТ с W3:
|
||||
- `gfx_*` — пишет в видеопамять `0xC000+` после swap W3 на video page
|
||||
- `bank_io` (mem_alloc_pages/bank_read/bank_write) — swap'ит W3 через `OUT (0xE2)`
|
||||
- Будущие ISR — прерывание может прийти когда W3 на чём угодно
|
||||
|
||||
В huge mode эти модули ДОЛЖНЫ остаться в HOME (W1). Когда будем делать `libc_banked`, **одновременно** выделяем `sprinter_home.lib` (HOME-only) из `sprinter.lib` (bankable). Финальная схема:
|
||||
```
|
||||
sprinter_home.lib HOME-only: gfx, bank_io, ISR shims
|
||||
sprinter.lib bankable: printf, malloc, string, conio, stdio, env, ...
|
||||
sprinter_banked.lib тот же sprinter.lib но --codeseg BANK0 (для huge)
|
||||
```
|
||||
Триггер: реализация `--memory huge` runtime.
|
||||
|
||||
**crt0 — по одному на mode:**
|
||||
- `crt0.s` — текущий, для **tiny/big**: SP=0xBFFE, парсит argv (W2-ресурс уже выделен DSS).
|
||||
- `crt0_minimal.s` — текущий, для tiny без argv.
|
||||
- `crt0_small.s` — **новый, step 3**: для **small/huge**, аллоцирует W2 через `mem_alloc_pages` ДО gsinit, маппит в порт `0xA2`, потом стандартный flow.
|
||||
- `crt0_banked.s` — текущий, для **big**: trampoline-таблица для W3 банков, CODE в W2.
|
||||
- `crt0_banked_small.s` — **новый**: huge = small (W2-alloc) + banked (W3 trampolines).
|
||||
|
||||
sprinter-cc подбирает crt0 по `--memory` mode (сейчас `--crt0=` это override).
|
||||
- [x] **Настраиваемый размер стека**: флаг `sprinter-cc --stack-size BYTES`. Wrapper генерирует `heap_top.s` с `___sdcc_heap_end = stack_top + 1 - stack_size`, отдельный .rel линкуется per-program. Default ≈1278 байт (heap_top=0xBB00) из `runtime/heap_top.s`. Реализовано 2026-05-30.
|
||||
|
||||
Интерфейс: `sprinter-cc --memory [tiny|small|big|huge|manual] [--memory-manual SPEC] [--stack-size N] foo.c`. `--memory-manual` имеет смысл только с `--memory manual`.
|
||||
|
||||
## Known issues / quirks
|
||||
|
||||
- **ESTEX $46 ENV API**: ✅ работает. Док-ция в `DiskSyscalls.txt v1.6` ошибочно описывает return-status — A=0 это NOT FOUND, не FOUND. Зафиксировано в `memory/sprinter_platform.md`.
|
||||
|
||||
## ОБЯЗАТЕЛЬНЫЕ ЗАДАЧИ ДЛЯ V2 (после релиза v1)
|
||||
|
||||
### Turbo-C-style graphics API (BGI-like) — **MUST для v2**
|
||||
|
||||
Расширить наш `gfx_*` API до уровня **Turbo-C `<graphics.h>`** (BGI) для MS-DOS.
|
||||
Программисты привыкшие к Turbo-C должны переносить графический код 1-в-1.
|
||||
|
||||
**Что должно быть** (на основе Borland BGI):
|
||||
|
||||
Setup/teardown:
|
||||
- `initgraph()` / `closegraph()` — у нас сейчас `gfx_init`/`gfx_done`, добавить alias
|
||||
- `getmaxx()` / `getmaxy()` — макрос на GFX_WIDTH-1 / GFX_HEIGHT-1
|
||||
- `cleardevice()` — alias to gfx_clear
|
||||
- `getgraphmode()` / `setgraphmode()` — у нас get_videomode/set_videomode
|
||||
|
||||
Color/palette:
|
||||
- `setcolor(c)`, `getcolor()` — current draw color
|
||||
- `setbkcolor(c)`, `getbkcolor()` — background color
|
||||
- `setpalette(idx, c)` — палитра entry
|
||||
- `getpalette(&info)` — read all palette
|
||||
|
||||
Primitives (мы уже имеем эквиваленты — добавить BGI-имена как aliases):
|
||||
- `putpixel(x, y, c)` — есть как gfx_putpixel
|
||||
- `getpixel(x, y)` — нужно реализовать (RMW обратное — IN)
|
||||
- `moveto(x, y)`, `lineto(x, y)`, `linerel(dx, dy)` — current point + line drawing
|
||||
- `line(x1, y1, x2, y2)` — есть как gfx_line
|
||||
- `rectangle(x1, y1, x2, y2)` — есть как gfx_rect (но другой API: x1,y1,x2,y2 vs x,y,w,h!)
|
||||
- `bar(x1, y1, x2, y2)` — есть как gfx_fill_rect
|
||||
- `bar3d(x1, y1, x2, y2, depth, topflag)` — новое: rect + 3d edges
|
||||
- `circle(x, y, r)`, `arc(...)`, `ellipse(...)`, `pieslice(...)` — новые primitives
|
||||
- `fillpoly()`, `drawpoly()` — полигоны
|
||||
- `floodfill(x, y, border_color)` — заливка
|
||||
|
||||
Text on graphics screen:
|
||||
- `outtext(s)` / `outtextxy(x, y, s)` — есть как gfx_text (alias)
|
||||
- `settextstyle(font, dir, size)` — multiple bitmap fonts
|
||||
- `gettextsettings(&info)`
|
||||
- `textwidth(s)` / `textheight(s)` — measure
|
||||
|
||||
Image manipulation:
|
||||
- `imagesize(x1, y1, x2, y2)` — bytes needed for getimage
|
||||
- `getimage(x1, y1, x2, y2, buf)` — save rect to buffer
|
||||
- `putimage(x, y, buf, op)` — paste back with COPY_PUT/XOR_PUT/AND_PUT/OR_PUT/NOT_PUT
|
||||
|
||||
Clipping/viewport:
|
||||
- `setviewport(x1, y1, x2, y2, clip)` — drawing clip rect
|
||||
- `getviewsettings(&info)`
|
||||
- `clearviewport()`
|
||||
- `setactivepage(p)` / `setvisualpage(p)` — двойная буферизация (Sprinter имеет 2 screen)
|
||||
|
||||
Line style:
|
||||
- `setlinestyle(style, pattern, thickness)` — SOLID_LINE / DOTTED_LINE / etc.
|
||||
- `getlinesettings(&info)`
|
||||
|
||||
**Acceptance:** перенос типичной Turbo-C BGI программы (рисующей с использованием
|
||||
moveto/lineto/circle/bar/setcolor) должен работать без существенных правок.
|
||||
|
||||
**Notes:**
|
||||
- BGI fonts (TRIPLEX/SANS_SERIF/GOTHIC) — у нас один BIOS font, остальные нужно
|
||||
добавить (как bitmap data в lib)
|
||||
- imagesize/getimage/putimage — самые востребованные для game/animation
|
||||
- Active/visual page (двойная буферизация) — Sprinter поддерживает 2 graphics pages,
|
||||
нужен API switching
|
||||
|
||||
См. также `examples/` Turbo C 2.x BGIDEMO как reference что нужно.
|
||||
|
||||
### IM2 Interrupt Handlers — **MUST для v2**
|
||||
|
||||
User-задаваемые ISR через Z80 IM 2 mode. Нужны для:
|
||||
- Timer ticks (50 Hz frame counter, плавная анимация)
|
||||
- Music playback (AY, COVOX)
|
||||
- Real-time games (input + game logic + render в interrupt-driven)
|
||||
- Async keyboard / mouse handling
|
||||
|
||||
**Status:** ОТЛОЖЕНО до v2. Полный research + design в `docs/im2_isr_design.md`.
|
||||
|
||||
**Решение по архитектуре:** реализовать как отдельный memory mode `--memory im2`
|
||||
(вместо того чтобы лезть во все существующие crt0). Detail'и в design-doc.
|
||||
|
||||
**Резюме research'а** (полный текст в `docs/im2_isr_design.md`):
|
||||
- Vector 0xFF — frame + keyboard + CBL. Disambiguation по портам 0x19 / 0xFE
|
||||
- Mouse hardware-IRQ не приходит (на текущей плате)
|
||||
- Vector table / ISR / stack ОБЯЗАНЫ быть в W2 (0x8000..0xBFFF)
|
||||
- DSS имеет свой IM 2 handler — нужно chain'иться (иначе клавиатура / SYSTIME ломаются)
|
||||
|
||||
### Прочие крупные пункты для v2
|
||||
|
||||
- [ ] **FILE API rewrite — buffered streams** — текущая реализация в
|
||||
`libc/stdio/file.c` это provisional unbuffered shim (каждый fputc/fgetc
|
||||
= один read/write syscall). Нужна полноценная buffered семантика
|
||||
как в Solid-C:
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
uint flags; // +0..1 file status flags
|
||||
int level; // +2..3 empty/fill level of buffer
|
||||
char *curp; // +4..5 current active pointer
|
||||
int fd; // +6..7 underlying low-level fd
|
||||
char *buffer; // +8..9 data transfer buffer
|
||||
char hold; // +10 ungetc byte if no buffer
|
||||
short token; // +11..12 reserved
|
||||
char dummy; // +13 reserved
|
||||
} FILE;
|
||||
```
|
||||
|
||||
stdin/stdout/stderr — fd-маркеры `0 / -1 / -2`. Отрицательные для
|
||||
stdout/stderr выбраны намеренно: ESTEX OPEN может вернуть positive
|
||||
small fd (1, 2, …) для обычного файла → если бы stdout=1, реальный
|
||||
fd=1 сталкивался бы с идентификатором. fd=0 для stdin безопасно
|
||||
(ESTEX 0 не возвращает). Сами fd не передаются в syscall'ы —
|
||||
диспетчеризация по флагам `_F_CONIN/_F_CONOUT`.
|
||||
|
||||
Принтер-потоки (stdaux/stdprn) НЕ реализуем — Sprinter принтерной
|
||||
API не имеет.
|
||||
|
||||
Альтернатива — взять реализацию из third_party/solid-c (sources в
|
||||
`SRC/CLIB/`); там есть готовый buffered FILE + fopen/fread/fwrite/
|
||||
fseek/setvbuf и т.д. Адаптировать к нашим open/read/write/lseek.
|
||||
|
||||
При rewrite заодно решить deferred issues stdio-review:
|
||||
- `fwrite` short-write должен ставить `_F_ERROR`
|
||||
- `fgets(buf, 1, fp)` — стандарт говорит "empty string", мы вернули NULL
|
||||
- `mode_to_flags` — break-out на '+' (cosmetic)
|
||||
|
||||
- [ ] **Audio API** — AY-3-8910 + COVOX через прерывания (требует IM2)
|
||||
- [ ] **ISA-8 slot support** — ZX-Bus карты (sound, network, etc.) — требует IM2 + чтения portов
|
||||
|
||||
## Прочие задачи (v1 backlog, не блокирующие)
|
||||
|
||||
- [x] **#9: text I/O split (Turbo-C style)** — stdio (puts/printf/putchar) теперь fast no-attr через PCHARS/PUTCHAR. conio (cputs/cprintf/putch) применяет attr через textcolor/textbackground/textattr. KEEP_EXIST_ATTR → conio fallback на fast path. Verified в hello.exe. См. `memory/text_output_api_split.md`. Реализовано 2026-05-31.
|
||||
- [x] **Mouse API полный** (резидентный driver, RST 30h) — все 14 функций обёрнуты (init/show/hide/refresh/read/goto/bounds/text_cursor/load_cursor/get_cursor/get/set_sensitivity/video_mode_changed). См. `memory/mouse_api.md`. Verified в MAME 2026-05-31. Sensitivity = divider (меньше = быстрее).
|
||||
- [ ] Interrupt handlers — IM 2 vector table в HOME для user ISR'ов
|
||||
- [ ] Поддержка `restore SP on EXIT` (паттерн из z88dk +pps) — проверить нужно ли
|
||||
- [ ] CI: автоматически запускать MAME с `-aviwrite` для screenshot-сравнения, чтобы тесты примеров проходили без человека
|
||||
|
||||
## Идеи на потом
|
||||
|
||||
- Поддержка `<setjmp.h>` (есть в SDCC stdlib — нужно протестировать что наш crt0 совместим)
|
||||
- `<time.h>` через ESTEX SYSTIME (`$21`) и CMOS BIOS-функции
|
||||
- ZX Spectrum-совместимый режим как отдельный target (для портирования спектрумовских программ)
|
||||
- Поддержка ZX-Bus карт (sound, network, etc.) — нужны драйверы
|
||||
- Profile-guided optimization tools (hot/cold detection) для крупных программ
|
||||
|
||||
## Linker duplicate-symbol warnings (благоприятные, отфильтрованы)
|
||||
|
||||
Когда мы сознательно overrides'им SDCC z80.lib функции собственной версией в `sprinter.lib`, `sdldz80` пишет `?ASlink-Warning-Definition of public symbol '...' found more than once`. Линкер берёт первое найденное определение (наше), поэтому поведение корректное — warning только noise.
|
||||
|
||||
Текущие overrides:
|
||||
- `_puts` — наша версия через PCHARS+\r\n vs SDCC posix puts
|
||||
- `___sdcc_heap` — наш heap в W2 vs SDCC's стандартный
|
||||
- `_asctime`, `_localtime` (и возможно другие из time) — наш `posix_time.c` через ESTEX SYSTIME vs SDCC's `time.rel` который зависит от `_RtcRead`
|
||||
|
||||
**Текущее решение:** `bin/sprinter-cc` отфильтровывает warning-блок (warning + 2 follow-up `Library:` строки) из вывода `sdcc`. Через `-v` (verbose) всё показывается. Реализовано через awk-pipe.
|
||||
|
||||
**Возможные улучшения:**
|
||||
- Перейти на explicit `--nostdlib` + ручной список нужных модулей из z80.lib (string, math, stdlib без override'нутых) — убрать ИСТОЧНИК warning'ов, не маскировать
|
||||
- Или: переименовать наши `_puts` → `_puts_sprinter` + alias через linker flag (не уверен что SDCC поддерживает)
|
||||
- Или: оставить как сейчас (рабочее и benign) — приоритет низкий
|
||||
|
||||
## TODO: проверить на реальном железе
|
||||
|
||||
- [ ] **Port_Y banking trick** (`docs/part2/SprinterGraphics programming.txt`):
|
||||
доку утверждает что после `OUT (0x89), Y` адреса 0xC000+0x400*N в окне W3
|
||||
маппятся на строки Y..Y+15 (одно программирование → 16 строк).
|
||||
Empirical 2026-06-01 в MAME 0.283 этот trick **не работает** — пиксели
|
||||
по адресам выше 0xC000+row_width уходят в невидимую область. Канонический
|
||||
`docs/samples/plasma2.asm` тоже не использует banking, переустанавливает
|
||||
Port_Y per row.
|
||||
План:
|
||||
1. Получить доступ к реальному Sprinter
|
||||
2. Запустить тест dual-write (`_gfx_putpixel_raw` + второй write в `0xD000+x`)
|
||||
3. Если на железе видны двойные линии → бага MAME, открыть issue с
|
||||
минимальным репро
|
||||
4. Если на железе тоже одна линия → документ неверный, удалить упоминание
|
||||
из доки и просто оставить текущую реализацию (Port_Y per pixel)
|
||||
5. Если banking работает на железе → внедрить кэширование Port_Y в
|
||||
`_gfx_putpixel_raw` (sentinel out-of-range, см. memory/gfx_port_y_banking.md)
|
||||
|
||||
Связанный выигрыш для Bresenham (60-pixel диагональ) — около 8× меньше
|
||||
OUT (0x89) операций, для `gfx_fill_rect 320x256` — 16× меньше. Не блокирует
|
||||
release v1.
|
||||
|
||||
## GFX: расширения по `docs/part2/accelerator_doc.txt`
|
||||
|
||||
После прочтения детального accelerator doc выявлены незакрытые направления.
|
||||
Сейчас в коде используется только горизонтальный/вертикальный Fill mode.
|
||||
|
||||
### Quick wins для текущих primitives
|
||||
|
||||
- [ ] **Заменить SMC на `LD A, (var)` для block-size**. Документ явно
|
||||
разрешает `LD A, (HL)`, `LD A, (BC)`, `LD A, (DE)` (но не `LD A, r`).
|
||||
Это уберёт SMC complexity в `gfx_lines.c:hfill_chunk/vfill_chunk` и
|
||||
`gfx_16.c:g16_hfill_chunk`. Запрещено только register-to-register.
|
||||
- [ ] **Кэширование block-size**. Документ показывает что accel запоминает
|
||||
block size между bursts (см. `Horizontal_Line_Fill`: устанавливают
|
||||
size + `LD B,B` отключение, потом включают Fill mode и используют
|
||||
сохранённый size). Для `gfx_fill_rect` с 100 одинаковыми
|
||||
строками — установить size 1 раз, а не 100.
|
||||
|
||||
### Bank-prefix modes (port 0xE2 bits)
|
||||
|
||||
Документ показывает три варианта банка видеостраницы помимо стандартного 0x50:
|
||||
|
||||
| Bank byte | Effect |
|
||||
|---|---|
|
||||
| 0x50 | Normal write — пишется в shadow + видимый |
|
||||
| 0x54 | "no copy in main shadow RAM" |
|
||||
| 0x58 | **"FF is transparent"** — байт 0xFF при write оставляет background |
|
||||
| 0x5C | both |
|
||||
|
||||
Bank 0x58 объясняет почему mouse cursor рисуется с 0xFF-прозрачностью.
|
||||
Это путь к **sprite-blending через accel block copy**:
|
||||
|
||||
- [ ] **`gfx_set_bank_transparent(on)`** или флаг в `gfx_set_bank` для
|
||||
выбора 0x50/0x58 при отрисовке sprite'ов
|
||||
- [ ] Использовать в новом `gfx_blit()` чтобы по факту получать
|
||||
transparent sprites через accel-копию
|
||||
|
||||
### Block copy mode (sprite blit'ы)
|
||||
|
||||
`LD L,L` (horizontal) и `LD A,A` (vertical) — режим копирования блока через
|
||||
256-байтную accel memory. Это базис для blit'ов.
|
||||
|
||||
- [ ] **`gfx_blit(src_data, x, y, w, h)`** — копирование sprite'а
|
||||
(произвольный размер, через accel)
|
||||
- [ ] **`gfx_blit_transparent(src, x, y, w, h)`** — с использованием bank 0x58
|
||||
|
||||
См. `Draw_Restangle_Data` в accelerator_doc.txt как референс.
|
||||
|
||||
### AND / OR / XOR operations через accel
|
||||
|
||||
Документ показывает что accel поддерживает логические операции с блоками
|
||||
данных. Применения:
|
||||
- XOR — инверсия области (выделение selection в UI)
|
||||
- OR / AND — masking, alpha-style blending
|
||||
- См. пример в accelerator_doc.txt: "256 bytes block coding via XOR"
|
||||
|
||||
- [ ] **`gfx_xor_rect`** / **`gfx_or_rect`** / **`gfx_and_rect`** —
|
||||
примитивы логических операций над прямоугольником
|
||||
- [ ] **`gfx_invert_rect(x, y, w, h)`** — alias на xor с 0xFF
|
||||
|
||||
### Bitmap fonts разных размеров
|
||||
|
||||
Сейчас `gfx_text` / `gfx_putchar` хардкоженно работают с 8×8 шрифтом
|
||||
(BIOS WIN_GET_ZG возвращает 256×8 байт). Для будущих UI / титульников
|
||||
нужны:
|
||||
- [ ] **`gfx_set_font_size(w, h)`** — переключить ширину/высоту glyph'а
|
||||
- [ ] **`gfx_set_font_data(ptr, w, h, advance)`** — заменить указатель
|
||||
на пользовательский шрифт + размеры
|
||||
- [ ] Поддержка **proportional** (advance != w) шрифтов — добавить
|
||||
array advance[256] на ширину каждого glyph'а
|
||||
- [ ] **Big-font режимы**: 8×16, 16×16, 16×8 (для титульников)
|
||||
- [ ] Возможно отдельный API `gfx_text_ex(x, y, str, font_id)` где
|
||||
font_id выбирает один из загруженных шрифтов
|
||||
- [ ] **Anti-alias 2-bit шрифты** (бит фон / бит граница / 2-бит alpha?)
|
||||
— far future, для smooth UI
|
||||
|
||||
## Финальный этап оптимизаций (не сейчас)
|
||||
|
||||
- **`gfx_line` через accel для пологих диагоналей** — Bresenham для линии с |dy| << |dx| (или наоборот) выдаёт длинные runs одинакового Y (или X): пиксель, пиксель, пиксель, шаг Y, пиксель, пиксель... Каждый такой run — это готовый аргумент для `gfx_hline` (или `vline`).
|
||||
План исследования: посчитать длину runs как функцию от наклона; решить минимальный run length, при котором выгоднее accel hline чем N×putpixel (overhead accel ~20µs, putpixel ~5µs — accel выгоднее при run ≥ 4-5 px); для крутых диагоналей (dx ≈ dy) оставить Bresenham, для пологих — run-length-based fill.
|
||||
Сейчас `gfx_line` orthogonal cases уже через accel — оптимизировать только косые.
|
||||
|
||||
- **`gfx_fill_rect` с одним W3-swap на всю операцию** — сейчас каждый внутренний `gfx_hline`/`gfx_vline` делает свой DI/save-W3/restore-W3/EI. Можно сделать internal `_fill_rect_inner` который держит W3 замапленным и DI весь цикл; ~20µs × количество строк/столбцов экономии. Применимо ко всем композитным примитивам.
|
||||
@@ -0,0 +1,28 @@
|
||||
# Тест UTF-8
|
||||
|
||||
Это **проверка** кодировки _UTF-8_ в `mdview2`.
|
||||
Кириллица должна читаться: съешь же ещё этих мягких булок.
|
||||
|
||||
## Символы и пунктуация
|
||||
|
||||
Тире — длинное, и – короткое. Кавычки: «ёлочки» и “лапки”.
|
||||
Многоточие… стрелки → ← ↑ ↓, галочка ✓ и крестик ✗.
|
||||
Градус 25°, пункт списка • буллет, номер № 7.
|
||||
|
||||
## Список
|
||||
|
||||
- Первый пункт
|
||||
- Второй пункт с длинным текстом, чтобы проверить перенос строки по словам на границе экрана восемьдесят символов
|
||||
- Ёжик, ёлка, объём
|
||||
|
||||
> Цитата: «Краткость — сестра таланта».
|
||||
|
||||
## Таблица
|
||||
|
||||
| Язык | Привет | Число |
|
||||
|----------|---------------|-------|
|
||||
| Русский | Здравствуйте | 42 |
|
||||
| English | Hello | 7 |
|
||||
| Deutsch | Grüße | 100 |
|
||||
|
||||
Конец файла.
|
||||
@@ -0,0 +1,144 @@
|
||||
# mdview2 — поддержка кодировок (CP866 / CP1251 / KOI8-R / UTF-8)
|
||||
|
||||
Статус: РЕАЛИЗОВАНО (2026-06-25). Все фазы сделаны; Фаза 4 — eager-кооперативным
|
||||
вариантом (см. ниже), а не idle-build. Осталась только проверка на железе и
|
||||
возможная оптимизация «не строить заведомо бесполезный вторичный набор».
|
||||
|
||||
## Цель
|
||||
|
||||
- CP1251, KOI8-R — простой 1:1 маппинг байтов [128-255] в CP866 (кириллица
|
||||
+ основная пунктуация). KOI8-R равнозначна, входит в общий цикл.
|
||||
- UTF-8 — декодирование с маппингом кириллицы в CP866 + подстановка части
|
||||
некириллических символов (стрелки, галочки, тире, буллеты, box-drawing) в
|
||||
CP866/ASCII-глифы.
|
||||
- Автоопределение кодировки при открытии (BOM + дешёвая эвристика).
|
||||
- F8 (Codepage) — переключение по циклу CP866 → CP1251 → KOI8-R → UTF8.
|
||||
|
||||
## Ключевые наблюдения (определяют архитектуру)
|
||||
|
||||
1. **8-битные кодировки имеют ОДНУ структуру.** markdown-разметка вся в ASCII
|
||||
(`# * | -` …); 8-битные различаются только глифами [128-255]. Значит индекс
|
||||
и кэш (смещения, переносы, ширины таблиц) для CP866/CP1251/KOI8-R —
|
||||
**общие**; переключение между ними = ремап глифов [128-255], БЕЗ
|
||||
переиндексации.
|
||||
2. **UTF-8 — другая структура** (кириллица 2 байта). Нужен отдельный
|
||||
конвертированный CP866-буфер со своим индексом/кэшем.
|
||||
3. **Проблема рамок таблиц / маркеров.** В кэше намешаны контентные глифы
|
||||
(байты-из-источника [128-255] — НАДО ремапить) и вставленные нами CP866-
|
||||
глифы (box `│─┼…`, маркер списка 0x07, цитата 0xB3, HR 0xC4 — ремапить
|
||||
НЕЛЬЗЯ). По значению байта не различить → различаем по **атрибуту**.
|
||||
|
||||
## Архитектура
|
||||
|
||||
### Буферы
|
||||
- `src_phys[]` — оригинальные байты файла (грузим как сейчас; храним всегда —
|
||||
под ремап и raw-view, см. memory/mdview2_file_phys_preserve).
|
||||
- 8-битный режим: `fb()` читает `src_phys` напрямую. Кэш контентных ячеек
|
||||
хранит **исходный байт** (не пред-конвертированный).
|
||||
- UTF-8 режим: отдельные EMM-страницы `utf_phys[]` = UTF8→CP866 конвертация
|
||||
src; свой индекс/кэш; `fb()` читает `utf_phys`.
|
||||
|
||||
### Различение контент/структура по attr
|
||||
Каждый структурный глиф получает НЕ-контентный attr:
|
||||
- box-рамка таблиц → новый `ATTR_BOX` (сейчас TBL_ATTR=ATTR_TEXT — поменять);
|
||||
- HR (0xC4) → ATTR_HR; маркер списка (0x07) → ATTR_LIST_MARKER; цитата
|
||||
(0xB3) → ATTR_QUOTE_MARKER — уже различимы.
|
||||
Правило ремапа: ремапить `char>=128` только если attr ∈ контентных
|
||||
(TEXT/BOLD/ITALIC/UNDER/CODE/STRIKE/TITLE1-4). Структурные — как есть.
|
||||
|
||||
### Ремап на этапе отрисовки
|
||||
`draw_line_from_cache`:
|
||||
- encoding == CP866 или UTF-8: прямой `win_rest` из кэш-страницы (ремап не
|
||||
нужен — CP866 identity; UTF-8-кэш уже CP866).
|
||||
- encoding == CP1251/KOI8-R: читаем кэш-строку в near-буфер, ремапим
|
||||
контентные байты [128-255] через активную таблицу (структурные пропускаем
|
||||
по attr), пишем в scratch-страницу, `win_rest` из неё. Только видимые ~30
|
||||
строк, на скролле — дёшево.
|
||||
|
||||
Итого: переключение между 8-битными — мгновенно (меняем активную таблицу +
|
||||
redraw, draw ремапит). Переиндексация только при переходе в/из UTF-8.
|
||||
|
||||
## Детекция кодировки (дешёвый скан байтов, ДО построения)
|
||||
|
||||
1. **BOM**: первые 3 байта EF BB BF → UTF8 (и пропустить BOM).
|
||||
2. **UTF-8 валидность** (если нет BOM): проход, проверка структуры
|
||||
(лид-байты 0xC2-0xDF/0xE0-0xEF/0xF0-0xF4 + континюэйшны 0x80-0xBF;
|
||||
одиночный 0x80-0xBF, 0xC0/0xC1, 0xF5+ → нарушение). 0 нарушений И есть
|
||||
≥1 multibyte → UTF8. Любое нарушение → 8-бит.
|
||||
3. **8-бит дизамбигуация** (CP866/CP1251/KOI8-R): счёт попаданий в байты
|
||||
самых ходовых строчных русских букв (о е а и н т с р в л) каждой кодировки;
|
||||
максимум выигрывает. (Предрасчёт байт-наборов по таблицам.)
|
||||
4. **Фолбэк**: нет байт ≥0x80 или неоднозначно → CP866 (родная).
|
||||
|
||||
## Таблицы (static const, CODE/const-сегмент)
|
||||
|
||||
- `cp1251_to_866[256]`, `koi8r_to_866[256]` — байт→байт (ASCII identity;
|
||||
кириллица по раскладкам; en/em-dash, «ёлочки», … → CP866-аналоги или '?').
|
||||
- UTF-8: `utf_cyr_to_866[]` для U+0400..U+045F + компактная таблица символов
|
||||
`utf_sym[]` (codepoint→CP866): U+2192→'>'/стрелка, U+2190→'<', U+2713/14 ✓
|
||||
→'v'/box, U+2022 •→0x07/0xF9, U+2014/2013 —→'-', U+2026 …→"...",
|
||||
U+00A0→' ', U+2500.. box→CP866 box; прочее → '?'.
|
||||
|
||||
## Поток загрузки
|
||||
|
||||
1. Грузим в `src_phys`. Детект-скан → кодировка E.
|
||||
2. Если E ∈ {UTF8}: строим UTF-8-набор (utf_phys + конвертация + индекс),
|
||||
показываем UTF-8. Иначе: показываем 8-битный (общий индекс на src,
|
||||
активная таблица = E).
|
||||
3. **Ленивый build второго набора в простое.** Главный цикл — НЕ блокирующий
|
||||
getkey, а kbhit-поллинг: пока нет клавиш и второй набор (UTF-8 при
|
||||
стартовом 8-бит, либо 8-бит при стартовом UTF-8) не построен — докручиваем
|
||||
его инкрементально. После — F8 в любую сторону мгновенно.
|
||||
|
||||
## F8 — переключение
|
||||
|
||||
- Цикл g_encoding: CP866 → CP1251 → KOI8-R → UTF8 → CP866.
|
||||
- 8-бит↔8-бит: сменить активную таблицу + redraw (без переиндексации).
|
||||
- в/из UTF-8: переключить активный индекс/кэш на соответствующий набор
|
||||
(если построен; иначе достроить — но при ленивом build обычно уже готов).
|
||||
- Статус-бар: имя кодировки; меню (строка 31): «F8 Codepage».
|
||||
|
||||
## Фазы реализации (ИТОГ)
|
||||
|
||||
1. ✅ **Ядро 8-бит**: ATTR_BOX; кэш хранит исходный байт; таблицы CP1251/KOI8R;
|
||||
ремап в draw (`win_rest_remap`).
|
||||
2. ✅ **UTF-8 набор**: `alloc_and_convert_utf8` + `utf8_convert` (декодер 1/2/3-
|
||||
байт, 4-байтные/битые → `?`); `utf_cyr_to_866[96]` + символьные подстановки
|
||||
в `conv_emit_cp` (стрелки 0x18-0x1B, галка 0xFB, буллет 0xF9, тире/кавычки/
|
||||
box/° и т.п.; «» → `<`/`>`, т.к. в CP866 гильеметов нет).
|
||||
3. ✅ **Детекция** (BOM + эвристика), сэмпл — первые **4 КБ** (быстро); хвост-
|
||||
обрезка multibyte на границе сэмпла не штрафуется.
|
||||
4. ✅ **Сосуществование 2 наборов** (`docset_t g_doc[2]` + `doc_save/load/switch`,
|
||||
свап «живых» глобалов). Сборка — **ленивая (build-on-demand)**: при старте
|
||||
строится только первичный (показываемый) набор; UTF-8 конвертация тоже
|
||||
ленивая (в `build_doc`, не до первого экрана → старт быстрый). Второй набор
|
||||
достраивается `switch_encoding()` при первом F8-переходе в него (спиннер,
|
||||
потом кэш). Eager-вариант отвергнут: удваивал старт и блокировал F8 на время
|
||||
фоновой сборки.
|
||||
5. ✅ **F8** полный цикл CP866→CP1251→KOI8R→UTF8→CP866 (внутри 8-бит — ремап,
|
||||
на границе — `doc_switch`/ленивая сборка). Статус: `enc_name` кол.37. Меню
|
||||
«F8 Codepage» показывается только когда переключение возможно (`g_f8_enabled`):
|
||||
во время сборки 8-битного первичного F8 разрешён в `load_key` (только цикл
|
||||
8-бит); во время сборки UTF-8 первичного метка F8 скрыта. F1-справка: секция
|
||||
Encoding.
|
||||
|
||||
NB: меню-строка разбита на 10 блоков по 8 колонок, метки Fn кладутся в блок
|
||||
(n-1)*8 (F1→0, F8→7, F10→9).
|
||||
|
||||
## Память
|
||||
- 8-бит: src + scratch-страница для ремапа (1 стр.). Доп. индекса нет.
|
||||
- UTF-8 набор: utf_phys (≤8 стр.) + свой индекс/кэш-контент. Строится лениво.
|
||||
- Бюджет 215 свободных страниц (sprinter_emm_budget) — с запасом.
|
||||
|
||||
## Открытые вопросы (к реализации)
|
||||
1. Имя кодировки в статусе — где (зона имени файла / отдельный слот).
|
||||
2. Набор UTF-8-подстановок символов — приоритет (→ ← ✓ ✗ • — … « » box).
|
||||
3. Глубина таблиц пунктуации CP1251/KOI8 (минимум кириллица+dash или полнее).
|
||||
4. Объём детект-сэмпла (весь файл или первые N КБ).
|
||||
|
||||
## Риски
|
||||
- Корректность различения контент/структура по attr — нужно, чтобы ВСЕ
|
||||
вставленные глифы имели не-контентный attr (проверить box/markers).
|
||||
- Два набора индекс/кэш (8-бит + UTF-8) + переключение активного — учёт
|
||||
страниц, чтобы не течь и не путать.
|
||||
- Точность таблиц (особенно UTF-8 символы) — итеративно по факту.
|
||||
@@ -0,0 +1,164 @@
|
||||
# mdview2 — план: рендер-кэш вместо живого парсинга на каждый скролл
|
||||
|
||||
Контекст и обоснование — см. обсуждение 2026-06-23 (после оптимизации mdview через
|
||||
`<bios/text.h>`, см. `mdview-модель-документа-и-рендеринг.md`). Идея: вместо того
|
||||
чтобы при каждом скролле повторно идти в файл (`fb()`, W3-банкинг) и гонять
|
||||
markdown inline-парсер (`handle_inline_marker`/emphasis state machine), один раз
|
||||
отрендерить каждую логическую строку в готовый байтовый буфер и дальше выводить
|
||||
его на экран напрямую — без парсинга, без обращения к исходному файлу.
|
||||
|
||||
Ключевые подтверждённые факты (эмпирически в MAME, не из документации):
|
||||
|
||||
- **Формат буфера ESTEX `WINCOPY`(59h)/`WINREST`(5Ah)** — пара байт `(char, attr)`
|
||||
на ячейку, по строкам, шаг строки = `width*2`, без паддинга. Можно генерировать
|
||||
самим, не вызывая `WINCOPY`. `WINREST` копирует сразу `H` строк одним вызовом.
|
||||
Детали ABI и регистров — см. `tests/winrest/winrest.c`.
|
||||
- **Бюджет EMM**: 256 страниц (4 МБ) total, 215 (3440 КБ) free на старте программы.
|
||||
Файл+индекс в худшем случае (128 КБ файл) съедают 16 страниц (256 КБ) — остаётся
|
||||
≈199 страниц (3184 КБ). Кэш всего документа целиком (даже худший случай:
|
||||
16384 строк × 160 байт = 2.6 МБ) укладывается без LRU/частичного кэша.
|
||||
|
||||
## Единый формат строки (пересмотрено 2026-06-23)
|
||||
|
||||
Исходно планировалось два типа строк (тип 1 — многоцветные, помещающиеся;
|
||||
тип 2 — nowrap/code, один стиль, чистый текст для экономии памяти). От этого
|
||||
деления отказались: строки таблиц (`IF_NOWRAP`, но не `IF_CODE`) всё равно
|
||||
проходят inline-парсер и могут содержать несколько атрибутов (bold/italic в
|
||||
ячейках) — предположение "один стиль" для них неверно. Бюджет EMM
|
||||
([[sprinter_emm_budget]]) с большим запасом покрывает (char,attr)-формат для
|
||||
ВСЕХ строк без исключения, поэтому усложнение не оправдано.
|
||||
|
||||
**Единый формат**: кэш-запись = `len` пар `(char,attr)` — реальная длина
|
||||
контента в ячейках, без паддинга до 80, капается на `MAX_CACHE_LINE_LEN`=255.
|
||||
Вывод: `bios_fillcharattr(' ', base_attr, SCREEN_W)` (очистить строку) →
|
||||
`win_rest(row, left_margin, 1, len, page)` (контент). Для широких nowrap-строк
|
||||
горизонтальный скролл (Фаза 5) — это просто смещение НАЧАЛА среза внутри ТОГО
|
||||
ЖЕ (char,attr)-буфера на `hscroll*2` байт, тот же `win_rest`, без отдельного
|
||||
плain-текстового формата и без необходимости на лету "разворачивать" текст+
|
||||
атрибут в пары.
|
||||
|
||||
## Фазы реализации
|
||||
|
||||
### Фаза 0 — скаффолдинг `examples/mdview2/` [СДЕЛАНО 2026-06-23]
|
||||
Новая директория со своим `Makefile`/`app.mk` (по аналогии с `mdview/`, не
|
||||
модифицируем `mdview.c`). `mdview2.c` — копия `mdview.c` без изменений логики
|
||||
(только usage-строка/заголовок комментария). Собирается чисто, дискета собрана.
|
||||
|
||||
### Фаза 1 — формат кэша и директория строк [СДЕЛАНО и ЗАКРЫТО 2026-06-23]
|
||||
Реализовано в `mdview2.c`:
|
||||
- `cache_rec_t` (РОВНО 8 байт: `page`, `off` (uint16_t), `len`, `flags`,
|
||||
`reserved`, `pad[2]`; размер задаётся `CACHE_DIR_REC_SIZE = sizeof(cache_rec_t)`,
|
||||
НЕ хардкодом — см. разобранный инцидент ниже) — **отдельная** директория
|
||||
(`cache_dir_phys[]`/`cache_dir_get`/`cache_dir_put`), своя ёмкость =
|
||||
`file_pages+1` страниц (как у индекса). **Важное уточнение к исходному тексту
|
||||
плана ниже**: директория НЕ переиспользует слоты `idx_rec_t` in-place —
|
||||
рендер-воркер (Фаза 2) при обработке строки N может заглядывать в idx-записи
|
||||
СОСЕДНИХ строк (откат cont-сегментов, line_idx+1 для границы сегмента); если
|
||||
бы строка N-1 была перезатёрта сразу после своего рендера, воркер строки N
|
||||
прочитал бы уже не исходные off/flags, а указатель в кэш — поломав откат.
|
||||
Бюджет EMM ([[sprinter_emm_budget]]) позволяет отдельный массив — он безопаснее.
|
||||
- Пул контента — отдельные EMM-страницы (`cache_content_phys[]`), ленивый рост
|
||||
по 1 странице в `cache_reserve()` (bump-allocator, запись никогда не
|
||||
разбивается через границу страницы; длина капается на `MAX_CACHE_LINE_LEN`=255
|
||||
ячеек).
|
||||
- `cache_commit()` = один `bank_write()` на строку (буфер строки собирается
|
||||
локально в W1/W2 заранее, не в W3 — иначе конфликт с `fb()` при чтении
|
||||
исходника во время рендера).
|
||||
- `win_rest()` (ESTEX WINREST 5Ah) — вывод готового буфера на экран.
|
||||
- `phase1_selftest()` — самопроверка (резервирует/коммитит 4 тестовые ячейки,
|
||||
кладёт запись в директорию по индексу 0, рисует через `win_rest` дважды
|
||||
подряд с координатами через параметры функции) — убрать в Фазе 2.
|
||||
|
||||
**Подтверждено визуально в MAME (2026-06-23)**: оба квадрата 2×2 на месте, без
|
||||
единой задержки, с координатами через переменные — Фаза 1 полностью закрыта.
|
||||
|
||||
**Разобранный инцидент (НЕ платформенный квирк)**: по пути долго казалось, что
|
||||
`win_rest()` после "всплеска" обычных BIOS print-вызовов рисует буфер в
|
||||
неправильном месте, причём воспроизводилось только когда `row`/`col` приходили
|
||||
через переменные, а не как константы — это и было ключом. Настоящая причина:
|
||||
`cache_rec_t` фактически занимал 7 байт (SDCC z80 не паддит структуры), а
|
||||
`CACHE_DIR_REC_SIZE` был захардкожен как 8 — `bank_read`/`bank_write` копировали
|
||||
8 байт в 7-байтный буфер на стеке, затирая соседнюю переменную (параметр `col`)
|
||||
вызывающей функции. Фикс — `cache_rec_t` явно до 8 байт + размер через
|
||||
`sizeof()`. Полная история и общий урок — [[sprinter_winrest_format]] и
|
||||
[[defer_unexplained_quirks]].
|
||||
|
||||
### Фаза 2 — рендер-воркер (бывший `render_line`) [СДЕЛАНО 2026-06-23]
|
||||
`render_line_to_cache(line_idx)` — адаптация `render_line()`: та же классификация
|
||||
строк/префиксов/inline-парсинг (`handle_inline_marker`, emphasis state machine,
|
||||
soft-wrap join) БЕЗ ИЗМЕНЕНИЙ, но вместо `bios_writeattr`/`flush_run`/`wrchar`/
|
||||
`bios_fillcharattr` на экран — пишем `(char,attr)` пары через `cc_put`/`cc_fill`
|
||||
в локальный `cellbuf` (без батчинга через `runbuf`/`flush_run` — тот паттерн был
|
||||
нужен только чтобы минимизировать число BIOS-вызовов, при записи в локальный
|
||||
буфер смысла нет, пишем посимвольно сразу по месту), затем ОДНИМ
|
||||
`cache_reserve()`+`cache_commit()`+`cache_dir_put()` коммитим всю строку.
|
||||
Кэшируется ПОЛНЫЙ контент строки до `MAX_CACHE_LINE_LEN`, без обрезки по
|
||||
`SCREEN_W` и без среза по `viewport_x` (view/scroll-time понятия, Фаза 4-5).
|
||||
Также найден и исправлен по ходу баг в `win_rest()`: `IX` всегда указывал на
|
||||
начало страницы (`#0xC000`), полностью игнорируя `off` — работало только в
|
||||
Фазе 1, где в кэше была ровно одна запись со смещением 0; как только Фаза 2
|
||||
начала пакетировать много строк на одной странице с разными `off`, все строки
|
||||
стали читаться с начала страницы. Фикс: `win_rest()` принимает `off`
|
||||
(uint16_t), `IX = 0xC000 + off` (новая раскладка ABI сверена через `sdcc -S`
|
||||
с непустым телом — у `__naked` с пустым телом SDCC не генерирует код доступа
|
||||
к параметрам, нужен пробный non-naked враппер). Подтверждено визуально в MAME.
|
||||
|
||||
### Фаза 3 — фоновый пре-рендер с прогрессом [СДЕЛАНО 2026-06-23]
|
||||
`emit_seg()` вызывает `render_line_to_cache()` **interleaved** с построением
|
||||
индекса — но с отставанием на одну строку: рендер строки N требует уже
|
||||
существующей idx-записи N+1 (источник `seg_end`), которой ещё нет в момент,
|
||||
когда строка N только создана. Поэтому `emit_seg()` для новой строки рендерит
|
||||
ПРЕДЫДУЩУЮ (`n_lines-2` после инкремента) — её флаги (`IF_NOWRAP`/`IF_CODE`/
|
||||
`IF_BLANK`) к этому моменту уже дописаны вызовом `set_*_cur()` на предыдущей
|
||||
итерации `index_lines()`. Последнюю строку файла (у которой "следующей" не
|
||||
будет) дорендеривает сам `index_lines()` после выхода из цикла, как и
|
||||
`render_line()` делал для последней строки при живом рендере (`seg_end =
|
||||
file_size`). Требует, чтобы `cache_dir_phys[]` был выделен ДО `index_lines()` —
|
||||
это уже так (`load_file()` выделяет директорию кэша, затем вызывается
|
||||
`index_lines()`).
|
||||
|
||||
**Важное сужение скоупа относительно исходного текста плана ниже**: пункты
|
||||
"спиннер крутится, пока `rendered_up_to < n_lines`" и "скролл ограничен
|
||||
диапазоном `[0, rendered_up_to]`" **не реализованы и не нужны** в этой
|
||||
архитектуре — рендеринг происходит СИНХРОННО внутри той же однопроходной
|
||||
`index_lines()`, без событийного цикла во время загрузки; пользователь
|
||||
физически не может начать скроллить, пока `index_lines()` не вернёт
|
||||
управление, а к этому моменту весь документ уже полностью в кэше. Спиннер
|
||||
из `index_lines()` (каждые 16 строк) сохранён как есть — он покрывает
|
||||
индексацию+рендер вместе, отдельный прогресс-индикатор не нужен.
|
||||
|
||||
### Фаза 4 — cache-only draw path при скролле
|
||||
Цикл перерисовки видимой области (после пре-рендера) идёт **только** по
|
||||
директории строк → `win_rest` (тип 1) или срез текста+`bios_writeattr` (тип 2).
|
||||
Никаких обращений к `fb()`/исходному файлу в steady-state скролле.
|
||||
|
||||
### Фаза 5 — горизонтальный скролл для широких nowrap-строк
|
||||
Срез ТОГО ЖЕ (char,attr)-кэш-буфера по текущему `hscroll`-офсету (смещение
|
||||
начала на `hscroll*2` байт внутри буфера строки), `win_rest(row, col, 1,
|
||||
visible_len, page)` с новым `off`. Никакого отдельного плоско-текстового
|
||||
формата не нужно (см. пересмотр "Единый формат строки" выше).
|
||||
|
||||
### Фаза 6 — тестирование в MAME
|
||||
Тот же набор паттернов, что использовался для mdview1 (заголовки/списки/цитаты/
|
||||
code-block/bold-italic/nowrap-обрезка), ПЛЮС: реальный замер EMM на большом
|
||||
документе (не синтетика — проверить, что бюджет из [[sprinter_emm_budget]]
|
||||
действительно держится на чём-то близком к 128 КБ); UX пре-рендера (спиннер +
|
||||
ограничение скролла, отсутствие "дыр" в недорендеренной области); проверка
|
||||
`win_rest` на РЕАЛЬНОМ отрендеренном контенте (не только синтетический A/B/C/D
|
||||
тест из `tests/winrest`).
|
||||
|
||||
## Открытые вопросы (решить по ходу, не блокируют старт Фазы 0)
|
||||
|
||||
- ~~Хранение длины строки в директории~~ — решено в Фазе 1: поле `len` (uint8_t)
|
||||
прямо в `cache_rec_t`.
|
||||
- ~~Деление строк на тип 1/тип 2~~ — отказались (см. "Единый формат строки"
|
||||
выше): единый (char,attr)-формат для всех строк, капается на
|
||||
`MAX_CACHE_LINE_LEN`=255 ячеек (с тем же индикатором обрезки на этапе вывода,
|
||||
что уже есть в рендере для nowrap-строк).
|
||||
- Освобождать ли страницы исходного файла после того, как все его строки
|
||||
отрендерены (вернуть EMM в пул) — даёт больше места про запас, но усложняет
|
||||
(нужна гарантия, что назад к файлу обращаться больше не придётся — а это не
|
||||
так, если позже добавится поиск по тексту). Не делать в v1.
|
||||
- BIOS-вариант `WIN_COPY_WIN`/`WIN_RESTORE_WIN` (0B2h/0B3h, RST 8) не проверен
|
||||
(см. [[sprinter_winrest_format]]) — ESTEX-варианта достаточно для v1, проверять
|
||||
BIOS-вариант только если понадобится экономия на RST-диспетчеризации.
|
||||
File diff suppressed because it is too large
Load Diff
+6
-1
@@ -25,8 +25,11 @@ LIBC_C := \
|
||||
libc/sys/atexit.c \
|
||||
libc/conio/conio.c \
|
||||
libc/conio/cprintf.c \
|
||||
libc/conio/text_palette.c \
|
||||
libc/bios/text.c \
|
||||
libc/io/dir.c \
|
||||
libc/video/videomode_raw.c \
|
||||
libc/video/palette.c \
|
||||
libc/errno/_errno_set.c \
|
||||
libc/env/env.c \
|
||||
libc/errno/errno.c \
|
||||
@@ -42,8 +45,10 @@ LIBC_C := \
|
||||
libc/io/stat.c \
|
||||
libc/mem/bank_io_w3.c \
|
||||
libc/mem/bank_io_w1.c \
|
||||
libc/mem/mem_alloc.c \
|
||||
libc/mem/mem_estex.c \
|
||||
libc/mem/mem_bios.c \
|
||||
libc/gfx/gfx_core.c \
|
||||
libc/gfx/gfx_palette.c \
|
||||
libc/gfx/gfx_raw_common.c \
|
||||
libc/gfx/gfx_raw_256.c \
|
||||
libc/gfx/gfx_raw_16.c \
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
/*
|
||||
* text.c — wrappers for the Sprinter BIOS text-output calls (RST 8,
|
||||
* 081h..08Eh "LP_*"). See <bios/text.h> for the full opcode -> name
|
||||
* mapping and semantics.
|
||||
*
|
||||
* ABI recap (docs/converted/bios.txt):
|
||||
* - 081h/082h/083h explicitly preserve HL and IX -> no push/pop ix
|
||||
* needed, and the result is the smallest/fastest of the set.
|
||||
* - 084h/08Eh (place get/set) are used without push/pop ix elsewhere
|
||||
* in this libc (libc/conio/conio.c gotoxy/wherex/wherey via the same
|
||||
* opcodes) — empirically safe, followed here too.
|
||||
* - Everything else clobbers IX like any other BIOS/ESTEX call, so
|
||||
* every RST 8 below is bracketed with push ix / pop ix.
|
||||
*
|
||||
* Stack-argument layout: SDCC __sdcccall(1) places the first 8-bit arg
|
||||
* in A, the second (only if the first was also 8-bit) in L; every arg
|
||||
* beyond that lands on the stack in left-to-right declaration order,
|
||||
* starting at [SP+2] (right after the 2-byte return address) — verified
|
||||
* empirically via `sdcc -S` for 2..6 uint8 args and for pointer+N uint8
|
||||
* args. IY is used as a scratch index register to read those without
|
||||
* disturbing IX (the caller's frame pointer).
|
||||
*/
|
||||
|
||||
#include <bios/text.h>
|
||||
|
||||
void bios_fillcharattr(char ch, uint8_t attr, uint8_t count) __naked
|
||||
{
|
||||
(void)ch; (void)attr; (void)count;
|
||||
__asm
|
||||
;; ch->A, attr->L, count on stack at [SP+2].
|
||||
;; LP_PRINT_ALL (081h): A=ch, E=attr, B=count. Preserves HL,IX.
|
||||
ld iy, #2
|
||||
add iy, sp
|
||||
ld b, 0 (iy) ; B = count
|
||||
ld e, l ; E = attr
|
||||
ld c, #0x81
|
||||
rst #0x08
|
||||
pop hl ; return address
|
||||
inc sp ; consume count byte
|
||||
jp (hl)
|
||||
__endasm;
|
||||
}
|
||||
|
||||
void bios_fillchar(char ch, uint8_t count) __naked
|
||||
{
|
||||
(void)ch; (void)count;
|
||||
__asm
|
||||
;; ch->A, count->L. LP_PRINT_SYM (082h): A=ch, B=count.
|
||||
;; Preserves HL,IX.
|
||||
ld b, l
|
||||
ld c, #0x82
|
||||
rst #0x08
|
||||
ret
|
||||
__endasm;
|
||||
}
|
||||
|
||||
void bios_fillattr(uint8_t attr, uint8_t count) __naked
|
||||
{
|
||||
(void)attr; (void)count;
|
||||
__asm
|
||||
;; attr->A, count->L. LP_PRINT_ATR (083h): E=attr, B=count.
|
||||
;; Preserves HL,IX.
|
||||
ld e, a
|
||||
ld b, l
|
||||
ld c, #0x83
|
||||
rst #0x08
|
||||
ret
|
||||
__endasm;
|
||||
}
|
||||
|
||||
void bios_set_place(uint8_t row, uint8_t col) __naked
|
||||
{
|
||||
(void)row; (void)col;
|
||||
__asm
|
||||
;; row->A, col->L. LP_SET_PLACE (084h): D=row, E=col.
|
||||
ld d, a
|
||||
ld e, l
|
||||
ld c, #0x84
|
||||
rst #0x08
|
||||
ret
|
||||
__endasm;
|
||||
}
|
||||
|
||||
uint16_t bios_get_place(void) __naked
|
||||
{
|
||||
__asm
|
||||
;; LP_GET_PLACE (08Eh): returns D=row, E=col — already the
|
||||
;; uint16_t return convention (DE); nothing left to rearrange.
|
||||
ld c, #0x8E
|
||||
rst #0x08
|
||||
ret
|
||||
__endasm;
|
||||
}
|
||||
|
||||
void bios_writeattr(const char *s, uint8_t len, uint8_t attr) __naked
|
||||
{
|
||||
(void)s; (void)len; (void)attr;
|
||||
__asm
|
||||
;; s->HL, len/attr on stack at [SP+2]/[SP+3].
|
||||
;; LP_PRINT_LN (085h): HL=s, B=len, E=attr.
|
||||
ld iy, #2
|
||||
add iy, sp
|
||||
ld b, 0 (iy) ; B = len
|
||||
ld e, 1 (iy) ; E = attr
|
||||
push ix
|
||||
ld c, #0x85
|
||||
rst #0x08
|
||||
pop ix
|
||||
pop hl ; return address
|
||||
inc sp
|
||||
inc sp ; consume len + attr
|
||||
jp (hl)
|
||||
__endasm;
|
||||
}
|
||||
|
||||
void bios_write(const char *s, uint8_t len) __naked
|
||||
{
|
||||
(void)s; (void)len;
|
||||
__asm
|
||||
;; s->HL, len on stack at [SP+2].
|
||||
;; LP_PRINT_LN2 (086h): HL=s, B=len.
|
||||
ld iy, #2
|
||||
add iy, sp
|
||||
ld b, 0 (iy) ; B = len
|
||||
push ix
|
||||
ld c, #0x86
|
||||
rst #0x08
|
||||
pop ix
|
||||
pop hl
|
||||
inc sp
|
||||
jp (hl)
|
||||
__endasm;
|
||||
}
|
||||
|
||||
void bios_writeattr_until(const char *s, uint8_t len, uint8_t attr, char sep) __naked
|
||||
{
|
||||
(void)s; (void)len; (void)attr; (void)sep;
|
||||
__asm
|
||||
;; s->HL, len/attr/sep on stack at [SP+2..4].
|
||||
;; LP_PRINT_LN3 (087h): HL=s, B=len, E=attr, D=sep.
|
||||
;; Pads with spaces after sep up to len.
|
||||
ld iy, #2
|
||||
add iy, sp
|
||||
ld b, 0 (iy) ; B = len
|
||||
ld e, 1 (iy) ; E = attr
|
||||
ld d, 2 (iy) ; D = sep
|
||||
push ix
|
||||
ld c, #0x87
|
||||
rst #0x08
|
||||
pop ix
|
||||
pop hl
|
||||
inc sp
|
||||
inc sp
|
||||
inc sp ; consume len + attr + sep
|
||||
jp (hl)
|
||||
__endasm;
|
||||
}
|
||||
|
||||
void bios_write_until(const char *s, uint8_t len, char sep) __naked
|
||||
{
|
||||
(void)s; (void)len; (void)sep;
|
||||
__asm
|
||||
;; s->HL, len/sep on stack at [SP+2..3].
|
||||
;; LP_PRINT_LN4 (088h): HL=s, B=len, D=sep. Pads with spaces.
|
||||
ld iy, #2
|
||||
add iy, sp
|
||||
ld b, 0 (iy) ; B = len
|
||||
ld d, 1 (iy) ; D = sep
|
||||
push ix
|
||||
ld c, #0x88
|
||||
rst #0x08
|
||||
pop ix
|
||||
pop hl
|
||||
inc sp
|
||||
inc sp
|
||||
jp (hl)
|
||||
__endasm;
|
||||
}
|
||||
|
||||
void bios_clearwin(uint8_t row, uint8_t col, uint8_t height, uint8_t width,
|
||||
uint8_t attr) __naked
|
||||
{
|
||||
(void)row; (void)col; (void)height; (void)width; (void)attr;
|
||||
__asm
|
||||
;; row->A, col->L, height/width/attr on stack at [SP+2..4].
|
||||
;; LP_CLS_WIN (089h): D=row, E=col, H=height, L=width, B=attr.
|
||||
ld iy, #2
|
||||
add iy, sp
|
||||
ld h, 0 (iy) ; H = height
|
||||
ld c, 1 (iy) ; stash width (L is still col for now)
|
||||
ld e, l ; E = col
|
||||
ld d, a ; D = row
|
||||
ld l, c ; L = width
|
||||
ld b, 2 (iy) ; B = attr
|
||||
push ix
|
||||
ld c, #0x89
|
||||
rst #0x08
|
||||
pop ix
|
||||
pop hl
|
||||
inc sp
|
||||
inc sp
|
||||
inc sp ; consume height + width + attr
|
||||
jp (hl)
|
||||
__endasm;
|
||||
}
|
||||
|
||||
void bios_scrollwin(uint8_t dir, uint8_t row, uint8_t count) __naked
|
||||
{
|
||||
(void)dir; (void)row; (void)count;
|
||||
__asm
|
||||
;; dir->A, row->L, count on stack at [SP+2].
|
||||
;; LP_SCROLL_UD (08Ah): B=dir(1 up/2 down), D=row, E=count.
|
||||
ld iy, #2
|
||||
add iy, sp
|
||||
ld e, 0 (iy) ; E = count
|
||||
ld d, l ; D = row
|
||||
ld b, a ; B = dir
|
||||
push ix
|
||||
ld c, #0x8A
|
||||
rst #0x08
|
||||
pop ix
|
||||
pop hl
|
||||
inc sp
|
||||
jp (hl)
|
||||
__endasm;
|
||||
}
|
||||
|
||||
void bios_writeattr_stop(const char *s, uint8_t maxlen, uint8_t attr,
|
||||
char sep) __naked
|
||||
{
|
||||
(void)s; (void)maxlen; (void)attr; (void)sep;
|
||||
__asm
|
||||
;; s->HL, maxlen/attr/sep on stack at [SP+2..4].
|
||||
;; LP_PRINT_LN5 (08Bh): HL=s, B=maxlen, E=attr, D=sep.
|
||||
;; Stops at sep (no padding).
|
||||
ld iy, #2
|
||||
add iy, sp
|
||||
ld b, 0 (iy) ; B = maxlen
|
||||
ld e, 1 (iy) ; E = attr
|
||||
ld d, 2 (iy) ; D = sep
|
||||
push ix
|
||||
ld c, #0x8B
|
||||
rst #0x08
|
||||
pop ix
|
||||
pop hl
|
||||
inc sp
|
||||
inc sp
|
||||
inc sp
|
||||
jp (hl)
|
||||
__endasm;
|
||||
}
|
||||
|
||||
void bios_write_stop(const char *s, uint8_t maxlen, char sep) __naked
|
||||
{
|
||||
(void)s; (void)maxlen; (void)sep;
|
||||
__asm
|
||||
;; s->HL, maxlen/sep on stack at [SP+2..3].
|
||||
;; LP_PRINT_LN6 (08Ch): HL=s, B=maxlen, D=sep. Stops at sep.
|
||||
ld iy, #2
|
||||
add iy, sp
|
||||
ld b, 0 (iy) ; B = maxlen
|
||||
ld d, 1 (iy) ; D = sep
|
||||
push ix
|
||||
ld c, #0x8C
|
||||
rst #0x08
|
||||
pop ix
|
||||
pop hl
|
||||
inc sp
|
||||
inc sp
|
||||
jp (hl)
|
||||
__endasm;
|
||||
}
|
||||
|
||||
void bios_clearwin_ch(uint8_t row, uint8_t col, uint8_t height,
|
||||
uint8_t width, uint8_t attr, char fillch) __naked
|
||||
{
|
||||
(void)row; (void)col; (void)height; (void)width; (void)attr; (void)fillch;
|
||||
__asm
|
||||
;; row->A, col->L, height/width/attr/fillch on stack [SP+2..5].
|
||||
;; LP_CLS_WIN2 (08Dh): D=row, E=col, H=height, L=width, B=attr,
|
||||
;; A=fillch.
|
||||
ld iy, #2
|
||||
add iy, sp
|
||||
ld d, a ; D = row (free A for fillch later)
|
||||
ld e, l ; E = col (free L for width later)
|
||||
ld h, 0 (iy) ; H = height
|
||||
ld c, 1 (iy) ; stash width
|
||||
ld b, 2 (iy) ; B = attr
|
||||
ld a, 3 (iy) ; A = fillch
|
||||
ld l, c ; L = width
|
||||
push ix
|
||||
ld c, #0x8D
|
||||
rst #0x08
|
||||
pop ix
|
||||
pop hl
|
||||
inc sp
|
||||
inc sp
|
||||
inc sp
|
||||
inc sp ; consume height + width + attr + fillch
|
||||
jp (hl)
|
||||
__endasm;
|
||||
}
|
||||
+135
-141
@@ -15,10 +15,18 @@
|
||||
#include <stdint.h>
|
||||
#include <errno.h>
|
||||
|
||||
|
||||
/* Forward extern — definition is further down (after putch/cputs which
|
||||
* reference it from asm by linker-symbol name). */
|
||||
extern int16_t g_text_attr;
|
||||
static two_bytes g_text_attr = {0};
|
||||
static uint8_t pc_ch = 0;
|
||||
extern two_bytes pc_place = {0};
|
||||
static uint8_t pc_raw_mode = 0;
|
||||
|
||||
|
||||
// TODO - проверить - ф-ии 30h-33h (kbhit/getch/getche/getkey)
|
||||
// не должны менять IX и им можно не делать push ix / pop ix
|
||||
//
|
||||
char kbhit(void) __naked
|
||||
{
|
||||
__asm
|
||||
@@ -91,10 +99,6 @@ uint16_t getkey(void) __naked
|
||||
* caller to use "\r\n" explicitly. Stdio puts/putchar do translate.
|
||||
*/
|
||||
|
||||
static uint8_t pc_ch = 0;
|
||||
static uint8_t pc_attr = 0;
|
||||
static uint8_t pc_row = 0;
|
||||
static uint8_t pc_col = 0;
|
||||
|
||||
/* Controls how _raw_putch treats control characters (< 0x20):
|
||||
* 0 (default) — BS/TAB/LF/CR are interpreted (no glyph output);
|
||||
@@ -104,7 +108,6 @@ static uint8_t pc_col = 0;
|
||||
* Only takes effect on the WRCHAR (attr ≤ 0xFF) path. When
|
||||
* g_text_attr is KEEP_EXIST_ATTR, ESTEX's own PUTCHAR/PCHARS handle
|
||||
* cursor and control chars — pc_raw_mode is irrelevant. */
|
||||
static uint8_t pc_raw_mode = 0;
|
||||
|
||||
void set_putch_raw_mode(uint8_t mode) { pc_raw_mode = mode; }
|
||||
uint8_t get_putch_raw_mode(void) { return pc_raw_mode; }
|
||||
@@ -115,14 +118,11 @@ uint8_t get_putch_raw_mode(void) { return pc_raw_mode; }
|
||||
static void _get_cursor(void) __naked
|
||||
{
|
||||
__asm
|
||||
push ix
|
||||
ld c, #0x53 ; ESTEX CURSOR
|
||||
rst #0x10
|
||||
ld a, d
|
||||
ld (_pc_row), a
|
||||
ld a, e
|
||||
ld (_pc_col), a
|
||||
pop ix
|
||||
; push ix
|
||||
ld c, #0x8e ; BIOS GetCursor
|
||||
rst #0x08
|
||||
ld (_pc_place), de
|
||||
; pop ix
|
||||
ret
|
||||
__endasm;
|
||||
}
|
||||
@@ -131,14 +131,11 @@ static void _get_cursor(void) __naked
|
||||
static void _set_cursor(void) __naked
|
||||
{
|
||||
__asm
|
||||
push ix
|
||||
ld a, (_pc_row)
|
||||
ld d, a
|
||||
ld a, (_pc_col)
|
||||
ld e, a
|
||||
ld c, #0x52 ; ESTEX LOCATE
|
||||
rst #0x10
|
||||
pop ix
|
||||
; push ix
|
||||
ld de, (_pc_place)
|
||||
ld c, #0x84 ; BIOS SetCursor
|
||||
rst #0x08
|
||||
; pop ix
|
||||
ret
|
||||
__endasm;
|
||||
}
|
||||
@@ -183,7 +180,7 @@ static char _bios_putchar(char ch) __naked
|
||||
* once before a sequence of _raw_putch calls and write it back once
|
||||
* after, so we pay the BIOS overhead per OPERATION instead of per CHAR. */
|
||||
/* Mode-0 worker: interprets BS/TAB/LF/CR, outputs other chars as glyphs. */
|
||||
static void _raw_putch_raw0(char ch, uint8_t attr) __naked
|
||||
static void _putch_wrchar(char ch, uint8_t attr) __naked
|
||||
{
|
||||
(void)ch; (void)attr;
|
||||
__asm
|
||||
@@ -191,6 +188,12 @@ static void _raw_putch_raw0(char ch, uint8_t attr) __naked
|
||||
;; Dispatch on control chars while A still holds ch (cp does not
|
||||
;; modify A). B/C only get loaded on the output path so the
|
||||
;; ctrl-char paths are cheaper.
|
||||
|
||||
;; ld c, a
|
||||
;; ld a, (_pc_ch) ; A = ch (`ld a,(nn)` does not touch flags)
|
||||
;; jr nz, _rp0_pri
|
||||
|
||||
;; ld a, c
|
||||
cp #0x08
|
||||
jr z, _rp0_bs
|
||||
cp #0x09
|
||||
@@ -201,18 +204,21 @@ static void _raw_putch_raw0(char ch, uint8_t attr) __naked
|
||||
jr z, _rp0_cr
|
||||
;; Anything else (printable or unrecognised ctrl) → glyph.
|
||||
|
||||
_rp0_pri:
|
||||
|
||||
ld c, a ; C = ch (save before A is clobbered)
|
||||
ld a, (_pc_row)
|
||||
ld a, (_pc_place + 1)
|
||||
cp #32
|
||||
ret nc ; off-screen bottom — silently skip
|
||||
ld d, a ; D = row (ESTEX WRCHAR convention)
|
||||
ld a, (_pc_col)
|
||||
ld a, (_pc_place)
|
||||
cp #80
|
||||
ret nc ; off-screen right — silently skip
|
||||
ld e, a ; E = col
|
||||
inc a
|
||||
ld (_pc_col), a ; pc_col++
|
||||
ld (_pc_place), a ; pc_col++
|
||||
|
||||
_rp0_wr:
|
||||
ld b, l ; B = attr
|
||||
ld a, c ; A = ch
|
||||
push ix
|
||||
@@ -222,56 +228,61 @@ static void _raw_putch_raw0(char ch, uint8_t attr) __naked
|
||||
ret
|
||||
|
||||
_rp0_bs:
|
||||
ld a, (_pc_col)
|
||||
ld a, (_pc_place)
|
||||
or a, a
|
||||
ret z ; already at col 0 — no change
|
||||
dec a
|
||||
ld (_pc_col), a
|
||||
ret
|
||||
ld (_pc_place), a
|
||||
ld e, a ; E = col
|
||||
ld a, (_pc_place + 1)
|
||||
ld d, a ; D = row (ESTEX WRCHAR convention)
|
||||
ld c, #0x20
|
||||
jr _rp0_wr
|
||||
|
||||
_rp0_tab:
|
||||
ld a, (_pc_col)
|
||||
and #0xF8 ; floor to mult of 8
|
||||
add a, #8 ; → next mult of 8
|
||||
cp #80
|
||||
jr c, _rp0_tab_store
|
||||
ld a, #80 ; cap at off-screen right
|
||||
_rp0_tab_store:
|
||||
ld (_pc_col), a
|
||||
ld a, (_pc_place)
|
||||
or #0x07 ; floor to mult of 8
|
||||
inc a ; → next mult of 8
|
||||
cp #81 ; сравнить A с 81 (0x51)
|
||||
jr c, _rp0_tab_skip ; если A < 81 (т.е. A ≤ 80), пропустить загрузку
|
||||
ld a, #80 ; иначе A > 80 → установить A = 80
|
||||
_rp0_tab_skip:
|
||||
ld (_pc_place), a
|
||||
ret
|
||||
|
||||
_rp0_lf:
|
||||
ld a, (_pc_row)
|
||||
ld a, (_pc_place + 1)
|
||||
cp #32
|
||||
ret nc ; already at bottom edge
|
||||
inc a
|
||||
ld (_pc_row), a
|
||||
ld (_pc_place + 1), a
|
||||
ret
|
||||
|
||||
_rp0_cr:
|
||||
xor a, a
|
||||
ld (_pc_col), a
|
||||
ld (_pc_place), a
|
||||
ret
|
||||
__endasm;
|
||||
}
|
||||
|
||||
/* Mode-1 worker: every byte goes through WRCHAR as a glyph. */
|
||||
static void _raw_putch_raw1(char ch, uint8_t attr) __naked
|
||||
static void _putch_wrchar_raw(char ch, uint8_t attr) __naked
|
||||
{
|
||||
(void)ch; (void)attr;
|
||||
__asm
|
||||
;; __sdcccall(1): ch in A, attr in L.
|
||||
ld c, a ; C = ch (save)
|
||||
ld a, (_pc_row)
|
||||
|
||||
ld a, (_pc_place + 1)
|
||||
cp #32
|
||||
ret nc ; off-screen bottom — silently skip
|
||||
ld d, a ; D = row
|
||||
ld a, (_pc_col)
|
||||
ld a, (_pc_place)
|
||||
cp #80
|
||||
ret nc ; off-screen right — silently skip
|
||||
ld e, a ; E = col
|
||||
inc a
|
||||
ld (_pc_col), a ; pc_col++
|
||||
ld (_pc_place), a ; pc_col++
|
||||
|
||||
ld b, l ; B = attr
|
||||
ld a, c ; A = ch
|
||||
@@ -283,18 +294,6 @@ static void _raw_putch_raw1(char ch, uint8_t attr) __naked
|
||||
__endasm;
|
||||
}
|
||||
|
||||
/* PCHARS (no attr) — used by cputs when KEEP_EXIST_ATTR is in effect. */
|
||||
static void _cputs_pchars(const char *s) __naked
|
||||
{
|
||||
(void)s;
|
||||
__asm
|
||||
push ix
|
||||
ld c, #0x5C ; ESTEX PCHARS
|
||||
rst #0x10
|
||||
pop ix
|
||||
ret
|
||||
__endasm;
|
||||
}
|
||||
|
||||
/* ---- Public putch / cputs --------------------------------------- *
|
||||
*
|
||||
@@ -306,6 +305,7 @@ static void _cputs_pchars(const char *s) __naked
|
||||
* folds the per-char CURSOR/LOCATE pair from the old design into a
|
||||
* single pair per operation. */
|
||||
|
||||
|
||||
char putch(char ch) __naked
|
||||
{
|
||||
(void)ch;
|
||||
@@ -313,38 +313,26 @@ char putch(char ch) __naked
|
||||
;; A = ch on entry; char return → A.
|
||||
ld (_pc_ch), a ; stash c (for both return and re-load)
|
||||
|
||||
;; KEEP_EXIST_ATTR? high byte of g_text_attr != 0
|
||||
ld a, (_g_text_attr + 1)
|
||||
or a, a
|
||||
jr nz, _putch_fast
|
||||
|
||||
;; --- WRCHAR path: cursor → worker → cursor ---
|
||||
call __get_cursor
|
||||
|
||||
;; Worker ABI (2 char/uint8 args): ch in A, attr in L.
|
||||
ld a, (_g_text_attr) ; A = low byte = attr
|
||||
ld l, a ; L = attr
|
||||
|
||||
ld a, (_pc_raw_mode)
|
||||
or a, a ; Z = (mode == 0)
|
||||
ld a, (_pc_ch) ; A = ch (`ld a,(nn)` does not touch flags)
|
||||
jr nz, _putch_use_raw1
|
||||
call __raw_putch_raw0
|
||||
jr nz, _putch_use_raw
|
||||
call __putch_wrchar
|
||||
jr _putch_after_raw
|
||||
_putch_use_raw1:
|
||||
call __raw_putch_raw1
|
||||
_putch_use_raw:
|
||||
call __putch_wrchar_raw
|
||||
_putch_after_raw:
|
||||
call __set_cursor
|
||||
ld a, (_pc_ch) ; return value
|
||||
ret
|
||||
|
||||
_putch_fast:
|
||||
ld a, (_pc_ch)
|
||||
call __bios_putchar ; __bios_putchar keeps AF
|
||||
ret
|
||||
__endasm;
|
||||
}
|
||||
|
||||
|
||||
char cputs(const char *s) __naked
|
||||
{
|
||||
(void)s;
|
||||
@@ -356,53 +344,48 @@ char cputs(const char *s) __naked
|
||||
or a, l
|
||||
ret z
|
||||
|
||||
push ix
|
||||
|
||||
call __get_cursor
|
||||
|
||||
;; KEEP_EXIST_ATTR? high byte of g_text_attr != 0
|
||||
ld a, (_g_text_attr + 1)
|
||||
or a, a
|
||||
jr nz, _cputs_fast
|
||||
|
||||
;; --- WRCHAR path: cursor → worker → cursor ---
|
||||
call __get_cursor
|
||||
|
||||
;; Worker walks the string via DE; HL is used to carry attr in L
|
||||
;; across iterations (RST 10 inside worker clobbers it, so we
|
||||
;; push/pop hl around each call).
|
||||
ex de, hl ; DE = s
|
||||
ld a, (_g_text_attr) ; A = attr (low byte)
|
||||
ld l, a ; L = attr (worker ABI: arg2 in L)
|
||||
|
||||
;; Pick worker once based on pc_raw_mode; IX = function pointer.
|
||||
ld a, (_pc_raw_mode)
|
||||
or a, a
|
||||
jr z, _cputs_use_raw0
|
||||
ld ix, #__raw_putch_raw1
|
||||
jr _cputs_loop
|
||||
_cputs_use_raw0:
|
||||
ld ix, #__raw_putch_raw0
|
||||
jr nz, _cputs_bios
|
||||
|
||||
_cputs_loop:
|
||||
ld a, (de)
|
||||
or a, a
|
||||
jr z, _cputs_loop_end
|
||||
inc de
|
||||
;; Z80 has no "call (ix)" — emulate via push-of-ret + jp (ix).
|
||||
push de ; save string pointer
|
||||
push hl ; save attr (in L)
|
||||
ld de, #_cputs_after_worker
|
||||
push de ; push return address
|
||||
jp (ix) ; "call" worker
|
||||
_cputs_after_worker:
|
||||
pop hl
|
||||
push hl
|
||||
pop de
|
||||
jr _cputs_loop
|
||||
_cputs_loop_end:
|
||||
|
||||
call __set_cursor
|
||||
xor a, a ; return 0
|
||||
ret
|
||||
_cputs_wrloop:
|
||||
ld a, (_g_text_attr)
|
||||
ld l, a
|
||||
ld a, (de) ; загрузить байт
|
||||
or a ; установить флаг Z, если A == 0
|
||||
jr z, _cputs_ex ; завершить подпрограмму (конец строки)
|
||||
push de
|
||||
call __putch_wrchar ; вывести символ (A передаётся как аргумент)
|
||||
pop de
|
||||
inc de ; перейти к следующему байту
|
||||
jr _cputs_wrloop ; повторить
|
||||
|
||||
_cputs_bios:
|
||||
ld a, (_g_text_attr)
|
||||
ld b, #0xFF
|
||||
ld d, #0x0
|
||||
ld e, a
|
||||
ld c, #0x8B ; BIOS LP_PRINT_LN5
|
||||
rst #0x08
|
||||
jr _cputs_ex
|
||||
_cputs_fast:
|
||||
call __cputs_pchars
|
||||
ld c, #0x5C ; ESTEX PCHARS
|
||||
rst #0x10
|
||||
_cputs_ex:
|
||||
call __set_cursor
|
||||
pop ix ; restore callers IX
|
||||
xor a, a ; return 0
|
||||
ret
|
||||
__endasm;
|
||||
@@ -439,12 +422,15 @@ void gotoxy(uint8_t x, uint8_t y) __naked
|
||||
__asm
|
||||
;; __sdcccall(1) 2 uint8 args: x in A, y in L.
|
||||
;; ESTEX LOCATE ($52) wants: D = row, E = col.
|
||||
push ix
|
||||
ld d, l ; D = row (y)
|
||||
ld e, a ; E = col (x)
|
||||
ld c, #0x52
|
||||
rst #0x10
|
||||
pop ix
|
||||
;; push ix
|
||||
ld d, l ; D = row (y)
|
||||
ld e, a ; E = col (x)
|
||||
ld (_pc_place), de
|
||||
ld c, #0x84 ; BIOS SetCursor
|
||||
rst #0x08
|
||||
;; ld c, #0x52
|
||||
;; rst #0x10
|
||||
;; pop ix
|
||||
ret
|
||||
__endasm;
|
||||
}
|
||||
@@ -453,11 +439,14 @@ uint8_t wherex(void) __naked
|
||||
{
|
||||
__asm
|
||||
;; ESTEX CURSOR ($53): D = row, E = col. Return col in DE.
|
||||
push ix
|
||||
ld c, #0x53
|
||||
rst #0x10
|
||||
pop ix
|
||||
ld a, e
|
||||
;; push ix
|
||||
ld c, #0x8e ; BIOS GetCursor
|
||||
rst #0x08
|
||||
;; ld c, #0x53
|
||||
;; rst #0x10
|
||||
;; pop ix
|
||||
ld a, e
|
||||
ld (_pc_place), de
|
||||
ret
|
||||
__endasm;
|
||||
}
|
||||
@@ -465,11 +454,14 @@ uint8_t wherex(void) __naked
|
||||
uint8_t wherey(void) __naked
|
||||
{
|
||||
__asm
|
||||
push ix
|
||||
ld c, #0x53
|
||||
rst #0x10
|
||||
pop ix
|
||||
ld a, d
|
||||
;; push ix
|
||||
ld c, #0x8e ; BIOS GetCursor
|
||||
rst #0x08
|
||||
;; ld c, #0x53
|
||||
;; rst #0x10
|
||||
;; pop ix
|
||||
ld a, d
|
||||
ld (_pc_place), de
|
||||
ret
|
||||
__endasm;
|
||||
}
|
||||
@@ -478,10 +470,13 @@ uint16_t wherexy(void) __naked
|
||||
{
|
||||
__asm
|
||||
;; ESTEX CURSOR ($53): D = row, E = col. Return col in DE.
|
||||
push ix
|
||||
ld c, #0x53
|
||||
rst #0x10
|
||||
pop ix
|
||||
;; push ix
|
||||
ld c, #0x8e ; BIOS GetCursor
|
||||
rst #0x08
|
||||
;; ld c, #0x53
|
||||
;; rst #0x10
|
||||
;; pop ix
|
||||
ld (_pc_place), de
|
||||
ret
|
||||
__endasm;
|
||||
}
|
||||
@@ -570,12 +565,12 @@ uint16_t rdchar(uint8_t x, uint8_t y) __naked
|
||||
extern uint8_t _videomode_raw_get(void);
|
||||
extern int _videomode_raw_set(uint8_t mode);
|
||||
|
||||
uint8_t get_videotextmode(void)
|
||||
uint8_t gettextmode(void)
|
||||
{
|
||||
return _videomode_raw_get();
|
||||
}
|
||||
|
||||
int set_videotextmode(uint8_t mode)
|
||||
int settextmode(uint8_t mode)
|
||||
{
|
||||
/* Refuse anything that isn't a known text mode — otherwise a stray
|
||||
* GFX_MODE_* value could swap the screen out from under text I/O. */
|
||||
@@ -593,18 +588,17 @@ int set_videotextmode(uint8_t mode)
|
||||
*
|
||||
* 0x00..0xFF — real attribute (4-bit FG | 3-bit BG | 1-bit blink)
|
||||
* KEEP_EXIST_ATTR (0xFFFF) — putch/cputs fall back to fast no-attr path */
|
||||
int16_t g_text_attr = 0x0F;
|
||||
|
||||
int16_t set_text_attr(int16_t attr)
|
||||
{
|
||||
int16_t prev = g_text_attr;
|
||||
g_text_attr = attr;
|
||||
int16_t prev = g_text_attr.value;
|
||||
g_text_attr.value = attr;
|
||||
return prev;
|
||||
}
|
||||
|
||||
int16_t get_text_attr(void)
|
||||
{
|
||||
return g_text_attr;
|
||||
return g_text_attr.value;
|
||||
}
|
||||
|
||||
/* ---- Turbo-C-style palette helpers --------------------------------
|
||||
@@ -613,21 +607,21 @@ int16_t get_text_attr(void)
|
||||
|
||||
void textcolor(uint8_t fg)
|
||||
{
|
||||
/* If we were KEEP_EXIST_ATTR, switch to a real attr first. */
|
||||
uint8_t cur = ((uint16_t)g_text_attr > 0xFF) ? 0x00 : (uint8_t)g_text_attr;
|
||||
g_text_attr = (int16_t)((cur & 0xF0) | (fg & 0x0F));
|
||||
fg = (fg & 0x07);
|
||||
g_text_attr.value = (g_text_attr.byte.low & 0xF0) | fg;
|
||||
// g_text_attr.byte.high = 0;
|
||||
}
|
||||
|
||||
void textbackground(uint8_t bg)
|
||||
{
|
||||
uint8_t cur = ((uint16_t)g_text_attr > 0xFF) ? 0x00 : (uint8_t)g_text_attr;
|
||||
/* Background uses 3 bits (4..6); preserve blink (bit 7) too. */
|
||||
g_text_attr = (int16_t)((cur & 0x8F) | ((bg & 0x07) << 4));
|
||||
bg = (bg & 0x07) << 4;
|
||||
g_text_attr.value = (g_text_attr.byte.low & 0x0F) | bg;
|
||||
// g_text_attr.byte.high = 0;
|
||||
}
|
||||
|
||||
void textattr(uint8_t attr)
|
||||
{
|
||||
g_text_attr = (int16_t)attr;
|
||||
g_text_attr.value = (uint16_t)attr;
|
||||
}
|
||||
|
||||
/* ---- Solid-C compatibility ---------------------------------------- */
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* text_palette.c — text-mode palette wrappers.
|
||||
*
|
||||
* Thin layer over libc/video/palette.c. Translates the text "plane
|
||||
* number" 0..3 (paper/ink/blink-paper/blink-ink) into the underlying
|
||||
* BIOS palette page 4..7 used by $A4.
|
||||
*
|
||||
* For an introduction to the four-plane text colour model see
|
||||
* <palette.h> (top-of-file doc-block).
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <conio.h>
|
||||
#include <palette.h>
|
||||
|
||||
void text_pal_load(uint8_t plane, uint8_t start, uint8_t count,
|
||||
const uint8_t *data)
|
||||
{
|
||||
pal_load((uint8_t)(plane + 4u), start, count, data);
|
||||
}
|
||||
|
||||
void text_pal_set_color(uint8_t plane, uint8_t attr,
|
||||
uint8_t r, uint8_t g, uint8_t b)
|
||||
{
|
||||
pal_set_color((uint8_t)(plane + 4u), attr, r, g, b);
|
||||
}
|
||||
|
||||
void text_pal_get(uint8_t plane, uint8_t start, uint8_t count, uint8_t *data)
|
||||
{
|
||||
pal_get((uint8_t)(plane + 4u), start, count, data);
|
||||
}
|
||||
|
||||
void text_pal_get_color(uint8_t plane, uint8_t attr,
|
||||
uint8_t *r, uint8_t *g, uint8_t *b)
|
||||
{
|
||||
pal_get_color((uint8_t)(plane + 4u), attr, r, g, b);
|
||||
}
|
||||
|
||||
void text_pal_reset(void)
|
||||
{
|
||||
pal_reset(PAL_CGA);
|
||||
}
|
||||
+4
-41
@@ -13,7 +13,7 @@
|
||||
* gfx_set_draw_page / get — updates _gfx_addr_base for the new page
|
||||
* gfx_set_bank / get — sets the W3 page byte (0x50..0x5F)
|
||||
* gfx_wait_vsync — EI; HALT until next frame interrupt
|
||||
* gfx_pal_load / gfx_pal_set — BIOS $A4 PIC_SET_PAL wrappers
|
||||
* (palette wrappers live in gfx_palette.c, backed by libc/video/palette.c)
|
||||
*
|
||||
* Shared state (extern from this file):
|
||||
* _gfx_addr_base — 0xC000 for page 0, 0xC140 for page 1. Every
|
||||
@@ -136,43 +136,6 @@ void gfx_wait_vsync(void) __naked
|
||||
__endasm;
|
||||
}
|
||||
|
||||
/* ---- Palette (BIOS $A4 PIC_SET_PAL) ----------------------------- */
|
||||
|
||||
static uint8_t pal_num_;
|
||||
static uint8_t pal_start_;
|
||||
static uint8_t pal_count_;
|
||||
static uint16_t pal_data_;
|
||||
|
||||
void gfx_pal_load(uint8_t pal_num, uint8_t start, uint8_t count,
|
||||
const uint8_t *data)
|
||||
{
|
||||
pal_num_ = pal_num;
|
||||
pal_start_ = start;
|
||||
pal_count_ = count;
|
||||
pal_data_ = (uint16_t)(uintptr_t)data;
|
||||
|
||||
__asm
|
||||
push ix
|
||||
ld a, (_pal_start_)
|
||||
ld e, a ; E = start
|
||||
ld a, (_pal_count_)
|
||||
ld d, a ; D = count (0 → 256)
|
||||
ld hl, (_pal_data_) ; HL = data
|
||||
ld b, #0xFF ; mask
|
||||
ld a, (_pal_num_) ; A = palette number
|
||||
ld c, #0xA4 ; BIOS PIC_SET_PAL
|
||||
rst #0x08
|
||||
pop ix
|
||||
__endasm;
|
||||
}
|
||||
|
||||
void gfx_pal_set(uint8_t pal_num, uint8_t idx,
|
||||
uint8_t r, uint8_t g, uint8_t b)
|
||||
{
|
||||
uint8_t entry[4];
|
||||
entry[0] = b;
|
||||
entry[1] = g;
|
||||
entry[2] = r;
|
||||
entry[3] = 0;
|
||||
gfx_pal_load(pal_num, idx, 1, entry);
|
||||
}
|
||||
/* Palette wrappers (gfx_pal_load / gfx_pal_set / gfx_pal_get /
|
||||
* gfx_pal_get_color / gfx_pal_reset) moved to gfx_palette.c — see also
|
||||
* libc/video/palette.c for the shared low-level $A4 / $A6 implementation. */
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* gfx_palette.c — graphics-side palette wrappers.
|
||||
*
|
||||
* The "legacy" entry points (gfx_pal_load / gfx_pal_set) keep their own
|
||||
* inline-asm implementation rather than thunking through pal_*; this
|
||||
* avoids depending on SDCC's __sdcccall(1) stack-shuffling for nested
|
||||
* calls, which empirically misbehaves here (the trampoline corrupts the
|
||||
* text palette so the next text-mode print hangs). Functionally these
|
||||
* are identical to the old gfx_core.c versions.
|
||||
*
|
||||
* The newer get / reset helpers are simple wrappers — they're either
|
||||
* not on hot paths or are first introduced here, so the thunk overhead
|
||||
* doesn't matter and we get to share the asm with libc/video/palette.c.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <gfx.h>
|
||||
#include <palette.h>
|
||||
|
||||
static uint8_t gpl_num_;
|
||||
static uint8_t gpl_start_;
|
||||
static uint8_t gpl_count_;
|
||||
static uint16_t gpl_data_;
|
||||
|
||||
void gfx_pal_load(uint8_t pal_num, uint8_t start, uint8_t count,
|
||||
const uint8_t *data)
|
||||
{
|
||||
gpl_num_ = pal_num;
|
||||
gpl_start_ = start;
|
||||
gpl_count_ = count;
|
||||
gpl_data_ = (uint16_t)(uintptr_t)data;
|
||||
|
||||
__asm
|
||||
push ix
|
||||
ld a, (_gpl_start_)
|
||||
ld e, a ; E = start
|
||||
ld a, (_gpl_count_)
|
||||
ld d, a ; D = count (0 → 256)
|
||||
ld hl, (_gpl_data_) ; HL = data
|
||||
ld b, #0xFF ; mask
|
||||
ld a, (_gpl_num_) ; A = palette number
|
||||
ld c, #0xA4 ; BIOS PIC_SET_PAL
|
||||
rst #0x08
|
||||
pop ix
|
||||
__endasm;
|
||||
}
|
||||
|
||||
void gfx_pal_set(uint8_t pal_num, uint8_t idx,
|
||||
uint8_t r, uint8_t g, uint8_t b)
|
||||
{
|
||||
uint8_t entry[4];
|
||||
entry[0] = b;
|
||||
entry[1] = g;
|
||||
entry[2] = r;
|
||||
entry[3] = 0;
|
||||
gfx_pal_load(pal_num, idx, 1, entry);
|
||||
}
|
||||
|
||||
void gfx_pal_get(uint8_t pal_num, uint8_t start, uint8_t count,
|
||||
uint8_t *data)
|
||||
{
|
||||
pal_get(pal_num, start, count, data);
|
||||
}
|
||||
|
||||
void gfx_pal_get_color(uint8_t pal_num, uint8_t idx,
|
||||
uint8_t *r, uint8_t *g, uint8_t *b)
|
||||
{
|
||||
pal_get_color(pal_num, idx, r, g, b);
|
||||
}
|
||||
|
||||
void gfx_pal_reset(void)
|
||||
{
|
||||
pal_reset(PAL_GRAPH);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* bios/text.h — direct wrappers around the Sprinter BIOS text-output
|
||||
* calls (RST 8, function codes 081h..08Eh, "LP_*" in the vendor docs).
|
||||
*
|
||||
* These bypass ESTEX's own PUTCHAR/PCHARS/WRCHAR (see <conio.h>) and talk
|
||||
* straight to the BIOS character-print routines. Use them when you need
|
||||
* BIOS-specific behaviour (run-length fills, pad/stop-at-separator field
|
||||
* printing, raw window clear/scroll) that ESTEX doesn't expose directly.
|
||||
*
|
||||
* Coordinates are 0-based (row = vertical, col = horizontal), matching
|
||||
* ESTEX/BIOS directly.
|
||||
*
|
||||
* Original BIOS name -> wrapper, with opcode (docs/converted/bios.txt):
|
||||
*
|
||||
* 081h LP_PRINT_ALL -> bios_fillchar fill run: char + attr
|
||||
* 082h LP_PRINT_SYM -> bios_fillchar_noattr fill run: char only
|
||||
* 083h LP_PRINT_ATR -> bios_fillattr fill run: attr only
|
||||
* 084h LP_SET_PLACE -> bios_set_place set BIOS print cursor
|
||||
* 08Eh LP_GET_PLACE -> bios_get_place read BIOS print cursor
|
||||
* 085h LP_PRINT_LN -> bios_writeattr write buf + attr
|
||||
* 086h LP_PRINT_LN2 -> bios_write write buf, keep attr
|
||||
* 087h LP_PRINT_LN3 -> bios_writeattr_until write to sep, pad + attr
|
||||
* 088h LP_PRINT_LN4 -> bios_write_until write to sep, pad
|
||||
* 089h LP_CLS_WIN -> bios_clearwin clear window (space fill)
|
||||
* 08Ah LP_SCROLL_UD -> bios_scrollwin scroll global window
|
||||
* 08Bh LP_PRINT_LN5 -> bios_writeattr_stop write to sep, stop + attr
|
||||
* 08Ch LP_PRINT_LN6 -> bios_write_stop write to sep, stop
|
||||
* 08Dh LP_CLS_WIN2 -> bios_clearwin_ch clear window (custom fill)
|
||||
*
|
||||
* "_until" variants print up to `sep` and then PAD the rest of the field
|
||||
* with spaces (output is always exactly `len` characters wide).
|
||||
* "_stop" variants print up to `sep` and STOP there (no padding; `sep`
|
||||
* itself is not printed; `maxlen` is just an upper bound).
|
||||
*
|
||||
* ABI: stack must already be in window 2 (guaranteed by crt0) — required
|
||||
* by the BIOS for its own page-swap scratch use. All registers (main +
|
||||
* shadow set) are clobbered except where a function says otherwise.
|
||||
*/
|
||||
|
||||
#ifndef BIOS_TEXT_H
|
||||
#define BIOS_TEXT_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* bios_scrollwin() direction values (BIOS B register). */
|
||||
#define BIOS_SCROLL_UP 1
|
||||
#define BIOS_SCROLL_DOWN 2
|
||||
|
||||
/* 081h LP_PRINT_ALL — print `count` copies of `ch` with attribute `attr`
|
||||
* starting at the current BIOS print position. Preserves HL and IX. */
|
||||
void bios_fillcharattr(char ch, uint8_t attr, uint8_t count);
|
||||
|
||||
/* 082h LP_PRINT_SYM — print `count` copies of `ch`, attribute unchanged
|
||||
* (whatever is already on screen). Preserves HL and IX. */
|
||||
void bios_fillchar(char ch, uint8_t count);
|
||||
|
||||
/* 083h LP_PRINT_ATR — overwrite `count` cells' attribute with `attr`;
|
||||
* the characters already on screen are left untouched. Preserves HL
|
||||
* and IX. */
|
||||
void bios_fillattr(uint8_t attr, uint8_t count);
|
||||
|
||||
/* 084h LP_SET_PLACE — set the BIOS print cursor. Out-of-range values
|
||||
* wrap (BIOS subtracts the window size, not an error). */
|
||||
void bios_set_place(uint8_t row, uint8_t col);
|
||||
|
||||
/* 08Eh LP_GET_PLACE — read the BIOS print cursor: (row << 8) | col. */
|
||||
uint16_t bios_get_place(void);
|
||||
|
||||
/* 085h LP_PRINT_LN — write `len` bytes from `s` with attribute `attr`,
|
||||
* starting at the current BIOS print position. `s` must point inside
|
||||
* window 2 (#4000..#BFFF). */
|
||||
void bios_writeattr(const char *s, uint8_t len, uint8_t attr);
|
||||
|
||||
/* 086h LP_PRINT_LN2 — like bios_writeattr(), attribute unchanged. */
|
||||
void bios_write(const char *s, uint8_t len);
|
||||
|
||||
/* 087h LP_PRINT_LN3 — write from `s` with attribute `attr` until the
|
||||
* byte `sep` is found, then pad with spaces so exactly `len` characters
|
||||
* are printed in total. */
|
||||
void bios_writeattr_until(const char *s, uint8_t len, uint8_t attr, char sep);
|
||||
|
||||
/* 088h LP_PRINT_LN4 — like bios_writeattr_until(), attribute unchanged. */
|
||||
void bios_write_until(const char *s, uint8_t len, char sep);
|
||||
|
||||
/* 089h LP_CLS_WIN — clear a `height` x `width` local window at
|
||||
* (row, col) by writing spaces with attribute `attr`. */
|
||||
void bios_clearwin(uint8_t row, uint8_t col, uint8_t height, uint8_t width,
|
||||
uint8_t attr);
|
||||
|
||||
/* 08Ah LP_SCROLL_UD — scroll `count` whole rows of the global window,
|
||||
* starting at `row`, in direction `dir` (BIOS_SCROLL_UP/_DOWN). */
|
||||
void bios_scrollwin(uint8_t dir, uint8_t row, uint8_t count);
|
||||
|
||||
/* 08Bh LP_PRINT_LN5 — write from `s` with attribute `attr`, stopping
|
||||
* (without padding) as soon as `sep` is seen; `maxlen` bounds the scan. */
|
||||
void bios_writeattr_stop(const char *s, uint8_t maxlen, uint8_t attr,
|
||||
char sep);
|
||||
|
||||
/* 08Ch LP_PRINT_LN6 — like bios_writeattr_stop(), attribute unchanged. */
|
||||
void bios_write_stop(const char *s, uint8_t maxlen, char sep);
|
||||
|
||||
/* 08Dh LP_CLS_WIN2 — like bios_clearwin(), but fills with `fillch`
|
||||
* instead of a space. */
|
||||
void bios_clearwin_ch(uint8_t row, uint8_t col, uint8_t height,
|
||||
uint8_t width, uint8_t attr, char fillch);
|
||||
|
||||
#endif
|
||||
+46
-2
@@ -74,12 +74,24 @@ uint16_t getkey(void);
|
||||
#define KEY_PGUP 0x59
|
||||
#define KEY_INS 0x50 /* numpad 0; not verified */
|
||||
#define KEY_DEL 0x55 /* numpad 5/.; not verified */
|
||||
|
||||
typedef union {
|
||||
uint16_t value;
|
||||
struct {
|
||||
uint8_t low;
|
||||
uint8_t high;
|
||||
} byte;
|
||||
} two_bytes;
|
||||
|
||||
|
||||
|
||||
char putch (char c);
|
||||
char cputs (const char *s);
|
||||
int cprintf(const char *fmt, ...);
|
||||
void clrscr(void);
|
||||
void gotoxy(uint8_t x, uint8_t y);
|
||||
|
||||
|
||||
/* Solid-C compatibility helpers. */
|
||||
#define home() gotoxy(0, 0)
|
||||
#define inp(port) z80_inp(port)
|
||||
@@ -124,8 +136,8 @@ void clrscr_attr(uint8_t attr);
|
||||
#define TEXT_MODE_40x32 0x02
|
||||
#define TEXT_MODE_80x32 0x03
|
||||
|
||||
uint8_t get_videotextmode(void);
|
||||
int set_videotextmode(uint8_t mode); /* 0 OK, -1 + errno on bad mode */
|
||||
uint8_t gettextmode(void);
|
||||
int settextmode(uint8_t mode); /* 0 OK, -1 + errno on bad mode */
|
||||
|
||||
/* ------------------------------------------------------------------ *
|
||||
* Text-output attribute (used by the conio set: putch / cputs / cprintf).
|
||||
@@ -201,4 +213,36 @@ enum {
|
||||
#define COLOR_BLINK 0x80u
|
||||
#define COLOR(fg, bg) ((uint8_t)((((bg) & 0x07) << 4) | ((fg) & 0x0F)))
|
||||
|
||||
/* Text-mode palette. The 16 logical CGA colours seen by COLOR(fg, bg)
|
||||
* actually live in four 256-entry hardware palette planes indexed by the
|
||||
* full 8-bit attribute byte:
|
||||
*
|
||||
* TEXT_PAL_PAPER — background colour, non-blink phase
|
||||
* TEXT_PAL_INK — foreground colour, non-blink phase
|
||||
* TEXT_PAL_BLINK_PAPER — background colour during the blink half-cycle
|
||||
* TEXT_PAL_BLINK_INK — foreground colour during the blink half-cycle
|
||||
*
|
||||
* For non-blinking attributes (bit 7 = 0) all four planes display the
|
||||
* same colours, so writing to PAPER/INK is enough. For blinking attrs
|
||||
* (bit 7 = 1) the renderer alternates between the non-blink and blink
|
||||
* planes — that's how flash is implemented in hardware.
|
||||
*
|
||||
* These wrappers add 4 to the plane index and forward to the low-level
|
||||
* <palette.h> API (pal_load / pal_set_color / pal_get / pal_get_color).
|
||||
* Use text_pal_reset() to restore the system default CGA palette. */
|
||||
#define TEXT_PAL_PAPER 0
|
||||
#define TEXT_PAL_INK 1
|
||||
#define TEXT_PAL_BLINK_PAPER 2
|
||||
#define TEXT_PAL_BLINK_INK 3
|
||||
|
||||
void text_pal_load (uint8_t plane, uint8_t start, uint8_t count,
|
||||
const uint8_t *bgr0);
|
||||
void text_pal_set_color(uint8_t plane, uint8_t attr,
|
||||
uint8_t r, uint8_t g, uint8_t b);
|
||||
void text_pal_get (uint8_t plane, uint8_t start, uint8_t count,
|
||||
uint8_t *bgr0);
|
||||
void text_pal_get_color(uint8_t plane, uint8_t attr,
|
||||
uint8_t *r, uint8_t *g, uint8_t *b);
|
||||
void text_pal_reset (void);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -145,4 +145,15 @@ void gfx_pal_load(uint8_t pal_num, uint8_t start, uint8_t count,
|
||||
void gfx_pal_set (uint8_t pal_num, uint8_t idx,
|
||||
uint8_t r, uint8_t g, uint8_t b);
|
||||
|
||||
/* Read a contiguous block of entries back from a graphics palette. */
|
||||
void gfx_pal_get (uint8_t pal_num, uint8_t start, uint8_t count,
|
||||
uint8_t *data);
|
||||
|
||||
/* Read one entry into R, G, B pointers (any may be NULL). */
|
||||
void gfx_pal_get_color(uint8_t pal_num, uint8_t idx,
|
||||
uint8_t *r, uint8_t *g, uint8_t *b);
|
||||
|
||||
/* Restore the system default graphics palette (BIOS $A6, type=1). */
|
||||
void gfx_pal_reset(void);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* palette.h — Sprinter palette (low-level).
|
||||
*
|
||||
* The Sprinter has eight 256-colour palette pages, shared by graphics and
|
||||
* text modes:
|
||||
*
|
||||
* pal_num 0..3 — graphics palettes. Each character cell selects which
|
||||
* of the four it uses through bits 7..6 of its mode byte.
|
||||
* Used by graphics modes 0x81 / 0x82.
|
||||
* pal_num 4..7 — text-mode planes — together they form the colour table
|
||||
* of the text-mode attribute byte:
|
||||
* 4 = paper (background, non-blink phase)
|
||||
* 5 = ink (foreground, non-blink phase)
|
||||
* 6 = paper-blink (background during blink half-cycle)
|
||||
* 7 = ink-blink (foreground during blink half-cycle)
|
||||
* Each plane holds 256 BGR colours indexed directly by
|
||||
* the 8-bit attribute byte of the cell.
|
||||
*
|
||||
* Entry format — 4 bytes per colour, in Blue-Green-Red-pad order; the pad
|
||||
* byte is reserved and must be 0.
|
||||
*
|
||||
* Notes on the underlying BIOS ($A4):
|
||||
* - `count = 0` means 256 entries (full plane).
|
||||
* - On write, data is AND-masked against `pal_mask` before reaching
|
||||
* VRAM. pal_load/pal_set_color hard-code mask = 0xFF (no masking).
|
||||
*
|
||||
* Blink semantics (text planes):
|
||||
* The hardware constantly alternates between planes 4↔6 (paper) and
|
||||
* 5↔7 (ink). To DISABLE blink everywhere in IBM-CGA style, copy plane
|
||||
* 4 → 6 and 5 → 7 (so both phases show the same colour). To ENABLE
|
||||
* ZX-Spectrum style flash, swap entries 6 ↔ 7 for the desired attrs.
|
||||
* The system default (PAL_CGA) leaves flash visible only for attribute
|
||||
* bytes with bit 7 set.
|
||||
*
|
||||
* Backed by BIOS PIC_SET_PAL/PIC_GET_PAL ($A4) and SET_PAL_INIT ($A6).
|
||||
* Higher-level wrappers live in <conio.h> (text_pal_*) and <gfx.h>
|
||||
* (gfx_pal_*); use those directly unless you need raw plane control.
|
||||
*/
|
||||
|
||||
#ifndef PALETTE_H
|
||||
#define PALETTE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Default-palette types for pal_reset(). Values match BIOS $A6 B-register. */
|
||||
#define PAL_GRAPH 1 /* graphics palette (planes 0..3) */
|
||||
#define PAL_SINCLAIR 2 /* Spectrum palette (text planes 4..7, ZX colours) */
|
||||
#define PAL_CGA 3 /* CGA text palette (text planes 4..7, IBM CGA) */
|
||||
|
||||
/* Load a contiguous block of palette entries.
|
||||
* pal_num: 0..7 (0..3 graphics, 4..7 text)
|
||||
* start: first slot (0..255)
|
||||
* count: number of slots (0 means 256)
|
||||
* bgr0: pointer to count entries of 4 bytes each: Blue, Green, Red, 0 */
|
||||
void pal_load(uint8_t pal_num, uint8_t start, uint8_t count,
|
||||
const uint8_t *bgr0);
|
||||
|
||||
/* Read a contiguous block of palette entries back into RAM.
|
||||
* Same parameter shape as pal_load — bgr0 here is a write buffer of
|
||||
* count*4 bytes that receives B,G,R,0 quadruples. */
|
||||
void pal_get (uint8_t pal_num, uint8_t start, uint8_t count,
|
||||
uint8_t *bgr0);
|
||||
|
||||
/* Convenience: set one entry from an RGB triple. */
|
||||
void pal_set_color(uint8_t pal_num, uint8_t slot,
|
||||
uint8_t r, uint8_t g, uint8_t b);
|
||||
|
||||
/* Convenience: read one entry into R,G,B pointers (any may be NULL). */
|
||||
void pal_get_color(uint8_t pal_num, uint8_t slot,
|
||||
uint8_t *r, uint8_t *g, uint8_t *b);
|
||||
|
||||
/* Restore a built-in default palette (BIOS $A6 SET_PAL_INIT).
|
||||
* type: PAL_GRAPH / PAL_SINCLAIR / PAL_CGA.
|
||||
* Internally:
|
||||
* PAL_GRAPH → A=0, E=0, B=1 (resets graphics palette 0)
|
||||
* PAL_SINCLAIR → A=0, E=0, B=2
|
||||
* PAL_CGA → A=0, E=0, B=3 (resets all text planes 4..7)
|
||||
*
|
||||
* Use pal_reset_at() if you need a non-zero page or a non-zero graphics
|
||||
* palette index. */
|
||||
void pal_reset(uint8_t type);
|
||||
|
||||
/* Full-control variant of pal_reset.
|
||||
* pal_page: BIOS A register — "palette page" hardware index.
|
||||
* graph_pal: BIOS E register — for PAL_GRAPH, target palette 0..3.
|
||||
* type: BIOS B register — PAL_GRAPH / PAL_SINCLAIR / PAL_CGA. */
|
||||
void pal_reset_at(uint8_t type, uint8_t pal_page, uint8_t graph_pal);
|
||||
|
||||
#endif
|
||||
@@ -47,23 +47,47 @@
|
||||
* n : 1..255
|
||||
* ret : blk_id (1..255) on success; 0 on failure with errno set.
|
||||
* The id is opaque — pass it to mem_get_page() and mem_free_block(). */
|
||||
uint8_t mem_alloc_pages(uint8_t n);
|
||||
uint8_t mem_alloc_pages_estex(uint8_t n);
|
||||
uint8_t mem_alloc_pages_bios(uint8_t n);
|
||||
|
||||
/* Release a block previously returned by mem_alloc_pages().
|
||||
* On error errno is set (e.g. EINVAL for unknown id). Double-free is
|
||||
* NOT idempotent: the second call sets errno. */
|
||||
void mem_free_block(uint8_t blk_id);
|
||||
void mem_free_block_estex(uint8_t blk_id);
|
||||
void mem_free_block_bios(uint8_t blk_id);
|
||||
|
||||
/* Translate (block, page-index) into a physical page number suitable
|
||||
* for sprinter_page_w1/w2/w3() or the bank_*() helpers below.
|
||||
* blk_id: from mem_alloc_pages()
|
||||
* idx : 0..(n-1)
|
||||
* ret : physical page (1..255) on success; 0 on failure (errno set). */
|
||||
uint8_t mem_get_page(uint8_t blk_id, uint8_t idx);
|
||||
uint8_t mem_get_page_bios(uint8_t blk_id, uint8_t idx);
|
||||
|
||||
/* Query the EMM allocator state. Both pointers must be non-NULL.
|
||||
* Cannot fail (no error path). */
|
||||
void mem_info(uint16_t *total, uint16_t *free_pages);
|
||||
void mem_info_estex(uint16_t *total, uint16_t *free_pages);
|
||||
void mem_info_bios(uint16_t *total, uint16_t *free_pages);
|
||||
|
||||
#define MEM_MANAGE_MODE_BIOS
|
||||
|
||||
#ifdef MEM_MANAGE_MODE_ESTEX
|
||||
|
||||
#define mem_alloc_pages mem_alloc_pages_estex
|
||||
#define mem_free_block mem_free_block_estex
|
||||
#define mem_info mem_info_estex
|
||||
|
||||
#define mem_get_page mem_get_page_bios
|
||||
|
||||
#elif defined MEM_MANAGE_MODE_BIOS
|
||||
|
||||
#define mem_alloc_pages mem_alloc_pages_bios
|
||||
#define mem_free_block mem_free_block_bios
|
||||
#define mem_info mem_info_bios
|
||||
|
||||
#define mem_get_page mem_get_page_bios
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
/* ===================================================================
|
||||
* Far-page accessors via window 3 (base 0xC000, port 0xE2)
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* mem_alloc_pages / mem_free_block / mem_get_page / mem_info — ESTEX EMM
|
||||
* wrappers for explicit 16 KB-page allocation.
|
||||
*
|
||||
* BIOS $C0 EMM_FN0 (MEMINFO) → HL=total pages, BC=free pages
|
||||
* BIOS $C2 EMM_FN2 (ALLOC) B=npages → A=block id, CF=err
|
||||
* BIOS $C3 EMM_FN3 (FREE) A=block id → CF=err
|
||||
* BIOS $C4 EMM_FN4 (PAGEID) A=blk, B=idx → A=physical page CF=err
|
||||
* BIOS $C6 EMM_FN6 (IOTEST) A=blk, B=idx → A=physical page CF=err
|
||||
*
|
||||
* Pattern: every RST 10h / RST 8 is bracketed with push/pop IX because
|
||||
* ESTEX/BIOS clobber it and the C caller uses it as a frame pointer.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <sprinter_mem.h>
|
||||
|
||||
/*
|
||||
* Allocate `n` contiguous 16-KB physical pages from the EMM pool.
|
||||
*
|
||||
* in: n — 1..255 (number of pages requested).
|
||||
* out: blk_id (1..255) on success; 0 on failure with errno set.
|
||||
*
|
||||
* The returned block id is opaque — pass it to mem_get_page() to obtain
|
||||
* each physical-page number, and to mem_free_block() when done. Block
|
||||
* ids start at 1; id 0 is reserved as the "allocation failed" sentinel.
|
||||
*/
|
||||
uint8_t mem_alloc_pages_bios(uint8_t n) __naked
|
||||
{
|
||||
(void)n;
|
||||
__asm
|
||||
;; SDCC single-uint8 arg → A on entry; ESTEX GETMEM wants n in B.
|
||||
push ix
|
||||
ld b, a
|
||||
ld c, #0xC2 ; BIOS EMM_FN2
|
||||
rst #0x08
|
||||
pop ix
|
||||
jr c, _alloc_fail
|
||||
ret ; CF=0 → A = blk_id, return as uint8 in A
|
||||
_alloc_fail:
|
||||
call __errno_set ; CF=1 → A = ESTEX errcode
|
||||
xor a, a ; return 0 = failure sentinel
|
||||
ret
|
||||
__endasm;
|
||||
}
|
||||
|
||||
/*
|
||||
* Release a block previously returned by mem_alloc_pages().
|
||||
*
|
||||
* in: blk_id (1..255).
|
||||
* out: void; on ESTEX error errno is set (e.g. EINVAL for unknown id).
|
||||
*
|
||||
* Idempotent guarantees are NOT provided — freeing the same block twice
|
||||
* sets errno on the second call. Caller is responsible for tracking
|
||||
* ownership.
|
||||
*/
|
||||
void mem_free_block_bios(uint8_t blk_id) __naked
|
||||
{
|
||||
(void)blk_id;
|
||||
__asm
|
||||
;; SDCC single-uint8 arg → A on entry.
|
||||
push ix
|
||||
ld c, #0xC3 ; ESTEX FREEMEM
|
||||
rst #0x08
|
||||
pop ix
|
||||
ret nc ; CF=0 → success
|
||||
jp __errno_set ; CF=1 → A = ESTEX errcode; tail-call helper
|
||||
__endasm;
|
||||
}
|
||||
|
||||
/*
|
||||
* Translate a (block, page-index) pair into a physical 16-KB page number,
|
||||
* suitable for OUT to PORT_PAGE_W1/W2/W3 or for bank_*() helpers.
|
||||
*
|
||||
* in: blk_id — id returned by mem_alloc_pages().
|
||||
* idx — 0..(n-1), where n was the count passed to alloc.
|
||||
* out: physical page number (1..255) on success;
|
||||
* 0 on failure with errno set (invalid block or idx out of range).
|
||||
*/
|
||||
uint8_t mem_get_page_bios(uint8_t blk_id, uint8_t idx) __naked
|
||||
{
|
||||
(void)blk_id; (void)idx;
|
||||
__asm
|
||||
;; 2-arg uint8/uint8: blk_id → A, idx → L.
|
||||
push ix
|
||||
ld b, l ; BIOS wants idx in B
|
||||
;; A still has blk_id
|
||||
ld c, #0xC4 ; BIOS EMM_GETPAGE
|
||||
rst #0x08
|
||||
pop ix
|
||||
ret nc ; CF=0 → A = phys page (return value)
|
||||
;; CF=1 → A = errcode; set errno, return 0 as sentinel.
|
||||
call __errno_set
|
||||
xor a, a
|
||||
ret
|
||||
__endasm;
|
||||
}
|
||||
|
||||
/*
|
||||
* Query the EMM allocator about its current state.
|
||||
*
|
||||
* *total ← number of 16-KB physical pages installed in the system
|
||||
* *free_pages ← number currently available for allocation
|
||||
*
|
||||
* Both pointers must be non-NULL writeable uint16_t locations.
|
||||
* No error path: ESTEX INFOMEM always succeeds.
|
||||
*/
|
||||
void mem_info_bios(uint16_t *total, uint16_t *free_pages) __naked
|
||||
{
|
||||
(void)total; (void)free_pages;
|
||||
__asm
|
||||
;; HL = total ptr, DE = free_pages ptr on entry.
|
||||
;; RST 10 clobbers both — stash on the stack across the call.
|
||||
push ix
|
||||
push hl ; later [SP+2] = total_ptr
|
||||
push de ; TOS [SP+0] = free_pages_ptr
|
||||
|
||||
ld c, #0xC0 ; BIOS INFOMEM → HL = total, BC = free
|
||||
rst #0x08
|
||||
|
||||
pop de ; DE = free_pages_ptr
|
||||
ld a, c
|
||||
ld (de), a
|
||||
inc de
|
||||
ld a, b
|
||||
ld (de), a ; *free_pages = BC
|
||||
|
||||
pop de ; DE = total_ptr
|
||||
ld a, l
|
||||
ld (de), a
|
||||
inc de
|
||||
ld a, h
|
||||
ld (de), a ; *total = HL
|
||||
|
||||
pop ix
|
||||
ret
|
||||
__endasm;
|
||||
}
|
||||
@@ -5,7 +5,6 @@
|
||||
* ESTEX $3C INFOMEM → HL=total pages, BC=free pages
|
||||
* ESTEX $3D GETMEM B=npages → A=block id, CF=err
|
||||
* ESTEX $3E FREEMEM A=block id → CF=err
|
||||
* BIOS $C4 EMM_GETPAGE A=blk, B=idx → A=physical page CF=err
|
||||
*
|
||||
* Pattern: every RST 10h / RST 8 is bracketed with push/pop IX because
|
||||
* ESTEX/BIOS clobber it and the C caller uses it as a frame pointer.
|
||||
@@ -24,7 +23,7 @@
|
||||
* each physical-page number, and to mem_free_block() when done. Block
|
||||
* ids start at 1; id 0 is reserved as the "allocation failed" sentinel.
|
||||
*/
|
||||
uint8_t mem_alloc_pages(uint8_t n) __naked
|
||||
uint8_t mem_alloc_pages_estex(uint8_t n) __naked
|
||||
{
|
||||
(void)n;
|
||||
__asm
|
||||
@@ -43,6 +42,7 @@ uint8_t mem_alloc_pages(uint8_t n) __naked
|
||||
__endasm;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Release a block previously returned by mem_alloc_pages().
|
||||
*
|
||||
@@ -53,7 +53,7 @@ uint8_t mem_alloc_pages(uint8_t n) __naked
|
||||
* sets errno on the second call. Caller is responsible for tracking
|
||||
* ownership.
|
||||
*/
|
||||
void mem_free_block(uint8_t blk_id) __naked
|
||||
void mem_free_block_estex(uint8_t blk_id) __naked
|
||||
{
|
||||
(void)blk_id;
|
||||
__asm
|
||||
@@ -67,6 +67,7 @@ void mem_free_block(uint8_t blk_id) __naked
|
||||
__endasm;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Translate a (block, page-index) pair into a physical 16-KB page number,
|
||||
* suitable for OUT to PORT_PAGE_W1/W2/W3 or for bank_*() helpers.
|
||||
@@ -76,24 +77,6 @@ void mem_free_block(uint8_t blk_id) __naked
|
||||
* out: physical page number (1..255) on success;
|
||||
* 0 on failure with errno set (invalid block or idx out of range).
|
||||
*/
|
||||
uint8_t mem_get_page(uint8_t blk_id, uint8_t idx) __naked
|
||||
{
|
||||
(void)blk_id; (void)idx;
|
||||
__asm
|
||||
;; 2-arg uint8/uint8: blk_id → A, idx → L.
|
||||
push ix
|
||||
ld b, l ; BIOS wants idx in B
|
||||
;; A still has blk_id
|
||||
ld c, #0xC4 ; BIOS EMM_GETPAGE
|
||||
rst #0x08
|
||||
pop ix
|
||||
ret nc ; CF=0 → A = phys page (return value)
|
||||
;; CF=1 → A = errcode; set errno, return 0 as sentinel.
|
||||
call __errno_set
|
||||
xor a, a
|
||||
ret
|
||||
__endasm;
|
||||
}
|
||||
|
||||
/*
|
||||
* Query the EMM allocator about its current state.
|
||||
@@ -104,7 +87,7 @@ uint8_t mem_get_page(uint8_t blk_id, uint8_t idx) __naked
|
||||
* Both pointers must be non-NULL writeable uint16_t locations.
|
||||
* No error path: ESTEX INFOMEM always succeeds.
|
||||
*/
|
||||
void mem_info(uint16_t *total, uint16_t *free_pages) __naked
|
||||
void mem_info_estex(uint16_t *total, uint16_t *free_pages) __naked
|
||||
{
|
||||
(void)total; (void)free_pages;
|
||||
__asm
|
||||
@@ -135,3 +118,4 @@ void mem_info(uint16_t *total, uint16_t *free_pages) __naked
|
||||
ret
|
||||
__endasm;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* palette.c — Sprinter palette (BIOS $A4 PIC_SET_PAL / PIC_GET_PAL,
|
||||
* BIOS $A6 SET_PAL_INIT).
|
||||
*
|
||||
* Direction in $A4 is selected by bit 7 of A:
|
||||
* A = pal_num → write entries from RAM to VRAM
|
||||
* A = 0x80 | pal_num → read entries from VRAM into RAM
|
||||
*
|
||||
* Lives under libc/video/ because palette control is shared between
|
||||
* graphics (mode 0x81/0x82) and text (mode 0x03) — pal_num 0..3 for
|
||||
* graphics, 4..7 for the four text-mode planes. The conio and gfx
|
||||
* subsystems each ship thin domain-specific wrappers on top.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <palette.h>
|
||||
|
||||
/* Statics used to pass parameters to the inline asm — SDCC __sdcccall(1)
|
||||
* gives us only HL natively, so the rest go through memory. */
|
||||
static uint8_t pal_num_;
|
||||
static uint8_t pal_start_;
|
||||
static uint8_t pal_count_;
|
||||
static uint16_t pal_data_;
|
||||
|
||||
/* ---- $A4 PIC_SET_PAL — write entries to VRAM --------------------- */
|
||||
|
||||
void pal_load(uint8_t pal_num, uint8_t start, uint8_t count,
|
||||
const uint8_t *bgr0)
|
||||
{
|
||||
pal_num_ = pal_num;
|
||||
pal_start_ = start;
|
||||
pal_count_ = count;
|
||||
pal_data_ = (uint16_t)(uintptr_t)bgr0;
|
||||
|
||||
__asm
|
||||
push ix
|
||||
ld a, (_pal_start_)
|
||||
ld e, a ; E = start
|
||||
ld a, (_pal_count_)
|
||||
ld d, a ; D = count (0 → 256)
|
||||
ld hl, (_pal_data_) ; HL = data
|
||||
ld b, #0xFF ; B = mask (no AND)
|
||||
ld a, (_pal_num_) ; A = palette number (bit7 = 0 → write)
|
||||
ld c, #0xA4 ; BIOS PIC_SET_PAL
|
||||
rst #0x08
|
||||
pop ix
|
||||
__endasm;
|
||||
}
|
||||
|
||||
/* ---- $A4 PIC_GET_PAL — read entries from VRAM -------------------- *
|
||||
* Same function id; bit 7 of A flips it to read mode. */
|
||||
|
||||
void pal_get(uint8_t pal_num, uint8_t start, uint8_t count, uint8_t *bgr0)
|
||||
{
|
||||
pal_num_ = (uint8_t)(pal_num | 0x80); /* bit 7 = read */
|
||||
pal_start_ = start;
|
||||
pal_count_ = count;
|
||||
pal_data_ = (uint16_t)(uintptr_t)bgr0;
|
||||
|
||||
__asm
|
||||
push ix
|
||||
ld a, (_pal_start_)
|
||||
ld e, a ; E = start
|
||||
ld a, (_pal_count_)
|
||||
ld d, a ; D = count (0 → 256)
|
||||
ld hl, (_pal_data_) ; HL = buffer
|
||||
ld b, #0xFF ; B = mask
|
||||
ld a, (_pal_num_) ; A = 0x80 | pal_num → read
|
||||
ld c, #0xA4 ; BIOS PIC_GET_PAL (= $A4)
|
||||
rst #0x08
|
||||
pop ix
|
||||
__endasm;
|
||||
}
|
||||
|
||||
/* ---- one-colour helpers ------------------------------------------ */
|
||||
|
||||
void pal_set_color(uint8_t pal_num, uint8_t slot,
|
||||
uint8_t r, uint8_t g, uint8_t b)
|
||||
{
|
||||
uint8_t entry[4];
|
||||
entry[0] = b;
|
||||
entry[1] = g;
|
||||
entry[2] = r;
|
||||
entry[3] = 0;
|
||||
pal_load(pal_num, slot, 1, entry);
|
||||
}
|
||||
|
||||
void pal_get_color(uint8_t pal_num, uint8_t slot,
|
||||
uint8_t *r, uint8_t *g, uint8_t *b)
|
||||
{
|
||||
uint8_t entry[4];
|
||||
pal_get(pal_num, slot, 1, entry);
|
||||
if (b) *b = entry[0];
|
||||
if (g) *g = entry[1];
|
||||
if (r) *r = entry[2];
|
||||
}
|
||||
|
||||
/* ---- $A6 SET_PAL_INIT — restore a built-in default palette ------- *
|
||||
* Signature: A = pal_page, E = graphics palette index (0..3), B = type.
|
||||
* Type is one of PAL_GRAPH (1) / PAL_SINCLAIR (2) / PAL_CGA (3). */
|
||||
|
||||
static uint8_t reset_page_;
|
||||
static uint8_t reset_graph_;
|
||||
static uint8_t reset_type_;
|
||||
|
||||
void pal_reset_at(uint8_t type, uint8_t pal_page, uint8_t graph_pal)
|
||||
{
|
||||
reset_type_ = type;
|
||||
reset_page_ = pal_page;
|
||||
reset_graph_ = graph_pal;
|
||||
|
||||
__asm
|
||||
push ix
|
||||
ld a, (_reset_graph_)
|
||||
ld e, a ; E = graphics palette index (0..3)
|
||||
ld a, (_reset_type_)
|
||||
ld b, a ; B = type (1=GRAPH, 2=SINCLAIR, 3=CGA)
|
||||
ld a, (_reset_page_) ; A = palette page (last — A is needed)
|
||||
ld c, #0xA6 ; BIOS SET_PAL_INIT
|
||||
rst #0x08
|
||||
pop ix
|
||||
__endasm;
|
||||
}
|
||||
|
||||
void pal_reset(uint8_t type)
|
||||
{
|
||||
pal_reset_at(type, 4, 0);
|
||||
pal_reset_at(type, 5, 0);
|
||||
pal_reset_at(type, 6, 0);
|
||||
pal_reset_at(type, 7, 0);
|
||||
}
|
||||
@@ -15,3 +15,17 @@ void bank1_func(int x) __banked
|
||||
putchar('0' + x % 10);
|
||||
putchar('\n');
|
||||
}
|
||||
|
||||
void bank1_func2(int x) __banked
|
||||
{
|
||||
(void)x;
|
||||
puts("BANK1-2: hello from a banked function (W1)!");
|
||||
puts("BANK1-2: window 1 phys page = ");
|
||||
hex8(_io_page_w1); /* should be BANK1's phys page */
|
||||
putchar('\n');
|
||||
putchar('1');
|
||||
putchar('=');
|
||||
putchar('0' + (x / 10) % 10);
|
||||
putchar('0' + x % 10);
|
||||
putchar('\n');
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#include <stdint.h>
|
||||
#include <sprinter.h>
|
||||
|
||||
void bank1_func2(int x) __banked;
|
||||
|
||||
void bank2_func(int x) __banked
|
||||
{
|
||||
(void)x;
|
||||
@@ -15,4 +17,6 @@ void bank2_func(int x) __banked
|
||||
putchar('0' + (x / 10) % 10);
|
||||
putchar('0' + x % 10);
|
||||
putchar('\n');
|
||||
|
||||
bank1_func2(10);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Build banked.exe — HUGE memory mode (small + banked code in W3).
|
||||
#
|
||||
# Layout:
|
||||
# CODE/HOME at 0x4100 (W1), DATA at 0x8000 (W2), banks at 0x{N}C000 (W3).
|
||||
# crt0_banked.s loads N banks from the .EXE before calling main.
|
||||
|
||||
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||
EXAMPLE := banked
|
||||
MEMORY := huge
|
||||
EXTRA_FLAGS := --bank 1=bank1.c --bank 2=bank2.c
|
||||
include $(PROJ_ROOT)/app.mk
|
||||
@@ -0,0 +1,18 @@
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <sprinter.h>
|
||||
|
||||
uint16_t bank1var = 0;
|
||||
extern uint16_t bank2var;
|
||||
|
||||
void bank1_func(void) __banked
|
||||
{
|
||||
printf("BANK1: hello from the first bank, phys page = 0x%02X, var = %u\n", _io_page_w3, bank1var);
|
||||
}
|
||||
|
||||
void bank1_func2(uint16_t x) __banked
|
||||
{
|
||||
printf("BANK1: hello from the first bank, phys page = 0x%02X, set var in SECOND bank to %u\n", _io_page_w3, x);
|
||||
bank2var = x;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <sprinter.h>
|
||||
|
||||
uint16_t bank2var = 0;
|
||||
|
||||
void bank2_func(void) __banked
|
||||
{
|
||||
printf("BANK2: hello from the second bank, phys page = 0x%02X, var = %u\n", _io_page_w3, bank2var);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <sprinter.h>
|
||||
|
||||
/* crt0_banked.s reads this constant to know how many banks to load. */
|
||||
const uint8_t n_banks = 2;
|
||||
|
||||
void bank1_func(void) __banked;
|
||||
void bank2_func(void) __banked;
|
||||
void bank1_func2(uint16_t) __banked;
|
||||
|
||||
extern uint16_t bank1var;
|
||||
extern uint16_t bank2var;
|
||||
|
||||
extern uint8_t bank_pages[]; /* filled by crt0_banked.s */
|
||||
|
||||
int main(void)
|
||||
{
|
||||
bank1var = 1;
|
||||
bank2var = 2;
|
||||
puts("HOME: program start.");
|
||||
printf("HOME: window 3 phys page = 0x%02X\n", _io_page_w3);
|
||||
printf("HOME: bank_pages[1] = 0x%02X\n", bank_pages[1]);
|
||||
printf("HOME: bank_pages[2] = 0x%02X\n", bank_pages[2]);
|
||||
|
||||
bank1_func();
|
||||
bank2_func();
|
||||
bank1_func2(25);
|
||||
bank2_func();
|
||||
|
||||
puts("Press any key to exit...");
|
||||
(void)getchar();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Build bios_text.exe — exercises <bios/text.h>, TINY memory mode.
|
||||
|
||||
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||
EXAMPLE := bios_text
|
||||
include $(PROJ_ROOT)/app.mk
|
||||
@@ -0,0 +1,110 @@
|
||||
#include <stdio.h>
|
||||
#include <conio.h>
|
||||
#include <bios/text.h>
|
||||
|
||||
/*
|
||||
* Exercises every BIOS text wrapper from <bios/text.h>:
|
||||
* bios_set_place / bios_get_place
|
||||
* bios_fillchar / bios_fillchar_noattr / bios_fillattr
|
||||
* bios_writeattr / bios_write
|
||||
* bios_writeattr_until / bios_write_until (pad to length)
|
||||
* bios_writeattr_stop / bios_write_stop (stop at separator)
|
||||
* bios_clearwin / bios_clearwin_ch
|
||||
* bios_scrollwin
|
||||
*
|
||||
* `s` below is a static buffer (not a string literal) so its address is
|
||||
* always inside window 2, as the BIOS print routines require.
|
||||
*/
|
||||
|
||||
static char s[16];
|
||||
|
||||
int main(void)
|
||||
{
|
||||
clrscr();
|
||||
|
||||
/* Step 0 check: does LP_PRINT_LN advance the BIOS "place" cursor
|
||||
* by the number of characters printed, or leave it untouched?
|
||||
* "Hello" is 5 chars -> if place advances, before=(20,5),
|
||||
* after=(20,10); if not, after == before. */
|
||||
s[0]='H'; s[1]='e'; s[2]='l'; s[3]='l'; s[4]='o';
|
||||
bios_set_place(20, 5);
|
||||
{
|
||||
uint16_t before = bios_get_place();
|
||||
bios_writeattr(s, 5, COLOR(COLOR_WHITE, COLOR_BLACK));
|
||||
uint16_t after = bios_get_place();
|
||||
gotoxy(2, 0);
|
||||
textattr(0x0F);
|
||||
printf("STEP0: before row=%u col=%u\n", before >> 8, before & 0xFF);
|
||||
printf("STEP0: after row=%u col=%u\n", after >> 8, after & 0xFF);
|
||||
printf("STEP0: expect after col=10 if place advances,\n");
|
||||
printf(" col=5 if it does NOT advance.\n");
|
||||
}
|
||||
(void)getch();
|
||||
|
||||
bios_set_place(1, 2);
|
||||
bios_fillcharattr('*', COLOR(COLOR_YELLOW, COLOR_BLUE), 10);
|
||||
(void)getch();
|
||||
|
||||
bios_set_place(2, 2);
|
||||
textattr(COLOR(COLOR_WHITE, COLOR_BLACK));
|
||||
cputs("attr already on screen ->");
|
||||
(void)getch();
|
||||
|
||||
bios_set_place(2, 27);
|
||||
bios_fillchar('#', 10);
|
||||
(void)getch();
|
||||
|
||||
bios_set_place(3, 2);
|
||||
bios_fillattr(COLOR(COLOR_BLACK, COLOR_LIGHTGREEN), 20);
|
||||
(void)getch();
|
||||
|
||||
s[0]='H'; s[1]='e'; s[2]='l'; s[3]='l'; s[4]='o';
|
||||
bios_set_place(5, 2);
|
||||
bios_writeattr(s, 5, COLOR(COLOR_LIGHTRED, COLOR_BLACK));
|
||||
(void)getch();
|
||||
|
||||
bios_set_place(6, 2);
|
||||
bios_write(s, 5);
|
||||
(void)getch();
|
||||
|
||||
s[0]='A'; s[1]='B'; s[2]=':'; s[3]='X'; s[4]='Y'; s[5]='Z';
|
||||
bios_set_place(8, 2);
|
||||
bios_writeattr_until(s, 12, COLOR(COLOR_CYAN, COLOR_BLACK), ':');
|
||||
(void)getch();
|
||||
|
||||
bios_set_place(9, 2);
|
||||
bios_write_until(s, 12, ':');
|
||||
(void)getch();
|
||||
|
||||
bios_set_place(10, 2);
|
||||
bios_writeattr_stop(s, 12, COLOR(COLOR_MAGENTA, COLOR_BLACK), ':');
|
||||
(void)getch();
|
||||
|
||||
bios_set_place(11, 2);
|
||||
bios_write_stop(s, 12, ':');
|
||||
(void)getch();
|
||||
|
||||
bios_clearwin(13, 2, 4, 20, COLOR(COLOR_BLACK, COLOR_RED));
|
||||
bios_clearwin_ch(13, 25, 4, 20, COLOR(COLOR_WHITE, COLOR_BLUE), '.');
|
||||
(void)getch();
|
||||
|
||||
bios_set_place(18, 2);
|
||||
(void)getch();
|
||||
cputs("place test ->");
|
||||
{
|
||||
uint16_t place = bios_get_place();
|
||||
printf(" row=%u col=%u\n", place >> 8, place & 0xFF);
|
||||
}
|
||||
(void)getch();
|
||||
|
||||
gotoxy(2, 20);
|
||||
for (int i = 0; i < 6; i++) {
|
||||
textattr(0x0F);
|
||||
printf("scroll line %d\n", i);
|
||||
}
|
||||
(void)getch();
|
||||
bios_scrollwin(BIOS_SCROLL_UP, 20, 3);
|
||||
|
||||
(void)getch();
|
||||
return 0;
|
||||
}
|
||||
+27
-5
@@ -1,6 +1,7 @@
|
||||
#include <stdio.h>
|
||||
#include <conio.h>
|
||||
#include <unistd.h> /* sleep */
|
||||
#include <sprinter_exit.h>
|
||||
|
||||
/*
|
||||
* Exercises every conio function:
|
||||
@@ -16,6 +17,8 @@
|
||||
* (0,0), (79,0), (0,31), (79,31).
|
||||
*/
|
||||
|
||||
extern two_bytes pc_place;
|
||||
|
||||
int main(void)
|
||||
{
|
||||
/* 1. clrscr + diagnostic probes + a centered banner.
|
||||
@@ -27,9 +30,16 @@ int main(void)
|
||||
*/
|
||||
clrscr();
|
||||
|
||||
gotoxy(0, 0); putch('0');
|
||||
gotoxy(1, 0);
|
||||
textattr(0x07);
|
||||
putch('0');
|
||||
textattr(0x70);
|
||||
gotoxy(1, 0); putch('1');
|
||||
putch('1');
|
||||
// gotoxy(10, 20);
|
||||
printf("X = %u, Y = %u\n", pc_place.byte.low, pc_place.byte.high);
|
||||
uint8_t x = wherex();
|
||||
uint8_t y = wherey();
|
||||
printf("X = %u, Y = %u\n", x, y);
|
||||
|
||||
set_putch_raw_mode(1);
|
||||
|
||||
@@ -51,11 +61,23 @@ int main(void)
|
||||
}
|
||||
}
|
||||
|
||||
set_text_attr(0x011D);
|
||||
set_putch_raw_mode(0);
|
||||
gotoxy(0,22);
|
||||
set_text_attr(0x000E);
|
||||
// cputs("Test message line");
|
||||
cputs("Test message line 1\nTest message line2\rline3\n\r\n\r1234\t5678\b90");
|
||||
cputs("Test message line 1\nTest message line2\rline3\n\r1234\t5678\b90\n\r");
|
||||
|
||||
set_putch_raw_mode(1);
|
||||
gotoxy(0,25);
|
||||
cputs("Test message line 1\nTest message line2\rline3\n\r1234\t5678\b90\n\r");
|
||||
|
||||
set_text_attr(0x001D);
|
||||
set_putch_raw_mode(0);
|
||||
gotoxy(0,28);
|
||||
cputs("Test message line 1\nTest message line2\rline3\n\r1234\t5678\b90\n\r");
|
||||
|
||||
set_putch_raw_mode(1);
|
||||
gotoxy(0,31);
|
||||
cputs("Test message line 1\nTest message line2\rline3\n\r1234\t5678\b90\n\r");
|
||||
|
||||
(void)getch();
|
||||
|
||||
|
||||
+2
-2
@@ -18,7 +18,7 @@ int main(void)
|
||||
{
|
||||
errno = -1;
|
||||
int16_t a0 = 0, a1 = 0;
|
||||
a0 = get_text_attr();
|
||||
a0 = get_text_attr();
|
||||
|
||||
uint8_t vMode = get_videotextmode();
|
||||
set_videotextmode(TEXT_MODE_80x32);
|
||||
@@ -52,7 +52,7 @@ int main(void)
|
||||
|
||||
/* Back to a normal attribute so the goodbye reads cleanly. */
|
||||
textattr(COLOR(COLOR_LIGHTGRAY, COLOR_BLACK));
|
||||
printf("a0=%d a1=%d now=%d errno=%d\n",
|
||||
cprintf("a0=%d a1=%d now=%d errno=%d\n\r",
|
||||
a0, a1, get_text_attr(), errno);
|
||||
#ifdef DEBUG_RT
|
||||
printf("w2_self_allocated = %u\n", w2_self_allocated);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Build hello.exe — uses lib/sprinter.lib in TINY memory mode.
|
||||
|
||||
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||
EXAMPLE := hello2
|
||||
include $(PROJ_ROOT)/app.mk
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* hello — smoke test for the two output APIs:
|
||||
*
|
||||
* stdio (printf/puts/putchar) — FAST, no attribute (ambient colour)
|
||||
* conio (cprintf/cputs/putch) — SLOWER, applies textcolor/textbackground
|
||||
*
|
||||
* Demonstrates textcolor/textbackground (Turbo-C-compatible names).
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <conio.h>
|
||||
#include <gfx.h>
|
||||
#include <sprinter.h>
|
||||
#include <sprinter_exit.h>
|
||||
#include <errno.h>
|
||||
|
||||
int main(void)
|
||||
{
|
||||
errno = -1;
|
||||
int16_t a0 = 0, a1 = 0;
|
||||
a0 = get_text_attr();
|
||||
|
||||
uint8_t vMode = gettextmode();
|
||||
settextmode(TEXT_MODE_80x32);
|
||||
|
||||
/* Wipe the screen with a known attribute so stdio printf/puts inherit
|
||||
* a clean colour for the cursor. */
|
||||
clrscr_attr(COLOR(COLOR_LIGHTGRAY, COLOR_BLACK));
|
||||
|
||||
/* --- stdio set: fast, no attribute ------------------------------- */
|
||||
puts("hello (stdio) - fast PCHARS, ambient colour");
|
||||
printf("printf: a0=%d (initial attr)\n", a0);
|
||||
|
||||
/* --- conio set: applies textcolor / textbackground --------------- */
|
||||
textcolor(COLOR_YELLOW);
|
||||
textbackground(COLOR_BLUE);
|
||||
cputs("conio: yellow on blue\r\n");
|
||||
|
||||
printf("printf: a0=%d (initial attr)\n", a0);
|
||||
|
||||
textcolor(COLOR_LIGHTRED);
|
||||
textbackground(COLOR_BLACK);
|
||||
cprintf("cprintf %d %d %s\r\n", 42, -7, "args");
|
||||
|
||||
printf("printf: a0=%d (initial attr)\n", a0);
|
||||
|
||||
/* set_text_attr / textattr replace the whole byte. */
|
||||
textattr(COLOR(COLOR_LIGHTGREEN, COLOR_BLACK));
|
||||
cputs("conio: light-green via textattr\r\n");
|
||||
|
||||
printf("printf: a0=%d (initial attr)\n", a0);
|
||||
|
||||
/* Opting out of attribute control — conio falls back to fast path. */
|
||||
set_text_attr(KEEP_EXIST_ATTR);
|
||||
cputs("conio with KEEP_EXIST_ATTR (fast path)\r\n");
|
||||
|
||||
printf("printf: a0=%d (initial attr)\n", a0);
|
||||
|
||||
a1 = get_text_attr();
|
||||
|
||||
printf("printf: a0=%d (initial attr)\n", a0);
|
||||
|
||||
/* Back to a normal attribute so the goodbye reads cleanly. */
|
||||
textattr(COLOR(COLOR_LIGHTCYAN, COLOR_BLACK));
|
||||
|
||||
printf("printf: a0=%d (initial attr)\n", a0);
|
||||
|
||||
cprintf("a0=%d a1=%d now=%d errno=%d\n\r",
|
||||
a0, a1, get_text_attr(), errno);
|
||||
// cprintf("a0=%d a1=%d",
|
||||
// a0, a1);
|
||||
// cprintf(" now=%d errno=%d\n\r",
|
||||
// get_text_attr(), errno);
|
||||
#ifdef DEBUG_RT
|
||||
printf("w2_self_allocated = %u\n", w2_self_allocated);
|
||||
#endif
|
||||
cputs("Press any key to exit...");
|
||||
|
||||
(void)getchar();
|
||||
settextmode(vMode);
|
||||
return 0;
|
||||
}
|
||||
@@ -16,6 +16,8 @@
|
||||
* 5. Free the block, show the free-page count again.
|
||||
*/
|
||||
|
||||
uint32_t buff[256];
|
||||
|
||||
static void show_mem(const char *label)
|
||||
{
|
||||
uint16_t total, free_pages;
|
||||
@@ -29,6 +31,13 @@ int main(void)
|
||||
puts("Sprinter page allocator demo");
|
||||
puts("");
|
||||
|
||||
memset(buff, 0, sizeof(buff));
|
||||
// printf("mem_io_ports_test() = 0x%02X\n", mem_io_ports_test());
|
||||
printf("buff size (sizeof) = %u\n", sizeof(buff));
|
||||
printf("buff size one element (sizeof) = %u\n", sizeof(buff[0]));
|
||||
printf("buff first element = %u\n", buff[0]);
|
||||
printf("buff last element = %u\n", buff[(sizeof(buff) / sizeof(buff[0])) - 1]);
|
||||
|
||||
show_mem("before:");
|
||||
|
||||
uint8_t blk = mem_alloc_pages(3);
|
||||
|
||||
+15
-12
@@ -10,6 +10,8 @@
|
||||
#include <conio.h>
|
||||
#include <mouse.h>
|
||||
|
||||
mouse_state_t st;
|
||||
|
||||
int main(void)
|
||||
{
|
||||
textattr(COLOR(COLOR_LIGHTGRAY, COLOR_BLACK));
|
||||
@@ -29,7 +31,6 @@ int main(void)
|
||||
mouse_bounds_y(0, 255);
|
||||
mouse_show();
|
||||
|
||||
mouse_state_t st;
|
||||
int last_x = -1, last_y = -1;
|
||||
uint8_t last_btn = 0xFF;
|
||||
/* Sensitivity is a "raw steps per cursor pixel" divider: smaller =
|
||||
@@ -41,11 +42,13 @@ int main(void)
|
||||
mouse_set_sensitivity(sens_x, sens_y);
|
||||
int sens_dirty = 1;
|
||||
|
||||
st.x = st.y = 0; st.buttons = 0;
|
||||
|
||||
while (1) {
|
||||
mouse_read(&st);
|
||||
if (st.x != last_x || st.y != last_y || st.buttons != last_btn) {
|
||||
gotoxy(0, 6);
|
||||
cprintf("x=%4u y=%4u text(%2u,%2u) buttons=0x%02X L%c R%c ",
|
||||
printf("x=%4u y=%4u text(%2u,%2u) buttons=0x%02X L%c R%c ",
|
||||
st.x, st.y,
|
||||
st.x / 8, st.y / 8,
|
||||
st.buttons,
|
||||
@@ -57,19 +60,19 @@ int main(void)
|
||||
}
|
||||
if (sens_dirty) {
|
||||
gotoxy(0, 8);
|
||||
cprintf("sensitivity horz=%3u vert=%3u ", sens_x, sens_y);
|
||||
printf("sensitivity horz=%3u vert=%3u ", sens_x, sens_y);
|
||||
sens_dirty = 0;
|
||||
}
|
||||
|
||||
if (!kbhit()) continue;
|
||||
int k = getch();
|
||||
if (k == 27) break; /* ESC */
|
||||
if (k == '1' && sens_x > 1) { sens_x -= 1; sens_dirty = 1; }
|
||||
if (k == '2' && sens_x < 254) { sens_x += 1; sens_dirty = 1; }
|
||||
if (k == '3' && sens_y > 1) { sens_y -= 1; sens_dirty = 1; }
|
||||
if (k == '4' && sens_y < 254) { sens_y += 1; sens_dirty = 1; }
|
||||
if (sens_dirty)
|
||||
mouse_set_sensitivity(sens_x, sens_y);
|
||||
if (!kbhit()) continue;
|
||||
int k = getch();
|
||||
if (k == 27) break; /* ESC */
|
||||
if (k == '1' && sens_x > 1) { sens_x -= 1; sens_dirty = 1; }
|
||||
if (k == '2' && sens_x < 254) { sens_x += 1; sens_dirty = 1; }
|
||||
if (k == '3' && sens_y > 1) { sens_y -= 1; sens_dirty = 1; }
|
||||
if (k == '4' && sens_y < 254) { sens_y += 1; sens_dirty = 1; }
|
||||
if (sens_dirty)
|
||||
mouse_set_sensitivity(sens_x, sens_y);
|
||||
}
|
||||
|
||||
mouse_hide();
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Build hello.exe — uses lib/sprinter.lib in TINY memory mode.
|
||||
|
||||
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||
EXAMPLE := simple
|
||||
include $(PROJ_ROOT)/app.mk
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* hello — smoke test for the two output APIs:
|
||||
*
|
||||
* stdio (printf/puts/putchar) — FAST, no attribute (ambient colour)
|
||||
* conio (cprintf/cputs/putch) — SLOWER, applies textcolor/textbackground
|
||||
*
|
||||
* Demonstrates textcolor/textbackground (Turbo-C-compatible names).
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <conio.h>
|
||||
#include <gfx.h>
|
||||
#include <sprinter.h>
|
||||
#include <sprinter_exit.h>
|
||||
#include <errno.h>
|
||||
|
||||
static uint8_t buff[] = {0xE4, 0x1B};
|
||||
|
||||
uint8_t get_style(uint32_t idx) {
|
||||
return (uint8_t)((buff[idx >> 2] >> ((idx & 3) << 1)) & 3u);
|
||||
}
|
||||
|
||||
void set_style(uint32_t idx, uint8_t style) {
|
||||
buff[idx >> 2] = buff[idx >> 2] & ~(3u << ((idx & 3) << 1)) | ((style & 3u) << ((idx & 3) << 1));
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
for(int i = 0; i < 8; i++) {
|
||||
dec8(get_style(i));
|
||||
puts("");
|
||||
}
|
||||
|
||||
for(int i = 0; i < 8; i++) {
|
||||
set_style(i, (i + 1) & 3);
|
||||
}
|
||||
puts("");
|
||||
for(int i = 0; i < 8; i++) {
|
||||
dec8(get_style(i));
|
||||
puts("");
|
||||
}
|
||||
|
||||
cputs("Press any key to exit...");
|
||||
(void)getchar();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Build text_palette.exe — uses lib/sprinter.lib in TINY memory mode.
|
||||
|
||||
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||
EXAMPLE := text_palette
|
||||
include $(PROJ_ROOT)/app.mk
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* text_palette — interactive smoke test for the text-mode palette API.
|
||||
*
|
||||
* Walks through four palette states. Each stage clears the screen first
|
||||
* and then renders 16 lines, one per ink-colour index, each line showing:
|
||||
*
|
||||
* <RGB values read back from the INK plane>
|
||||
* < part 1: fg = N on bg = 0 — colour N text on the default black >
|
||||
* < part 2: fg = 15 on bg = N — bright-white text on a coloured bg >
|
||||
*
|
||||
* Stages:
|
||||
* 1) Initial CGA palette (boot default).
|
||||
* 2) Grayscale ramp installed via text_pal_load() in all four text
|
||||
* planes (4..7) so flash is fully suppressed; both ink (low nibble)
|
||||
* and paper (mid nibble) get 16 shades — bit 7 of the attribute
|
||||
* byte is folded into the paper index so we cover all 16 bg slots.
|
||||
* 3) CGA defaults restored via pal_reset_at(PAL_CGA, plane, 0) for
|
||||
* plane = 4..7 — explicit per-plane reset in case BIOS interprets
|
||||
* the A register narrowly.
|
||||
* 4) Sinclair palette via pal_reset_at(PAL_SINCLAIR, plane, 0) for
|
||||
* plane = 4..7.
|
||||
*
|
||||
* Memory: tiny (one bank). The 1 KB palette buffer for text_pal_load()
|
||||
* is static — keeps it out of the (small) stack frame.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <conio.h>
|
||||
#include <palette.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* 256 entries × 4 bytes (B, G, R, 0). Static so it lives in DATA. */
|
||||
static uint8_t pal_buf[256 * 4];
|
||||
|
||||
#define ATTR_DEFAULT COLOR(COLOR_LIGHTGRAY, COLOR_BLACK)
|
||||
|
||||
/* For each colour index N=0..15 print:
|
||||
* - RGB triple read back from the INK plane
|
||||
* - sample text in fg=N on bg=0
|
||||
* - sample text in fg=15 on bg=N
|
||||
*
|
||||
* The bg=N attribute uses ALL FOUR bits of the mid nibble (including the
|
||||
* blink bit). In the CGA default this means N >= 8 will flash; with our
|
||||
* grayscale loader the four planes are identical, so it just selects one
|
||||
* of 16 paper colours. */
|
||||
static void show_palette_state(void)
|
||||
{
|
||||
for (uint8_t n = 0; n < 16; n++) {
|
||||
uint8_t r = 0, g = 0, b = 0;
|
||||
text_pal_get_color(TEXT_PAL_INK, n, &r, &g, &b);
|
||||
|
||||
textattr(ATTR_DEFAULT);
|
||||
cprintf(" #%X R=%3u G=%3u B=%3u ", n, r, g, b);
|
||||
|
||||
/* part 1: fg=N on bg=0 */
|
||||
textattr((uint8_t)(n & 0x0F));
|
||||
cprintf(" fg=%X on bg=0 ", n);
|
||||
|
||||
/* part 2: fg=15 on bg=N (4-bit bg slot, may carry blink bit) */
|
||||
textattr((uint8_t)(((n & 0x0F) << 4) | 0x0F));
|
||||
cprintf(" white on bg=%X ", n);
|
||||
|
||||
textattr(ATTR_DEFAULT);
|
||||
cputs("\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
/* Build a 16-step grayscale ramp and load it into ALL FOUR text planes
|
||||
* (paper, ink, blink-paper, blink-ink). Identical paper/blink-paper and
|
||||
* ink/blink-ink means there is nothing to alternate between → no flash.
|
||||
*
|
||||
* INK / BLINK_INK index by attr's low nibble: 16 fg grey levels
|
||||
* PAPER / BLINK_PAPER index by attr's mid nibble: 16 bg grey levels
|
||||
* (full 4 bits incl. bit 7) */
|
||||
static void load_grayscale_all_planes(void)
|
||||
{
|
||||
/* INK and BLINK_INK: shade taken from attr & 0x0F. */
|
||||
for (uint16_t a = 0; a < 256; a++) {
|
||||
uint8_t v = (uint8_t)((a & 0x0F) * 17u); /* 0,17,34,...,255 */
|
||||
pal_buf[a * 4 + 0] = v; /* B */
|
||||
pal_buf[a * 4 + 1] = v; /* G */
|
||||
pal_buf[a * 4 + 2] = v; /* R */
|
||||
pal_buf[a * 4 + 3] = 0;
|
||||
}
|
||||
text_pal_load(TEXT_PAL_INK, 0, 0, pal_buf);
|
||||
text_pal_load(TEXT_PAL_BLINK_INK, 0, 0, pal_buf);
|
||||
|
||||
/* PAPER and BLINK_PAPER: shade taken from (attr >> 4) & 0x0F. */
|
||||
for (uint16_t a = 0; a < 256; a++) {
|
||||
uint8_t v = (uint8_t)(((a >> 4) & 0x0F) * 17u);
|
||||
pal_buf[a * 4 + 0] = v;
|
||||
pal_buf[a * 4 + 1] = v;
|
||||
pal_buf[a * 4 + 2] = v;
|
||||
pal_buf[a * 4 + 3] = 0;
|
||||
}
|
||||
text_pal_load(TEXT_PAL_PAPER, 0, 0, pal_buf);
|
||||
text_pal_load(TEXT_PAL_BLINK_PAPER, 0, 0, pal_buf);
|
||||
}
|
||||
|
||||
/* Reset all four text-mode planes (BIOS palette pages 4..7) to a built-in
|
||||
* default. We loop over the pages explicitly because the BIOS doc isn't
|
||||
* crisp about whether one SET_PAL_INIT call covers all four pages or
|
||||
* just the one named in A. Looping is always safe. */
|
||||
static void reset_all_text_planes(uint8_t type)
|
||||
{
|
||||
for (uint8_t p = 4; p <= 7; p++) {
|
||||
pal_reset_at(type, p, 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void wait_key(const char *prompt)
|
||||
{
|
||||
cputs(prompt);
|
||||
(void)getch();
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
uint8_t prev_mode = get_videotextmode();
|
||||
set_videotextmode(TEXT_MODE_80x32);
|
||||
textattr(ATTR_DEFAULT);
|
||||
|
||||
/* ----- (1) Initial CGA palette ---------------------------------- */
|
||||
clrscr();
|
||||
cputs("[1] Initial CGA palette (boot default)\r\n");
|
||||
cputs("---------------------------------------\r\n");
|
||||
show_palette_state();
|
||||
wait_key("\r\nPress any key to install grayscale...");
|
||||
|
||||
/* ----- (2) Grayscale ramp in all four planes -------------------- */
|
||||
load_grayscale_all_planes();
|
||||
clrscr();
|
||||
cputs("[2] Grayscale ramp via text_pal_load() (planes 4..7)\r\n");
|
||||
cputs("-----------------------------------------------------\r\n");
|
||||
show_palette_state();
|
||||
wait_key("\r\nPress any key to restore CGA...");
|
||||
|
||||
/* ----- (3) Restore CGA defaults --------------------------------- */
|
||||
reset_all_text_planes(PAL_CGA);
|
||||
clrscr();
|
||||
cputs("[3] CGA palette restored (planes 4..7)\r\n");
|
||||
cputs("---------------------------------------\r\n");
|
||||
show_palette_state();
|
||||
wait_key("\r\nPress any key to install Sinclair...");
|
||||
|
||||
/* ----- (4) Sinclair palette ------------------------------------- */
|
||||
reset_all_text_planes(PAL_SINCLAIR);
|
||||
clrscr();
|
||||
cputs("[4] Sinclair/Spectrum palette (planes 4..7)\r\n");
|
||||
cputs("--------------------------------------------\r\n");
|
||||
show_palette_state();
|
||||
wait_key("\r\nPress any key to exit...");
|
||||
|
||||
/* Leave the system in a sane state. */
|
||||
reset_all_text_planes(PAL_CGA);
|
||||
textattr(ATTR_DEFAULT);
|
||||
clrscr();
|
||||
set_videotextmode(prev_mode);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Build winrest.exe — probes ESTEX WINCOPY/WINREST buffer format, TINY mode.
|
||||
|
||||
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||
EXAMPLE := winrest
|
||||
include $(PROJ_ROOT)/app.mk
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Probe for ESTEX WINCOPY (59h) / WINREST (5Ah) — "save/restore screen
|
||||
* window" calls (docs/part2/DSS 1.60 rst 10.txt:1379-1417):
|
||||
*
|
||||
* D=row E=col H=height L=width B=buffer page IX=buffer address
|
||||
*
|
||||
* Goal: reverse-engineer the on-disk cell format (char/attr order,
|
||||
* per-row stride/padding) by writing four DISTINCT (char,attr) cells,
|
||||
* capturing them with WINCOPY, and hex-dumping the captured page.
|
||||
* A second WINREST to a different screen location confirms the
|
||||
* round-trip visually.
|
||||
*
|
||||
* Register layout verified via `sdcc -S` for win_copy(row,col,h,w,page):
|
||||
* A=row, L=col, stack [SP+2]=h [SP+3]=w [SP+4]=page (sdcccall(1)).
|
||||
*
|
||||
* CONFIRMED in MAME (2026-06-23): buffer format is (char,attr) pairs,
|
||||
* row-major, stride = width*2 bytes, no padding. See memory note
|
||||
* sprinter_winrest_format.
|
||||
*
|
||||
* Also reports mem_info() (total/free 16 KB EMM pages) at program
|
||||
* start, to size a future mdview2 pre-render cache budget.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <conio.h>
|
||||
#include <sprinter.h>
|
||||
#include <sprinter_mem.h>
|
||||
|
||||
#define BUF_ADDR 0xC000u
|
||||
|
||||
static void win_copy(uint8_t row, uint8_t col, uint8_t h, uint8_t w, uint8_t page) __naked
|
||||
{
|
||||
(void)row; (void)col; (void)h; (void)w; (void)page;
|
||||
__asm
|
||||
ld iy, #2
|
||||
add iy, sp
|
||||
ld d, a ; D = row
|
||||
ld e, l ; E = col
|
||||
ld h, 0 (iy) ; H = height
|
||||
ld a, 1 (iy) ; width -> L
|
||||
ld l, a
|
||||
ld a, 2 (iy) ; page -> B
|
||||
ld b, a
|
||||
push ix
|
||||
ld ix, #0xC000
|
||||
ld c, #0x59 ; ESTEX WINCOPY
|
||||
di
|
||||
rst #0x10
|
||||
ei
|
||||
pop ix
|
||||
pop hl ; return address
|
||||
inc sp
|
||||
inc sp
|
||||
inc sp ; discard h,w,page
|
||||
jp (hl)
|
||||
__endasm;
|
||||
}
|
||||
|
||||
static void win_rest(uint8_t row, uint8_t col, uint8_t h, uint8_t w, uint8_t page) __naked
|
||||
{
|
||||
(void)row; (void)col; (void)h; (void)w; (void)page;
|
||||
__asm
|
||||
ld iy, #2
|
||||
add iy, sp
|
||||
ld d, a ; D = row
|
||||
ld e, l ; E = col
|
||||
ld h, 0 (iy) ; H = height
|
||||
ld a, 1 (iy) ; width -> L
|
||||
ld l, a
|
||||
ld a, 2 (iy) ; page -> B
|
||||
ld b, a
|
||||
push ix
|
||||
ld ix, #0xC000
|
||||
ld c, #0x5A ; ESTEX WINREST
|
||||
di
|
||||
rst #0x10
|
||||
ei
|
||||
pop ix
|
||||
pop hl
|
||||
inc sp
|
||||
inc sp
|
||||
inc sp
|
||||
jp (hl)
|
||||
__endasm;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
clrscr();
|
||||
|
||||
/* --- mem_info() baseline, before any allocation by this program. */
|
||||
uint16_t total_pages = 0, free_pages = 0;
|
||||
mem_info(&total_pages, &free_pages);
|
||||
|
||||
textattr(0x0F);
|
||||
gotoxy(0, 0);
|
||||
printf("EMM pages: total=%u free=%u (each = 16 KB)\n", total_pages, free_pages);
|
||||
printf(" => total %lu KB, free %lu KB\n",
|
||||
(uint32_t)total_pages * 16UL, (uint32_t)free_pages * 16UL);
|
||||
printf("Press a key to continue to WINCOPY/WINREST probe...\n");
|
||||
(void)getchar();
|
||||
|
||||
/* Four distinguishable cells: char and attr both easy to recognise
|
||||
* in a hex dump (A/B/C/D = 0x41..0x44, attrs 0x11/0x22/0x33/0x44). */
|
||||
gotoxy(5, 10); textattr(0x11); putch('A');
|
||||
gotoxy(6, 10); textattr(0x22); putch('B');
|
||||
gotoxy(5, 11); textattr(0x33); putch('C');
|
||||
gotoxy(6, 11); textattr(0x44); putch('D');
|
||||
|
||||
textattr(0x0F);
|
||||
gotoxy(0, 13);
|
||||
printf("Captured window drawn above (row 10-11, col 5-6).\n");
|
||||
printf("Press a key to WINCOPY it...\n");
|
||||
(void)getchar();
|
||||
|
||||
uint8_t blk = mem_alloc_pages(1);
|
||||
uint8_t page = mem_get_page(blk, 0);
|
||||
|
||||
win_copy(10, 5, 2, 2, page);
|
||||
|
||||
/* Map the same physical page into our own W3 to inspect the bytes
|
||||
* WINCOPY just wrote there. */
|
||||
sprinter_page_w3(page);
|
||||
{
|
||||
volatile uint8_t *buf = (volatile uint8_t *)BUF_ADDR;
|
||||
gotoxy(0, 15);
|
||||
printf("Buffer dump (first 16 bytes of page %u):\n", page);
|
||||
for (uint8_t i = 0; i < 16; i++) {
|
||||
printf("%02X ", buf[i]);
|
||||
}
|
||||
printf("\n");
|
||||
for (uint8_t i = 0; i < 16; i++) {
|
||||
char c = (char)buf[i];
|
||||
putchar((c >= 0x20 && c < 0x7F) ? c : '.');
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
printf("Press a key to WINREST it to row 20...\n");
|
||||
(void)getchar();
|
||||
|
||||
win_rest(20, 5, 2, 2, page);
|
||||
|
||||
gotoxy(0, 23);
|
||||
printf("Restored at row 20-21, col 5-6 -- compare colours/chars\n");
|
||||
printf("with the original at row 10-11.\n");
|
||||
printf("Press a key to exit...\n");
|
||||
(void)getchar();
|
||||
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user