# Управление памятью в sprinter-cc

Документ описывает модель памяти Sprinter (Sp2000), режимы памяти обёртки
`sprinter-cc` и способы размещения кода/данных: single-page, split, банки
(трамплины) и резидентный код окна W3 (`--w3`).

Всё в этом документе подтверждено сборкой и прогоном в MAME v3.06 / DSS 1.71.57
(см. `tests/w3probe`, `tests/banktest`, `tests/banklocl`).

---

## 1. Аппаратная модель памяти

Z80 видит 64 КБ, разбитые на **четыре окна по 16 КБ**. Каждое окно независимо
маппится на физическую страницу через порт-регистр страницы:

| Окно | Адреса        | Порт страницы | Назначение по умолчанию |
|------|---------------|---------------|--------------------------|
| W0   | `0x0000-0x3FFF` | `0x82`      | **DSS / BIOS** (RST-ы, системные вызовы) |
| W1   | `0x4000-0x7FFF` | `0xA2`      | приложение |
| W2   | `0x8000-0xBFFF` | `0xC2`      | приложение |
| W3   | `0xC000-0xFFFF` | `0xE2`      | приложение / графика / банки |

- Запись в порт `0x{8/A/C/E}2` меняет физ-страницу окна; чтение возвращает
  текущую страницу.
- **W0 занят DSS/BIOS** — там живут RST-обработчики (`RST #08` BIOS, `RST #10`
  ESTEX). Пока в W0 стоит системная страница, вызовы доступны; подменять W0
  нельзя без потери RST-ов.
- «Неиспользуемое» окно маппится на **специальную страницу `#FF`**: чтение даёт
  `0xFF`, запись игнорируется. Это ключевая причина «молчаливой» порчи данных —
  см. §7.

### Порты в C

`<sprinter.h>` даёт SFR-обёртки: `_io_page_w0..w3` (чтение/запись порта),
`sprinter_page_w0..w3(page)`.

---

## 2. Как DSS загружает EXE

Из документации DSS, последовательность `EXEC`:

1. Открыть exe-файл на чтение.
2. Считать префикс exe в рабочую область.
3. **Выделить блок памяти** размером под весь файл (если `loader==0`) или под
   первичный загрузчик (`loader>0`).
4. Сохранить стек.
5. **Подключить страницы** выделенного блока в окна (последовательно от окна
   адреса загрузки: W1→W2→W3…).
6. Построить префикс запуска → регистр `IX`.
7. Считать файл по адресу загрузки (смещение 16 в заголовке).
8. Закрыть exe, **если это не первичный загрузчик** (`loader==0`).
9. Установить `SP` = значение из смещения 20 (адрес стека).
10. Передать управление по адресу из смещения 18 (entry).

**Следствия, на которых стоит вся схема памяти:**

- DSS выделяет **число страниц по размеру образа**: `<16 КБ` → 1 страница,
  `16..32 КБ` → 2, `32..48 КБ` → 3. Лишние окна = страница `#FF`.
- Страницы маппятся **подряд** начиная с окна адреса загрузки. Образ, тянущийся
  `0x4100..0xFFFF` (3 страницы), даёт W1+W2+W3 замапленными автоматически —
  **это и есть база для резидентного кода W3** (§6, подход A).
- `loader>0` (multi-bank .exe) → DSS грузит только HOME-часть и **оставляет
  файл открытым** (handle в `IX-3`), а crt0 дочитывает банки сам (§5).

Упаковкой в этот формат занимается `toolchain/mkexe`.

---

## 3. Правила стека (критично)

- **`SP ≤ 0xBFFF`** при вызовах DSS (ESTEX, `RST #10`).
- **`SP ≥ 0x8000`** при вызовах некоторых функций BIOS (`RST #08`).
- Пересечение этих требований → **стек обязан жить в W2** (`0x8000-0xBFFF`).
  По умолчанию `SP` инициализируется в `0xBFFE`.

Проверено (`tests/w3probe`): во всех режимах на входе `main` `SP ≈ 0xBFFC`, т.е.
в W2.

