Compare commits
33 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 | |||
| 035d93ab51 | |||
| 0dedc4dac8 | |||
| 737c974400 |
+18
-1
@@ -9,7 +9,8 @@ build/
|
|||||||
# sprinter-cc per-example intermediate directory
|
# sprinter-cc per-example intermediate directory
|
||||||
.sprinter-cc-*/
|
.sprinter-cc-*/
|
||||||
|
|
||||||
# Per-example final/intermediate outputs landing alongside the source
|
# Per-program final/intermediate outputs landing alongside the source
|
||||||
|
# (real apps under examples/ and libc feature tests under tests/).
|
||||||
examples/*/*.exe
|
examples/*/*.exe
|
||||||
examples/*/*.asm
|
examples/*/*.asm
|
||||||
examples/*/*.lst
|
examples/*/*.lst
|
||||||
@@ -23,6 +24,22 @@ examples/*/*.cdb
|
|||||||
examples/*/*.mem
|
examples/*/*.mem
|
||||||
examples/*/*.rst
|
examples/*/*.rst
|
||||||
|
|
||||||
|
# Temporary build directory for floppy disk image preparation
|
||||||
|
examples/*/.disk_tmp/
|
||||||
|
|
||||||
|
tests/*/*.exe
|
||||||
|
tests/*/*.asm
|
||||||
|
tests/*/*.lst
|
||||||
|
tests/*/*.lk
|
||||||
|
tests/*/*.ihx
|
||||||
|
tests/*/*.noi
|
||||||
|
tests/*/*.sym
|
||||||
|
tests/*/*.map
|
||||||
|
tests/*/*.rel
|
||||||
|
tests/*/*.cdb
|
||||||
|
tests/*/*.mem
|
||||||
|
tests/*/*.rst
|
||||||
|
|
||||||
# libc archive (built from libc/, see lib/Makefile)
|
# libc archive (built from libc/, see lib/Makefile)
|
||||||
lib/*.lib
|
lib/*.lib
|
||||||
|
|
||||||
|
|||||||
@@ -1,58 +1,41 @@
|
|||||||
# Sprinter C Compiler — top-level Makefile
|
# Sprinter C Compiler — top-level Makefile
|
||||||
#
|
#
|
||||||
# make build host tools, libc archive, and all examples
|
# make build host tools, libc archive, all tests, all apps
|
||||||
# make tools build only host tools (mkexe)
|
# make tools build only host tools (mkexe)
|
||||||
# make lib build lib/sprinter.lib (libc archive used by sprinter-cc)
|
# make lib build lib/sprinter.lib (libc archive used by sprinter-cc)
|
||||||
# make examples build all examples
|
# make tests build all libc feature tests under tests/
|
||||||
# make floppy package every example + test files into mame/v306/IMG/mc.img
|
# make examples build all real applications under examples/
|
||||||
|
# make floppy package every .exe + test fixtures into mame/v306/IMG/mc.img
|
||||||
# make check run mkexe unit tests
|
# make check run mkexe unit tests
|
||||||
# make clean remove all build artefacts
|
# make clean remove all build artefacts
|
||||||
# make sdcc download/extract vendored SDCC
|
# make sdcc download/extract vendored SDCC
|
||||||
#
|
#
|
||||||
# Most heavy lifting is delegated to sub-Makefiles.
|
# Most heavy lifting is delegated to sub-Makefiles.
|
||||||
|
|
||||||
EXAMPLES := hello banked bankedbg strtest cat seek malloc mem_test argv errno rt_test openenv ls conio attrprob timedir mouse banklocl stdlib assrtest ptime stattest filetest gfx_demo gfx_d16 gfx_text gfx_mous
|
# Small libc-feature tests (one program per .c-language feature or libc API).
|
||||||
|
TESTS := hello banked bankedbg strtest cat seek malloc mem_test argv errno \
|
||||||
|
rt_test openenv ls conio attrprob timedir mouse banklocl stdlib \
|
||||||
|
assrtest ptime stattest filetest gfx_demo gfx_d16 gfx_text gfx_mous
|
||||||
|
|
||||||
|
# Larger end-user applications under examples/.
|
||||||
|
APPS := mdview
|
||||||
|
|
||||||
MAME_DIR := mame/v306
|
MAME_DIR := mame/v306
|
||||||
FLOPPY_IMG := $(MAME_DIR)/IMG/mc.img
|
FLOPPY_IMG := $(MAME_DIR)/IMG/mc.img
|
||||||
MAKE_DISK := $(MAME_DIR)/make_disk.py
|
MAKE_DISK := $(MAME_DIR)/make_disk.py
|
||||||
|
|
||||||
EXE_FILES := \
|
TEST_EXES := $(foreach t,$(TESTS),tests/$(t)/$(t).exe)
|
||||||
examples/hello/hello.exe \
|
APP_EXES := $(foreach a,$(APPS),examples/$(a)/$(a).exe)
|
||||||
examples/banked/banked.exe \
|
ALL_EXES := $(TEST_EXES) $(APP_EXES)
|
||||||
examples/bankedbg/bankedbg.exe \
|
|
||||||
examples/strtest/strtest.exe \
|
|
||||||
examples/cat/cat.exe \
|
|
||||||
examples/seek/seek.exe \
|
|
||||||
examples/malloc/malloc.exe \
|
|
||||||
examples/mem_test/mem_test.exe \
|
|
||||||
examples/argv/argv.exe \
|
|
||||||
examples/errno/errno.exe \
|
|
||||||
examples/rt_test/rt_test.exe \
|
|
||||||
examples/openenv/openenv.exe \
|
|
||||||
examples/ls/ls.exe \
|
|
||||||
examples/conio/conio.exe \
|
|
||||||
examples/attrprob/attrprob.exe \
|
|
||||||
examples/timedir/timedir.exe \
|
|
||||||
examples/mouse/mouse.exe \
|
|
||||||
examples/banklocl/banklocl.exe \
|
|
||||||
examples/stdlib/stdlib.exe \
|
|
||||||
examples/assrtest/assrtest.exe \
|
|
||||||
examples/ptime/ptime.exe \
|
|
||||||
examples/stattest/stattest.exe \
|
|
||||||
examples/filetest/filetest.exe \
|
|
||||||
examples/gfx_demo/gfx_demo.exe \
|
|
||||||
examples/gfx_d16/gfx_d16.exe \
|
|
||||||
examples/gfx_text/gfx_text.exe \
|
|
||||||
examples/gfx_mous/gfx_mous.exe
|
|
||||||
|
|
||||||
DATA_FILES := \
|
DATA_FILES := \
|
||||||
examples/cat/test.txt \
|
tests/cat/test.txt \
|
||||||
examples/seek/big.txt
|
tests/seek/big.txt \
|
||||||
|
examples/mdview/SAMPLE.MD
|
||||||
|
|
||||||
.PHONY: all tools lib examples check clean sdcc floppy $(EXAMPLES)
|
.PHONY: all tools lib tests examples check clean sdcc floppy $(TESTS) $(APPS)
|
||||||
|
|
||||||
all: tools lib examples
|
all: tools lib tests examples
|
||||||
|
|
||||||
tools:
|
tools:
|
||||||
$(MAKE) -C toolchain/mkexe
|
$(MAKE) -C toolchain/mkexe
|
||||||
@@ -63,18 +46,22 @@ lib:
|
|||||||
check: tools
|
check: tools
|
||||||
$(MAKE) -C toolchain/mkexe check
|
$(MAKE) -C toolchain/mkexe check
|
||||||
|
|
||||||
examples: $(EXAMPLES)
|
tests: $(TESTS)
|
||||||
|
examples: $(APPS)
|
||||||
|
|
||||||
$(EXAMPLES): tools lib
|
$(TESTS): tools lib
|
||||||
|
$(MAKE) -C tests/$@
|
||||||
|
|
||||||
|
$(APPS): tools lib
|
||||||
$(MAKE) -C examples/$@
|
$(MAKE) -C examples/$@
|
||||||
|
|
||||||
# Generate big.txt if missing (gen_bigfile.py creates 100 KB marker file).
|
# Generate big.txt if missing (gen_bigfile.py creates 100 KB marker file).
|
||||||
examples/seek/big.txt:
|
tests/seek/big.txt:
|
||||||
cd examples/seek && python3 gen_bigfile.py big.txt 102400
|
cd tests/seek && python3 gen_bigfile.py big.txt 102400
|
||||||
|
|
||||||
# Re-pack the MAME floppy image with every built example + needed data files.
|
# Re-pack the MAME floppy image with every built exe + needed data files.
|
||||||
floppy: examples examples/seek/big.txt
|
floppy: tests examples tests/seek/big.txt
|
||||||
python3 $(MAKE_DISK) $(FLOPPY_IMG) $(EXE_FILES) $(DATA_FILES)
|
python3 $(MAKE_DISK) $(FLOPPY_IMG) $(ALL_EXES) $(DATA_FILES)
|
||||||
@echo
|
@echo
|
||||||
@echo "Floppy ready: $(FLOPPY_IMG)"
|
@echo "Floppy ready: $(FLOPPY_IMG)"
|
||||||
@echo "Run: cd $(MAME_DIR) && ./run_mame.sh"
|
@echo "Run: cd $(MAME_DIR) && ./run_mame.sh"
|
||||||
@@ -82,7 +69,8 @@ floppy: examples examples/seek/big.txt
|
|||||||
clean:
|
clean:
|
||||||
$(MAKE) -C toolchain/mkexe clean
|
$(MAKE) -C toolchain/mkexe clean
|
||||||
$(MAKE) -C lib clean
|
$(MAKE) -C lib clean
|
||||||
@for e in $(EXAMPLES); do $(MAKE) -C examples/$$e clean; done
|
@for t in $(TESTS); do $(MAKE) -C tests/$$t clean; done
|
||||||
|
@for a in $(APPS); do $(MAKE) -C examples/$$a clean; done
|
||||||
|
|
||||||
sdcc:
|
sdcc:
|
||||||
bash third_party/setup-sdcc.sh
|
bash third_party/setup-sdcc.sh
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
# example.mk — shared Makefile fragment for simple Sprinter ESTEX examples.
|
# app.mk — shared Makefile fragment for any standalone Sprinter ESTEX
|
||||||
|
# program — used both by libc feature tests under tests/ and by real
|
||||||
|
# applications under examples/.
|
||||||
#
|
#
|
||||||
# Usage in an example's Makefile:
|
# Usage in a per-program Makefile:
|
||||||
#
|
#
|
||||||
# PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
# PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||||
# EXAMPLE := my_example # base name (matches my_example.c)
|
# EXAMPLE := my_program # base name (matches my_program.c)
|
||||||
#
|
#
|
||||||
# # Optional overrides (any combination):
|
# # Optional overrides (any combination):
|
||||||
# # MEMORY := small # tiny | small | big | huge | manual
|
# # MEMORY := small # tiny | small | big | huge | manual
|
||||||
@@ -12,7 +14,7 @@
|
|||||||
# # EXTRA_FLAGS := --crt0=minimal # passed through to sprinter-cc
|
# # EXTRA_FLAGS := --crt0=minimal # passed through to sprinter-cc
|
||||||
# # EXTRA_DATA := test.txt # extra files to add to `make floppy`
|
# # EXTRA_DATA := test.txt # extra files to add to `make floppy`
|
||||||
#
|
#
|
||||||
# include $(PROJ_ROOT)/examples/example.mk
|
# include $(PROJ_ROOT)/app.mk
|
||||||
#
|
#
|
||||||
# Pipeline (all driven by sprinter-cc):
|
# Pipeline (all driven by sprinter-cc):
|
||||||
# crt0 + EXAMPLE.c + EXTRA_SRCS --sdcc--> .ihx
|
# crt0 + EXAMPLE.c + EXTRA_SRCS --sdcc--> .ihx
|
||||||
@@ -61,9 +63,9 @@ $(LIB):
|
|||||||
clean:
|
clean:
|
||||||
rm -rf .sprinter-cc-* $(EXAMPLE).exe
|
rm -rf .sprinter-cc-* $(EXAMPLE).exe
|
||||||
|
|
||||||
# `make floppy` packs ONLY this example (+ optional EXTRA_DATA files) into
|
# `make floppy` packs ONLY this program (+ optional EXTRA_DATA files) into
|
||||||
# the MAME floppy image, replacing whatever was there. Handy for trying a
|
# the MAME floppy image, replacing whatever was there. Handy for trying a
|
||||||
# single program without rebuilding all 27 examples.
|
# single program without rebuilding everything.
|
||||||
floppy: $(EXAMPLE).exe
|
floppy: $(EXAMPLE).exe
|
||||||
python3 $(MAKE_DISK) $(FLOPPY_IMG) $(EXAMPLE).exe $(EXTRA_DATA)
|
python3 $(MAKE_DISK) $(FLOPPY_IMG) $(EXAMPLE).exe $(EXTRA_DATA)
|
||||||
@echo
|
@echo
|
||||||
@@ -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`.
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Build mdview.exe — Markdown viewer for Sprinter.
|
||||||
|
#
|
||||||
|
# small memory mode: code in W1, data/stack/heap in W2 (32 KB total).
|
||||||
|
# W3 stays free for the file buffer (EMM-mapped).
|
||||||
|
|
||||||
|
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||||
|
EXAMPLE := mdview
|
||||||
|
MEMORY := small
|
||||||
|
|
||||||
|
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
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
# MDView — Просмотрщик Markdown для Sprinter
|
||||||
|
|
||||||
|
**MDView** — программа для просмотра документов в формате *Markdown* на компьютере
|
||||||
|
Sprinter (процессор Z80). Документ хранится в отдельном W3 окне и не занимает
|
||||||
|
основную RAM программы.
|
||||||
|
|
||||||
|
## Возможности
|
||||||
|
|
||||||
|
- Документы до **128 КБ** (8 страниц EMM по 16 КБ каждая)
|
||||||
|
- До **16 384** экранных строк в индексе
|
||||||
|
- Автоматический перенос слов по ширине экрана (80 столбцов)
|
||||||
|
- Горизонтальный сдвиг для широких строк (блоки кода, таблицы)
|
||||||
|
- Статус-бар: имя файла, диапазон строк, процент прокрутки
|
||||||
|
- Спиннер в строке состояния во время загрузки и индексации
|
||||||
|
- Поддержка «мягкого» склеивания строк в абзацах и цитатах
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```
|
||||||
|
mdview [имя_файла.md]
|
||||||
|
```
|
||||||
|
|
||||||
|
Если имя файла не задано, загружается `README.MD`.
|
||||||
|
|
||||||
|
## Управление
|
||||||
|
|
||||||
|
```
|
||||||
|
Клавиша Действие
|
||||||
|
───────────── ────────────────────────────────────────
|
||||||
|
Up Down Прокрутка на одну строку вверх / вниз
|
||||||
|
PgUp PgDn Прокрутка на страницу (30 строк)
|
||||||
|
Home Начало документа
|
||||||
|
End Конец документа
|
||||||
|
Left Right Горизонтальный сдвиг (только nowrap-строки)
|
||||||
|
F1 Окно справки
|
||||||
|
F10 / Esc Выход из программы
|
||||||
|
```
|
||||||
|
|
||||||
|
## Синтаксис Markdown
|
||||||
|
|
||||||
|
### Заголовки
|
||||||
|
|
||||||
|
Поддерживаются уровни H1–H4. Уровни H5 и H6 отображаются как H4.
|
||||||
|
|
||||||
|
# Заголовок первого уровня
|
||||||
|
## Заголовок второго уровня
|
||||||
|
### Заголовок третьего уровня
|
||||||
|
#### Заголовок четвёртого уровня
|
||||||
|
|
||||||
|
### Текстовое форматирование
|
||||||
|
|
||||||
|
**Жирный текст** выделяется двойными звёздочками: `**текст**`
|
||||||
|
|
||||||
|
*Курсив* выделяется одиночными звёздочками `*текст*` или знаком подчёркивания `_текст_`
|
||||||
|
|
||||||
|
`Встроенный код` обозначается обратными кавычками
|
||||||
|
|
||||||
|
~~Зачёркнутый текст~~ — двойные тильды: `~~текст~~`
|
||||||
|
|
||||||
|
### Ненумерованный список
|
||||||
|
|
||||||
|
Маркеры `-`, `*` или `+`:
|
||||||
|
|
||||||
|
- Первый пункт списка
|
||||||
|
- Второй пункт списка
|
||||||
|
- Третий пункт с достаточно длинным текстом, который при необходимости
|
||||||
|
будет перенесён на следующую строку с сохранением отступа
|
||||||
|
|
||||||
|
### Нумерованный список
|
||||||
|
|
||||||
|
1. Первый элемент
|
||||||
|
2. Второй элемент
|
||||||
|
3. Третий элемент
|
||||||
|
|
||||||
|
### Цитата
|
||||||
|
|
||||||
|
> Блок цитаты начинается с символа `>`. Несколько последовательных
|
||||||
|
> строк одной цитаты склеиваются в единый абзац с автоматическим
|
||||||
|
> переносом слов.
|
||||||
|
|
||||||
|
### Блок кода (verbatim)
|
||||||
|
|
||||||
|
Блок кода заключается в тройные обратные кавычки. Внутри блока
|
||||||
|
текст отображается «как есть» без разбора Markdown:
|
||||||
|
|
||||||
|
```
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <sprinter.h>
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
puts("Hello, Sprinter!");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Горизонтальная линия
|
||||||
|
|
||||||
|
Три или более символов `---`, `***` или `___` на отдельной строке:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Технические характеристики
|
||||||
|
|
||||||
|
- **Платформа:** Sprinter, процессор Z80 @ 21 МГц
|
||||||
|
- **Кодировка:** CP866 (DOS Cyrillic)
|
||||||
|
- **Максимальный размер файла:** 128 КБ
|
||||||
|
- **Максимальное число строк в индексе:** 16 384
|
||||||
|
- **Режим памяти:** small
|
||||||
|
- Код программы, cтек, данные, куча — окнa W1-W2 (32 КБ, адреса 0x4000–0xBFFF).
|
||||||
|
- Буфер файла — страницы EMM, отображаемые в W3 (0xC000–0xFFFF)
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
1. **Увеличение размера документов.** Снять лимит 128 КБ: Достаточно
|
||||||
|
разрешить работать с большим кол-вом страниц памяти, пока оттестированно
|
||||||
|
на работе с 8-мю страницами по 16Кб.
|
||||||
|
|
||||||
|
2. **Форматированные таблицы.** Разбирать строки вида `| ячейка | ячейка |`
|
||||||
|
с автоматическим выравниванием столбцов и отрисовкой разделительных
|
||||||
|
линий (строки `|---|---|`). На текущий момент таблицы отображаются
|
||||||
|
как обычные nowrap-строки без выравнивания.
|
||||||
|
|
||||||
|
3. **Поддержка кодировок CP1251 и UTF-8.** Автоопределение кодировки
|
||||||
|
по BOM, явное указание через аргумент командной строки (`--encoding cp1251`),
|
||||||
|
возможность переключения кодировки во время просмотра (`F8`).
|
||||||
|
Нужно прежде всего для документов на русском языке; CP866 кодировака уже поддерживается.
|
||||||
|
|
||||||
|
4. **Ускорение рендеринга.** Кэш строк экрана. Оптимизация цикла вывода
|
||||||
|
символов через BIOS WRCHAR (пакетный вывод, DMA).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*MDView v0.2 · (c) 2026 Петров А.Г.*
|
||||||
@@ -0,0 +1,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
|
||||||
|
План реализуется одним агентом (последовательно в одном файле), параллелизм не требуется. Дочерние агенты не используются.
|
||||||
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
+40
-16
@@ -22,25 +22,49 @@ BUILD := $(PROJ_ROOT)/lib/build
|
|||||||
|
|
||||||
# All libc C modules.
|
# All libc C modules.
|
||||||
LIBC_C := \
|
LIBC_C := \
|
||||||
libc/io/atexit.c libc/io/conio.c libc/io/cprintf.c libc/io/dir.c \
|
libc/sys/atexit.c \
|
||||||
libc/io/videomode_raw.c \
|
libc/conio/conio.c \
|
||||||
libc/io/_errno_set.c \
|
libc/conio/cprintf.c \
|
||||||
libc/io/env.c libc/io/errno.c libc/io/fsdir.c \
|
libc/conio/text_palette.c \
|
||||||
libc/io/lseek.c libc/io/mouse.c libc/io/open.c \
|
libc/bios/text.c \
|
||||||
libc/io/read.c libc/io/sleep.c \
|
libc/io/dir.c \
|
||||||
libc/io/time.c libc/io/posix_time.c libc/io/unlink.c \
|
libc/video/videomode_raw.c \
|
||||||
|
libc/video/palette.c \
|
||||||
|
libc/errno/_errno_set.c \
|
||||||
|
libc/env/env.c \
|
||||||
|
libc/errno/errno.c \
|
||||||
|
libc/io/fsdir.c \
|
||||||
|
libc/io/lseek.c \
|
||||||
|
libc/mouse/mouse.c \
|
||||||
|
libc/io/open.c \
|
||||||
|
libc/io/read.c \
|
||||||
|
libc/time/sleep.c \
|
||||||
|
libc/time/time.c \
|
||||||
|
libc/time/posix_time.c \
|
||||||
|
libc/io/unlink.c \
|
||||||
libc/io/stat.c \
|
libc/io/stat.c \
|
||||||
libc/mem/bank_io_w3.c libc/mem/bank_io_w1.c libc/mem/mem_alloc.c \
|
libc/mem/bank_io_w3.c \
|
||||||
libc/gfx/gfx_core.c libc/gfx/gfx_raw_common.c \
|
libc/mem/bank_io_w1.c \
|
||||||
libc/gfx/gfx_raw_256.c libc/gfx/gfx_raw_16.c \
|
libc/mem/mem_estex.c \
|
||||||
libc/gfx/gfx_256.c libc/gfx/gfx_16.c \
|
libc/mem/mem_bios.c \
|
||||||
libc/gfx/gfx_font.c libc/gfx/gfx_text_256.c \
|
libc/gfx/gfx_core.c \
|
||||||
|
libc/gfx/gfx_palette.c \
|
||||||
|
libc/gfx/gfx_raw_common.c \
|
||||||
|
libc/gfx/gfx_raw_256.c \
|
||||||
|
libc/gfx/gfx_raw_16.c \
|
||||||
|
libc/gfx/gfx_256.c \
|
||||||
|
libc/gfx/gfx_16.c \
|
||||||
|
libc/gfx/gfx_font.c \
|
||||||
|
libc/gfx/gfx_text_256.c \
|
||||||
libc/gfx/gfx_text_16.c \
|
libc/gfx/gfx_text_16.c \
|
||||||
libc/stdio/getchar.c \
|
libc/stdio/getchar.c \
|
||||||
libc/stdio/putchar.c libc/stdio/puts.c libc/stdio/file.c \
|
libc/stdio/putchar.c \
|
||||||
libc/stdio/hex_print.c libc/stdio/dec_print.c \
|
libc/stdio/puts.c \
|
||||||
libc/stdio/solid_helpers.c \
|
libc/file/file.c \
|
||||||
libc/io/solid_compat.c
|
libc/stdio/hex_print.c \
|
||||||
|
libc/stdio/dec_print.c \
|
||||||
|
libc/string/strlwr.c \
|
||||||
|
libc/string/strupr.c
|
||||||
|
|
||||||
# Runtime modules to bundle (pulled by symbol references from libc-using code).
|
# Runtime modules to bundle (pulled by symbol references from libc-using code).
|
||||||
# NOTE: runtime/bank.s is NOT bundled — its trampoline depends on the banking
|
# NOTE: runtime/bank.s is NOT bundled — its trampoline depends on the banking
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -58,6 +66,30 @@ char getche(void) __naked
|
|||||||
__endasm;
|
__endasm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* getkey — like getch() but exposes BOTH the ASCII value and the
|
||||||
|
* positional scan code, so callers can distinguish extended keys
|
||||||
|
* (arrows, F1..F12, PgUp/PgDn, Home/End, Ins/Del — all of which carry
|
||||||
|
* ASCII == 0 from ESTEX) from plain ASCII keys.
|
||||||
|
*
|
||||||
|
* return = (scan << 8) | ascii
|
||||||
|
*
|
||||||
|
* For plain keys: ascii holds the character, scan holds the positional
|
||||||
|
* code (bit 7 set when Ctrl/Alt/Shift is held).
|
||||||
|
* For extended keys: ascii == 0, scan identifies the key (see KEY_* in
|
||||||
|
* <conio.h>).
|
||||||
|
*/
|
||||||
|
uint16_t getkey(void) __naked
|
||||||
|
{
|
||||||
|
__asm
|
||||||
|
push ix
|
||||||
|
ld c, #0x30 ; ESTEX WAITKEY: A=ASCII, D=scan, E=ASCII
|
||||||
|
rst #0x10
|
||||||
|
pop ix
|
||||||
|
ld e, a ; E = ASCII (defensive: A is the canonical copy)
|
||||||
|
ret ; __sdcccall(1) returns uint16_t in DE
|
||||||
|
__endasm;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- putch / cputs: Turbo-C conio convention ---------------------- *
|
/* ---- putch / cputs: Turbo-C conio convention ---------------------- *
|
||||||
* Both APPLY the current text attribute (g_text_attr). When attr is
|
* Both APPLY the current text attribute (g_text_attr). When attr is
|
||||||
* KEEP_EXIST_ATTR (>0xFF), they short-circuit to the FAST stdio path
|
* KEEP_EXIST_ATTR (>0xFF), they short-circuit to the FAST stdio path
|
||||||
@@ -67,10 +99,6 @@ char getche(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);
|
||||||
@@ -80,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; }
|
||||||
@@ -91,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;
|
||||||
}
|
}
|
||||||
@@ -107,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;
|
||||||
}
|
}
|
||||||
@@ -159,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
|
||||||
@@ -167,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
|
||||||
@@ -177,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
|
||||||
@@ -198,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
|
||||||
@@ -259,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 --------------------------------------- *
|
||||||
*
|
*
|
||||||
@@ -282,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;
|
||||||
@@ -289,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;
|
||||||
@@ -332,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;
|
||||||
@@ -415,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;
|
||||||
}
|
}
|
||||||
@@ -429,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;
|
||||||
}
|
}
|
||||||
@@ -441,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;
|
||||||
}
|
}
|
||||||
@@ -454,14 +470,48 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* wrchar(uint8_t x, uint8_t y, char ch, uint8_t attr)
|
||||||
|
*
|
||||||
|
* SDCC __sdcccall(1): x in A, y in L (2 uint8 → A, L); ch and attr
|
||||||
|
* packed and pushed on the stack as a single 16-bit value (caller does
|
||||||
|
* `ld hl, #(attr<<8)|ch; push hl`). Layout after CALL:
|
||||||
|
* [SP+0..1] = return address
|
||||||
|
* [SP+2] = ch (low half of pushed pair)
|
||||||
|
* [SP+3] = attr (high half)
|
||||||
|
* Void return → callee-pops the 2 stack-arg bytes via `pop bc` + jp (iy).
|
||||||
|
*/
|
||||||
|
void scroll(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t direction, uint8_t clear) __naked
|
||||||
|
{
|
||||||
|
(void)x; (void)y; (void)w; (void)h; (void)direction; (void)clear;
|
||||||
|
__asm
|
||||||
|
pop iy ; return address
|
||||||
|
ld d, l ; D = row (y)
|
||||||
|
ld e, a ; E = col (x)
|
||||||
|
pop hl ; H = heigth(h), L = width(w)
|
||||||
|
pop bc ; C = direction, B = clear
|
||||||
|
ld a, b ; A = clear(B)
|
||||||
|
ld b, c ; B = direction(C)
|
||||||
|
push ix
|
||||||
|
ld c, #0x55 ; ESTEX SCROLL
|
||||||
|
rst #0x10
|
||||||
|
pop ix
|
||||||
|
jp (iy)
|
||||||
|
__endasm;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* wrchar(uint8_t x, uint8_t y, char ch, uint8_t attr)
|
/* wrchar(uint8_t x, uint8_t y, char ch, uint8_t attr)
|
||||||
*
|
*
|
||||||
@@ -515,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. */
|
||||||
@@ -538,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 --------------------------------
|
||||||
@@ -558,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);
|
||||||
|
}
|
||||||
Vendored
@@ -143,7 +143,8 @@ int fputc(int c, FILE *fp)
|
|||||||
{
|
{
|
||||||
if (!fp) { errno = EBADF; return EOF; }
|
if (!fp) { errno = EBADF; return EOF; }
|
||||||
if (fp->flags & _F_CONOUT) {
|
if (fp->flags & _F_CONOUT) {
|
||||||
return putchar(c);
|
putchar(c);
|
||||||
|
return (int)c;
|
||||||
}
|
}
|
||||||
if (!(fp->flags & _F_WRITE)) { errno = EBADF; return EOF; }
|
if (!(fp->flags & _F_WRITE)) { errno = EBADF; return EOF; }
|
||||||
uint8_t ch = (uint8_t)c;
|
uint8_t ch = (uint8_t)c;
|
||||||
@@ -175,7 +176,7 @@ int fputs(const char *s, FILE *fp)
|
|||||||
if (!fp || !s) { errno = EBADF; return EOF; }
|
if (!fp || !s) { errno = EBADF; return EOF; }
|
||||||
if (fp->flags & _F_CONOUT) {
|
if (fp->flags & _F_CONOUT) {
|
||||||
while (*s) {
|
while (*s) {
|
||||||
if (putchar((unsigned char)*s++) == EOF) return EOF;
|
putchar((unsigned char)*s++);;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -239,7 +240,7 @@ size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *fp)
|
|||||||
const char *p = (const char *)ptr;
|
const char *p = (const char *)ptr;
|
||||||
size_t total = size * nmemb;
|
size_t total = size * nmemb;
|
||||||
for (size_t i = 0; i < total; i++) {
|
for (size_t i = 0; i < total; i++) {
|
||||||
if (putchar((unsigned char)p[i]) == EOF) return i / size;
|
putchar((unsigned char)p[i]);
|
||||||
}
|
}
|
||||||
return nmemb;
|
return nmemb;
|
||||||
}
|
}
|
||||||
+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
|
||||||
+100
-6
@@ -38,12 +38,60 @@
|
|||||||
char kbhit (void);
|
char kbhit (void);
|
||||||
char getch (void);
|
char getch (void);
|
||||||
char getche(void);
|
char getche(void);
|
||||||
|
|
||||||
|
/* Extended-key reader. Returns (scan << 8) | ascii. Plain ASCII keys
|
||||||
|
* have ascii in the low byte; extended keys (arrows / F1..F12 /
|
||||||
|
* PgUp/PgDn / Home / End / Ins / Del) carry ascii == 0 and the
|
||||||
|
* KEY_* code in the high byte. */
|
||||||
|
uint16_t getkey(void);
|
||||||
|
|
||||||
|
/* Scan codes returned in the high byte of getkey() when the low byte
|
||||||
|
* (ASCII) is 0. Empirically verified in MAME — the ProgrammerManual.txt
|
||||||
|
* "positional code" column is misleading; BIOS returns IBM-style codes
|
||||||
|
* for the F-keys and a "5N + numpad-position" pattern for the cursor /
|
||||||
|
* editing keys.
|
||||||
|
*
|
||||||
|
* Verified 2026-06-04 by reading raw getkey() output. */
|
||||||
|
#define KEY_F1 0x3B
|
||||||
|
#define KEY_F2 0x3C
|
||||||
|
#define KEY_F3 0x3D
|
||||||
|
#define KEY_F4 0x3E
|
||||||
|
#define KEY_F5 0x3F
|
||||||
|
#define KEY_F6 0x40
|
||||||
|
#define KEY_F7 0x41
|
||||||
|
#define KEY_F8 0x42
|
||||||
|
#define KEY_F9 0x43
|
||||||
|
#define KEY_F10 0x44
|
||||||
|
#define KEY_F11 0x45 /* not verified */
|
||||||
|
#define KEY_F12 0x46 /* not verified */
|
||||||
|
#define KEY_END 0x51
|
||||||
|
#define KEY_DOWN 0x52
|
||||||
|
#define KEY_PGDN 0x53
|
||||||
|
#define KEY_LEFT 0x54
|
||||||
|
#define KEY_RIGHT 0x56
|
||||||
|
#define KEY_HOME 0x57
|
||||||
|
#define KEY_UP 0x58
|
||||||
|
#define KEY_PGUP 0x59
|
||||||
|
#define KEY_INS 0x50 /* numpad 0; not verified */
|
||||||
|
#define KEY_DEL 0x55 /* numpad 5/.; not verified */
|
||||||
|
|
||||||
|
typedef union {
|
||||||
|
uint16_t value;
|
||||||
|
struct {
|
||||||
|
uint8_t low;
|
||||||
|
uint8_t high;
|
||||||
|
} byte;
|
||||||
|
} two_bytes;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
char putch (char c);
|
char 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)
|
||||||
@@ -63,6 +111,8 @@ uint8_t wherex (void);
|
|||||||
uint8_t wherey (void);
|
uint8_t wherey (void);
|
||||||
uint16_t wherexy(void); // high byte = Y, low byte = X coords.
|
uint16_t wherexy(void); // high byte = Y, low byte = X coords.
|
||||||
|
|
||||||
|
void scroll(uint8_t, uint8_t, uint8_t, uint8_t, uint8_t, uint8_t);
|
||||||
|
|
||||||
/* Direct character/attribute screen access (ESTEX $57 / $58).
|
/* Direct character/attribute screen access (ESTEX $57 / $58).
|
||||||
* wrchar — write char + attribute at (x, y); does NOT advance the cursor
|
* wrchar — write char + attribute at (x, y); does NOT advance the cursor
|
||||||
* and does NOT interpret control characters. Useful for
|
* and does NOT interpret control characters. Useful for
|
||||||
@@ -86,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).
|
||||||
@@ -143,12 +193,56 @@ uint8_t get_putch_raw_mode(void);
|
|||||||
* Colour order is standard CGA / Borland-conio.h. Constants 0..7 are
|
* Colour order is standard CGA / Borland-conio.h. Constants 0..7 are
|
||||||
* usable for both fg and bg; 8..15 are foreground-only. */
|
* usable for both fg and bg; 8..15 are foreground-only. */
|
||||||
enum {
|
enum {
|
||||||
COLOR_BLACK = 0, COLOR_BLUE, COLOR_GREEN, COLOR_CYAN,
|
COLOR_BLACK = 0,
|
||||||
COLOR_RED, COLOR_MAGENTA, COLOR_BROWN, COLOR_LIGHTGRAY,
|
COLOR_BLUE,
|
||||||
COLOR_DARKGRAY, COLOR_LIGHTBLUE, COLOR_LIGHTGREEN, COLOR_LIGHTCYAN,
|
COLOR_GREEN,
|
||||||
COLOR_LIGHTRED, COLOR_LIGHTMAGENTA, COLOR_YELLOW, COLOR_WHITE
|
COLOR_CYAN,
|
||||||
|
COLOR_RED,
|
||||||
|
COLOR_MAGENTA,
|
||||||
|
COLOR_BROWN,
|
||||||
|
COLOR_LIGHTGRAY,
|
||||||
|
COLOR_DARKGRAY,
|
||||||
|
COLOR_LIGHTBLUE,
|
||||||
|
COLOR_LIGHTGREEN,
|
||||||
|
COLOR_LIGHTCYAN,
|
||||||
|
COLOR_LIGHTRED,
|
||||||
|
COLOR_LIGHTMAGENTA,
|
||||||
|
COLOR_YELLOW,
|
||||||
|
COLOR_WHITE
|
||||||
};
|
};
|
||||||
#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
|
||||||
|
|||||||
+15
-7
@@ -16,14 +16,22 @@
|
|||||||
#ifndef FCNTL_H
|
#ifndef FCNTL_H
|
||||||
#define FCNTL_H
|
#define FCNTL_H
|
||||||
|
|
||||||
#define O_RDONLY 0
|
#ifndef _STD_SEEK_
|
||||||
#define O_WRONLY 1
|
#define _STD_SEEK_
|
||||||
#define O_RDWR 2
|
/* constants to be used as 3rd argument for "fseek" function */
|
||||||
|
#define SEEK_SET 0
|
||||||
|
#define SEEK_CUR 1
|
||||||
|
#define SEEK_END 2
|
||||||
|
#endif
|
||||||
|
|
||||||
#define O_CREAT 0x040
|
/* Definition "open flags" */
|
||||||
#define O_EXCL 0x080
|
#define O_WRONLY 0x01 /* 0 file write only */
|
||||||
#define O_TRUNC 0x200
|
#define O_RDONLY 0x02 /* 1 file read only */
|
||||||
#define O_APPEND 0x400
|
#define O_RDWR 0x03 /* 1,0 file read/write */
|
||||||
|
#define O_TRUNC 0x04 /* 2 open with truncation */
|
||||||
|
#define O_CREAT 0x08 /* 3 create and open file */
|
||||||
|
#define O_EXCL 0x10 /* 4 exclusive open */
|
||||||
|
#define O_APPEND 0x20 /* 5 to end of file */
|
||||||
|
|
||||||
int open (const char *path, int flags);
|
int open (const char *path, int flags);
|
||||||
int creat(const char *path, int mode); /* mode arg ignored on Sprinter */
|
int creat(const char *path, int mode); /* mode arg ignored on Sprinter */
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ int vprintf(const char *, va_list);
|
|||||||
int vsprintf(char *, const char *, va_list);
|
int vsprintf(char *, const char *, va_list);
|
||||||
|
|
||||||
/* puts / putchar / getchar — overridden by our libc to use ESTEX. */
|
/* puts / putchar / getchar — overridden by our libc to use ESTEX. */
|
||||||
int puts (const char *);
|
char puts (const char *);
|
||||||
int putchar(int);
|
int putchar(int);
|
||||||
int getchar(void);
|
int getchar(void);
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
/*
|
|
||||||
* solid_compat.c — Solid-C compatibility helpers that need real code
|
|
||||||
* (rather than just header macros).
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <sprinter_compat.h>
|
|
||||||
#include <ctype.h>
|
|
||||||
|
|
||||||
char *strlwr(char *s)
|
|
||||||
{
|
|
||||||
char *p = s;
|
|
||||||
while (*p) {
|
|
||||||
if (*p >= 'A' && *p <= 'Z') *p += 'a' - 'A';
|
|
||||||
p++;
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
char *strupr(char *s)
|
|
||||||
{
|
|
||||||
char *p = s;
|
|
||||||
while (*p) {
|
|
||||||
if (*p >= 'a' && *p <= 'z') *p -= 'a' - 'A';
|
|
||||||
p++;
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* div() comes from SDCC's z80.lib. */
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,13 +17,7 @@ int getchar(void) __naked
|
|||||||
ld c, #0x30 ; ESTEX WAITKEY
|
ld c, #0x30 ; ESTEX WAITKEY
|
||||||
rst #0x10
|
rst #0x10
|
||||||
pop ix
|
pop ix
|
||||||
ld a, e ; E = ASCII (already the low byte of our return DE)
|
|
||||||
or a, a
|
|
||||||
jr Z, no_ascii
|
|
||||||
ld d, #0
|
ld d, #0
|
||||||
ret
|
ret
|
||||||
no_ascii:
|
|
||||||
ld de, #-1
|
|
||||||
ret
|
|
||||||
__endasm;
|
__endasm;
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-14
@@ -19,23 +19,15 @@ int putchar(int c) __naked
|
|||||||
{
|
{
|
||||||
(void)c;
|
(void)c;
|
||||||
__asm
|
__asm
|
||||||
ld a, l ; SDCC __sdcccall(1) int → HL
|
ld a, l
|
||||||
push ix
|
|
||||||
cp #0x0A
|
cp #0x0A
|
||||||
jr nz, _pc_emit
|
jr nz, cputc
|
||||||
ld a, #0x0D ; CR before LF
|
call cputc
|
||||||
push af
|
ld a, #0x0D
|
||||||
|
cputc:
|
||||||
ld c, #0x5B
|
ld c, #0x5B
|
||||||
rst #0x10
|
rst #0x10
|
||||||
pop af
|
ld e, l
|
||||||
ld a, #0x0A
|
|
||||||
_pc_emit:
|
|
||||||
push af
|
|
||||||
ld c, #0x5B
|
|
||||||
rst #0x10
|
|
||||||
pop af
|
|
||||||
pop ix
|
|
||||||
ld e, a
|
|
||||||
ld d, #0
|
ld d, #0
|
||||||
ret
|
ret
|
||||||
__endasm;
|
__endasm;
|
||||||
|
|||||||
+21
-34
@@ -14,17 +14,11 @@
|
|||||||
* - Avoid trailing PUTCHAR after PCHARS — empirically that sometimes
|
* - Avoid trailing PUTCHAR after PCHARS — empirically that sometimes
|
||||||
* drops the next char. Embed the line ending inside the PCHARS
|
* drops the next char. Embed the line ending inside the PCHARS
|
||||||
* buffer instead.
|
* buffer instead.
|
||||||
* - Strings longer than the buffer fall back to per-char putchar so
|
|
||||||
* we never silently truncate.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
#define PUTS_BUF_SIZE 256 /* body bytes before CR expansion */
|
|
||||||
|
|
||||||
static char puts_buf[PUTS_BUF_SIZE + 3]; /* +3 for trailing CR LF NUL */
|
|
||||||
|
|
||||||
static void pchars(const char *s) __naked
|
static void pchars(const char *s) __naked
|
||||||
{
|
{
|
||||||
(void)s;
|
(void)s;
|
||||||
@@ -37,33 +31,26 @@ static void pchars(const char *s) __naked
|
|||||||
__endasm;
|
__endasm;
|
||||||
}
|
}
|
||||||
|
|
||||||
int puts(const char *s)
|
char puts(const char *s) __naked
|
||||||
{
|
{
|
||||||
uint16_t n = 0;
|
(void)s;
|
||||||
uint16_t i = 0;
|
__asm
|
||||||
|
puts_:
|
||||||
while (s[i] && n < PUTS_BUF_SIZE - 1) {
|
ld a, (hl)
|
||||||
char c = s[i++];
|
or a
|
||||||
if (c == '\n') {
|
jr z, fin_
|
||||||
puts_buf[n++] = '\r';
|
push hl
|
||||||
puts_buf[n++] = '\n';
|
ld l, a
|
||||||
} else {
|
ld h, #0
|
||||||
puts_buf[n++] = c;
|
call _putchar
|
||||||
}
|
pop hl
|
||||||
}
|
inc hl
|
||||||
|
jp puts_
|
||||||
if (s[i]) {
|
;
|
||||||
/* Overflow — char-by-char fallback so we never truncate. */
|
fin_:
|
||||||
for (uint16_t k = 0; s[k]; k++)
|
ld l, #0x0A
|
||||||
putchar((unsigned char)s[k]);
|
ld h, #0
|
||||||
putchar('\n');
|
call _putchar
|
||||||
return 0;
|
ret
|
||||||
}
|
__endasm;
|
||||||
|
|
||||||
puts_buf[n++] = '\r';
|
|
||||||
puts_buf[n++] = '\n';
|
|
||||||
puts_buf[n] = 0;
|
|
||||||
|
|
||||||
pchars(puts_buf);
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
/*
|
|
||||||
* solid_helpers.c — small Solid-C compatibility helpers.
|
|
||||||
*
|
|
||||||
* dec8 / dec16 / dec32 / hex8 / hex16 / hex32 are in dec_hex.c (compact
|
|
||||||
* asm port from solid-c's STDLIB.ASM, ~150 bytes total — vs ~3-5 KB if
|
|
||||||
* routed through printf). This file now only holds gets().
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <stdio.h>
|
|
||||||
|
|
||||||
/* ---- gets — dangerous but Solid-C provides it ---------------------- */
|
|
||||||
char *gets(char *buf)
|
|
||||||
{
|
|
||||||
int i = 0;
|
|
||||||
int c;
|
|
||||||
for (;;) {
|
|
||||||
c = getchar();
|
|
||||||
if (c == EOF) {
|
|
||||||
if (i == 0) return 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (c == '\n' || c == '\r') break;
|
|
||||||
buf[i++] = (char)c;
|
|
||||||
}
|
|
||||||
buf[i] = 0;
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* solid_compat.c — Solid-C compatibility helpers that need real code
|
||||||
|
* (rather than just header macros).
|
||||||
|
*
|
||||||
|
* CP866 Cyrillic support: strlwr/strupr handle uppercase/lowercase
|
||||||
|
* conversion for both Latin and Cyrillic characters in CP866 code page
|
||||||
|
* (bytes 0x80–0xFF).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <sprinter_compat.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
|
||||||
|
char *strlwr(char *s)
|
||||||
|
{
|
||||||
|
char *p = s;
|
||||||
|
while (*p) {
|
||||||
|
if ((*p >= 'A' && *p <= 'Z') || (*p >= 0x80 && *p <= 0x8F)) *p += 'a' - 'A';
|
||||||
|
else if ((*p >= 0x90 && *p <= 0x9F)) *p += 0x50;
|
||||||
|
else if ((*p == 0xF0)) *p = 0xF1;
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* solid_compat.c — Solid-C compatibility helpers that need real code
|
||||||
|
* (rather than just header macros).
|
||||||
|
*
|
||||||
|
* CP866 Cyrillic support: strlwr/strupr handle uppercase/lowercase
|
||||||
|
* conversion for both Latin and Cyrillic characters in CP866 code page
|
||||||
|
* (bytes 0x80–0xFF).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <sprinter_compat.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
|
||||||
|
char *strupr(char *s)
|
||||||
|
{
|
||||||
|
char *p = s;
|
||||||
|
while (*p) {
|
||||||
|
if ((*p >= 'a' && *p <= 'z') || (*p >= 0xA0 && *p <= 0xAF)) *p -= 'a' - 'A';
|
||||||
|
else if ((*p >= 0xE0 && *p <= 0xEF)) *p -= 0x50;
|
||||||
|
else if ((*p == 0xF1)) *p = 0xF0;
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||||
EXAMPLE := argv
|
EXAMPLE := argv
|
||||||
include $(PROJ_ROOT)/examples/example.mk
|
include $(PROJ_ROOT)/app.mk
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||||
EXAMPLE := assrtest
|
EXAMPLE := assrtest
|
||||||
include $(PROJ_ROOT)/examples/example.mk
|
include $(PROJ_ROOT)/app.mk
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||||
EXAMPLE := attrprob
|
EXAMPLE := attrprob
|
||||||
include $(PROJ_ROOT)/examples/example.mk
|
include $(PROJ_ROOT)/app.mk
|
||||||
@@ -8,4 +8,4 @@ PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
|||||||
EXAMPLE := banked
|
EXAMPLE := banked
|
||||||
MEMORY := huge
|
MEMORY := huge
|
||||||
EXTRA_FLAGS := --bank 1=bank1.c --bank 2=bank2.c
|
EXTRA_FLAGS := --bank 1=bank1.c --bank 2=bank2.c
|
||||||
include $(PROJ_ROOT)/examples/example.mk
|
include $(PROJ_ROOT)/app.mk
|
||||||
@@ -9,4 +9,4 @@ PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
|||||||
EXAMPLE := bankedbg
|
EXAMPLE := bankedbg
|
||||||
MEMORY := big
|
MEMORY := big
|
||||||
EXTRA_FLAGS := --bank 1=bank1.c --bank 2=bank2.c
|
EXTRA_FLAGS := --bank 1=bank1.c --bank 2=bank2.c
|
||||||
include $(PROJ_ROOT)/examples/example.mk
|
include $(PROJ_ROOT)/app.mk
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -9,4 +9,4 @@ PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
|||||||
EXAMPLE := banklocl
|
EXAMPLE := banklocl
|
||||||
MEMORY := huge
|
MEMORY := huge
|
||||||
EXTRA_FLAGS := --bank 1=bank1.c --mkexe -p --mkexe 0
|
EXTRA_FLAGS := --bank 1=bank1.c --mkexe -p --mkexe 0
|
||||||
include $(PROJ_ROOT)/examples/example.mk
|
include $(PROJ_ROOT)/app.mk
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -3,4 +3,4 @@
|
|||||||
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||||
EXAMPLE := cat
|
EXAMPLE := cat
|
||||||
EXTRA_DATA := test.txt
|
EXTRA_DATA := test.txt
|
||||||
include $(PROJ_ROOT)/examples/example.mk
|
include $(PROJ_ROOT)/app.mk
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||||
EXAMPLE := conio
|
EXAMPLE := conio
|
||||||
include $(PROJ_ROOT)/examples/example.mk
|
include $(PROJ_ROOT)/app.mk
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||||
EXAMPLE := conio2
|
EXAMPLE := conio2
|
||||||
include $(PROJ_ROOT)/examples/example.mk
|
include $(PROJ_ROOT)/app.mk
|
||||||
@@ -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();
|
||||||
|
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||||
EXAMPLE := dec_test
|
EXAMPLE := dec_test
|
||||||
include $(PROJ_ROOT)/examples/example.mk
|
include $(PROJ_ROOT)/app.mk
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||||
EXAMPLE := errno
|
EXAMPLE := errno
|
||||||
include $(PROJ_ROOT)/examples/example.mk
|
include $(PROJ_ROOT)/app.mk
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||||
EXAMPLE := filetest
|
EXAMPLE := filetest
|
||||||
include $(PROJ_ROOT)/examples/example.mk
|
include $(PROJ_ROOT)/app.mk
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Build cat.exe — uses lib/sprinter.lib in TINY memory mode.
|
||||||
|
|
||||||
|
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||||
|
EXAMPLE := gets
|
||||||
|
include $(PROJ_ROOT)/app.mk
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
char buff [256];
|
||||||
|
|
||||||
|
puts("--- gets test ---");
|
||||||
|
|
||||||
|
gets(buff);
|
||||||
|
|
||||||
|
puts("");
|
||||||
|
puts("loaded string:");
|
||||||
|
puts(buff);
|
||||||
|
puts("done");
|
||||||
|
(void)getchar();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||||
EXAMPLE := gfx_d16
|
EXAMPLE := gfx_d16
|
||||||
include $(PROJ_ROOT)/examples/example.mk
|
include $(PROJ_ROOT)/app.mk
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||||
EXAMPLE := gfx_dbuf
|
EXAMPLE := gfx_dbuf
|
||||||
include $(PROJ_ROOT)/examples/example.mk
|
include $(PROJ_ROOT)/app.mk
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user