# Platform Reference — особенности разработки под Sprinter

Документ собирает все нетривиальные нюансы платформы Sprinter и нашего
C-toolchain'а, накопленные в процессе разработки.  Если вы пишете
программу и что-то "молча не работает" — скорее всего ответ здесь.

---

## 1. Архитектура Sprinter (cheat-sheet)

* **CPU:** Z84C15 (Z80 совместимый), 21 МГц / 3.5 МГц.
* **Адресное пространство:** 4 окна по 16 КБ:
  * **W0** (0x0000..0x3FFF) — ESTEX DSS система
  * **W1** (0x4000..0x7FFF) — обычно HOME-программа
  * **W2** (0x8000..0xBFFF) — обычно данные / стек
  * **W3** (0xC000..0xFFFF) — обычно banked / видео
* **Порты page-select:** 0x82 / 0xA2 / 0xC2 / 0xE2 для W0..W3 соответственно.
  Запись номера страницы переключает окно.  **Чтение** — возвращает текущий
  номер страницы (полезно для детектирования).
* **Системные вызовы:**
  * `RST 10h` — ESTEX DSS, номер функции в C
  * `RST 8`   — BIOS, номер функции в C
  * `RST 30h` — Mouse driver, номер функции в C
* **Видеорежимы:** 320×256×256, 640×256×16, 80×32 text, 40×32 text.
* **Формат EXE:** SprintEXE (512-байтный header + образ HOME + опциональные банки).
* **Fast RAM («КЭШ-ОЗУ»):** 64 КБ SRAM, процессор работает без wait-state.  В
  toolchain'е **не используется** — несовместима с акселератором, перекрывает
  окно 0 (DSS), не переживает вызовы DSS/BIOS.  Подробности и паттерн работы —
  `docs/fast_ram.md`.

---

## 2. Подводные камни вызовов системы

### IX **обязательно** сохранять

ESTEX и BIOS клобберят IX без предупреждения.  SDCC использует IX как
frame pointer.  Каждая обёртка над `RST 10h` / `RST 8` / `RST 30h`
ДОЛЖНА оборачивать вызов в `push ix` / `pop ix`:

```asm
push  ix
ld    c, #0x47          ; ESTEX APPINFO
rst   #0x10
pop   ix
```

Забудешь — frame pointer уедет, локальные переменные станут мусором,
debug будет долгим.

### BIOS требует стек в W2

BIOS-вызовы (`RST 8` / `CALL 3D13h`) требуют SP в диапазоне 0x8000..0xBFFF
(W2).  ESTEX-вызовы — нет (они используют свой стек).

Практическое следствие: в `crt0_small.s` нельзя выделять W2 через
`BIOS $C4 EMM_GETPAGE + OUT (0xC2)`, потому что на этот момент стек
ещё в W1.  Делается через **ESTEX `$3A SETWIN2`** — он маппит страницу
сразу из ESTEX без BIOS.

### ESTEX `$46 ENV` — баг в документации

`DiskSyscalls.txt v1.6` пишет: `A=0 — FOUND, A=1 — NOT FOUND`.  Реально
наоборот: **A=0 — NOT FOUND**.  Все наши getenv/putenv учитывают это.

### ESTEX `$21 SYSTIME` — день недели 1-based

`dow` (day-of-week) возвращается как `1..7` где `1 = Sunday`, `7 = Saturday`.
Если код ждёт `0..6` или `1..7` начиная с Monday — будет смещение.

### ESTEX `$19 F_FIRST` и каталоги "." / ".."

Запись с именем `"."` или `".."` для родительского каталога **не возвращается**
функцией F_FIRST при поиске `"*.*"` в подкаталоге.  Если они нужны —
итерироваться через явные `"."` и `".."` запросы.

### `puts()` после `PCHARS` иногда теряет следующий байт

ESTEX `$5C PCHARS` после себя оставляет cursor state в котором
**следующий** `$5B PUTCHAR` может быть проигнорирован.  Решение:
если нужна новая строка после PCHARS — встроить `\r\n` ВНУТРИ строки
для PCHARS, не вызывать отдельный PUTCHAR.  Наш `puts()` так и делает.

---

## 3. SDCC ABI — нетривиальные моменты

### `__sdcccall(1)` — смешанная схема передачи

* **1-й 16-битный аргумент** → HL
* **2-й 16-битный аргумент** → DE
* **3-й и далее** → стек
* **1-й uint8_t / char** → A (не L!)
* **Long-аргументы на стеке** → caller pops
* **Int-аргументы на стеке** → callee pops

### Возврат значений