**Chicken-and-egg для split-режимов** (small/huge): DSS ставит `SP=0xBFFE` из
заголовка, но для программ `<16 КБ` окно W2 ещё не выделено (там `#FF`). Пуши
туда теряются, первый `call` возвращается в мусор. Поэтому `crt0_small`/
`crt0_banked` сначала работают на **загрузочном стеке в W1** (реальное ОЗУ),
маппят W2 и только потом переставляют `SP=0xBFFE`. Маппинг W2 делается через
**ESTEX `$3A SETWIN2`** (не BIOS `$C4`+OUT — тому нужен стек уже в W2).

---

## 4. Режимы памяти

Выбираются флагом `sprinter-cc --memory MODE` (по умолчанию `tiny`). Режим задаёт
адрес кода, размещение данных, crt0 и наличие банков.

| Режим  | CODE | DATA/BSS | Стек | Банки | crt0 | Первичный загрузчик |
|--------|------|----------|------|-------|------|----------------------|
| `tiny`   | W2 `0x8100` | за кодом (W2) | W2 | — | `crt0.s` | нет (1 страница) |
| `small`  | W1 `0x4100` | за кодом (W1→W2) | W2 | — | `crt0_small.s` | да (сам маппит W2) |
| `big`    | W2 `0x8100` | за кодом (W2) | W2 | W1 (трамплины) | `crt0_banked.s` (BANK_W1) | да |
| `huge`   | W1 `0x4100` | за кодом (W1→W2) | W2 | W3 (трамплины) | `crt0_banked.s` | да |
| `manual` | явно | явно | W2 | — | `crt0.s` | зависит |

Во всех режимах **DATA цепляется линкером сразу за кодом** (`--data-loc 0`), а не
кладётся по фиксированному адресу. Раньше `huge` использовал фиксированный
`DATA=0x8000`, что ломалось при коде `>16 КБ` (код перетекал в W2 и накрывал
DATA); сейчас DATA динамически идёт за концом кода.

### 4.1 `tiny` — всё в одной странице

```
0x8000..0x80FF  зарезервировано (startup-prefix)
0x8100          _start / _CODE … _DATA … _BSS … _HEAP
0xBB00          heap top (по умолчанию)
0xBFFE          стек ↓
```

- Один блок 16 КБ, DSS маппит его в W2. W1 и W3 = `#FF`.
- Ничего выделять/маппить не надо; `crt0.s` предполагает, что W2 уже дан DSS.
- Практический потолок кода+данных+кучи+стека ≈ 14 КБ.
- `--code-loc 0x8100 --data-loc 0`; mkexe `-L 0x8100 -E 0x8100 -S 0xBFFE`.

### 4.2 `small` — CODE в W1, данные перетекают в W2

```
0x4100          _CODE …            (W1)
  … за кодом →  _DATA _BSS _HEAP   (W1, перетекает в W2)
0xBB00          heap top
0xBFFE          стек ↓             (W2)
```

- Покрывает ~0..30 КБ (код+данные вместе).
- `crt0_small.s` авто-определяет W2: читает порт `0xC2`. Если `≠0xFF` — DSS уже
  дал W2 (образ `>16 КБ`), маппить не надо. Если `=0xFF` — сам выделяет страницу
  (`ESTEX $3D GETMEM`) и маппит (`ESTEX $3A SETWIN2`).
- `--code-loc 0x4100 --data-loc 0`.

### 4.3 `big` — tiny + банки в W1

- База как `tiny` (CODE+DATA+стек в W2, `0x8100`).
- **Свапаемые банки в W1** (`0x4000-0x7FFF`, порт `0xA2`), вызываются через
  трамплины (§5). `crt0_banked.s` собирается с `BANK_W1=1`.
- mkexe получает `-B 0x4000` (банки живут в W1). Виртуальный адрес банка N =
  `0x{N}4000`.
- W3 свободно — доступно под графику или резидентный код (`--w3`).

### 4.4 `huge` — small + банки в W3

- База как `small` (CODE `0x4100`, DATA за кодом, авто-детект W2).
- **Свапаемые банки в W3** (`0xC000-0xFFFF`, порт `0xE2`), через трамплины.
  Виртуальный адрес банка N = `0x{N}C000`.
- `crt0_banked.s` (без `BANK_W1`) грузит банки из .exe после старта.

### 4.5 `manual` — явное размещение

