Compare commits
30 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 | |||
| 07c398a560 | |||
| 4bed9d3f3f | |||
| 463a058f56 | |||
| 8abc1d6c16 | |||
| 982af12710 | |||
| 394ee3a2cd | |||
| 47c9cd326a | |||
| ca5f30b332 | |||
| 0ad0559fc8 |
@@ -24,6 +24,9 @@ examples/*/*.cdb
|
|||||||
examples/*/*.mem
|
examples/*/*.mem
|
||||||
examples/*/*.rst
|
examples/*/*.rst
|
||||||
|
|
||||||
|
# Temporary build directory for floppy disk image preparation
|
||||||
|
examples/*/.disk_tmp/
|
||||||
|
|
||||||
tests/*/*.exe
|
tests/*/*.exe
|
||||||
tests/*/*.asm
|
tests/*/*.asm
|
||||||
tests/*/*.lst
|
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:
|
on what your program needs:
|
||||||
|
|
||||||
| Mode | Code lives in | Banking | Use when | Note |
|
| Mode | Code lives in | Banking | Use when | Note |
|
||||||
|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `tiny` (default) | W2 (0x8100+) | no | code+data < 14 KB | |
|
| `tiny` (default) | W2 (0x8100+) | no | code+data < 14 KB | |
|
||||||
| `small` | W1-W2 (0x4100+) | no | code+data < 30 KB | |
|
| `small` | W1-W2 (0x4100+) | no | code+data < 30 KB | |
|
||||||
| `big` | W2 + W1 banking | yes (W1) | tiny + extra code modules | |
|
| `big` | W2 + W1 banking | yes (W1) | tiny + extra code modules | |
|
||||||
@@ -217,7 +217,6 @@ This repository contains:
|
|||||||
|
|
||||||
* **Sprinter / Peters Plus** — Иван Мак, Дмитрий Паринов and the original team
|
* **Sprinter / Peters Plus** — Иван Мак, Дмитрий Паринов and the original team
|
||||||
* **SDCC** — for the underlying Z80 compiler
|
* **SDCC** — for the underlying Z80 compiler
|
||||||
* **z88dk +pps** — Дмитрий M. for paving the way with the first Sprinter target
|
|
||||||
* **MAME** — for the Sprinter Sp2000 emulation
|
* **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.
|
# Build mdview.exe — Markdown viewer for Sprinter.
|
||||||
#
|
#
|
||||||
# small memory mode: code in W1, data/stack/heap in W2 (32 KB total).
|
# 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)/../..)
|
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||||
EXAMPLE := mdview
|
EXAMPLE := mdview
|
||||||
MEMORY := small
|
MEMORY := small
|
||||||
EXTRA_DATA := SAMPLE.MD PLAN_866.md
|
|
||||||
include $(PROJ_ROOT)/app.mk
|
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.
|
|
||||||
@@ -1,165 +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.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Section: Inline emphasis
|
|
||||||
|
|
||||||
Plain words mixed with **bold words**, *italic words*, _underscore
|
|
||||||
words_ and `code words` to verify all four styles render with their
|
|
||||||
own colours.
|
|
||||||
|
|
||||||
A single **bold** stretch, then a single *italic* stretch, then a single
|
|
||||||
_underscore_ stretch, then a single `code` stretch, all on the same line.
|
|
||||||
|
|
||||||
A *long italic run that spans multiple words and several columns before
|
|
||||||
it closes here* and continues plain.
|
|
||||||
|
|
||||||
Inline code with punctuation: call `printf("%d\n", x)` then check the
|
|
||||||
result; or use `argv[0]` to grab the program name.
|
|
||||||
|
|
||||||
Unclosed emphasis (open **bold left dangling) — should auto-close at the
|
|
||||||
end of the line so the next line starts clean.
|
|
||||||
|
|
||||||
Conflict cases: **bold with a stray * inside** stays bold, and *italic
|
|
||||||
with a stray _ inside* stays italic, and `code with **bold** inside`
|
|
||||||
stays code.
|
|
||||||
|
|
||||||
## Section: Lorem ipsum
|
|
||||||
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod
|
|
||||||
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
|
|
||||||
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
|
|
||||||
commodo consequat.
|
|
||||||
|
|
||||||
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
|
|
||||||
dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
|
|
||||||
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
|
||||||
|
|
||||||
## Section: Lists
|
|
||||||
|
|
||||||
Unordered (dash):
|
|
||||||
|
|
||||||
- alpha
|
|
||||||
- bravo
|
|
||||||
- charlie
|
|
||||||
|
|
||||||
Unordered (asterisk):
|
|
||||||
|
|
||||||
* delta
|
|
||||||
* echo
|
|
||||||
|
|
||||||
Unordered (plus):
|
|
||||||
|
|
||||||
+ foxtrot
|
|
||||||
+ golf
|
|
||||||
|
|
||||||
Numbered list (digits + period highlighted):
|
|
||||||
|
|
||||||
1. first
|
|
||||||
2. second
|
|
||||||
3. third
|
|
||||||
10. tenth — verifies multi-digit numbering
|
|
||||||
99. ninety-ninth
|
|
||||||
|
|
||||||
Numbered with parens:
|
|
||||||
|
|
||||||
1) parenthesised one
|
|
||||||
2) parenthesised two
|
|
||||||
|
|
||||||
Nested lists (light v1 — leading spaces enable detection at non-zero col;
|
|
||||||
no hanging indent in wrap continuation yet):
|
|
||||||
|
|
||||||
- top level alpha
|
|
||||||
- nested level 1 bravo
|
|
||||||
- nested level 1 charlie
|
|
||||||
- nested level 2 delta
|
|
||||||
- nested level 2 echo
|
|
||||||
- nested level 3 foxtrot
|
|
||||||
- nested level 1 golf
|
|
||||||
- top level hotel
|
|
||||||
|
|
||||||
Mixed nested:
|
|
||||||
|
|
||||||
1. first top
|
|
||||||
1. nested numeric
|
|
||||||
2. nested numeric
|
|
||||||
- mixed bullet under numeric
|
|
||||||
- another bullet
|
|
||||||
3. nested numeric
|
|
||||||
2. second top
|
|
||||||
|
|
||||||
## Section: Blockquote
|
|
||||||
|
|
||||||
> Single-line blockquote.
|
|
||||||
|
|
||||||
> Multi-line blockquote starts here.
|
|
||||||
> Each line keeps the > prefix highlighted
|
|
||||||
> while the body stays plain text.
|
|
||||||
|
|
||||||
## Section: Code
|
|
||||||
|
|
||||||
Inline code: `printf("hello")`.
|
|
||||||
|
|
||||||
Fenced block (lines inside should render with code attribute on the
|
|
||||||
full row, no inline parsing — note the **stars** and *italics* below
|
|
||||||
remain literal):
|
|
||||||
|
|
||||||
```c
|
|
||||||
int main(void) {
|
|
||||||
/* **not bold**, *not italic*, `not nested` */
|
|
||||||
return 42;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
After the block, normal **bold** parsing resumes.
|
|
||||||
|
|
||||||
## Section: Long line truncation
|
|
||||||
|
|
||||||
This line is intentionally long to verify that the viewer truncates at 80 columns instead of wrapping or scrolling horizontally. Anything past the 80th visible column should simply not appear on screen.
|
|
||||||
|
|
||||||
## Section: Tabs
|
|
||||||
|
|
||||||
Indented with tabs:
|
|
||||||
|
|
||||||
tab-indent level 1
|
|
||||||
tab-indent level 2
|
|
||||||
tab-indent level 3
|
|
||||||
|
|
||||||
## Section: Blockquote
|
|
||||||
|
|
||||||
> Markdown blockquotes start with a greater-than sign at column zero.
|
|
||||||
> Multiple lines look like this.
|
|
||||||
|
|
||||||
### Subsection: deeper heading (H3)
|
|
||||||
|
|
||||||
The line above is an H3 — it should render in a colour distinct from
|
|
||||||
H1 and H2.
|
|
||||||
|
|
||||||
#### Subsubsection: H4 and below
|
|
||||||
|
|
||||||
H4 (and the rarely-seen H5/H6) all share the H4 colour slot.
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
## Section: Filler
|
|
||||||
|
|
||||||
The remaining content exists to make the document scroll past one viewport.
|
|
||||||
|
|
||||||
Line 50 ........... approx.
|
|
||||||
Line 51 ........... .
|
|
||||||
Line 52 ........... ..
|
|
||||||
Line 53 ........... ...
|
|
||||||
Line 54 ........... ....
|
|
||||||
Line 55 ........... .....
|
|
||||||
Line 56 ........... ......
|
|
||||||
Line 57 ........... .......
|
|
||||||
Line 58 ........... ........
|
|
||||||
Line 59 ........... .........
|
|
||||||
Line 60 ........... ..........
|
|
||||||
|
|
||||||
End-of-document marker. If you can see this line you can use Home / End
|
|
||||||
to bounce between the start and finish of the file.
|
|
||||||
@@ -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
|
||||||
|
План реализуется одним агентом (последовательно в одном файле), параллелизм не требуется. Дочерние агенты не используются.
|
||||||
+1398
-641
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/sys/atexit.c \
|
||||||
libc/conio/conio.c \
|
libc/conio/conio.c \
|
||||||
libc/conio/cprintf.c \
|
libc/conio/cprintf.c \
|
||||||
|
libc/conio/text_palette.c \
|
||||||
|
libc/bios/text.c \
|
||||||
libc/io/dir.c \
|
libc/io/dir.c \
|
||||||
libc/video/videomode_raw.c \
|
libc/video/videomode_raw.c \
|
||||||
|
libc/video/palette.c \
|
||||||
libc/errno/_errno_set.c \
|
libc/errno/_errno_set.c \
|
||||||
libc/env/env.c \
|
libc/env/env.c \
|
||||||
libc/errno/errno.c \
|
libc/errno/errno.c \
|
||||||
@@ -42,8 +45,10 @@ LIBC_C := \
|
|||||||
libc/io/stat.c \
|
libc/io/stat.c \
|
||||||
libc/mem/bank_io_w3.c \
|
libc/mem/bank_io_w3.c \
|
||||||
libc/mem/bank_io_w1.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_core.c \
|
||||||
|
libc/gfx/gfx_palette.c \
|
||||||
libc/gfx/gfx_raw_common.c \
|
libc/gfx/gfx_raw_common.c \
|
||||||
libc/gfx/gfx_raw_256.c \
|
libc/gfx/gfx_raw_256.c \
|
||||||
libc/gfx/gfx_raw_16.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;
|
||||||
|
}
|
||||||
+138
-144
@@ -15,10 +15,18 @@
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
|
||||||
|
|
||||||
/* Forward extern — definition is further down (after putch/cputs which
|
/* Forward extern — definition is further down (after putch/cputs which
|
||||||
* reference it from asm by linker-symbol name). */
|
* 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
|
char kbhit(void) __naked
|
||||||
{
|
{
|
||||||
__asm
|
__asm
|
||||||
@@ -91,10 +99,6 @@ uint16_t getkey(void) __naked
|
|||||||
* caller to use "\r\n" explicitly. Stdio puts/putchar do translate.
|
* 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):
|
/* Controls how _raw_putch treats control characters (< 0x20):
|
||||||
* 0 (default) — BS/TAB/LF/CR are interpreted (no glyph output);
|
* 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
|
* Only takes effect on the WRCHAR (attr ≤ 0xFF) path. When
|
||||||
* g_text_attr is KEEP_EXIST_ATTR, ESTEX's own PUTCHAR/PCHARS handle
|
* g_text_attr is KEEP_EXIST_ATTR, ESTEX's own PUTCHAR/PCHARS handle
|
||||||
* cursor and control chars — pc_raw_mode is irrelevant. */
|
* 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; }
|
void set_putch_raw_mode(uint8_t mode) { pc_raw_mode = mode; }
|
||||||
uint8_t get_putch_raw_mode(void) { return pc_raw_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
|
static void _get_cursor(void) __naked
|
||||||
{
|
{
|
||||||
__asm
|
__asm
|
||||||
push ix
|
; push ix
|
||||||
ld c, #0x53 ; ESTEX CURSOR
|
ld c, #0x8e ; BIOS GetCursor
|
||||||
rst #0x10
|
rst #0x08
|
||||||
ld a, d
|
ld (_pc_place), de
|
||||||
ld (_pc_row), a
|
; pop ix
|
||||||
ld a, e
|
|
||||||
ld (_pc_col), a
|
|
||||||
pop ix
|
|
||||||
ret
|
ret
|
||||||
__endasm;
|
__endasm;
|
||||||
}
|
}
|
||||||
@@ -131,14 +131,11 @@ static void _get_cursor(void) __naked
|
|||||||
static void _set_cursor(void) __naked
|
static void _set_cursor(void) __naked
|
||||||
{
|
{
|
||||||
__asm
|
__asm
|
||||||
push ix
|
; push ix
|
||||||
ld a, (_pc_row)
|
ld de, (_pc_place)
|
||||||
ld d, a
|
ld c, #0x84 ; BIOS SetCursor
|
||||||
ld a, (_pc_col)
|
rst #0x08
|
||||||
ld e, a
|
; pop ix
|
||||||
ld c, #0x52 ; ESTEX LOCATE
|
|
||||||
rst #0x10
|
|
||||||
pop ix
|
|
||||||
ret
|
ret
|
||||||
__endasm;
|
__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
|
* 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. */
|
* 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. */
|
/* 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;
|
(void)ch; (void)attr;
|
||||||
__asm
|
__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
|
;; 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
|
;; modify A). B/C only get loaded on the output path so the
|
||||||
;; ctrl-char paths are cheaper.
|
;; 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
|
cp #0x08
|
||||||
jr z, _rp0_bs
|
jr z, _rp0_bs
|
||||||
cp #0x09
|
cp #0x09
|
||||||
@@ -201,18 +204,21 @@ static void _raw_putch_raw0(char ch, uint8_t attr) __naked
|
|||||||
jr z, _rp0_cr
|
jr z, _rp0_cr
|
||||||
;; Anything else (printable or unrecognised ctrl) → glyph.
|
;; Anything else (printable or unrecognised ctrl) → glyph.
|
||||||
|
|
||||||
|
_rp0_pri:
|
||||||
|
|
||||||
ld c, a ; C = ch (save before A is clobbered)
|
ld c, a ; C = ch (save before A is clobbered)
|
||||||
ld a, (_pc_row)
|
ld a, (_pc_place + 1)
|
||||||
cp #32
|
cp #32
|
||||||
ret nc ; off-screen bottom — silently skip
|
ret nc ; off-screen bottom — silently skip
|
||||||
ld d, a ; D = row (ESTEX WRCHAR convention)
|
ld d, a ; D = row (ESTEX WRCHAR convention)
|
||||||
ld a, (_pc_col)
|
ld a, (_pc_place)
|
||||||
cp #80
|
cp #80
|
||||||
ret nc ; off-screen right — silently skip
|
ret nc ; off-screen right — silently skip
|
||||||
ld e, a ; E = col
|
ld e, a ; E = col
|
||||||
inc a
|
inc a
|
||||||
ld (_pc_col), a ; pc_col++
|
ld (_pc_place), a ; pc_col++
|
||||||
|
|
||||||
|
_rp0_wr:
|
||||||
ld b, l ; B = attr
|
ld b, l ; B = attr
|
||||||
ld a, c ; A = ch
|
ld a, c ; A = ch
|
||||||
push ix
|
push ix
|
||||||
@@ -222,56 +228,61 @@ static void _raw_putch_raw0(char ch, uint8_t attr) __naked
|
|||||||
ret
|
ret
|
||||||
|
|
||||||
_rp0_bs:
|
_rp0_bs:
|
||||||
ld a, (_pc_col)
|
ld a, (_pc_place)
|
||||||
or a, a
|
or a, a
|
||||||
ret z ; already at col 0 — no change
|
ret z ; already at col 0 — no change
|
||||||
dec a
|
dec a
|
||||||
ld (_pc_col), a
|
ld (_pc_place), a
|
||||||
ret
|
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:
|
_rp0_tab:
|
||||||
ld a, (_pc_col)
|
ld a, (_pc_place)
|
||||||
and #0xF8 ; floor to mult of 8
|
or #0x07 ; floor to mult of 8
|
||||||
add a, #8 ; → next mult of 8
|
inc a ; → next mult of 8
|
||||||
cp #80
|
cp #81 ; сравнить A с 81 (0x51)
|
||||||
jr c, _rp0_tab_store
|
jr c, _rp0_tab_skip ; если A < 81 (т.е. A ≤ 80), пропустить загрузку
|
||||||
ld a, #80 ; cap at off-screen right
|
ld a, #80 ; иначе A > 80 → установить A = 80
|
||||||
_rp0_tab_store:
|
_rp0_tab_skip:
|
||||||
ld (_pc_col), a
|
ld (_pc_place), a
|
||||||
ret
|
ret
|
||||||
|
|
||||||
_rp0_lf:
|
_rp0_lf:
|
||||||
ld a, (_pc_row)
|
ld a, (_pc_place + 1)
|
||||||
cp #32
|
cp #32
|
||||||
ret nc ; already at bottom edge
|
ret nc ; already at bottom edge
|
||||||
inc a
|
inc a
|
||||||
ld (_pc_row), a
|
ld (_pc_place + 1), a
|
||||||
ret
|
ret
|
||||||
|
|
||||||
_rp0_cr:
|
_rp0_cr:
|
||||||
xor a, a
|
xor a, a
|
||||||
ld (_pc_col), a
|
ld (_pc_place), a
|
||||||
ret
|
ret
|
||||||
__endasm;
|
__endasm;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mode-1 worker: every byte goes through WRCHAR as a glyph. */
|
/* 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;
|
(void)ch; (void)attr;
|
||||||
__asm
|
__asm
|
||||||
;; __sdcccall(1): ch in A, attr in L.
|
;; __sdcccall(1): ch in A, attr in L.
|
||||||
ld c, a ; C = ch (save)
|
ld c, a ; C = ch (save)
|
||||||
ld a, (_pc_row)
|
|
||||||
|
ld a, (_pc_place + 1)
|
||||||
cp #32
|
cp #32
|
||||||
ret nc ; off-screen bottom — silently skip
|
ret nc ; off-screen bottom — silently skip
|
||||||
ld d, a ; D = row
|
ld d, a ; D = row
|
||||||
ld a, (_pc_col)
|
ld a, (_pc_place)
|
||||||
cp #80
|
cp #80
|
||||||
ret nc ; off-screen right — silently skip
|
ret nc ; off-screen right — silently skip
|
||||||
ld e, a ; E = col
|
ld e, a ; E = col
|
||||||
inc a
|
inc a
|
||||||
ld (_pc_col), a ; pc_col++
|
ld (_pc_place), a ; pc_col++
|
||||||
|
|
||||||
ld b, l ; B = attr
|
ld b, l ; B = attr
|
||||||
ld a, c ; A = ch
|
ld a, c ; A = ch
|
||||||
@@ -283,18 +294,6 @@ static void _raw_putch_raw1(char ch, uint8_t attr) __naked
|
|||||||
__endasm;
|
__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 --------------------------------------- *
|
/* ---- 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
|
* folds the per-char CURSOR/LOCATE pair from the old design into a
|
||||||
* single pair per operation. */
|
* single pair per operation. */
|
||||||
|
|
||||||
|
|
||||||
char putch(char ch) __naked
|
char putch(char ch) __naked
|
||||||
{
|
{
|
||||||
(void)ch;
|
(void)ch;
|
||||||
@@ -313,38 +313,26 @@ char putch(char ch) __naked
|
|||||||
;; A = ch on entry; char return → A.
|
;; A = ch on entry; char return → A.
|
||||||
ld (_pc_ch), a ; stash c (for both return and re-load)
|
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 a, (_g_text_attr) ; A = low byte = attr
|
||||||
ld l, a ; L = attr
|
ld l, a ; L = attr
|
||||||
|
|
||||||
ld a, (_pc_raw_mode)
|
ld a, (_pc_raw_mode)
|
||||||
or a, a ; Z = (mode == 0)
|
or a, a ; Z = (mode == 0)
|
||||||
ld a, (_pc_ch) ; A = ch (`ld a,(nn)` does not touch flags)
|
ld a, (_pc_ch) ; A = ch (`ld a,(nn)` does not touch flags)
|
||||||
jr nz, _putch_use_raw1
|
jr nz, _putch_use_raw
|
||||||
call __raw_putch_raw0
|
call __putch_wrchar
|
||||||
jr _putch_after_raw
|
jr _putch_after_raw
|
||||||
_putch_use_raw1:
|
_putch_use_raw:
|
||||||
call __raw_putch_raw1
|
call __putch_wrchar_raw
|
||||||
_putch_after_raw:
|
_putch_after_raw:
|
||||||
call __set_cursor
|
call __set_cursor
|
||||||
ld a, (_pc_ch) ; return value
|
ld a, (_pc_ch) ; return value
|
||||||
ret
|
ret
|
||||||
|
|
||||||
_putch_fast:
|
|
||||||
ld a, (_pc_ch)
|
|
||||||
call __bios_putchar ; __bios_putchar keeps AF
|
|
||||||
ret
|
|
||||||
__endasm;
|
__endasm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
char cputs(const char *s) __naked
|
char cputs(const char *s) __naked
|
||||||
{
|
{
|
||||||
(void)s;
|
(void)s;
|
||||||
@@ -356,53 +344,48 @@ char cputs(const char *s) __naked
|
|||||||
or a, l
|
or a, l
|
||||||
ret z
|
ret z
|
||||||
|
|
||||||
|
push ix
|
||||||
|
|
||||||
|
call __get_cursor
|
||||||
|
|
||||||
;; KEEP_EXIST_ATTR? high byte of g_text_attr != 0
|
;; KEEP_EXIST_ATTR? high byte of g_text_attr != 0
|
||||||
ld a, (_g_text_attr + 1)
|
ld a, (_g_text_attr + 1)
|
||||||
or a, a
|
or a, a
|
||||||
jr nz, _cputs_fast
|
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)
|
ld a, (_pc_raw_mode)
|
||||||
or a, a
|
or a, a
|
||||||
jr z, _cputs_use_raw0
|
jr nz, _cputs_bios
|
||||||
ld ix, #__raw_putch_raw1
|
|
||||||
jr _cputs_loop
|
|
||||||
_cputs_use_raw0:
|
|
||||||
ld ix, #__raw_putch_raw0
|
|
||||||
|
|
||||||
_cputs_loop:
|
push hl
|
||||||
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
|
|
||||||
pop de
|
pop de
|
||||||
jr _cputs_loop
|
|
||||||
_cputs_loop_end:
|
_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:
|
||||||
|
ld c, #0x5C ; ESTEX PCHARS
|
||||||
|
rst #0x10
|
||||||
|
_cputs_ex:
|
||||||
call __set_cursor
|
call __set_cursor
|
||||||
xor a, a ; return 0
|
pop ix ; restore callers IX
|
||||||
ret
|
|
||||||
|
|
||||||
_cputs_fast:
|
|
||||||
call __cputs_pchars
|
|
||||||
xor a, a ; return 0
|
xor a, a ; return 0
|
||||||
ret
|
ret
|
||||||
__endasm;
|
__endasm;
|
||||||
@@ -439,12 +422,15 @@ void gotoxy(uint8_t x, uint8_t y) __naked
|
|||||||
__asm
|
__asm
|
||||||
;; __sdcccall(1) 2 uint8 args: x in A, y in L.
|
;; __sdcccall(1) 2 uint8 args: x in A, y in L.
|
||||||
;; ESTEX LOCATE ($52) wants: D = row, E = col.
|
;; ESTEX LOCATE ($52) wants: D = row, E = col.
|
||||||
push ix
|
;; push ix
|
||||||
ld d, l ; D = row (y)
|
ld d, l ; D = row (y)
|
||||||
ld e, a ; E = col (x)
|
ld e, a ; E = col (x)
|
||||||
ld c, #0x52
|
ld (_pc_place), de
|
||||||
rst #0x10
|
ld c, #0x84 ; BIOS SetCursor
|
||||||
pop ix
|
rst #0x08
|
||||||
|
;; ld c, #0x52
|
||||||
|
;; rst #0x10
|
||||||
|
;; pop ix
|
||||||
ret
|
ret
|
||||||
__endasm;
|
__endasm;
|
||||||
}
|
}
|
||||||
@@ -453,11 +439,14 @@ uint8_t wherex(void) __naked
|
|||||||
{
|
{
|
||||||
__asm
|
__asm
|
||||||
;; ESTEX CURSOR ($53): D = row, E = col. Return col in DE.
|
;; ESTEX CURSOR ($53): D = row, E = col. Return col in DE.
|
||||||
push ix
|
;; push ix
|
||||||
ld c, #0x53
|
ld c, #0x8e ; BIOS GetCursor
|
||||||
rst #0x10
|
rst #0x08
|
||||||
pop ix
|
;; ld c, #0x53
|
||||||
ld a, e
|
;; rst #0x10
|
||||||
|
;; pop ix
|
||||||
|
ld a, e
|
||||||
|
ld (_pc_place), de
|
||||||
ret
|
ret
|
||||||
__endasm;
|
__endasm;
|
||||||
}
|
}
|
||||||
@@ -465,11 +454,14 @@ uint8_t wherex(void) __naked
|
|||||||
uint8_t wherey(void) __naked
|
uint8_t wherey(void) __naked
|
||||||
{
|
{
|
||||||
__asm
|
__asm
|
||||||
push ix
|
;; push ix
|
||||||
ld c, #0x53
|
ld c, #0x8e ; BIOS GetCursor
|
||||||
rst #0x10
|
rst #0x08
|
||||||
pop ix
|
;; ld c, #0x53
|
||||||
ld a, d
|
;; rst #0x10
|
||||||
|
;; pop ix
|
||||||
|
ld a, d
|
||||||
|
ld (_pc_place), de
|
||||||
ret
|
ret
|
||||||
__endasm;
|
__endasm;
|
||||||
}
|
}
|
||||||
@@ -478,10 +470,13 @@ uint16_t wherexy(void) __naked
|
|||||||
{
|
{
|
||||||
__asm
|
__asm
|
||||||
;; ESTEX CURSOR ($53): D = row, E = col. Return col in DE.
|
;; ESTEX CURSOR ($53): D = row, E = col. Return col in DE.
|
||||||
push ix
|
;; push ix
|
||||||
ld c, #0x53
|
ld c, #0x8e ; BIOS GetCursor
|
||||||
rst #0x10
|
rst #0x08
|
||||||
pop ix
|
;; ld c, #0x53
|
||||||
|
;; rst #0x10
|
||||||
|
;; pop ix
|
||||||
|
ld (_pc_place), de
|
||||||
ret
|
ret
|
||||||
__endasm;
|
__endasm;
|
||||||
}
|
}
|
||||||
@@ -570,12 +565,12 @@ uint16_t rdchar(uint8_t x, uint8_t y) __naked
|
|||||||
extern uint8_t _videomode_raw_get(void);
|
extern uint8_t _videomode_raw_get(void);
|
||||||
extern int _videomode_raw_set(uint8_t mode);
|
extern int _videomode_raw_set(uint8_t mode);
|
||||||
|
|
||||||
uint8_t get_videotextmode(void)
|
uint8_t gettextmode(void)
|
||||||
{
|
{
|
||||||
return _videomode_raw_get();
|
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
|
/* 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. */
|
* 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)
|
* 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 */
|
* 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 set_text_attr(int16_t attr)
|
||||||
{
|
{
|
||||||
int16_t prev = g_text_attr;
|
int16_t prev = g_text_attr.value;
|
||||||
g_text_attr = attr;
|
g_text_attr.value = attr;
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
|
|
||||||
int16_t get_text_attr(void)
|
int16_t get_text_attr(void)
|
||||||
{
|
{
|
||||||
return g_text_attr;
|
return g_text_attr.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Turbo-C-style palette helpers --------------------------------
|
/* ---- Turbo-C-style palette helpers --------------------------------
|
||||||
@@ -613,21 +607,21 @@ int16_t get_text_attr(void)
|
|||||||
|
|
||||||
void textcolor(uint8_t fg)
|
void textcolor(uint8_t fg)
|
||||||
{
|
{
|
||||||
/* If we were KEEP_EXIST_ATTR, switch to a real attr first. */
|
fg = (fg & 0x07);
|
||||||
uint8_t cur = ((uint16_t)g_text_attr > 0xFF) ? 0x00 : (uint8_t)g_text_attr;
|
g_text_attr.value = (g_text_attr.byte.low & 0xF0) | fg;
|
||||||
g_text_attr = (int16_t)((cur & 0xF0) | (fg & 0x0F));
|
// g_text_attr.byte.high = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void textbackground(uint8_t bg)
|
void textbackground(uint8_t bg)
|
||||||
{
|
{
|
||||||
uint8_t cur = ((uint16_t)g_text_attr > 0xFF) ? 0x00 : (uint8_t)g_text_attr;
|
bg = (bg & 0x07) << 4;
|
||||||
/* Background uses 3 bits (4..6); preserve blink (bit 7) too. */
|
g_text_attr.value = (g_text_attr.byte.low & 0x0F) | bg;
|
||||||
g_text_attr = (int16_t)((cur & 0x8F) | ((bg & 0x07) << 4));
|
// g_text_attr.byte.high = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void textattr(uint8_t attr)
|
void textattr(uint8_t attr)
|
||||||
{
|
{
|
||||||
g_text_attr = (int16_t)attr;
|
g_text_attr.value = (uint16_t)attr;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Solid-C compatibility ---------------------------------------- */
|
/* ---- 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_draw_page / get — updates _gfx_addr_base for the new page
|
||||||
* gfx_set_bank / get — sets the W3 page byte (0x50..0x5F)
|
* gfx_set_bank / get — sets the W3 page byte (0x50..0x5F)
|
||||||
* gfx_wait_vsync — EI; HALT until next frame interrupt
|
* 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):
|
* Shared state (extern from this file):
|
||||||
* _gfx_addr_base — 0xC000 for page 0, 0xC140 for page 1. Every
|
* _gfx_addr_base — 0xC000 for page 0, 0xC140 for page 1. Every
|
||||||
@@ -136,43 +136,6 @@ void gfx_wait_vsync(void) __naked
|
|||||||
__endasm;
|
__endasm;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Palette (BIOS $A4 PIC_SET_PAL) ----------------------------- */
|
/* 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
|
||||||
static uint8_t pal_num_;
|
* libc/video/palette.c for the shared low-level $A4 / $A6 implementation. */
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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_PGUP 0x59
|
||||||
#define KEY_INS 0x50 /* numpad 0; not verified */
|
#define KEY_INS 0x50 /* numpad 0; not verified */
|
||||||
#define KEY_DEL 0x55 /* numpad 5/.; 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 putch (char c);
|
||||||
char cputs (const char *s);
|
char cputs (const char *s);
|
||||||
int cprintf(const char *fmt, ...);
|
int cprintf(const char *fmt, ...);
|
||||||
void clrscr(void);
|
void clrscr(void);
|
||||||
void gotoxy(uint8_t x, uint8_t y);
|
void gotoxy(uint8_t x, uint8_t y);
|
||||||
|
|
||||||
|
|
||||||
/* Solid-C compatibility helpers. */
|
/* Solid-C compatibility helpers. */
|
||||||
#define home() gotoxy(0, 0)
|
#define home() gotoxy(0, 0)
|
||||||
#define inp(port) z80_inp(port)
|
#define inp(port) z80_inp(port)
|
||||||
@@ -124,8 +136,8 @@ void clrscr_attr(uint8_t attr);
|
|||||||
#define TEXT_MODE_40x32 0x02
|
#define TEXT_MODE_40x32 0x02
|
||||||
#define TEXT_MODE_80x32 0x03
|
#define TEXT_MODE_80x32 0x03
|
||||||
|
|
||||||
uint8_t get_videotextmode(void);
|
uint8_t gettextmode(void);
|
||||||
int set_videotextmode(uint8_t mode); /* 0 OK, -1 + errno on bad mode */
|
int settextmode(uint8_t mode); /* 0 OK, -1 + errno on bad mode */
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ *
|
/* ------------------------------------------------------------------ *
|
||||||
* Text-output attribute (used by the conio set: putch / cputs / cprintf).
|
* Text-output attribute (used by the conio set: putch / cputs / cprintf).
|
||||||
@@ -201,4 +213,36 @@ enum {
|
|||||||
#define COLOR_BLINK 0x80u
|
#define COLOR_BLINK 0x80u
|
||||||
#define COLOR(fg, bg) ((uint8_t)((((bg) & 0x07) << 4) | ((fg) & 0x0F)))
|
#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
|
#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,
|
void gfx_pal_set (uint8_t pal_num, uint8_t idx,
|
||||||
uint8_t r, uint8_t g, uint8_t b);
|
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
|
#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
|
* n : 1..255
|
||||||
* ret : blk_id (1..255) on success; 0 on failure with errno set.
|
* 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(). */
|
* 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().
|
/* Release a block previously returned by mem_alloc_pages().
|
||||||
* On error errno is set (e.g. EINVAL for unknown id). Double-free is
|
* On error errno is set (e.g. EINVAL for unknown id). Double-free is
|
||||||
* NOT idempotent: the second call sets errno. */
|
* 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
|
/* Translate (block, page-index) into a physical page number suitable
|
||||||
* for sprinter_page_w1/w2/w3() or the bank_*() helpers below.
|
* for sprinter_page_w1/w2/w3() or the bank_*() helpers below.
|
||||||
* blk_id: from mem_alloc_pages()
|
* blk_id: from mem_alloc_pages()
|
||||||
* idx : 0..(n-1)
|
* idx : 0..(n-1)
|
||||||
* ret : physical page (1..255) on success; 0 on failure (errno set). */
|
* 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.
|
/* Query the EMM allocator state. Both pointers must be non-NULL.
|
||||||
* Cannot fail (no error path). */
|
* 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)
|
* 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 $3C INFOMEM → HL=total pages, BC=free pages
|
||||||
* ESTEX $3D GETMEM B=npages → A=block id, CF=err
|
* ESTEX $3D GETMEM B=npages → A=block id, CF=err
|
||||||
* ESTEX $3E FREEMEM 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
|
* 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.
|
* 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
|
* 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.
|
* 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;
|
(void)n;
|
||||||
__asm
|
__asm
|
||||||
@@ -43,6 +42,7 @@ uint8_t mem_alloc_pages(uint8_t n) __naked
|
|||||||
__endasm;
|
__endasm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Release a block previously returned by mem_alloc_pages().
|
* 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
|
* sets errno on the second call. Caller is responsible for tracking
|
||||||
* ownership.
|
* ownership.
|
||||||
*/
|
*/
|
||||||
void mem_free_block(uint8_t blk_id) __naked
|
void mem_free_block_estex(uint8_t blk_id) __naked
|
||||||
{
|
{
|
||||||
(void)blk_id;
|
(void)blk_id;
|
||||||
__asm
|
__asm
|
||||||
@@ -67,6 +67,7 @@ void mem_free_block(uint8_t blk_id) __naked
|
|||||||
__endasm;
|
__endasm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Translate a (block, page-index) pair into a physical 16-KB page number,
|
* 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.
|
* 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;
|
* out: physical page number (1..255) on success;
|
||||||
* 0 on failure with errno set (invalid block or idx out of range).
|
* 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.
|
* 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.
|
* Both pointers must be non-NULL writeable uint16_t locations.
|
||||||
* No error path: ESTEX INFOMEM always succeeds.
|
* 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;
|
(void)total; (void)free_pages;
|
||||||
__asm
|
__asm
|
||||||
@@ -135,3 +118,4 @@ void mem_info(uint16_t *total, uint16_t *free_pages) __naked
|
|||||||
ret
|
ret
|
||||||
__endasm;
|
__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('0' + x % 10);
|
||||||
putchar('\n');
|
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 <stdint.h>
|
||||||
#include <sprinter.h>
|
#include <sprinter.h>
|
||||||
|
|
||||||
|
void bank1_func2(int x) __banked;
|
||||||
|
|
||||||
void bank2_func(int x) __banked
|
void bank2_func(int x) __banked
|
||||||
{
|
{
|
||||||
(void)x;
|
(void)x;
|
||||||
@@ -15,4 +17,6 @@ void bank2_func(int x) __banked
|
|||||||
putchar('0' + (x / 10) % 10);
|
putchar('0' + (x / 10) % 10);
|
||||||
putchar('0' + x % 10);
|
putchar('0' + x % 10);
|
||||||
putchar('\n');
|
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 <stdio.h>
|
||||||
#include <conio.h>
|
#include <conio.h>
|
||||||
#include <unistd.h> /* sleep */
|
#include <unistd.h> /* sleep */
|
||||||
|
#include <sprinter_exit.h>
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Exercises every conio function:
|
* Exercises every conio function:
|
||||||
@@ -16,6 +17,8 @@
|
|||||||
* (0,0), (79,0), (0,31), (79,31).
|
* (0,0), (79,0), (0,31), (79,31).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
extern two_bytes pc_place;
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
/* 1. clrscr + diagnostic probes + a centered banner.
|
/* 1. clrscr + diagnostic probes + a centered banner.
|
||||||
@@ -27,9 +30,16 @@ int main(void)
|
|||||||
*/
|
*/
|
||||||
clrscr();
|
clrscr();
|
||||||
|
|
||||||
gotoxy(0, 0); putch('0');
|
gotoxy(1, 0);
|
||||||
|
textattr(0x07);
|
||||||
|
putch('0');
|
||||||
textattr(0x70);
|
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);
|
set_putch_raw_mode(1);
|
||||||
|
|
||||||
@@ -51,11 +61,23 @@ int main(void)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set_text_attr(0x011D);
|
||||||
set_putch_raw_mode(0);
|
set_putch_raw_mode(0);
|
||||||
gotoxy(0,22);
|
gotoxy(0,22);
|
||||||
set_text_attr(0x000E);
|
cputs("Test message line 1\nTest message line2\rline3\n\r1234\t5678\b90\n\r");
|
||||||
// cputs("Test message line");
|
|
||||||
cputs("Test message line 1\nTest message line2\rline3\n\r\n\r1234\t5678\b90");
|
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();
|
(void)getch();
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -18,7 +18,7 @@ int main(void)
|
|||||||
{
|
{
|
||||||
errno = -1;
|
errno = -1;
|
||||||
int16_t a0 = 0, a1 = 0;
|
int16_t a0 = 0, a1 = 0;
|
||||||
a0 = get_text_attr();
|
a0 = get_text_attr();
|
||||||
|
|
||||||
uint8_t vMode = get_videotextmode();
|
uint8_t vMode = get_videotextmode();
|
||||||
set_videotextmode(TEXT_MODE_80x32);
|
set_videotextmode(TEXT_MODE_80x32);
|
||||||
@@ -52,7 +52,7 @@ int main(void)
|
|||||||
|
|
||||||
/* Back to a normal attribute so the goodbye reads cleanly. */
|
/* Back to a normal attribute so the goodbye reads cleanly. */
|
||||||
textattr(COLOR(COLOR_LIGHTGRAY, COLOR_BLACK));
|
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);
|
a0, a1, get_text_attr(), errno);
|
||||||
#ifdef DEBUG_RT
|
#ifdef DEBUG_RT
|
||||||
printf("w2_self_allocated = %u\n", w2_self_allocated);
|
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.
|
* 5. Free the block, show the free-page count again.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
uint32_t buff[256];
|
||||||
|
|
||||||
static void show_mem(const char *label)
|
static void show_mem(const char *label)
|
||||||
{
|
{
|
||||||
uint16_t total, free_pages;
|
uint16_t total, free_pages;
|
||||||
@@ -28,6 +30,13 @@ int main(void)
|
|||||||
{
|
{
|
||||||
puts("Sprinter page allocator demo");
|
puts("Sprinter page allocator demo");
|
||||||
puts("");
|
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:");
|
show_mem("before:");
|
||||||
|
|
||||||
|
|||||||
+15
-12
@@ -10,6 +10,8 @@
|
|||||||
#include <conio.h>
|
#include <conio.h>
|
||||||
#include <mouse.h>
|
#include <mouse.h>
|
||||||
|
|
||||||
|
mouse_state_t st;
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
textattr(COLOR(COLOR_LIGHTGRAY, COLOR_BLACK));
|
textattr(COLOR(COLOR_LIGHTGRAY, COLOR_BLACK));
|
||||||
@@ -29,7 +31,6 @@ int main(void)
|
|||||||
mouse_bounds_y(0, 255);
|
mouse_bounds_y(0, 255);
|
||||||
mouse_show();
|
mouse_show();
|
||||||
|
|
||||||
mouse_state_t st;
|
|
||||||
int last_x = -1, last_y = -1;
|
int last_x = -1, last_y = -1;
|
||||||
uint8_t last_btn = 0xFF;
|
uint8_t last_btn = 0xFF;
|
||||||
/* Sensitivity is a "raw steps per cursor pixel" divider: smaller =
|
/* Sensitivity is a "raw steps per cursor pixel" divider: smaller =
|
||||||
@@ -41,11 +42,13 @@ int main(void)
|
|||||||
mouse_set_sensitivity(sens_x, sens_y);
|
mouse_set_sensitivity(sens_x, sens_y);
|
||||||
int sens_dirty = 1;
|
int sens_dirty = 1;
|
||||||
|
|
||||||
|
st.x = st.y = 0; st.buttons = 0;
|
||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
mouse_read(&st);
|
mouse_read(&st);
|
||||||
if (st.x != last_x || st.y != last_y || st.buttons != last_btn) {
|
if (st.x != last_x || st.y != last_y || st.buttons != last_btn) {
|
||||||
gotoxy(0, 6);
|
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, st.y,
|
||||||
st.x / 8, st.y / 8,
|
st.x / 8, st.y / 8,
|
||||||
st.buttons,
|
st.buttons,
|
||||||
@@ -57,19 +60,19 @@ int main(void)
|
|||||||
}
|
}
|
||||||
if (sens_dirty) {
|
if (sens_dirty) {
|
||||||
gotoxy(0, 8);
|
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;
|
sens_dirty = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!kbhit()) continue;
|
if (!kbhit()) continue;
|
||||||
int k = getch();
|
int k = getch();
|
||||||
if (k == 27) break; /* ESC */
|
if (k == 27) break; /* ESC */
|
||||||
if (k == '1' && sens_x > 1) { sens_x -= 1; sens_dirty = 1; }
|
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 == '2' && sens_x < 254) { sens_x += 1; sens_dirty = 1; }
|
||||||
if (k == '3' && sens_y > 1) { sens_y -= 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 (k == '4' && sens_y < 254) { sens_y += 1; sens_dirty = 1; }
|
||||||
if (sens_dirty)
|
if (sens_dirty)
|
||||||
mouse_set_sensitivity(sens_x, sens_y);
|
mouse_set_sensitivity(sens_x, sens_y);
|
||||||
}
|
}
|
||||||
|
|
||||||
mouse_hide();
|
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