* `int` / `uint16_t` / `pointer` → **DE** (НЕ HL как в старых SDCC!)
* `char` / `uint8_t` → A (low byte of DE)
* `long` / `uint32_t` → DE:HL (DE=low word, HL=high word)
* `float` → DE:HL по тому же layout

Самая частая ошибка — `ld a, l` в обёртках для char-возврата.  Нужно
`ld a, e` (потому что char идёт в low byte регистра возврата = E).

### `__asm` блок клобберит DE, но SDCC об этом не знает

SDCC иногда сохраняет указатель аргумента в DE между C-кодом и
inline asm.  Если внутри `__asm` написать `ld a, d` или `ld a, e`
(например для извлечения возврата из RST), DE будет клобберн, и
post-asm код типа `c->field = ...` запишет в случайный адрес.

**Решение:** парковать указатель в static BSS перед `__asm`, после
загружать заново:

```c
static mouse_cursor_t *dest = 0;
void mouse_get_cursor(mouse_cursor_t *c) {
    dest = c;
    __asm
        ; ... clobbers DE ...
    __endasm;
    mouse_cursor_t *p = dest;  // SDCC fetches fresh from BSS
    p->width = mc_width;       // writes to correct address
}
```

### "Static без инициализатора" грабли BSS

Несколько подряд `static uint8_t x;` БЕЗ `=0` могут сколлапсировать в
**один и тот же адрес** — записи в одну стомпают другие.

```c
static uint8_t a;  // адрес 0x9100
static uint8_t b;  // ТОЖЕ адрес 0x9100!
static uint8_t c;  // ТОЖЕ 0x9100!

a = 0xAA;  b = 0xBB;  c = 0xCC;
// a == b == c == 0xCC
```

**Решение:** всегда инициализировать: `static uint8_t a = 0;` — SDCC
гарантированно резервирует разные адреса.

### z80.lib почти полная — НЕ переписывать

SDCC z80.lib содержит работающие реализации:
- `atoi / atol / atof / strtol / strtoul`
- `malloc / free / calloc / realloc` (мы только переопределили heap location)
- `qsort / bsearch / rand / srand / abs / div`
- Полный `<string.h>` (memcpy/memset/strlen/strcmp/strcpy/strchr/strstr/strtok/etc.)
- `<ctype.h>` (toupper/tolower/isalpha/isdigit/etc.)
- `<math.h>` (sinf/cosf/sqrtf/etc.)

Линкер автоматически тянет нужное из z80.lib когда есть unresolved
symbol.  Не переписывать ради переписывания.

---

## 4. Banking — нюансы

### ABI banked-вызовов

SDCC эмитит для `void f(int x) __banked`:
- символ `b_f = N` (bank id = число из `--codeseg BANKn`)
- символ `_f` = адрес внутри банка (с `bank_id` в верхнем 8-битном байте)

Вызов:
```asm
ld   hl, #arg_value
push hl
ld   e, #b_f          ; E = bank id
ld   hl, #_f          ; HL = target addr (low 16 bits)
call ___sdcc_bcall_ehl
pop  af               ; caller cleans up arg
```

### Стековый "spacer" в trampoline

Между ret-адресом callee'я и аргументами трамплин ОБЯЗАН вставить
**ровно 3 байта** (1 сохранённая страница + 2 байта внутреннего
bcall return).  SDCC компилирует доступ к аргументам с offset'ом +5
от стека.  Любая разница ломает все banked-вызовы.

### CRITICAL: `pop af; out (n), a` клобберит A

Старый trampoline восстанавливал W3-страницу через `pop af` —
**клоббит A**, а SDCC возвращает uint8_t/char именно в A.  Все
banked-функции с char-возвратом тихо теряли результат.

Текущая версия использует `pop bc; ld c, #port; out (c), b` — порт
через C, значение через B, A сохраняется нетронутым.

### Bank-local статические данные

Для модуля целиком в банке:
```sh
sdcc --codeseg BANK1 --constseg BANK1 --dataseg BANK1 -c bank1.c
```
Всё (код + const + BSS) живёт в банке.  `mkexe -p 0` нужен чтобы BSS
загружался обнулённым (иначе будет FF из padding).

Heap через `malloc()` из banked-функции работает прозрачно — heap в
W2 (HOME), W2 trampoline никогда не свопит, указатель валиден из
любого контекста.

---

## 5. Видео-режимы и графика

### Mode 0x81 (320×256×256)

* Адресация: pixel `(x, y)` → CPU-адрес `0xC000 + x` с **Port_Y (0x89) = y**
* 320 байт на видимую строку
* Палитра: 256 цветов из 4 палитр (BIOS `$A4 PIC_SET_PAL`)
* Cleanup: 0x300..0x39F = mode-descriptors, 0x3E0..0x3FF = palette данные — НЕ трогать в обычной отрисовке