`--memory manual --memory-manual SPEC`, где SPEC = список `KEY=VAL`:
`CODE=W1|W2`, `DATA=W1|W2|SAME`, `BANKED=W1|W3`. Плюс прямые `--code-loc` /
`--data-loc` / `-L` / `-E` / `-S` перекрывают дефолты любого режима.

---

## 5. Банки и трамплины (`--bank`)

Для `big`/`huge`. Модуль-банк собирается в отдельную область и линкуется по
**виртуальному 24-битному адресу** (`bank_id` в старшем байте):

```
sprinter-cc --memory huge --bank 1=engine.c --bank 2=audio.c -o app.exe main.c
```

- Банк N компилируется `--codeseg/--constseg/--dataseg BANKN`, линкуется
  `-Wl-b_BANKN=0x{N}C000` (huge) или `0x{N}4000` (big).
- `main.c` обязан объявить `const uint8_t n_banks = N;` — `crt0_banked` читает
  это **до gsinit**, поэтому только `const` (инициализатор ещё не скопирован).
- Функции банка помечаются `__banked` — SDCC генерирует вызов через трамплин
  `___sdcc_bcall_ehl` (в `_CODE`/W1, всегда замаплен): он сохраняет текущую
  страницу окна, маппит нужный банк (`_bank_pages[id]`), `jp` в функцию, по
  возврату восстанавливает страницу.
- Загрузка: mkexe пакует `header + HOME + bank1(16К) + bank2(16К)…`,
  `loader=размер HOME`; `crt0_banked` выделяет страницы (`GETMEM`), маппит и
  дочитывает каждый банк `ESTEX READ` из открытого файла.
- Проверка размеров банков — `toolchain/check_banks.py` (часть отчёта
  раскладки, см. §10).

Writable bank-local данные возможны, но с оговорками — см.
`memory/bank_local_data_pattern`.

---

## 6. Резидентный код окна W3 (`--w3`)

**Альтернатива банкам без трамплинов.** Модуль размещается резидентно в W3
(`0xC000`) и вызывается **прямым `call`** — как обычная функция. Работает во
**всех режимах** (`tiny|small|big|huge`); по умолчанию подразумевает `small`.

```
sprinter-cc --w3 render.c -o app.exe main.c            # → small
sprinter-cc --memory huge --w3 render.c --bank 1=lvl.c -o app.exe main.c
```

**Как работает (подход A):** образ с областью `W3CODE`@`0xC000` тянется до `0xC0xx`
(≥3 страницы), и DSS сам маппит W3 при загрузке (§2) — **загрузчик в crt0 не
нужен** (кроме huge). Прямые вызовы резолвятся линкером в реальные `0xC0xx`.

**Механизм сборки:**
- W3-модуль компилируется `--codeseg W3CODE --constseg W3CODE` — код и rodata в
  W3. **`--dataseg` НЕ переопределяется**: писучие статики уходят в обычный
  `_DATA` (W2). Отсюда правило «в W3 только код + rodata».
- Линк `-Wl-b_W3CODE=0xC000`.

**Раскладка страниц по режимам (проверено MAME):**

| Режим | W1 | W2 | W3 |
|-------|----|----|----|
| tiny  | `#FF` (не исп.) | код+данные | **резидент** |
| small | код | данные | **резидент** |
| big   | банк (трамплин) | код+данные | **резидент** |
| huge  | код | данные | **резидент делит окно с трамплин-банками** |

**huge — особый случай (резидент + банки в одном окне W3):**
- `crt0_banked` под `.ifdef W3_RESIDENT` захватывает физ-страницу резидента
  (`in a,(0xE2)`) сразу после загрузки DSS и **возвращает её дефолтом** после
  цикла загрузки банков (иначе в W3 остался бы последний банк).
- Трамплин на каждый `__banked`-вызов сам сохраняет/восстанавливает страницу
  W3 — поэтому дефолтная страница обязана быть резидентной.
- mkexe получает флаг `-W` (разрешить HOME тянуться в W3 при наличии W3-банков —
  намеренное совмещение).

**Правила разработчика:**
- Код в W3 **не переключает** страницу W3.
- Код в W1/W2, свапающий W3 на другую страницу (скретч, графика), обязан
  **вернуть исходную** (`in a,(0xE2)` → работа → `out (0xE2),a`); оборачивать в
  `DI/EI`, если есть ISR, дёргающий W3.
- В W3 — **только код + rodata**, писучих переменных там быть не должно.
- Из `__banked`-контекста (пока в W3 замаплен банк) резидентный W3-код
  **недостижим** транзитивно. Обратное — резидент → `__banked` через трамплин
  W1 — **работает** (трамплин вернёт резидентную страницу перед `ret`).

Подробности и артефакты — `memory/w3_resident_code`, тест `tests/w3probe`.

---

## 7. Куча и стек

- **Стек**: init `SP=0xBFFE`, растёт вниз. Меняется через `-S 0xADDR`.
- **Куча**: от конца `_BSS` вверх до `___sdcc_heap_end` (по умолчанию `0xBB00` →
  ~1278 байт под стек). `malloc` берёт `&___sdcc_heap_end` как потолок.
- `runtime/heap.s` — динамическая куча (авто-размер = зазор BSS…heap_top), НЕ
  фиксированный `.ds`.
- `--stack-size N` регенерирует `heap_top` как `HEAP_TOP = 0xBFFF - N`, зажимая
  рост кучи ради стека.

---

## 8. Семейство crt0

Выбирается режимом; `--crt0=TYPE` перекрывает.

| crt0 | Файл | Для чего |
|------|------|----------|
| `default` | `runtime/crt0.s` | tiny/manual; парсит argv, argv[0] через APPINFO |
| `minimal` | `runtime/crt0_minimal.s` | tiny без argv (меньше размер) |
| `small`   | `runtime/crt0_small.s` | small; авто-детект/выделение W2 |
| `banked`  | `runtime/crt0_banked.s` | big/huge; авто-детект W2 + загрузка банков |

`crt0`/`bank.s` собираются **пер-сборка** внутри `sprinter-cc` (с префиксами
`BANK_W1` / `W3_RESIDENT` / `DEBUG_RT`), а не бандлятся в библиотеку.

---

## 9. Типовые грабли

- **`static`-переменные читаются как `0xFF` / не меняются** — DATA попала в
  невыделенное окно (`#FF`). Причина: код и данные в разных окнах при образе
  `<16 КБ`, где DSS дал только одну страницу. Лечится правильным режимом
  (`small`/`huge`) или единым окном (`tiny`). См.
  `memory/sprinter_memory_modes`.
- **Крэш после первого `call` в split-режиме** — стек ещё в невыделенном W2.
  Решает загрузочный стек в W1 (уже в crt0).
- **Банк «прыгает в мусор»** — таблица `_bank_pages[]` в `_DATA` занулилась
  gsinit’ом; она обязана жить в `_CODE`. Уже исправлено в `bank.s`.
- **`--w3`: резидент недоступен из банка** — ожидаемо (см. §6); держи вход в
  резидент только из W1/W2 или из самого W3-кода.

---

## 10. Отчёт по раскладке памяти (после линковки)

`sprinter-cc` печатает отчёт `toolchain/check_banks.py` для **любой** модели
памяти (по `.map`).  Показывает, где легли код/данные и **сколько свободно**
в каждом окне и банке:

```
memory: huge — CODE в W1 (0x4100), DATA→W2, банки в W3
  _CODE         @ 0x4100  size   3767  (W1)  → 0x4FB7
  данные        @ 0x4FB7  size    336  (W1)  → 0x5107   [_DATA/_BSS/_INITIALIZED/…]
  статика до 0x5107  —  куча 0x5107..0xBB00 (27129 Б), стек 0xBB00..0xBFFE (1279 Б)
  _W3CODE       @ 0xC000  size    249  (резидент W3)  → 0xC0F9, 16135 Б свободно до 0x10000  OK
  _BANK1        @ 0x0001C000  size    219 / 16384 ( 1.3%)  → 16165 Б свободно  OK
```

- `_CODE` / `данные` — окно (W1/W2), размер, конец; строка `статика … куча …
  стек …` = свободное место в W1/W2 (под кучу malloc до `___sdcc_heap_end` и
  под стек).
- `_W3CODE` — только при `--w3`: остаток окна W3.
- `_BANKn` — только при `--bank`: занятость/остаток каждого 16 КБ-банка.
- Ненулевой выход (проглатывается `|| true`), если банк > 16 КБ или статика
  заехала за `heap_top`/`0xC000`.
```