### Mode 0x82 (640×256×16)

* Та же row-addressing что и 320 mode (320 байт на строку)
* НО каждый байт = 2 пикселя по 4 бита
* **HIGH nibble (биты 7-4) = LEFT пиксель** (even x)
* **LOW nibble (биты 3-0) = RIGHT пиксель** (odd x)
* Доки пишут "первыми младшие 4 бита" — это про **временной** порядок
  в FPGA-сериализаторе, **не пространственный** на экране
* Палитра: 16 нижних цветов из любой из 4 палитр

### Графический Accelerator

CPU-опкоды используются как control-сигналы (NOP-tricks):
- `LD D,D` (0x52) — "ждать LD A, imm для размера блока"
- `LD C,C` (0x49) — Fill mode (горизонталь): `LD (HL),A` заполняет N байт
- `LD E,E` (0x5B) — Fill mode (вертикаль): `LD (HL),A` заполняет N pix вертикально (авто-Y инкремент)
- `LD L,L` (0x6D) — Copy (horizontal)
- `LD A,A` (0x7F) — Copy (vertical)
- `LD B,B` (0x40) — выключить accel

**КРИТИЧНО:** размер блока должен быть **immediate операндом** `LD A, n` (опкод 0x3E nn) сразу после `LD D,D`.  Accel snoop'ит этот байт.  `LD A, (mem)` (опкод 0x3A) — другой 2-й байт, accel захватит мусор.

**КРИТИЧНО 2:** между `LD C,C` (Fill mode) и `LD (HL),A` (fire) **нельзя** ставить второй `LD A, #imm` — accel re-interpret'ит его как новый block-size.  Color через `C`/`B` регистр + `LD A, C` (опкод 0x79, 1 байт).

**Скорость:** ~7 µs/byte vs ~14-20 µs/byte ручного цикла → ~2-3× быстрее.

**Прерывания:** DI/EI обязательны вокруг accel — он подменяет систему команд CPU, ISR в это время крашит.

Реализация в `libc/gfx/gfx_lines.c` — `gfx_hline`/`gfx_vline`/`gfx_rect`/`gfx_fill_rect`.  `gfx_clear` использует accel-burst column-major (320 vfill'ов × 256 пикселей за burst, ~4× быстрее ручного цикла).

### Формат шрифта BIOS

Шрифт 2 КБ = 256 chars × 8 rows × 1 byte/row, **interleaved row-major**:
`offset = row * 256 + char_code`.

То есть row 0 всех 256 chars лежит в 0x000..0x0FF, row 1 в 0x100..0x1FF, ...
Не "char 0 в 0x000..0x007, char 1 в 0x008..0x00F"!  Наивное `font[char*8+row]` даст нечитаемую кашу.

Bit order внутри byte: **MSB-first**.  Bit 7 = крайний левый пиксель.

Получить системный шрифт: BIOS `$B8 WIN_GET_ZG`, DE = destination, читает 2 КБ.

---

## 6. Memory modes

DSS выделяет страницы RAM по размеру программы.  **Программа ≤16 КБ получает только 1 страницу.**  В остальные окна подключается "страница `0xFF`" (read = 0xFF, write игнорируется).

Из-за этого классическая раскладка "код в W1 (0x4100+), данные в W2 (0x8000+)" для маленькой программы **молча не работает** — write в W2 уходит в никуда.

Решение — наши 5 режимов:

| Mode | Layout | Trick |
|---|---|---|
| tiny | CODE + DATA в W2 | DSS гарантирует W2 (загружает в неё образ) |
| small | CODE в W1, DATA chained | crt0_small читает порт 0xC2 — если 0xFF, выделяет W2 через ESTEX `$3D`/`$3A` |
| big  | tiny + банки в W1 | crt0_banked с BANK_W1=1, trampoline свопит порт 0xA2 |
| huge | small + банки в W3 | crt0_banked с default BANK_W1=0, trampoline на port 0xE2 |

### Детектирование W2 — порт 0xC2

`IN A, (0xC2)` возвращает текущую страницу в W2.  `0xFF` = "не выделена".  Используется в crt0_small для auto-detect.

**Грабли:** в первой версии стоял `IN A, (0xA2)` — это **W1**, не W2!  Для small mode там всегда code-page (не 0xFF) → auto-detect не срабатывал, W2 не выделялась, программа крашилась.

---

## 7. Mouse driver

* Всё через `RST 30h`, номер функции в `C`.
* Координаты в **пикселях**.  Для text mode 03h (80×32) делить x/8 и y/8.
* **Sensitivity = divider** (не коэффициент): меньше = быстрее курсор.
  Документация ProgrammerManual пишет наоборот — это ошибка.
* MAME `$0E GET_SENSITIVE` возвращает 0 (stub).  Workaround: всегда
  ставить значение через `mouse_set_sensitivity()` при старте.
* MAME `$0B RETURN_CURSOR` пишет битмап в IX-буфер, но не обновляет H/L/D/E.
  `mouse_get_cursor()` вернёт width/height/hot_x/hot_y как 0 — это известное ограничение эмулятора.
* **Cursor bitmap format**: 1 byte per pixel, row-major; `0xFF = transparent`.
  Cursor живёт в отдельном видео-банке, не в 0x50 page.

### `$81 CHANGE VIDEO MODE`

При смене видеорежима — звать `mouse_video_mode_changed(new_mode)`.
**Важно:** аргумент `A = режим экрана` обязателен, в документации
указан но легко пропустить.  Без него драйвер не пересинхронизирует
координаты, и в graphics mode может остаться text-mode XOR-курсор.

---

## 8. Linker warnings — что есть и почему

`sdldz80` пишет `?ASlink-Warning-Definition of public symbol '_X' found more than once` когда наша `sprinter.lib` override'ит функцию из SDCC's `z80.lib`.

Текущие overrides:
- `_puts` — наш через PCHARS+\r\n vs SDCC стандартный
- `___sdcc_heap` — наш heap в W2 vs стандартный
- `_asctime`, `_localtime` — наш `posix_time.c` vs SDCC's `time.rel` (требует _RtcRead)

Линкер берёт **первое найденное** определение — это наши.  Warning только шум.

`sprinter-cc` отфильтровывает эти warning-блоки из вывода `sdcc` (3 строки: warning + 2 follow-up `Library:` строк).  Через `-v` всё видно.

---

## 9. Текстовый вывод — Turbo-C convention

В Sprinter нет ESTEX-функции "set persistent attribute" — только WRCHAR пишет char+attr единоразово.  Поэтому два набора функций:

| Группа | Header | Скорость | Цвет |
|---|---|---|---|
| stdio | `<stdio.h>` | Fast (~5 µs/char через PCHARS / PUTCHAR) | НЕТ — ambient |
| conio | `<conio.h>` | Slow (~50 µs/char через WRCHAR) | ДА — `g_text_attr` |

`puts` / `printf` / `putchar` — быстрые без цвета.  Цвет = whatever shell оставил.  Программа должна `clrscr_attr(attr)` если нужен конкретный default.

`cputs` / `cprintf` / `putch` — медленные с цветом.  Применяют `g_text_attr` (`textcolor`/`textbackground`/`textattr`).  При `g_text_attr == KEEP_EXIST_ATTR` (0xFFFF) — fallback на fast path.

**Cputs/putch НЕ делают `\n` → CR LF translation** (как в Turbo C).  Caller должен явно писать `"\r\n"`.  `puts` делает.

---

## 10. Прочее

### `dec/hex8/16/32` — мини-форматтеры

Solid-C-style минимальный вывод чисел без формата:
```c
hex8(0xAB);    // печатает "AB"
hex16(0xCAFE); // "CAFE"
dec16(50000);  // "50000"
```
Использовать когда не хочется тащить полный printf.

### `<sprinter_compat.h>`

Единый header который подтягивает все стандартные + добавляет Solid-C
shims: `BOOL`/`uint`/`WORD`/`f_point` types, `setmem`/`movmem`/`min`/`max`
макросы, `inp`/`outp`, `enable`/`disable`, `ms_*` mouse aliases.
Программы из Solid-C 2004 портируются с минимальными правками.

### `--debug` runtime flag

```sh
sprinter-cc --debug -o foo.exe foo.c
```
Prepend'ит `DEBUG_RT = 1` в crt0 + передаёт `-DDEBUG_RT` в SDCC.  Открывает symbol `_w2_self_allocated` (uint8_t) — runtime diagnostic кто аллоцировал W2 (0 = DSS, 1 = crt0 сам).  Полезно для troubleshooting'а в small mode.

### MAME testing workflow

```sh
make floppy             # пакует все .exe + data в mame/v306/IMG/mc.img
cd mame/v306 && ./run_mame.sh
```
Имена файлов на флопе должны быть 8.3 (FAT12).  Все примеры названы соответственно — `banked_big → bankedbg`, `seek_demo → seek`, `time_dir_test → timedir`, etc.

---

## История изменений

- 2026-06-01 — первый релиз v1.0
- 2026-06-26 — добавлен пункт о Fast RAM в cheat-sheet (детали — `docs/fast_ram.md`)
