Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7fd7f28ffc | |||
| f4b4852d51 | |||
| b6699b3aef | |||
| 536c60d14c | |||
| e2730f78e0 | |||
| 3ea4546656 | |||
| 8458ec65f4 |
@@ -0,0 +1,11 @@
|
|||||||
|
[mcp_servers.mame-z80]
|
||||||
|
command = "/Users/alex/.local/bin/uv"
|
||||||
|
args = [
|
||||||
|
"run",
|
||||||
|
"--python",
|
||||||
|
"3.12",
|
||||||
|
"--no-project",
|
||||||
|
"--with",
|
||||||
|
"mcp<2",
|
||||||
|
"/Volumes/SAM8/Projects/DIY/Z80/Sprinter/C-Compiler/mame/sources/MAME/src/mame_mcp.py",
|
||||||
|
]
|
||||||
@@ -8,6 +8,7 @@ build/
|
|||||||
|
|
||||||
# sprinter-cc per-example intermediate directory
|
# sprinter-cc per-example intermediate directory
|
||||||
.sprinter-cc-*/
|
.sprinter-cc-*/
|
||||||
|
.resource-stamps/
|
||||||
|
|
||||||
# Per-program 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/).
|
# (real apps under examples/ and libc feature tests under tests/).
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# Sprinter C-Compiler — правила проекта
|
||||||
|
|
||||||
|
Target-слой SDCC 4.5 (z80) для компьютера Sprinter Sp2000: crt0,
|
||||||
|
линковка, libc, mkexe. Общение и комментарии — на русском.
|
||||||
|
|
||||||
|
## Сборка и проверка
|
||||||
|
|
||||||
|
```
|
||||||
|
make # tools + lib + libbgi + все тесты (45) + examples
|
||||||
|
make -C libc # только libc → lib/sprinter.lib (fast) + sprinter_safe.lib
|
||||||
|
make -C libbgi # только BGI → lib/bgi256.lib (fast) + bgi256_safe.lib
|
||||||
|
make floppy # упаковать все .exe в mame/v306/IMG/mc.img
|
||||||
|
make size-check # размерный регресс: _CODE vs docs/size_baseline.tsv
|
||||||
|
make size-baseline # принять текущие размеры эталоном
|
||||||
|
```
|
||||||
|
|
||||||
|
Обе библиотеки собираются в двух вариантах: fast (дефолт; `-D*_NOCHECK` —
|
||||||
|
параметр-валидации вырезаны) и safe (линкуется по `sprinter-cc --safe`).
|
||||||
|
Критичные гарды (напр. _fd_guard — 9-й OPEN вешает DSS) — в ОБОИХ.
|
||||||
|
|
||||||
|
Графика (BGI) — отдельная библиотека libbgi/ (см. ниже). Программа,
|
||||||
|
использующая graphics.h, собирается с `--gfx 256` (или `--gfx 16` в
|
||||||
|
Фазе 2): sprinter-cc подлинкует lib/bgi256.lib и добавит -I libbgi/include.
|
||||||
|
|
||||||
|
Одиночный тест: `cd tests/<имя> && make run` (пакует exe + EXTRA_DATA на
|
||||||
|
дискету и запускает MAME автоматически через `toolchain/mame_interactive.py`,
|
||||||
|
снимает скриншоты, выводит пути). Для сложных сценариев (диалог, несколько
|
||||||
|
шагов ввода) — прямой вызов:
|
||||||
|
`python3 toolchain/mame_interactive.py tests/<имя>/<имя>.exe --snap T1,T2 --timeout T`.
|
||||||
|
Скриншоты лежат в `mame/v306/snap_auto/sprinter/` (читаются инструментом Read).
|
||||||
|
Подробности: `docs/mame-autotest.md`.
|
||||||
|
|
||||||
|
После правок libc/libbgi: пересборка от чистого листа (`make -C libc clean`
|
||||||
|
/ `make -C libbgi clean`) не обязательна — stale .rel чистятся
|
||||||
|
автоматически; `make size-check` обязателен (рост _CODE без причины —
|
||||||
|
регрессия).
|
||||||
|
|
||||||
|
## Правила libc
|
||||||
|
|
||||||
|
- **1 публичная функция = 1 .c-модуль** (линкер тянет .rel целиком —
|
||||||
|
гранулярность файлов = гранулярность DCE). Никакой группировки
|
||||||
|
«используются вместе». Internal-хелперы — тоже по одному на модуль
|
||||||
|
(`_`-префикс); общие статики — в отдельные data-модули
|
||||||
|
(`_xxx_state.c`); internal-заголовки (`_file.h`, `_gfx.h`, …) —
|
||||||
|
рядом с исходниками, НЕ в libc/include.
|
||||||
|
- Имя файла = имя функции. libc/Makefile собирает wildcard'ом —
|
||||||
|
ничего регистрировать не надо.
|
||||||
|
- Комментарии — на русском; шапка модуля объясняет что/зачем + ABI.
|
||||||
|
- File-scope переменные НЕ инициализировать `= 0` (crt0 зануляет
|
||||||
|
_DATA; см. memory/sdcc_static_storage_gotcha).
|
||||||
|
- asm-связки между модулями: `call/jp _global` — ок; `jr/djnz` через
|
||||||
|
границу и fall-through — НЕЛЬЗЯ (docs/libc-split-asm-cases.md).
|
||||||
|
- Заголовки: сначала пробовать include_next-паттерн; полная замена
|
||||||
|
SDCC-заголовка обязана дублировать его контракт
|
||||||
|
(docs/libc-headers.md).
|
||||||
|
- Справочник API — docs/libc-reference.md (обновлять при добавлении
|
||||||
|
функций).
|
||||||
|
|
||||||
|
## ABI и платформа (кратко; детали в memory/)
|
||||||
|
|
||||||
|
- SDCC `__sdcccall(1)`: arg1 → HL (8-бит → A), arg2 → DE, остальные
|
||||||
|
на стеке (callee-pops в __naked); **возврат int/ptr в DE**, uint8 в A.
|
||||||
|
IX callee-saved (в __naked с IX — push/pop обязательны).
|
||||||
|
- ESTEX (rst #0x10): CF=1 — ошибка, код в A → `call __errno_set`;
|
||||||
|
все регистры клобберятся (IX сохранять); стек обязан быть в W2.
|
||||||
|
- BIOS (rst #0x08): строки/буферы в #4000-#BFFF.
|
||||||
|
- Квирки: ESTEX WRITE возвращает DE=0 на успехе (судить по CF/A);
|
||||||
|
лимит 8 файловых манипуляторов, 9-й OPEN ВЕШАЕТ DSS (_fd_guard);
|
||||||
|
ENV $46: A=0 = NOT FOUND.
|
||||||
|
- Перед обвинением компилятора/железа — подтвердить артефактом
|
||||||
|
(сгенерированный .asm в libc/build/ или libbgi/build/, дамп, репро) — см.
|
||||||
|
memory/defer_unexplained_quirks.
|
||||||
|
|
||||||
|
## Структура
|
||||||
|
|
||||||
|
- `libc/<area>/*.c` — модули libc (ядро, БЕЗ графики); `libc/include/` — публичные заголовки libc
|
||||||
|
- `libbgi/` — графика BGI (отдельная библиотека): `common/` — mode-agnostic (один исходник, .rel в обеих driver-библиотеках), `bgi256/` + `bgi16/` — mode-specific leaf'ы (реальные реализации, без обёрток); `include/` — graphics.h + gfx.h; `_bgi.h` — внутренний заголовок. Собирает `lib/bgi256.lib` (и `bgi16.lib` в Фазе 2). Выбор режима линковкой: `--gfx 256` / `--gfx 16`.
|
||||||
|
- `runtime/` — crt0-семейство, heap, bank (bank.s собирается per-build)
|
||||||
|
- `bin/sprinter-cc` — обёртка компилятора; `toolchain/mkexe` — упаковщик
|
||||||
|
- `tests/` — по одному API/фиче; `examples/` — реальные приложения
|
||||||
|
- `docs/` — дизайн-доки; `docs/TODO.md` — roadmap
|
||||||
|
- `third_party/solid-c/` — нативный Sprinter C (референс, CP866;
|
||||||
|
их ABI несовместим — только как образец)
|
||||||
@@ -144,7 +144,8 @@ sprinter-cc -o foo.exe foo.c [more.c ...] [options]
|
|||||||
--memory-manual SPEC explicit placement (CODE=W1|W2,DATA=W1|W2|SAME,BANKED=W1|W3)
|
--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)
|
--stack-size N bytes reserved for the stack (default ~1278)
|
||||||
--crt0=TYPE default | minimal | banked | small
|
--crt0=TYPE default | minimal | banked | small
|
||||||
--bank N=FILE.c compile FILE.c into bank N (repeatable, max 15)
|
--bank N=FILE.c compile FILE.c into bank N (repeatable, consecutive 1..15;
|
||||||
|
crt0 bank count is generated automatically)
|
||||||
--debug enable runtime diagnostics (defines DEBUG_RT)
|
--debug enable runtime diagnostics (defines DEBUG_RT)
|
||||||
-I PATH extra include path
|
-I PATH extra include path
|
||||||
-L 0xADDR / -E / -S override load / entry / stack addresses
|
-L 0xADDR / -E / -S override load / entry / stack addresses
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ RUN_MAME := $(MAME_DIR)/run_mame.sh
|
|||||||
# Optional knobs — see top of file.
|
# Optional knobs — see top of file.
|
||||||
MEMORY ?= tiny
|
MEMORY ?= tiny
|
||||||
SOURCES := $(EXAMPLE).c $(EXTRA_SRCS)
|
SOURCES := $(EXAMPLE).c $(EXTRA_SRCS)
|
||||||
|
# Аргументы упаковщика HDD. Обычно это exe и EXTRA_DATA; приложение со
|
||||||
|
# своей раскладкой каталогов может переопределить переменную до include.
|
||||||
|
HDD_PACK_ARGS ?= $(EXAMPLE).exe $(EXTRA_DATA)
|
||||||
|
|
||||||
CC_FLAGS := --memory $(MEMORY)
|
CC_FLAGS := --memory $(MEMORY)
|
||||||
ifneq ($(STACK_SIZE),)
|
ifneq ($(STACK_SIZE),)
|
||||||
@@ -92,7 +95,7 @@ run: floppy
|
|||||||
# используется MCP-мостом к MAME (run_bridge.sh). После пересборки образа
|
# используется MCP-мостом к MAME (run_bridge.sh). После пересборки образа
|
||||||
# MAME ОБЯЗАН полный рестарт (chdman -f = новый inode; см. memory).
|
# MAME ОБЯЗАН полный рестарт (chdman -f = новый inode; см. memory).
|
||||||
hdd: $(EXAMPLE).exe
|
hdd: $(EXAMPLE).exe
|
||||||
$(MAKE_HDD) $(HDD_IMG) $(EXAMPLE).exe $(EXTRA_DATA)
|
$(MAKE_HDD) $(HDD_IMG) $(HDD_PACK_ARGS)
|
||||||
@echo
|
@echo
|
||||||
@echo "HDD (D:) ready: $(HDD_IMG) (with $(EXAMPLE).exe$(if $(EXTRA_DATA), + $(EXTRA_DATA)))"
|
@echo "HDD (D:) ready: $(HDD_IMG) (with $(EXAMPLE).exe$(if $(EXTRA_DATA), + $(EXTRA_DATA)))"
|
||||||
@echo "ВНИМАНИЕ: перезапусти MAME (run_bridge.sh) — образ пересобран."
|
@echo "ВНИМАНИЕ: перезапусти MAME (run_bridge.sh) — образ пересобран."
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# `applications/PoP/docs` — индекс + сводка по форматам ресурсов
|
# `applications/PoP/docs` — индекс + сводка по форматам ресурсов
|
||||||
|
|
||||||
## Индекс документов (актуальность на 2026-08-01)
|
## Индекс документов (актуальность на 2026-08-22)
|
||||||
|
|
||||||
**Живые планы — читать перед работой:**
|
**Живые планы — читать перед работой:**
|
||||||
|
|
||||||
@@ -13,7 +13,9 @@
|
|||||||
| [`perf_green_phase.md`](perf_green_phase.md) | **ЗЕЛЁНАЯ фаза (слой фона)**: раскладка тактов, способы ускорения (G1..G6), журнал правок — рабочий документ между сессиями. 2026-08-17 |
|
| [`perf_green_phase.md`](perf_green_phase.md) | **ЗЕЛЁНАЯ фаза (слой фона)**: раскладка тактов, способы ускорения (G1..G6), журнал правок — рабочий документ между сессиями. 2026-08-17 |
|
||||||
| [`perf_cyan_phase.md`](perf_cyan_phase.md) | **ЦИАН фаза (персонажи + передний слой)**: раскладка тактов, способы ускорения (C1..C7), журнал правок — рабочий документ между сессиями. 2026-08-17 |
|
| [`perf_cyan_phase.md`](perf_cyan_phase.md) | **ЦИАН фаза (персонажи + передний слой)**: раскладка тактов, способы ускорения (C1..C7), журнал правок — рабочий документ между сессиями. 2026-08-17 |
|
||||||
| [`perf_backlog.md`](perf_backlog.md) | Отложенная оптимизация отрисовки с замерами 2026-08-10 + **как мерить** (wait-state'ы, границы кадра). Позиции 1–7 переехали в фазовые документы выше |
|
| [`perf_backlog.md`](perf_backlog.md) | Отложенная оптимизация отрисовки с замерами 2026-08-10 + **как мерить** (wait-state'ы, границы кадра). Позиции 1–7 переехали в фазовые документы выше |
|
||||||
| [`quicksave_plan.md`](quicksave_plan.md) | **QuickSave/QuickLoad**: разбор (это enhancement SDLPoP, в оригинале 1989 его НЕТ), инвентаризация нашего состояния, формат снимка, шаги QS1..QS6. План, код не начат. 2026-08-17 |
|
| [`quicksave_plan.md`](quicksave_plan.md) | **QuickSave/QuickLoad** (✅ реализовано, F6/F9, POP.SAV+BAK): разбор (это enhancement SDLPoP, в оригинале 1989 его НЕТ), инвентаризация нашего состояния, формат снимка 'POPQ' v3, шаги QS1..QS6. Справочник. 2026-08-22 |
|
||||||
|
| [`full_game_plan.md`](full_game_plan.md) | **Полноценная игра**: app state machine, title/intro, demo level 0, таймер, cutscenes, уровни 1..14, ending и Hall of Fame. Уровень 15 исключён. 2026-08-21 |
|
||||||
|
| [`menu_settings_plan.md`](menu_settings_plan.md) | **Pause menu и Settings**: QuickSave/QuickLoad в основном menu, `POP.CFG`, один `POP.SAV` + `POP.BAK`, текущий VANILLA и задел под ENHANCED; §10 — выбор UI-рендера (текстовые строки + свой растровый рендерер, референс SDLPoP: два шрифта), restart без подтверждения. 2026-08-22 |
|
||||||
| [`levels_plan.md`](levels_plan.md) | Следующий этап: уровни 2+, второй тайлсет, читы SDLPoP |
|
| [`levels_plan.md`](levels_plan.md) | Следующий этап: уровни 2+, второй тайлсет, читы SDLPoP |
|
||||||
| [`levels_12_15_plan.md`](levels_12_15_plan.md) | **Уровни 12/13** (тень, Джафар, падающие плиты) + что такое 14/15 и 0. 2026-08-13 |
|
| [`levels_12_15_plan.md`](levels_12_15_plan.md) | **Уровни 12/13** (тень, Джафар, падающие плиты) + что такое 14/15 и 0. 2026-08-13 |
|
||||||
| [`midtable_analysis.md`](midtable_analysis.md) | **Слои отрисовки**: как устроены back/mid/fore и objtable в оригинале, чего стоит порт, развилки. 2026-08-13 |
|
| [`midtable_analysis.md`](midtable_analysis.md) | **Слои отрисовки**: как устроены back/mid/fore и objtable в оригинале, чего стоит порт, развилки. 2026-08-13 |
|
||||||
|
|||||||
@@ -0,0 +1,349 @@
|
|||||||
|
# От roomtest к полноценной игре — сценарий и оболочка
|
||||||
|
|
||||||
|
Статус: **согласованный план, код не начат** (2026-08-21).
|
||||||
|
|
||||||
|
Этот документ описывает превращение текущего игрового цикла
|
||||||
|
`roomtest` в законченную игру: заставка, интро, демонстрационный уровень,
|
||||||
|
сцены между уровнями, таймер, финал и Hall of Fame. План меню и постоянных
|
||||||
|
настроек вынесен в [`menu_settings_plan.md`](menu_settings_plan.md),
|
||||||
|
детальный план QuickSave — в [`quicksave_plan.md`](quicksave_plan.md).
|
||||||
|
|
||||||
|
## 1. Зафиксированный scope
|
||||||
|
|
||||||
|
- Целевая последовательность — оригинальная SDLPoP/DOS PoP с уровнями
|
||||||
|
**1..14**. Уровень 14 — скрытая финальная часть после Джаффара: его номер
|
||||||
|
игроку не показывается, победа наступает в комнате 5.
|
||||||
|
- Уровень **15 удаляется полностью**: не пакуется на HDD, не загружается,
|
||||||
|
отсутствует в переходах, читах и UI; специальная логика potions/copy
|
||||||
|
protection level удаляется.
|
||||||
|
- Уровень **0** остаётся только демонстрационным (attract mode), а не частью
|
||||||
|
новой игры.
|
||||||
|
- Title sequence повторяет SDLPoP. Перед ней допускается отдельный
|
||||||
|
пропускаемый экран с информацией о Sprinter-сборке.
|
||||||
|
- Первая версия использует текущий профиль поведения `VANILLA`. Сейчас это
|
||||||
|
означает **существующую реализацию roomtest**, включая уже встроенные
|
||||||
|
исправления. Аудит и разведение `VANILLA/ENHANCED` — будущая задача.
|
||||||
|
- Программа работает **только с HDD**. Варианты без сохранения для floppy не
|
||||||
|
проектируются.
|
||||||
|
- Моды и выбор levelset в этот план не входят.
|
||||||
|
|
||||||
|
## 2. Что делает SDLPoP
|
||||||
|
|
||||||
|
Источники истины в локальном SDLPoP:
|
||||||
|
|
||||||
|
- `src/seg000.c`: `start_game()`, `show_title()`, demo mode, общий кадр,
|
||||||
|
проверка финала;
|
||||||
|
- `src/seg003.c`: `init_game()`, `play_level()`, `play_level_2()`;
|
||||||
|
- `src/seg001.c`: cutscene engine, `pv_scene()`, сцены 2/4/6/8/9/12,
|
||||||
|
`time_expired()`, `end_sequence()` и Hall of Fame;
|
||||||
|
- `src/data.h`: `tbl_cutscenes`, параметры уровня 0, win level/room;
|
||||||
|
- `data/TITLE`, `data/PV`, `data/LEVELS/res2000.bin`: ресурсы оболочки.
|
||||||
|
|
||||||
|
Штатный маршрут:
|
||||||
|
|
||||||
|
```text
|
||||||
|
boot
|
||||||
|
-> title / story screens
|
||||||
|
-> Princess + Jaffar intro
|
||||||
|
-> credits / Hall of Fame
|
||||||
|
-> demo level 0
|
||||||
|
-> title или новая игра
|
||||||
|
-> levels 1..14
|
||||||
|
before 2 -> princess cutscene
|
||||||
|
before 4 -> princess cutscene
|
||||||
|
before 6 -> princess cutscene
|
||||||
|
before 8 -> princess + mouse
|
||||||
|
before 9 -> princess + mouse
|
||||||
|
before 12 -> scene selected by remaining time
|
||||||
|
-> level 14, room 5
|
||||||
|
-> embrace + mouse
|
||||||
|
-> ending text/music
|
||||||
|
-> Hall of Fame
|
||||||
|
-> title
|
||||||
|
```
|
||||||
|
|
||||||
|
Кроме уровней, здесь есть глобальный 60-минутный таймер, сцена истечения
|
||||||
|
времени, пропуск сцен клавишей, fade/flash, ожидание музыки и возврат в
|
||||||
|
attract loop после демо или финала.
|
||||||
|
|
||||||
|
## 3. Текущее состояние roomtest
|
||||||
|
|
||||||
|
Уже реализованы игровой кадр, комнаты, уровни, тайлсеты, Kid/Guard/Shadow,
|
||||||
|
Джаффар, специальные события, checkpoint, переходы уровней, бесшовный
|
||||||
|
выход 12-го уровня, перенос максимального HP и звуковые эффекты.
|
||||||
|
|
||||||
|
Отсутствуют:
|
||||||
|
|
||||||
|
- верхнеуровневая оболочка приложения;
|
||||||
|
- title/story sequence и текстовые экраны;
|
||||||
|
- уровень 0 и записанное управление демо;
|
||||||
|
- общий cutscene engine и PV-ресурсы;
|
||||||
|
- сцены между уровнями;
|
||||||
|
- глобальный таймер и time-expired sequence;
|
||||||
|
- корректный переход из уровня 14 в ending;
|
||||||
|
- финальная сцена и Hall of Fame;
|
||||||
|
- возврат к title без перезапуска программы.
|
||||||
|
|
||||||
|
## 4. Архитектура: автомат состояний приложения
|
||||||
|
|
||||||
|
Нельзя наращивать все режимы условиями внутри кадрового цикла. Текущий
|
||||||
|
цикл должен стать реализацией одного состояния `PLAYING`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
BOOT -> BUILD_INFO -> TITLE -> INTRO -> DEMO
|
||||||
|
| |
|
||||||
|
+---- NEW_GAME <-+
|
||||||
|
|
||||||
|
NEW_GAME -> LEVEL_LOAD -> PLAYING <-> PAUSE_MENU
|
||||||
|
|
|
||||||
|
+-> CUTSCENE -> LEVEL_LOAD
|
||||||
|
+-> TIME_EXPIRED -> TITLE
|
||||||
|
+-> ENDING -> HALL_OF_FAME -> TITLE
|
||||||
|
```
|
||||||
|
|
||||||
|
Минимальный контекст оболочки:
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef enum {
|
||||||
|
POP_APP_BOOT,
|
||||||
|
POP_APP_BUILD_INFO,
|
||||||
|
POP_APP_TITLE,
|
||||||
|
POP_APP_INTRO,
|
||||||
|
POP_APP_DEMO,
|
||||||
|
POP_APP_LEVEL_LOAD,
|
||||||
|
POP_APP_PLAYING,
|
||||||
|
POP_APP_PAUSE_MENU,
|
||||||
|
POP_APP_CUTSCENE,
|
||||||
|
POP_APP_TIME_EXPIRED,
|
||||||
|
POP_APP_ENDING,
|
||||||
|
POP_APP_HALL_OF_FAME,
|
||||||
|
POP_APP_QUIT
|
||||||
|
} pop_app_state_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
Переходы задаются результатом состояния, а не прямыми рекурсивными
|
||||||
|
вызовами наподобие SDLPoP `start_game()`/`longjmp()`. На Z80 это проще для
|
||||||
|
стека и позволяет освобождать ресурсы каждого режима в одном месте.
|
||||||
|
|
||||||
|
## 5. Ресурсная модель
|
||||||
|
|
||||||
|
Title и cutscene-ресурсы нельзя постоянно держать рядом с игровыми
|
||||||
|
атласами. Для каждого состояния нужен явный lifecycle:
|
||||||
|
|
||||||
|
```text
|
||||||
|
enter: pause sound -> unload incompatible set -> load set -> apply palette
|
||||||
|
run: process input/timer/animation
|
||||||
|
leave: stop sound -> release EMM pages -> clear transient state
|
||||||
|
```
|
||||||
|
|
||||||
|
Новые группы HDD:
|
||||||
|
|
||||||
|
```text
|
||||||
|
TITLE\ title/story images, palette, optional build-screen assets
|
||||||
|
PV\ princess room, Princess/Jaffar/mouse frames, palettes
|
||||||
|
MUSIC\ intro, cutscene and ending tracks/samples
|
||||||
|
LEVELS\ res2000..res2014.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
Конкретный формат атласов выбирает упаковщик. Runtime не должен разбирать
|
||||||
|
PNG/DAT: как и игровые спрайты, он получает подготовленные `.atl`/`.bin`.
|
||||||
|
|
||||||
|
## 6. Экран Sprinter build
|
||||||
|
|
||||||
|
Отдельное состояние перед оригинальной заставкой:
|
||||||
|
|
||||||
|
```text
|
||||||
|
PRINCE OF PERSIA
|
||||||
|
SPRINTER SP2000 BUILD
|
||||||
|
version / date / build id
|
||||||
|
```
|
||||||
|
|
||||||
|
Требования:
|
||||||
|
|
||||||
|
- пропускается любой клавишей;
|
||||||
|
- выключается в Settings;
|
||||||
|
- не запускает музыку оригинального title и не меняет её тайминги;
|
||||||
|
- данные версии генерируются сборкой, а не правятся вручную в C;
|
||||||
|
- отсутствие экрана приводит прямо к `TITLE`.
|
||||||
|
|
||||||
|
## 7. Title и текстовая подсистема
|
||||||
|
|
||||||
|
Порядок переносится из `show_title()`:
|
||||||
|
|
||||||
|
1. основной титульный экран;
|
||||||
|
2. Presents;
|
||||||
|
3. название игры и Jordan Mechner;
|
||||||
|
4. story frame / “In the absence…”;
|
||||||
|
5. intro Princess + Jaffar;
|
||||||
|
6. story “Marry Jaffar…”;
|
||||||
|
7. credits;
|
||||||
|
8. Hall of Fame, если таблица непуста;
|
||||||
|
9. demo level 0.
|
||||||
|
|
||||||
|
Нужны общие примитивы: загрузить full-screen image, вывести строку,
|
||||||
|
показать экран заданное время, transition left-to-right, fade in/out,
|
||||||
|
прервать ожидание клавишей. Текст и меню должны использовать один renderer.
|
||||||
|
|
||||||
|
Критерий: последовательность и музыкальные точки совпадают с SDLPoP;
|
||||||
|
Sprinter build screen не сдвигает оригинальный soundtrack.
|
||||||
|
|
||||||
|
## 8. Demo level 0
|
||||||
|
|
||||||
|
- Добавить на HDD `res2000.bin`.
|
||||||
|
- Загружать уровень обычным loader, но выставлять demo HP и demo mode.
|
||||||
|
- Воспроизводить `demo_moves` как синтетический источник `control_*`.
|
||||||
|
- Пользовательский ввод прерывает демо и начинает новую игру.
|
||||||
|
- Достижение demo end room (у SDLPoP — 24), смерть или конец скрипта
|
||||||
|
возвращают в `TITLE`.
|
||||||
|
- Pause menu, QuickSave и cheats в demo недоступны.
|
||||||
|
- RNG демо и начальное состояние должны быть детерминированы.
|
||||||
|
|
||||||
|
Критерий: без ввода attract loop не требует перезапуска процесса;
|
||||||
|
title -> demo -> title повторяется неограниченно.
|
||||||
|
|
||||||
|
## 9. Глобальный таймер
|
||||||
|
|
||||||
|
Состояние: минуты, тики и флаг показа. Таймер создаётся при New Game,
|
||||||
|
переносится между уровнями и входит в QuickSave.
|
||||||
|
|
||||||
|
Правила `VANILLA`:
|
||||||
|
|
||||||
|
- на pause menu, загрузке HDD, QuickSave/QuickLoad время не идёт;
|
||||||
|
- игровые тики следуют темпу логического кадра, а не частоте render loop;
|
||||||
|
- поведение во время level-end sound и cutscenes сверяется буквально с
|
||||||
|
SDLPoP;
|
||||||
|
- после Джаффара/на финальном уровне время не должно вызвать поражение;
|
||||||
|
- ноль времени переводит приложение в `TIME_EXPIRED`.
|
||||||
|
|
||||||
|
Критерий: одинаковый игровой отрезок в NORMAL даёт то же уменьшение времени,
|
||||||
|
что SDLPoP; сохранение/загрузка не добавляет и не отнимает тики.
|
||||||
|
|
||||||
|
## 10. Cutscene engine
|
||||||
|
|
||||||
|
Сцены SDLPoP состоят из небольшого набора повторяемых команд. Вместо набора
|
||||||
|
крупных C-функций нужен компактный интерпретатор:
|
||||||
|
|
||||||
|
```text
|
||||||
|
SET_ACTOR actor
|
||||||
|
SET_POS x,y,dir
|
||||||
|
START_SEQ seq
|
||||||
|
WAIT_FRAMES n
|
||||||
|
PLAY_SOUND id
|
||||||
|
WAIT_SOUND
|
||||||
|
SET_HOURGLASS frame
|
||||||
|
SET_SAND state
|
||||||
|
FLASH color,frames
|
||||||
|
FADE_IN / FADE_OUT
|
||||||
|
CLEAR_ACTOR actor
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипты — `const` в холодном банке или подготовленный бинарный ресурс.
|
||||||
|
Interpreter обязан:
|
||||||
|
|
||||||
|
- исполнять один шаг/кадр без блокирующих длинных циклов;
|
||||||
|
- поддерживать пропуск сцены;
|
||||||
|
- при пропуске выполнять cleanup и выходить в заранее заданное состояние;
|
||||||
|
- освобождать PV-ресурсы перед загрузкой игрового тайлсета;
|
||||||
|
- не разрешать pause menu/QuickSave внутри сцены.
|
||||||
|
|
||||||
|
Порядок переноса: intro, 2/6, 4, 8, 9, 12, time expired, ending. Сцена 12
|
||||||
|
выбирает короткий или обычный вариант по остатку времени.
|
||||||
|
|
||||||
|
## 11. Переходы между уровнями
|
||||||
|
|
||||||
|
Таблица сценария должна быть отдельна от таблиц механики уровня:
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct {
|
||||||
|
uint8_t level;
|
||||||
|
uint8_t pre_cutscene;
|
||||||
|
uint8_t show_level_number;
|
||||||
|
uint8_t ending_rule;
|
||||||
|
} pop_level_flow_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
Особые правила:
|
||||||
|
|
||||||
|
- New Game начинает уровень 1;
|
||||||
|
- перед 2/4/6/8/9/12 запускается сцена;
|
||||||
|
- 12 -> 13 остаётся бесшовным;
|
||||||
|
- после победы над Джаффаром переход идёт в 14;
|
||||||
|
- номер 14 не показывается;
|
||||||
|
- вход в комнату 5 уровня 14 переводит в `ENDING`;
|
||||||
|
- значения больше 14 недопустимы и дают диагностическую ошибку, а не
|
||||||
|
попытку открыть файл.
|
||||||
|
|
||||||
|
## 12. Ending и Hall of Fame
|
||||||
|
|
||||||
|
Ending:
|
||||||
|
|
||||||
|
1. загрузить PV-набор;
|
||||||
|
2. встреча Kid и Princess;
|
||||||
|
3. объятие;
|
||||||
|
4. появление мыши;
|
||||||
|
5. ending music;
|
||||||
|
6. финальные story/title экраны;
|
||||||
|
7. переход в Hall of Fame.
|
||||||
|
|
||||||
|
Hall of Fame хранится на HDD в отдельном версионированном `POP.HOF`.
|
||||||
|
Сохраняются имя и результат; ввод имени использует тот же текстовый/UI слой.
|
||||||
|
Повреждённый или неизвестный формат означает пустую таблицу, но не мешает
|
||||||
|
запуску игры. После показа — возврат в `TITLE`.
|
||||||
|
|
||||||
|
## 13. Удаление уровня 15
|
||||||
|
|
||||||
|
Отдельный ранний этап, чтобы новый flow не наследовал лишний маршрут:
|
||||||
|
|
||||||
|
- убрать `res2015.bin` из `LVL_NUMS` и HDD image;
|
||||||
|
- заменить последний игровой уровень на 14;
|
||||||
|
- остановить Shift+L и прочую навигацию на 14;
|
||||||
|
- удалить `POP_POTIONS_LEVEL` и специальный половинный урон синих зелий;
|
||||||
|
- исключить copy protection из конфигурации и меню;
|
||||||
|
- добавить тест: после уровня 14 приложение входит в ending и никогда не
|
||||||
|
запрашивает `res2015.bin`.
|
||||||
|
|
||||||
|
## 14. Этапы реализации
|
||||||
|
|
||||||
|
| этап | результат | критерий приёмки |
|
||||||
|
|---|---|---|
|
||||||
|
| **FG0** | удалить уровень 15, формализовать 1..14 | HDD не содержит res2015; переход выше 14 невозможен |
|
||||||
|
| **FG1** | автомат состояний, текущая игра = PLAYING | старт/рестарт/выход проходят без рекурсии и утечки EMM |
|
||||||
|
| **FG2** | QuickSave/QuickLoad | критерии `quicksave_plan.md`, включая POP.BAK |
|
||||||
|
| **FG3** | минимальный pause menu | Resume/Save/Load/Restart/Settings/Quit работают |
|
||||||
|
| **FG4** | глобальный таймер | совпадение с SDLPoP и корректный save/load |
|
||||||
|
| **FG5** | text/full-screen/fade primitives | тестовые экраны и переходы на Sprinter |
|
||||||
|
| **FG6** | build info + оригинальный title | точный порядок, корректный пропуск |
|
||||||
|
| **FG7** | demo level 0 | бесконечный attract loop, ввод начинает игру |
|
||||||
|
| **FG8** | cutscene interpreter + intro | интро проходит и пропускается без утечек |
|
||||||
|
| **FG9** | сцены 2/4/6/8/9/12 | правильный вызов один раз перед уровнем |
|
||||||
|
| **FG10** | time expired | отдельная сцена и возврат на title |
|
||||||
|
| **FG11** | level 14 -> ending | полный маршрут после Джаффара |
|
||||||
|
| **FG12** | Hall of Fame | запись HDD, ввод имени, возврат к attract loop |
|
||||||
|
|
||||||
|
QuickSave допускается реализовать до остальных частей оболочки: FG2 — одна
|
||||||
|
из ближайших самостоятельных задач.
|
||||||
|
|
||||||
|
## 15. Проверки
|
||||||
|
|
||||||
|
- Host-тест автомата: все допустимые переходы и отсутствие уровня 15.
|
||||||
|
- Host-тест cutscene interpreter на синтетическом скрипте и skip в каждой
|
||||||
|
ожидающей команде.
|
||||||
|
- Host-тест demo input: одинаковый seed даёт одинаковый поток управления.
|
||||||
|
- MAME: cold boot -> build info -> title -> demo -> title.
|
||||||
|
- MAME: новая игра -> принудительный переход по всем pre-level scenes.
|
||||||
|
- MAME: time expired и пропуск сцены.
|
||||||
|
- MAME: 13 -> 14 -> room 5 -> ending -> HOF -> title.
|
||||||
|
- Проверка EMM/FD до и после каждого состояния: число страниц и открытых
|
||||||
|
файлов возвращается к базовому.
|
||||||
|
- `make size-check`; крупный cold-код размещать в банках и отдельно следить
|
||||||
|
за лимитом 16 КБ каждого банка.
|
||||||
|
|
||||||
|
## 16. Не входит в план
|
||||||
|
|
||||||
|
- уровень 15 и copy protection;
|
||||||
|
- моды и выбор levelset;
|
||||||
|
- replay/recording;
|
||||||
|
- точная эмуляция SDL video/controller options;
|
||||||
|
- профиль ENHANCED и индивидуальные switches fixes.
|
||||||
|
|
||||||
@@ -0,0 +1,426 @@
|
|||||||
|
# Pause menu и Settings для Sprinter PoP
|
||||||
|
|
||||||
|
Статус: **согласованный план, код не начат** (2026-08-21; обновлён
|
||||||
|
2026-08-22 — решение о рендеринге UI см. §10, Restart Level/Game без
|
||||||
|
подтверждения).
|
||||||
|
|
||||||
|
Связанные документы:
|
||||||
|
|
||||||
|
- [`full_game_plan.md`](full_game_plan.md) — автомат состояний, title,
|
||||||
|
demo, cutscenes и ending;
|
||||||
|
- [`quicksave_plan.md`](quicksave_plan.md) — состав и восстановление снимка.
|
||||||
|
|
||||||
|
## 1. Решения
|
||||||
|
|
||||||
|
- Программа работает только с HDD; настройки, QuickSave и Hall of Fame
|
||||||
|
всегда могут быть постоянными файлами.
|
||||||
|
- Основной pause menu обязательно содержит QuickSave и QuickLoad.
|
||||||
|
- QuickSave имеет один слот `POP.SAV`; предыдущая корректная запись хранится
|
||||||
|
как `POP.BAK`.
|
||||||
|
- Первая версия имеет один профиль `VANILLA`. Под этим именем пока понимается
|
||||||
|
**текущее поведение roomtest**, включая уже встроенные исправления.
|
||||||
|
- Дизайн файла и API предусматривает будущий `ENHANCED`, но аудит и
|
||||||
|
переключение fixes сейчас не выполняются.
|
||||||
|
- Уровень 15/copy protection отсутствует.
|
||||||
|
- Моды, levelsets и меню Mods отложены.
|
||||||
|
|
||||||
|
## 2. Что есть в SDLPoP
|
||||||
|
|
||||||
|
`src/menu.c` содержит:
|
||||||
|
|
||||||
|
- Resume, QuickSave, QuickLoad, Restart Level, Settings, Restart Game, Quit;
|
||||||
|
- General, Gameplay, Visuals, Mods, Controls;
|
||||||
|
- toggle/number/key controls, пояснения, scroll и confirmation dialogs;
|
||||||
|
- большой список fixes/enhancements и custom level options.
|
||||||
|
|
||||||
|
На Sprinter не переносятся SDL-специфичные параметры: fullscreen, hardware
|
||||||
|
acceleration, scaling, aspect ratio, rumble. UI берёт структуру SDLPoP, но
|
||||||
|
набор настроек соответствует платформе.
|
||||||
|
|
||||||
|
## 3. Pause menu первой версии
|
||||||
|
|
||||||
|
```text
|
||||||
|
RESUME
|
||||||
|
QUICKSAVE
|
||||||
|
QUICKLOAD
|
||||||
|
RESTART LEVEL
|
||||||
|
SETTINGS
|
||||||
|
RESTART GAME
|
||||||
|
QUIT
|
||||||
|
```
|
||||||
|
|
||||||
|
Поведение:
|
||||||
|
|
||||||
|
- `Esc` в `PLAYING` открывает меню; повторный Esc или Resume возвращает игру;
|
||||||
|
- игра, логический таймер и звуковой насос ставятся на паузу согласованно;
|
||||||
|
- QuickSave/QuickLoad только взводят запрос, фактическая операция идёт на
|
||||||
|
безопасной границе кадра;
|
||||||
|
- QuickLoad disabled/показывает `NO QUICKLOAD`, если нет валидных SAV/BAK;
|
||||||
|
- Restart Level и Restart Game выполняются сразу, БЕЗ подтверждения
|
||||||
|
(2026-08-22): обе операции дёшево обратимы — Restart Level перечитывает
|
||||||
|
уровень, после Restart Game можно тут же сделать QuickLoad из POP.SAV;
|
||||||
|
- Quit требует подтверждения и закрывает файлы/каналы штатным путём;
|
||||||
|
- меню недоступно в demo, cutscene, time-expired и ending;
|
||||||
|
- отдельная debug-комбинация немедленного выхода может остаться только в
|
||||||
|
отладочной сборке.
|
||||||
|
|
||||||
|
## 4. Settings первой версии
|
||||||
|
|
||||||
|
```text
|
||||||
|
GENERAL
|
||||||
|
Sound ON / OFF
|
||||||
|
Show Sprinter screen ON / OFF
|
||||||
|
Restore defaults...
|
||||||
|
|
||||||
|
GAMEPLAY
|
||||||
|
Speed NORMAL / FAST / FASTEST
|
||||||
|
Gameplay profile VANILLA
|
||||||
|
Cheats ON / OFF
|
||||||
|
|
||||||
|
CONTROLS
|
||||||
|
Show key bindings
|
||||||
|
|
||||||
|
BACK
|
||||||
|
```
|
||||||
|
|
||||||
|
`Gameplay profile: VANILLA` показывается read-only: место в модели уже есть,
|
||||||
|
но пользователь не может выбрать ещё не реализованный ENHANCED.
|
||||||
|
|
||||||
|
Отладочные параметры `ROOMNAV`, border profiling, stop-frame и переключение
|
||||||
|
double buffering не являются пользовательскими Settings. Они остаются
|
||||||
|
compile-time/debug функциями и скрываются из release UI.
|
||||||
|
|
||||||
|
## 5. Модель настроек
|
||||||
|
|
||||||
|
Игровой код не должен читать UI-структуры. Единственный runtime-контракт:
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef enum {
|
||||||
|
POP_PROFILE_VANILLA = 0,
|
||||||
|
POP_PROFILE_ENHANCED = 1
|
||||||
|
} pop_gameplay_profile_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t sound_enabled;
|
||||||
|
uint8_t speed_mode;
|
||||||
|
uint8_t gameplay_profile;
|
||||||
|
uint8_t cheats_enabled;
|
||||||
|
uint8_t show_build_info;
|
||||||
|
uint16_t enhancement_flags;
|
||||||
|
} pop_settings_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
В первой версии загрузчик принимает только `POP_PROFILE_VANILLA`. Значение
|
||||||
|
ENHANCED из более нового/ручного файла заменяется на VANILLA с диагностикой,
|
||||||
|
а не включает частично реализованный режим.
|
||||||
|
|
||||||
|
Будущий профиль задаёт маску возможностей централизованно:
|
||||||
|
|
||||||
|
```text
|
||||||
|
VANILLA -> текущий согласованный набор
|
||||||
|
ENHANCED -> будущий рекомендуемый набор fixes
|
||||||
|
CUSTOM -> только если позже действительно понадобится
|
||||||
|
```
|
||||||
|
|
||||||
|
До отдельного аудита существующие `fix_exit_door`, feather guard behavior,
|
||||||
|
jump grab и sound priorities не переключаются и считаются частью текущего
|
||||||
|
VANILLA.
|
||||||
|
|
||||||
|
## 6. Файл POP.CFG
|
||||||
|
|
||||||
|
Бинарный, компактный, версионированный формат:
|
||||||
|
|
||||||
|
```text
|
||||||
|
+0 "PCFG" magic, 4 Б
|
||||||
|
+4 format_version 1 Б
|
||||||
|
+5 payload_size 2 Б
|
||||||
|
+7 payload фиксированные поля little-endian
|
||||||
|
.. checksum 2 Б
|
||||||
|
```
|
||||||
|
|
||||||
|
Требования:
|
||||||
|
|
||||||
|
- путь рядом с exe/в выделенном каталоге игры на HDD;
|
||||||
|
- неизвестная версия, неверная длина или checksum -> defaults;
|
||||||
|
- неизвестные будущие хвостовые поля можно пропустить по `payload_size`;
|
||||||
|
- запись только после Apply/выхода из Settings, не на каждый шаг курсора;
|
||||||
|
- ошибка записи не завершает игру: показать сообщение и оставить runtime
|
||||||
|
значения;
|
||||||
|
- Restore defaults меняет RAM только после подтверждения и затем сохраняет.
|
||||||
|
|
||||||
|
CFG не содержит состояние уровня, QuickSave или Hall of Fame.
|
||||||
|
|
||||||
|
## 7. QuickSave / QuickLoad в меню
|
||||||
|
|
||||||
|
Детальный состав снимка и порядок восстановления — в
|
||||||
|
[`quicksave_plan.md`](quicksave_plan.md). Здесь фиксируется UI и файловая
|
||||||
|
транзакция.
|
||||||
|
|
||||||
|
### Один слот и backup
|
||||||
|
|
||||||
|
Файлы:
|
||||||
|
|
||||||
|
```text
|
||||||
|
POP.SAV текущий слот
|
||||||
|
POP.BAK предыдущий валидный слот
|
||||||
|
POP.NEW временный файл во время записи
|
||||||
|
```
|
||||||
|
|
||||||
|
Безопасная запись:
|
||||||
|
|
||||||
|
1. записать полный снимок в `POP.NEW`;
|
||||||
|
2. закрыть файл;
|
||||||
|
3. повторно открыть/прочитать заголовок и checksum;
|
||||||
|
4. старый валидный `POP.SAV` перенести/скопировать в `POP.BAK`;
|
||||||
|
5. `POP.NEW` сделать новым `POP.SAV`;
|
||||||
|
6. при любой ошибке сохранить прежний `POP.SAV`.
|
||||||
|
|
||||||
|
Точную последовательность rename/copy выбрать после характеризации DSS.
|
||||||
|
Если атомарный rename не гарантирован, использовать copy + fsync/close и
|
||||||
|
никогда не удалять единственную валидную копию до проверки новой.
|
||||||
|
|
||||||
|
### Загрузка
|
||||||
|
|
||||||
|
1. проверить `POP.SAV`;
|
||||||
|
2. если он отсутствует/повреждён/несовместим — проверить `POP.BAK`;
|
||||||
|
3. при валидном BAK показать `LOAD BACKUP?`;
|
||||||
|
4. несовместимая версия — `INCOMPATIBLE SAVE`, без частичной загрузки;
|
||||||
|
5. после успеха закрыть menu, перерисовать обе страницы, перезапустить звук.
|
||||||
|
|
||||||
|
### Сообщения
|
||||||
|
|
||||||
|
Минимальный набор:
|
||||||
|
|
||||||
|
```text
|
||||||
|
QUICKSAVED
|
||||||
|
QUICKLOADED
|
||||||
|
NO QUICKLOAD
|
||||||
|
SAVE ERROR
|
||||||
|
INCOMPATIBLE SAVE
|
||||||
|
LOAD BACKUP?
|
||||||
|
```
|
||||||
|
|
||||||
|
Сообщение показывается UI-слоем, но операция завершается до возврата в
|
||||||
|
игровой кадр.
|
||||||
|
|
||||||
|
## 8. Restart Level / Restart Game
|
||||||
|
|
||||||
|
Restart Level:
|
||||||
|
|
||||||
|
- использует существующий штатный reset текущего уровня;
|
||||||
|
- не перечитывает CFG;
|
||||||
|
- не меняет `POP.SAV`;
|
||||||
|
- сбрасывает состояние, которое сбрасывает текущая реализация roomtest.
|
||||||
|
|
||||||
|
Restart Game:
|
||||||
|
|
||||||
|
- выполняется сразу, без подтверждения;
|
||||||
|
- завершить текущий gameplay session;
|
||||||
|
- освободить level/atlas/temporary EMM;
|
||||||
|
- создать новую игру с уровня 1 и новым глобальным таймером;
|
||||||
|
- настройки оставить;
|
||||||
|
- QuickSave не удалять.
|
||||||
|
|
||||||
|
## 9. Controls
|
||||||
|
|
||||||
|
Первая версия только показывает активную раскладку. Переназначение клавиш
|
||||||
|
откладывается: raw PS/2 канал имеет особенности Shift и расширенных кодов,
|
||||||
|
поэтому generic key-binding UI требует отдельного проекта.
|
||||||
|
|
||||||
|
Экран должен перечислить минимум:
|
||||||
|
|
||||||
|
- движение и Shift/action;
|
||||||
|
- Esc/menu;
|
||||||
|
- F6/F9 QuickSave/QuickLoad;
|
||||||
|
- Ctrl+S sound;
|
||||||
|
- P speed;
|
||||||
|
- доступные cheats, только если они включены.
|
||||||
|
|
||||||
|
## 10. UI renderer и ввод
|
||||||
|
|
||||||
|
### 10.1. Выбор способа отрисовки: текст против спрайт-атласов
|
||||||
|
|
||||||
|
Ограничение платформы: стандартный текстовый вывод libbgi (`outtextxy`)
|
||||||
|
не годится — он тянет системный знакогенератор в `_gfx_font_buf` (2 КБ
|
||||||
|
статики в W2) плюс жирный резидентный код, а W1/W2 забиты игрой
|
||||||
|
(тот же вывод зафиксирован комментарием в `roomtest_cold.c`, где отладочный
|
||||||
|
борд рисуется палочками именно поэтому). Значит, любой вариант требует
|
||||||
|
СВОЕЙ реализации вывода меню, живущей в отдельном банке (память на банк
|
||||||
|
есть; скорость не критична — меню работает на паузе).
|
||||||
|
|
||||||
|
Рассматривались два подхода.
|
||||||
|
|
||||||
|
**Вариант A — текстовые строки + собственный растровый рендерер.**
|
||||||
|
|
||||||
|
Плюсы:
|
||||||
|
- минимальные данные: шрифт 2–4 КБ + таблицы строк по сотни байт на язык;
|
||||||
|
- весь динамический текст бесплатно: значения опций (ON/OFF,
|
||||||
|
NORMAL/FAST/FASTEST), сообщения (`QUICKSAVED`, `INCOMPATIBLE SAVE`),
|
||||||
|
диалоги (`LOAD BACKUP?`), экран Controls, будущий ввод инициалов
|
||||||
|
Hall of Fame — без текстового движка HoF вообще не сделать;
|
||||||
|
- правка формулировки = правка C-строки, мгновенные итерации;
|
||||||
|
- локализация = вторая таблица строк (+ вторая половина глифов);
|
||||||
|
- **решающий аргумент: так сделано в самом SDLPoP** — см. §10.2.
|
||||||
|
|
||||||
|
Минусы:
|
||||||
|
- надо написать рендерер (блиттер глифа + строка + центрирование +
|
||||||
|
подсветка) — небольшой, но свой;
|
||||||
|
- вид определяется качеством шрифта-ассета.
|
||||||
|
|
||||||
|
**Вариант B — готовые спрайт-атласы** (атлас главного меню с активными/
|
||||||
|
неактивными пунктами, атлас вложенного меню, атлас каждой опции
|
||||||
|
On/Off и т.д.).
|
||||||
|
|
||||||
|
Плюсы:
|
||||||
|
- аутентичный вид: любая типографика/декор запекаются при упаковке;
|
||||||
|
- вывод = существующий блит атласов, текстовый движок не нужен;
|
||||||
|
- язык = другой файл атласа с диска, ноль логики.
|
||||||
|
|
||||||
|
Минусы:
|
||||||
|
- комбинаторика ассетов: 7 пунктов × состояния + вложенные меню + значения
|
||||||
|
всех опций + все сообщения + все диалоги ≈ десятки КБ raw на язык до RLE;
|
||||||
|
второй язык удваивает;
|
||||||
|
- любая правка текста = перегенерация ассетов + перекладка ресурсов;
|
||||||
|
- динамический текст (HoF initials) всё равно потребует шрифтового движка —
|
||||||
|
получили бы ОБЕ системы сразу.
|
||||||
|
|
||||||
|
**Решение (2026-08-22): Вариант A**, шрифт — ассет. Спрайты остаются только
|
||||||
|
для нетекстового декора (рамка/фон меню, маркер выделения — как arrowheads
|
||||||
|
в SDLPoP). Титульный экран — полноэкранная картинка, тема `full_game_plan.md`.
|
||||||
|
|
||||||
|
### 10.2. Референс: как устроено меню в SDLPoP
|
||||||
|
|
||||||
|
`SDLPoP/src/menu.c` + текстовый движок `seg009` — источник структуры:
|
||||||
|
|
||||||
|
- **Текстовые строки + встроенный пропорциональный bitmap-шрифт**
|
||||||
|
`hc_small_font_data[]` (menu.c:2488): символы 32..126, каждый глиф —
|
||||||
|
монохромное изображение переменной ширины; `font_type`
|
||||||
|
{first_char, last_char, space_between_chars, height_above_baseline, chtab}.
|
||||||
|
Никаких per-item атласов, хотя SDL_ttf доступен.
|
||||||
|
- Вывод — портированный движок оригинального DOS PoP (seg009):
|
||||||
|
`draw_text_character` → `method_3_blit_mono(image, x, y, textblit,
|
||||||
|
textcolor)`; `get_line_width` для центрирования; перенос по словам.
|
||||||
|
Тем же движком рисуются in-game тексты и copy protection.
|
||||||
|
- Пункты меню — data-driven C-структуры `{id, previous, next, required,
|
||||||
|
char text[32]}` + таблицы `pause_menu_items[]` / `settings_menu_items[]`;
|
||||||
|
`required` — указатель на флаг disabled, такие пункты пропускаются при
|
||||||
|
навигации (prev/next пересчитываются).
|
||||||
|
- Выделенный пункт = смена цвета текста (bright-white против обычного) +
|
||||||
|
рамка-контур `draw_rect_contours(selection_box, lightgray)`; НЕ отдельный
|
||||||
|
спрайт «активного пункта».
|
||||||
|
- Фон меню — затемнение замороженного игрового кадра:
|
||||||
|
`draw_rect_with_alpha(black, alpha=120)`, внизу просвечивает «GAME PAUSED».
|
||||||
|
- Settings — декларативная таблица `setting_type` со стилями TOGGLE / NUMBER /
|
||||||
|
TEXT_ONLY / KEY, геттером/сеттером/increase/decrease значения, строкой-
|
||||||
|
explanation внизу экрана, скроллом длинных списков и фокусом «левая половина
|
||||||
|
(список) / правая половина (значения)».
|
||||||
|
- Диалоги — один общий `draw_confirmation_dialog(text)` + обработчик
|
||||||
|
результата; диалог возвращает решение автомату меню.
|
||||||
|
- Мини-спрайты только для декора значений (arrowheads up/down/left/right).
|
||||||
|
- Навигация озвучена (menu tick), ввод клавиатура+мышь, hover по прямоугольникам.
|
||||||
|
|
||||||
|
### 10.3. Наша реализация
|
||||||
|
|
||||||
|
- Новый банк (свободный номер, автонумерация sprinter-cc): код рендерера,
|
||||||
|
шрифт, таблицы строк, автомат меню. Резидентно — только request-flag и
|
||||||
|
вызов процесса на границе кадра (паттерн pop_qsave_io).
|
||||||
|
- Рендерер портирует контракт seg009, упрощённо: `font_type` + массив
|
||||||
|
глифов переменной ширины, блит монохромного глифа в теневую страницу
|
||||||
|
через W0-окно (как весь остальной код рисования), `draw_string(x,y,color)`
|
||||||
|
+ центрирование по сумме ширин. Пропорциональность — сразу, API не меняется
|
||||||
|
от моноширинного.
|
||||||
|
- Шрифт — АССЕТ, генерируемый паковщиком toolchain из TTF (красивый, сразу
|
||||||
|
с кириллицей), а НЕ системный ZG. Быстрый прототип для MS4 — системный
|
||||||
|
CP866 знакогенератор через существующий `bios_get_zg` в буфер банка
|
||||||
|
(паттерн `gfx_load_default_font`, но буфер в банке, не `_gfx_font_buf`);
|
||||||
|
потом файл шрифта заменяется без смены API.
|
||||||
|
- Двуязычность (eng/rus): строки храним в CP866 — латиница и кириллица одним
|
||||||
|
байтовым порядком, одна кодировка на оба алфавита. Локаль = пара
|
||||||
|
(указатель на таблицу строк, файл шрифта); переключатель — одна настройка.
|
||||||
|
Русские строки длиннее английских ~10–15% — раскладку экранов и ширину
|
||||||
|
колонок закладывать по русской. Второй язык можно добавить позже без
|
||||||
|
переделки: сначала eng.
|
||||||
|
- Подсветка выделенного пункта: инверсия прямоугольника или контур + цвет,
|
||||||
|
как в SDLPoP. Цвета текста брать из стабильного диапазона палитры
|
||||||
|
0x3A..0x3F (его никто не перезаписывает, проверено на борде-индикаторе).
|
||||||
|
- Фон открытого меню: затемнение замороженного кадра (запечь тёмный прямоуг.
|
||||||
|
в теневую копию страницы поверх сохранённого фона) — двойную перерисовку
|
||||||
|
игры под меню делать не нужно, игра стоит.
|
||||||
|
- Навигация: вверх/вниз по пунктам, left/right для значения, Enter/Esc;
|
||||||
|
edge-triggered поверх существующего `kbd_raw`. Звук навигации — menu tick
|
||||||
|
из имеющихся сэмплов.
|
||||||
|
|
||||||
|
Первый UI может быть визуально простым. Критично отсутствие потери клавиш,
|
||||||
|
предсказуемая пауза и отсутствие повреждения игрового back buffer.
|
||||||
|
|
||||||
|
## 11. Диалоги
|
||||||
|
|
||||||
|
Общий диалог подтверждения:
|
||||||
|
|
||||||
|
```text
|
||||||
|
QUIT GAME?
|
||||||
|
RESTORE DEFAULTS?
|
||||||
|
LOAD BACKUP?
|
||||||
|
|
||||||
|
YES / NO
|
||||||
|
```
|
||||||
|
|
||||||
|
Диалог не выполняет действие напрямую: он возвращает решение автомату меню,
|
||||||
|
который формирует команду приложению. Так UI не зависит от gameplay-модулей.
|
||||||
|
|
||||||
|
## 12. Будущий ENHANCED
|
||||||
|
|
||||||
|
Не реализуется сейчас, но дизайн обязан позволять:
|
||||||
|
|
||||||
|
- добавить второй профиль без смены всего UI;
|
||||||
|
- хранить `enhancement_flags` в CFG;
|
||||||
|
- отличать технические исправления порта (всегда включены) от изменений
|
||||||
|
оригинальной механики;
|
||||||
|
- провести аудит уже встроенных исправлений;
|
||||||
|
- покрыть каждый переключаемый fix host/MAME тестом;
|
||||||
|
- при необходимости добавить Advanced screen, не раздувая основной menu.
|
||||||
|
|
||||||
|
До этого момента нельзя рассыпать проверки `if (enhanced)` по горячему коду.
|
||||||
|
Сначала составляется реестр и выбирается минимальная битовая модель.
|
||||||
|
|
||||||
|
## 13. Этапы реализации
|
||||||
|
|
||||||
|
| этап | результат | критерий приёмки |
|
||||||
|
|---|---|---|
|
||||||
|
| **MS0** | определить команды app/menu и структуру settings | UI не вызывает gameplay internals напрямую |
|
||||||
|
| **MS1** | проверить запись/rename/copy на HDD DSS | crash/power-loss сценарий не теряет обе копии save |
|
||||||
|
| **MS2** | `POP.CFG`: defaults, load, validate, save | повреждённый CFG безопасно даёт defaults |
|
||||||
|
| **MS3** | QuickSave hotkeys + POP.SAV/BAK | полный критерий `quicksave_plan.md` |
|
||||||
|
| **MS4** | текстовый рендерер + два шрифта (малый для пунктов, крупный для сообщений) + минимальный pause menu | рендерер строк (глифы, локали, центрирование); все семь пунктов доступны и корректно паузят игру |
|
||||||
|
| **MS5** | General/Gameplay Settings | значения применяются и переживают рестарт |
|
||||||
|
| **MS6** | dialogs + backup recovery | подтверждения и fallback на POP.BAK |
|
||||||
|
| **MS7** | Controls help | полная актуальная раскладка на экране |
|
||||||
|
| **MS8** | интеграция с title/build info | CFG применяется до первого экрана |
|
||||||
|
|
||||||
|
QuickSave (`MS1/MS3`) можно реализовать раньше визуального menu: сначала
|
||||||
|
F6/F9 и сообщения, затем подключить те же команды к пунктам UI.
|
||||||
|
|
||||||
|
## 14. Тесты
|
||||||
|
|
||||||
|
- Host: CFG round-trip, defaults, bad magic/version/size/checksum.
|
||||||
|
- Host: меню navigation, disabled items, confirmations, команды приложению.
|
||||||
|
- Host: рендерер строк — вывод глифов обеих локалей, центрирование,
|
||||||
|
ширина строки для малого и крупного шрифта.
|
||||||
|
- Host: SAV invalid -> BAK valid; оба invalid -> NO QUICKLOAD.
|
||||||
|
- MAME: F6, изменение сцены, F9; затем рестарт программы и повторный F9.
|
||||||
|
- MAME: прервать запись/испортить SAV — BAK остаётся загружаемым.
|
||||||
|
- MAME: pause на бое/падении, Resume не меняет состояние и таймер.
|
||||||
|
- MAME: Settings сохраняются после полного выхода и запуска с HDD.
|
||||||
|
- Проверка лимита 8 DSS handles на каждом error path.
|
||||||
|
- `make size-check`; menu/text строки не должны съесть резидентный бюджет.
|
||||||
|
|
||||||
|
## 15. Не входит в план
|
||||||
|
|
||||||
|
- Mods и выбор levelset;
|
||||||
|
- уровень 15/copy protection;
|
||||||
|
- несколько save slots;
|
||||||
|
- replay/recording;
|
||||||
|
- key rebinding;
|
||||||
|
- SDL visual/controller options;
|
||||||
|
- фактическая реализация ENHANCED и individual fix switches.
|
||||||
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
# QuickSave / QuickLoad — разбор оригинала и план реализации
|
# QuickSave / QuickLoad — разбор оригинала и план реализации
|
||||||
|
|
||||||
Статус: **план, код не начат** (2026-08-17). Задача на доске —
|
Статус: **РЕАЛИЗОВАНО и проверено в MAME** (2026-08-22; F6/F9, POP.SAV +
|
||||||
|
POP.BAK — см. коммит `v0.6-pop-quicksave`). Документ оставлен как
|
||||||
|
справочник по формату снимка и разбору. Задача на доске —
|
||||||
[`../roomtest/TASKS_OPEN.md#qsave`](../roomtest/TASKS_OPEN.md#qsave).
|
[`../roomtest/TASKS_OPEN.md#qsave`](../roomtest/TASKS_OPEN.md#qsave).
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -180,12 +182,14 @@ SDLPoP, сохраняются.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Куда писать снимок: файл, а не EMM-страница
|
## 4. Куда писать снимок: HDD-файл, а не EMM-страница
|
||||||
|
|
||||||
Рекомендация: **основной путь — файл `QUICKSAVE.SAV`; EMM-страница —
|
Решение: **один основной слот `POP.SAV` на HDD; предыдущая
|
||||||
необязательный второй слот.**
|
валидная запись хранится в `POP.BAK`.** EMM-слота нет: программа
|
||||||
|
работает только с HDD, а главный сценарий QuickSave обязан переживать
|
||||||
|
перезапуск игры.
|
||||||
|
|
||||||
> Пересмотрено 2026-08-17 по вопросу пользователя «почему EMM, а не файл».
|
> Пересмотрено 2026-08-21 по вопросу пользователя «почему EMM, а не файл».
|
||||||
> Первая редакция плана рекомендовала EMM — это была ошибка: она взвешивала
|
> Первая редакция плана рекомендовала EMM — это была ошибка: она взвешивала
|
||||||
> скорость и недооценивала главный сценарий использования. Разбор оставлен
|
> скорость и недооценивала главный сценарий использования. Разбор оставлен
|
||||||
> целиком, потому что довод переносится и на другие «положить в память
|
> целиком, потому что довод переносится и на другие «положить в память
|
||||||
@@ -214,10 +218,8 @@ MAME, обязательный после каждой пересборки об
|
|||||||
`_fd_guard` в libc и так стоит), а «не нужен путь и права» — экономия одной
|
`_fd_guard` в libc и так стоит), а «не нужен путь и права» — экономия одной
|
||||||
строки.
|
строки.
|
||||||
|
|
||||||
Что остаётся за EMM: мгновенный слот для «переиграть» без обращения к диску.
|
Обход состояния всё равно писать с абстракцией чтения/записи, как у SDLPoP
|
||||||
Делается тем же сериализатором и добавляется, если понадобится. Поэтому
|
через `process_func`, но второй EMM-слот в scope не входит.
|
||||||
обход состояния писать сразу так, чтобы «куда» было параметром — как у
|
|
||||||
SDLPoP через `process_func`.
|
|
||||||
|
|
||||||
**Проверить ДО кодинга:** пишется ли `test_hdd.chd` из-под MAME. Если образ
|
**Проверить ДО кодинга:** пишется ли `test_hdd.chd` из-под MAME. Если образ
|
||||||
только на чтение, файловый путь упрётся в это на первом же шаге и порядок
|
только на чтение, файловый путь упрётся в это на первом же шаге и порядок
|
||||||
@@ -234,6 +236,7 @@ roomtest.
|
|||||||
+5 pop_current_level 1 Б
|
+5 pop_current_level 1 Б
|
||||||
+6 длина полезной части 2 Б (контроль, что обход совпал)
|
+6 длина полезной части 2 Б (контроль, что обход совпал)
|
||||||
+8 ... поля встык, ОДИН порядок на запись и на чтение ...
|
+8 ... поля встык, ОДИН порядок на запись и на чтение ...
|
||||||
|
.. checksum 2 Б (заголовок + payload)
|
||||||
```
|
```
|
||||||
|
|
||||||
Версия проверяется первой; несовпадение — отказ, как в SDLPoP. Никаких
|
Версия проверяется первой; несовпадение — отказ, как в SDLPoP. Никаких
|
||||||
@@ -282,13 +285,12 @@ static void qs_walk(qs_io_t io) /* io = запись или чтение */
|
|||||||
|
|
||||||
| шаг | что | критерий готовности |
|
| шаг | что | критерий готовности |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **QS1** | Аксессоры/сериализаторы для `static`-состояния банковых модулей: `pop_trob.c` (`room_modif`, `room_seen`, `trobs`, `trob_seed`), `pop_room.c` (`mobs_live`), страница уровня (чтение `fg`) | хост-тест `tests-host/t_qsave.c`: обход туда-обратно на синтетическом состоянии даёт байт-в-байт исходное |
|
|
||||||
| **QS0** | Проверить, что `D:` пишется из-под MAME (пробный файл из roomtest) | файл создался и читается обратно после рестарта программы |
|
| **QS0** | Проверить, что `D:` пишется из-под MAME (пробный файл из roomtest) | файл создался и читается обратно после рестарта программы |
|
||||||
| **QS2** | Ядро: `qs_walk` + запись/чтение файла `QUICKSAVE.SAV`, магия и версия, отказ при несовпадении | сохранение и загрузка **в той же комнате, без движения** — картинка и состояние не изменились |
|
| **QS1** | Аксессоры/сериализаторы для `static`-состояния банковых модулей: `pop_trob.c` (`room_modif`, `room_seen`, `trobs`, `trob_seed`), `pop_room.c` (`mobs_live`), страница уровня (чтение `fg`) | хост-тест `tests-host/t_qsave.c`: обход туда-обратно на синтетическом состоянии даёт байт-в-байт исходное |
|
||||||
|
| **QS2** | Ядро: `qs_walk` + `POP.SAV`, магия/версия/checksum, безопасная замена с предыдущей валидной копией в `POP.BAK` | сохранение и загрузка **в той же комнате, без движения**; порча SAV не портит BAK |
|
||||||
| **QS3** | Восстановление отрисовки (§6), включая обе страницы дабл-буфера | загрузка после перехода в другую комнату; нет мерцания через кадр |
|
| **QS3** | Восстановление отрисовки (§6), включая обе страницы дабл-буфера | загрузка после перехода в другую комнату; нет мерцания через кадр |
|
||||||
| **QS4** | Клавиши **F6/F9** (или свободные из `pop_cheat.h`) через `<kbd_raw.h>`, флаги `need_quick_save/load`, обработка **между кадрами** | загрузка посреди боя/падения не ломает `play_seq` |
|
| **QS4** | Клавиши **F6/F9** (или свободные из `pop_cheat.h`) через `<kbd_raw.h>`, флаги `need_quick_save/load`, обработка **между кадрами** | загрузка посреди боя/падения не ломает `play_seq` |
|
||||||
| **QS5** | Загрузка с **другого уровня** (перезагрузка уровня и атласов) | сохранить на ур. 2, уйти на ур. 12, загрузить — тайлсет и стражи верные |
|
| **QS5** | Загрузка с **другого уровня** (перезагрузка уровня и атласов) | сохранить на ур. 2, уйти на ур. 12, загрузить — тайлсет и стражи верные |
|
||||||
| **QS6** | Опционально: второй слот в EMM-странице тем же сериализатором | мгновенное «переиграть» без обращения к диску |
|
|
||||||
|
|
||||||
Порядок не переставлять: QS0 первым (он может изменить весь план), QS3 без
|
Порядок не переставлять: QS0 первым (он может изменить весь план), QS3 без
|
||||||
QS2 нечего проверять, а QS5 обязан идти после QS3 — иначе смена тайлсета
|
QS2 нечего проверять, а QS5 обязан идти после QS3 — иначе смена тайлсета
|
||||||
@@ -320,5 +322,7 @@ QS2 нечего проверять, а QS5 обязан идти после QS3
|
|||||||
5. **Дабл-буфер.** Самый вероятный источник «почти работает»: забыть вторую
|
5. **Дабл-буфер.** Самый вероятный источник «почти работает»: забыть вторую
|
||||||
страницу. Симптом — мерцание через кадр
|
страницу. Симптом — мерцание через кадр
|
||||||
(см. `roomtest/CLAUDE.md`, раздел про дабл-буфер).
|
(см. `roomtest/CLAUDE.md`, раздел про дабл-буфер).
|
||||||
6. **Открытый вопрос:** нужен ли снимок в файле вообще, или EMM-страницы
|
6. **Транзакция SAV/BAK.** До кодинга проверить на DSS семантику
|
||||||
достаточно. Решать после QS3, по факту использования.
|
rename/replace. Если атомарная замена не гарантирована, писать через
|
||||||
|
`POP.NEW`, проверять его после close и не удалять единственную валидную копию
|
||||||
|
до завершения новой.
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# Бюджет резидента W1/W2: как мерить и как освобождать
|
||||||
|
|
||||||
|
Резидент huge-режима — окно `0x4100..0xBB00` (стек с 0xBB00): код в W1,
|
||||||
|
данные в W2, между концом данных и стеком остаётся куча. Всё, что туда не
|
||||||
|
влезло, живёт в банках.
|
||||||
|
|
||||||
|
## Как СМОТРЕТЬ, а не гадать
|
||||||
|
|
||||||
|
**Карта линкера врёт.** File-static SDCC в неё не попадает, и «дырка» между
|
||||||
|
двумя именованными символами приписывается предыдущему целиком. По карте
|
||||||
|
выходило, что у `pop_bg` 1529 Б данных (на деле 289), а у `pop_t_win_clear`
|
||||||
|
1282 Б кода — при том, что это однострочник, а 1282 Б это два статических
|
||||||
|
помощника соседнего `pop_blit_b`.
|
||||||
|
|
||||||
|
Точный источник — объектные файлы: строки `A <area> size <n> flags <f>` в
|
||||||
|
`.rel` дают ровный размер каждой области модуля, а `S <sym> Def/Ref` — кто
|
||||||
|
символ определяет и кто на него ссылается.
|
||||||
|
|
||||||
|
**Частоту вызовов мерить в MAME счётчиком**, а не оценивать по смыслу:
|
||||||
|
|
||||||
|
```
|
||||||
|
bpset <frame_probe>,1,{printf "F %d ...",temp0,...; temp0=0;...; g}
|
||||||
|
bpset <func_addr>,1,{temp0=temp0+1; g}
|
||||||
|
```
|
||||||
|
|
||||||
|
Обязательна **канарейка** — счётчик заведомо горячей функции в том же
|
||||||
|
прогоне. Дважды спасала: один раз показала, что перехода комнаты в окне
|
||||||
|
замера не было (все нули), другой — что зонды вообще не встали (в zsh
|
||||||
|
`set -- $pair` НЕ разбивает строку на слова, и адрес уезжал в мусор).
|
||||||
|
Полную перерисовку комнаты форсировать читом `+`/`-`, ходьбой ненадёжно.
|
||||||
|
|
||||||
|
## Сделано
|
||||||
|
|
||||||
|
### 1. malloc вон из резидента (−613 Б)
|
||||||
|
|
||||||
|
`cbl_open` держал `malloc`/`free` в мёртвой ветке `CBL_UNDERRUN_SILENCE`, а
|
||||||
|
линкер тянет `.rel` целиком — и куча приезжала каждому приложению. Разведены
|
||||||
|
две публичные точки входа (`cbl_open` / `cbl_open_silence`) поверх общего
|
||||||
|
`_cbl_open_raw`; `cbl_close` больше не зовёт `free`.
|
||||||
|
|
||||||
|
### 2. Разрез pop_tile: холодная половина в банк 5 (−1788 Б)
|
||||||
|
|
||||||
|
`pop_tile.c` был крупнейшим жильцом резидента (5 972 Б кода). Целиком он не
|
||||||
|
уедет: его const-таблицы (`POP_TILE_DIV/MOD`, `pop_tile_table`, таблицы
|
||||||
|
кадров) читают банки 2, 3, 7 и 8, а таблица в чужом банке не видна.
|
||||||
|
|
||||||
|
Отбирали ЗАМЕРОМ, на двух тайлсетах (подземелье ур. 1 и дворец ур. 4 —
|
||||||
|
`pop_mem_b` рисует композитный кусок и мог оказаться дворцовым). Порог —
|
||||||
|
пик не больше 3 вызовов на кадр.
|
||||||
|
|
||||||
|
| уехало в банк 5 | пик/кадр | | осталось в резиденте | пик/кадр |
|
||||||
|
|---|---:|---|---|---:|
|
||||||
|
| `pop_mem_b` | 0 | | `pop_tile_code` | 296 |
|
||||||
|
| `pop_cd_hit` (+`hit_rect`) | 0 | | `pop_cd_touch` | 198 |
|
||||||
|
| `pop_t_win_set/clear` | 0..1 | | `pop_blit_b` (+2 статика) | 184 |
|
||||||
|
| `pop_heal_off` | 0..1 | | `pop_wall_modifier` | 101 |
|
||||||
|
| `pop_potion_flask` | 0..1 | | `pop_env_b` | 73 |
|
||||||
|
| `pop_room_set_above/below` | 1 | | `pop_tile_mod` | 70 |
|
||||||
|
| `pop_cd_init/clear` | 1 | | `pop_cd_batch_end` | 40 |
|
||||||
|
| `pop_bar_black` | 3 | | `pop_fore_set_clip` | 2 |
|
||||||
|
| `pop_cd_hit_slot` | 2..3 | | все const-таблицы | — |
|
||||||
|
|
||||||
|
`pop_fore_set_clip` (88 Б) оставлен намеренно: не стоит отказа от прямого
|
||||||
|
вызова из банка 4, ради которого он и заводился.
|
||||||
|
|
||||||
|
**Цена трамплина замерена**: 252 такта пролог + 84 эпилог + ~50 на стороне
|
||||||
|
вызывающего = **~410 тактов** на вызов. Итого ~1 000 тактов на кадр покоя
|
||||||
|
(0,2 % работы) и ~3 700 на кадр редрава (0,009 растра).
|
||||||
|
|
||||||
|
**Ключ, почему это безопасно:** вызов банк → резидент ПРЯМОЙ, трамплин не
|
||||||
|
нужен (W1/W2 замаплены всегда). Поэтому `blit_b_clip` просто перестал быть
|
||||||
|
`static` и объявлен в `_pop_tile.h`, а не переехал следом за `pop_mem_b`.
|
||||||
|
|
||||||
|
### Итог
|
||||||
|
|
||||||
|
| | было | стало |
|
||||||
|
|---|---:|---:|
|
||||||
|
| `_CODE` резидента | 24 329 | **21 928** |
|
||||||
|
| свободно до стека | **129 Б** | **2 535 Б** |
|
||||||
|
| BANK5 | 2 080 (13 %) | 3 954 (24 %) |
|
||||||
|
|
||||||
|
Проверено в MAME: уровень 1 (подземелье) и уровень 4 (дворец), переходы
|
||||||
|
комнат читом `+`, ходьба — фон, факелы, решётки, гобелены, колонны без
|
||||||
|
искажений.
|
||||||
|
|
||||||
|
## ЛОВУШКА: данные банка в его страницу — НЕ ДЕЛАТЬ без разбора
|
||||||
|
|
||||||
|
Отдельная попытка (`--bank-data=SRC`, коммиты 3545826/9025573) **откачена**:
|
||||||
|
перенос писучих данных банкового модуля в его 16-КБ страницу давал цветной
|
||||||
|
мусор блоками и ронял DSS.
|
||||||
|
|
||||||
|
У `pop_trob` причина найдена: `pop_trob_modif()` ВОЗВРАЩАЕТ УКАЗАТЕЛЬ на
|
||||||
|
`room_modif[24][30]`, а зовут её из банков 2, 3, 7 и резидента — после
|
||||||
|
переноса они пишут по 0xC000+ в СВОЮ страницу, поверх чужого кода.
|
||||||
|
Def/Ref-анализ такого не видит: снаружи ссылки на символ нет, есть ссылка на
|
||||||
|
функцию, отдающую его адрес. Но и `pop_room`, у которого утечки указателя
|
||||||
|
найти не удалось, ломался так же — механизм понят не до конца.
|
||||||
|
|
||||||
|
Нулевая инициализация при этом ни при чём: `mkexe -p 0` был проверен по
|
||||||
|
образу (прогон нулей 14 304 Б, самый длинный прогон 0xFF — 14).
|
||||||
|
|
||||||
|
**Перенос КОДА в банк — штатный путь, на нём стоят все наши банки. Ломался
|
||||||
|
именно перенос ДАННЫХ.**
|
||||||
|
|
||||||
|
## Что осталось
|
||||||
|
|
||||||
|
- `roomtest.c` 2 508 Б и `pop_kid.c` 2 418 Б — следующие по величине, но оба
|
||||||
|
горячие (главный цикл и `play_seq`).
|
||||||
|
- `pop_level.c` 956 Б кода + 660 Б данных (из них `pop_dl1`/`pop_dl2` по
|
||||||
|
256 Б — таблицы дверных связей).
|
||||||
|
- BANK7 на 77 %: если понадобится место в нём — выносить `pop_redraw.c`.
|
||||||
@@ -35,7 +35,32 @@ MEMORY ?= huge # small-раскладка + банки кода в W
|
|||||||
# ВАЖНО: любое сравнение занятости банков имеет смысл только при ОДНОМ и том
|
# ВАЖНО: любое сравнение занятости банков имеет смысл только при ОДНОМ и том
|
||||||
# же ALLOCS — иначе сравниваются не правки, а уровни оптимизации.
|
# же ALLOCS — иначе сравниваются не правки, а уровни оптимизации.
|
||||||
ALLOCS ?= 3000
|
ALLOCS ?= 3000
|
||||||
EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/../poc/res/bg --max-allocs $(ALLOCS) --bank 2=pop_bg.c --bank 7=pop_room.c --bank 4=pop_cdraw.c --bank 4=pop_kdraw.c --bank 3=pop_map.c --bank 1=guards.c --bank 5=pop_ctrl.c --bank 6=pop_trob.c --bank 7=pop_redraw.c --bank 8=roomtest_cold.c --bank 8=pop_level_cold.c --bank 8=pop_kboot.c --bank 8=pop_guard_cold.c --bank 8=pop_shadow.c --bank 8=pop_sfx_cold.c $(PROF_FLAGS)
|
# Единственный список банковых исходников: из него строятся И ключи
|
||||||
|
# sprinter-cc, И зависимости roomtest.exe. Раньше эти списки жили отдельно,
|
||||||
|
# поэтому добавленные pop_shadow.c/pop_sfx_cold.c компилировались, но make не
|
||||||
|
# замечал их последующие изменения и оставлял устаревший exe.
|
||||||
|
BANK1_SRCS := guards.c
|
||||||
|
BANK2_SRCS := pop_bg.c
|
||||||
|
BANK3_SRCS := pop_map.c
|
||||||
|
BANK4_SRCS := pop_cdraw.c pop_kdraw.c
|
||||||
|
BANK5_SRCS := pop_ctrl.c pop_tile_cold.c
|
||||||
|
BANK6_SRCS := pop_trob.c
|
||||||
|
BANK7_SRCS := pop_room.c pop_redraw.c
|
||||||
|
BANK8_SRCS := roomtest_cold.c pop_level_cold.c pop_kboot.c pop_guard_cold.c \
|
||||||
|
pop_shadow.c pop_sfx_cold.c pop_qsave.c pop_qsave_coldio.c \
|
||||||
|
pop_level_qsave.c
|
||||||
|
BANKED_SRCS := $(BANK1_SRCS) $(BANK2_SRCS) $(BANK3_SRCS) $(BANK4_SRCS) \
|
||||||
|
$(BANK5_SRCS) $(BANK6_SRCS) $(BANK7_SRCS) $(BANK8_SRCS)
|
||||||
|
BANK_FLAGS := $(foreach f,$(BANK1_SRCS),--bank 1=$(f)) \
|
||||||
|
$(foreach f,$(BANK2_SRCS),--bank 2=$(f)) \
|
||||||
|
$(foreach f,$(BANK3_SRCS),--bank 3=$(f)) \
|
||||||
|
$(foreach f,$(BANK4_SRCS),--bank 4=$(f)) \
|
||||||
|
$(foreach f,$(BANK5_SRCS),--bank 5=$(f)) \
|
||||||
|
$(foreach f,$(BANK6_SRCS),--bank 6=$(f)) \
|
||||||
|
$(foreach f,$(BANK7_SRCS),--bank 7=$(f)) \
|
||||||
|
$(foreach f,$(BANK8_SRCS),--bank 8=$(f))
|
||||||
|
EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/../poc/res/bg --max-allocs $(ALLOCS) \
|
||||||
|
$(BANK_FLAGS) $(PROF_FLAGS)
|
||||||
# Профилирование полосами бордюра (см. PROF() в roomtest.c) — ВКЛЮЧЕНО по
|
# Профилирование полосами бордюра (см. PROF() в roomtest.c) — ВКЛЮЧЕНО по
|
||||||
# умолчанию, пока идёт работа с производительностью; make PROF=0 выключает.
|
# умолчанию, пока идёт работа с производительностью; make PROF=0 выключает.
|
||||||
# Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте
|
# Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте
|
||||||
@@ -57,7 +82,7 @@ POS ?= 2
|
|||||||
ifneq ($(strip $(ROOM)),)
|
ifneq ($(strip $(ROOM)),)
|
||||||
PROF_FLAGS += -DDBG_START_ROOM=$(ROOM) -DDBG_START_POS=$(POS)
|
PROF_FLAGS += -DDBG_START_ROOM=$(ROOM) -DDBG_START_POS=$(POS)
|
||||||
endif
|
endif
|
||||||
EXTRA_SRCS := pop_vflip.c pop_pace.c pop_sfx.c pop_state.c pop_draw.c pop_tile.c pop_kid.c pop_level.c pop_geom.c pop_guard.c
|
EXTRA_SRCS := pop_vflip.c pop_pace.c pop_sfx.c pop_state.c pop_draw.c pop_tile.c pop_kid.c pop_level.c pop_geom.c pop_guard.c pop_qsave_io.c
|
||||||
|
|
||||||
BG_DIR := $(CURDIR)/../poc/res/bg
|
BG_DIR := $(CURDIR)/../poc/res/bg
|
||||||
KID_DIR := $(CURDIR)/../poc/res/kid
|
KID_DIR := $(CURDIR)/../poc/res/kid
|
||||||
@@ -127,17 +152,13 @@ SND_DATA := $(foreach n,0 1 2 3 4 5 6 7,$(SND_DIR)/s$(n).bin)
|
|||||||
|
|
||||||
EXTRA_DATA := $(BG_DATA) $(KID_DATA) $(KID_BIN) $(GUARD_DATA) $(SKEL_DATA) \
|
EXTRA_DATA := $(BG_DATA) $(KID_DATA) $(KID_BIN) $(GUARD_DATA) $(SKEL_DATA) \
|
||||||
$(VIZIER_DATA) $(SHADOW_DATA) $(SND_DATA) $(LVL_DATA)
|
$(VIZIER_DATA) $(SHADOW_DATA) $(SND_DATA) $(LVL_DATA)
|
||||||
include $(PROJ_ROOT)/app.mk
|
|
||||||
|
|
||||||
# Образ HDD для MAME: ресурсы раскладываются ПО КАТАЛОГАМ (8.3, как в DSS),
|
# Образ HDD для MAME: ресурсы раскладываются ПО КАТАЛОГАМ (8.3, как в DSS),
|
||||||
# чтобы корень диска не зарастал десятками .atl.
|
# чтобы корень диска не зарастал десятками .atl.
|
||||||
# BG\ — фон (env/wall/fore/pot)
|
# BG\ — фон (env/wall/fore/pot)
|
||||||
# KID\ — персонаж (kid0..27, палитра, меч, таблицы анимации)
|
# KID\ — персонаж (kid0..27, палитра, меч, таблицы анимации)
|
||||||
# GUARD\ — стражи (появятся здесь же)
|
# GUARD\ — стражи (появятся здесь же)
|
||||||
# LEVELS\ — уровни
|
# LEVELS\ — уровни
|
||||||
hdd: $(EXAMPLE).exe
|
HDD_PACK_ARGS := $(EXAMPLE).exe \
|
||||||
$(PROJ_ROOT)/toolchain/make_hdd.sh $(PROJ_ROOT)/mame/v306/IMG/test_hdd.chd \
|
|
||||||
$(EXAMPLE).exe \
|
|
||||||
$(foreach f,$(BG_DATA),BG:$(f)) \
|
$(foreach f,$(BG_DATA),BG:$(f)) \
|
||||||
$(foreach f,$(KID_DATA) $(KID_BIN),KID:$(f)) \
|
$(foreach f,$(KID_DATA) $(KID_BIN),KID:$(f)) \
|
||||||
$(foreach f,$(GUARD_DATA),GUARD:$(f)) \
|
$(foreach f,$(GUARD_DATA),GUARD:$(f)) \
|
||||||
@@ -147,27 +168,105 @@ hdd: $(EXAMPLE).exe
|
|||||||
$(foreach f,$(SND_DATA),SND:$(f)) \
|
$(foreach f,$(SND_DATA),SND:$(f)) \
|
||||||
$(foreach f,$(LVL_DATA),LEVELS:$(f))
|
$(foreach f,$(LVL_DATA),LEVELS:$(f))
|
||||||
|
|
||||||
|
include $(PROJ_ROOT)/app.mk
|
||||||
|
|
||||||
TC := $(PROJ_ROOT)/applications/PoP/toolchain
|
TC := $(PROJ_ROOT)/applications/PoP/toolchain
|
||||||
$(BG_DATA): $(TC)/pop_pack_bg.py $(TC)/render_room.py
|
|
||||||
|
# Упаковщики создают сразу ГРУППУ файлов. Обычное multi-output правило
|
||||||
|
# заставляло GNU make при `-B` запускать один и тот же упаковщик отдельно для
|
||||||
|
# каждого .atl/.bin. Stamp-файл представляет один запуск генератора; все
|
||||||
|
# созданные им файлы зависят от этого stamp без собственных рецептов.
|
||||||
|
RESOURCE_STAMP_DIR := .resource-stamps
|
||||||
|
BG_STAMP := $(RESOURCE_STAMP_DIR)/bg
|
||||||
|
GUARD_STAMP := $(RESOURCE_STAMP_DIR)/guard
|
||||||
|
SKEL_STAMP := $(RESOURCE_STAMP_DIR)/skel
|
||||||
|
VIZIER_STAMP := $(RESOURCE_STAMP_DIR)/vizier
|
||||||
|
SHADOW_STAMP := $(RESOURCE_STAMP_DIR)/shadow
|
||||||
|
SOUND_STAMP := $(RESOURCE_STAMP_DIR)/sound
|
||||||
|
KID_STAMP := $(RESOURCE_STAMP_DIR)/kid
|
||||||
|
KID_BIN_STAMP := $(RESOURCE_STAMP_DIR)/kid-bin
|
||||||
|
|
||||||
|
$(RESOURCE_STAMP_DIR):
|
||||||
|
mkdir -p $@
|
||||||
|
|
||||||
|
$(BG_STAMP): $(TC)/pop_pack_bg.py $(TC)/render_room.py | $(RESOURCE_STAMP_DIR)
|
||||||
cd $(TC) && python3 pop_pack_bg.py dungeon && python3 pop_pack_bg.py palace
|
cd $(TC) && python3 pop_pack_bg.py dungeon && python3 pop_pack_bg.py palace
|
||||||
$(GUARD_DATA): $(TC)/pop_pack_guard.py
|
touch $@
|
||||||
|
$(BG_DATA): $(BG_STAMP)
|
||||||
|
@test -f $@ || { $(MAKE) -B $(BG_STAMP); test -f $@; }
|
||||||
|
|
||||||
|
$(GUARD_STAMP): $(TC)/pop_pack_guard.py | $(RESOURCE_STAMP_DIR)
|
||||||
cd $(TC) && python3 pop_pack_guard.py
|
cd $(TC) && python3 pop_pack_guard.py
|
||||||
|
touch $@
|
||||||
|
$(GUARD_DATA): $(GUARD_STAMP)
|
||||||
|
@test -f $@ || { $(MAKE) -B $(GUARD_STAMP); test -f $@; }
|
||||||
|
|
||||||
$(SKEL_DATA): $(TC)/pop_pack_guard.py
|
$(SKEL_STAMP): $(TC)/pop_pack_guard.py | $(RESOURCE_STAMP_DIR)
|
||||||
cd $(TC) && python3 pop_pack_guard.py SKEL
|
cd $(TC) && python3 pop_pack_guard.py SKEL
|
||||||
|
touch $@
|
||||||
|
$(SKEL_DATA): $(SKEL_STAMP)
|
||||||
|
@test -f $@ || { $(MAKE) -B $(SKEL_STAMP); test -f $@; }
|
||||||
|
|
||||||
$(VIZIER_DATA): $(TC)/pop_pack_guard.py
|
$(VIZIER_STAMP): $(TC)/pop_pack_guard.py | $(RESOURCE_STAMP_DIR)
|
||||||
cd $(TC) && python3 pop_pack_guard.py VIZIER
|
cd $(TC) && python3 pop_pack_guard.py VIZIER
|
||||||
|
touch $@
|
||||||
|
$(VIZIER_DATA): $(VIZIER_STAMP)
|
||||||
|
@test -f $@ || { $(MAKE) -B $(VIZIER_STAMP); test -f $@; }
|
||||||
|
|
||||||
$(SHADOW_DATA) pop_shadow_atlas.h: $(TC)/pop_pack_shadow.py $(TC)/pop_pack_bg.py $(TC)/pop_pack_kid.py
|
$(SHADOW_STAMP): $(TC)/pop_pack_shadow.py $(TC)/pop_pack_bg.py $(TC)/pop_pack_kid.py | $(RESOURCE_STAMP_DIR)
|
||||||
cd $(TC) && python3 pop_pack_shadow.py
|
cd $(TC) && python3 pop_pack_shadow.py
|
||||||
|
touch $@
|
||||||
|
$(SHADOW_DATA) pop_shadow_atlas.h: $(SHADOW_STAMP)
|
||||||
|
@test -f $@ || { $(MAKE) -B $(SHADOW_STAMP); test -f $@; }
|
||||||
|
|
||||||
$(SND_DATA) pop_sound_tbl.h: $(TC)/pop_pack_sound.py
|
$(SOUND_STAMP): $(TC)/pop_pack_sound.py | $(RESOURCE_STAMP_DIR)
|
||||||
cd $(TC) && python3 pop_pack_sound.py
|
cd $(TC) && python3 pop_pack_sound.py
|
||||||
|
touch $@
|
||||||
|
$(SND_DATA) pop_sound_tbl.h: $(SOUND_STAMP)
|
||||||
|
@test -f $@ || { $(MAKE) -B $(SOUND_STAMP); test -f $@; }
|
||||||
|
|
||||||
$(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py
|
$(KID_STAMP): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py | $(RESOURCE_STAMP_DIR)
|
||||||
cd $(TC) && python3 pop_pack_kid.py
|
cd $(TC) && python3 pop_pack_kid.py
|
||||||
kid_data.h $(KID_BIN): $(TC)/pop_extract_kid_data.py
|
touch $@
|
||||||
cd $(TC) && python3 pop_extract_kid_data.py
|
$(KID_DATA): $(KID_STAMP)
|
||||||
|
@test -f $@ || { $(MAKE) -B $(KID_STAMP); test -f $@; }
|
||||||
|
|
||||||
$(EXAMPLE).exe: roomtest_cold.c roomtest_cold.h guards.c pop_guard.c pop_guard_cold.c pop_guard.h pop_cdraw.c pop_cdraw.h pop_draw.c _pop_draw.h pop_tile.c pop_tile.h pop_state.c pop_state.h pop_cheat.h pop_bg.c pop_bg.h pop_room.c _pop_bg.h pop_geom.c pop_geom.h pop_redraw.c pop_redraw.h pop_vflip.c pop_vflip.h pop_kid.c pop_kdraw.c pop_kboot.c _pop_kid.h _pop_kdraw.h pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h pop_level.c pop_level_cold.c _pop_level.h pop_level.h pop_trob.c pop_trob.h kid_data.h $(BG_DATA) $(KID_DATA) $(KID_BIN) $(LVL_DATA)
|
$(KID_BIN_STAMP): $(TC)/pop_extract_kid_data.py | $(RESOURCE_STAMP_DIR)
|
||||||
|
cd $(TC) && python3 pop_extract_kid_data.py
|
||||||
|
touch $@
|
||||||
|
kid_data.h $(KID_BIN): $(KID_BIN_STAMP)
|
||||||
|
@test -f $@ || { $(MAKE) -B $(KID_BIN_STAMP); test -f $@; }
|
||||||
|
|
||||||
|
# Явные цели позволяют собирать ресурсы без `make -B`, который по смыслу
|
||||||
|
# принудительно пересобирает также libc, libbgi и само приложение.
|
||||||
|
resources-bg: $(BG_DATA)
|
||||||
|
resources-kid: $(KID_DATA) $(KID_BIN)
|
||||||
|
resources-actors: $(GUARD_DATA) $(SKEL_DATA) $(VIZIER_DATA) $(SHADOW_DATA)
|
||||||
|
resources-sound: $(SND_DATA)
|
||||||
|
resources-levels: $(LVL_DATA)
|
||||||
|
resources: resources-bg resources-kid resources-actors resources-sound resources-levels
|
||||||
|
resources-rebuild:
|
||||||
|
$(MAKE) -B resources
|
||||||
|
|
||||||
|
# Принудительная перелинковка только roomtest, без глобального `-B` и без
|
||||||
|
# принудительной пересборки библиотек/ресурсов.
|
||||||
|
relink: resources
|
||||||
|
$(MAKE) -W Makefile $(EXAMPLE).exe
|
||||||
|
|
||||||
|
# HDD_PACK_ARGS содержит все эти файлы; make обязан подготовить их до запуска
|
||||||
|
# упаковщика и при чистой сборке, а не полагаться на старое содержимое дерева.
|
||||||
|
hdd: resources
|
||||||
|
|
||||||
|
# sprinter-cc собирает все модули одним вызовом и depfile пока не выдаёт.
|
||||||
|
# Банковые .c берём из того же списка, что формирует --bank; любой локальный
|
||||||
|
# заголовок также обязан перелинковать приложение. wildcard намеренно
|
||||||
|
# широкий: цена лишней полной сборки ниже риска получить stale-бинарник.
|
||||||
|
LOCAL_HEADERS := $(wildcard *.h)
|
||||||
|
$(EXAMPLE).exe: Makefile $(PROJ_ROOT)/app.mk $(BANKED_SRCS) $(LOCAL_HEADERS) \
|
||||||
|
$(BG_DATA) $(KID_DATA) $(KID_BIN) $(LVL_DATA)
|
||||||
|
|
||||||
|
clean-resources:
|
||||||
|
rm -rf $(RESOURCE_STAMP_DIR)
|
||||||
|
|
||||||
|
.PHONY: resources resources-rebuild resources-bg resources-kid resources-actors relink \
|
||||||
|
resources-sound resources-levels clean-resources
|
||||||
|
|||||||
@@ -11,6 +11,33 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## QuickSave
|
||||||
|
|
||||||
|
<a id="qsave"></a>
|
||||||
|
### QSAVE. QuickSave / QuickLoad — **РЕАЛИЗОВАНО И ПРОВЕРЕНО 2026-08-22** (коммит f4b4852, тэг v0.6-pop-quicksave)
|
||||||
|
|
||||||
|
F6/F9 в roomtest. Формат снимка `'POPQ'` v3: заголовок
|
||||||
|
{P,O,P,Q,ver=3,level,reserved}, payload всех игровых переменных (Kid/Guard
|
||||||
|
через qs_char, HP, fight seed, читы speed/immortal/sound с range-валидацией
|
||||||
|
на загрузке), хвост = размер payload + XOR-контрольная сумма (старший байт =
|
||||||
|
инвертированный XOR). Носитель — файл на HDD: транзакционная запись
|
||||||
|
`unlink POP.NEW → bank_save_file → rename SAV→BAK → rename NEW→SAV`
|
||||||
|
с откатом; загрузка пробует POP.SAV, потом POP.BAK; при другом уровне —
|
||||||
|
`pop_next_level+pop_level_switch`. Сериализация через резидентные W0-примитивы
|
||||||
|
`pop_qs_*` (паттерн request-flag + `pop_qsave_process()` на границе кадра вне
|
||||||
|
Char-окон); сериализаторы добавлены в pop_map/pop_loose_mob/pop_trob/
|
||||||
|
pop_guard_ai с восстановлением инвариантов (mobs_live, trob_drawn, redraw).
|
||||||
|
Дабл-буфер закрыт `pop_qsave_restore_room()`: полная перезагрузка комнаты +
|
||||||
|
перерисовка ОБЕИХ страниц + инвалидация кэшей спрайтов и HP. Три ГСЧ
|
||||||
|
(`pop_t_seed`, `trob_seed`, `pop_fight_seed`) — все в payload.
|
||||||
|
Файловое I/O — новые `bank_load_file`/`bank_save_file` libc (без правила W3).
|
||||||
|
|
||||||
|
Критерий приёмки выполнен: сохранить, выйти по ESC, запустить заново,
|
||||||
|
загрузить — оказались там же; проверено в MAME. Полный разбор и формат —
|
||||||
|
[`../docs/quicksave_plan.md`](../docs/quicksave_plan.md) (теперь справочник).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Уровни: спецсобытия
|
## Уровни: спецсобытия
|
||||||
|
|
||||||
<a id="l8-mouse"></a>
|
<a id="l8-mouse"></a>
|
||||||
|
|||||||
@@ -1090,37 +1090,12 @@ SPIKE-BAKED, а до того — мерцание плиты, ради кото
|
|||||||
раскладки в `tests-host/README.md`).
|
раскладки в `tests-host/README.md`).
|
||||||
|
|
||||||
|
|
||||||
### <a id="qsave"></a>QSAVE. QuickSave / QuickLoad
|
### <a id="qsave"></a>QSAVE. QuickSave / QuickLoad — ЗАКРЫТА 2026-08-22
|
||||||
|
|
||||||
> **План целиком — [`../docs/quicksave_plan.md`](../docs/quicksave_plan.md)**
|
**Закрыта** (F6/F9, POP.SAV + POP.BAK, коммит f4b4852, тэг v0.6-pop-quicksave).
|
||||||
> (разбор SDLPoP, инвентаризация нашего состояния, формат снимка, порядок
|
Протокол и устройство — [`TASKS_CLOSED.md`](TASKS_CLOSED.md#qsave);
|
||||||
> восстановления, разбиение на шаги QS1…QS6, риски). Изучено 2026-08-17,
|
формат снимка и разбор SDLPoP —
|
||||||
> **код не начат.**
|
[`../docs/quicksave_plan.md`](../docs/quicksave_plan.md).
|
||||||
|
|
||||||
Оговорка, с которой начинается план: **в оригинале 1989 года этого нет
|
|
||||||
вовсе**, QuickSave — enhancement SDLPoP (`seg000.c`, `USE_QUICKSAVE`,
|
|
||||||
F6/F9). Значит повторяем не букву, а устройство.
|
|
||||||
|
|
||||||
Что взять у SDLPoP без изменений: снимок — плоский список полей, один обход
|
|
||||||
на запись и на чтение (`#define process(x)`), совместимость держится
|
|
||||||
строкой версии и ничем больше; клавиша только взводит флаг, работа идёт
|
|
||||||
**между кадрами**; состояние ОТРИСОВКИ не сохраняется вовсе — комната
|
|
||||||
перерисовывается с нуля.
|
|
||||||
|
|
||||||
Чем наш случай тяжелее: уровень лежит в EMM-странице, а не в ОЗУ;
|
|
||||||
`room_modif`/`trobs` — `static` в банковом `pop_trob.c` (нужны
|
|
||||||
сериализаторы); ГСЧ у нас **три** (`pop_t_seed`, `trob_seed`,
|
|
||||||
`pop_fight_seed`), и пропуск любого даст «загрузилось, но играется иначе»;
|
|
||||||
и главное — **дабл-буфер**: перерисовать после загрузки надо ОБЕ страницы,
|
|
||||||
иначе старая картинка мигнёт через кадр.
|
|
||||||
|
|
||||||
Носитель — **файл** (`QUICKSAVE.SAV`, ≈1,9 КБ), EMM-страница отвергнута:
|
|
||||||
она не переживает рестарт программы, а именно рестарт — тот случай, ради
|
|
||||||
которого QuickSave и нужен (сцену каскада на 13/23 воспроизводит только
|
|
||||||
`ESC` → запуск заново). Мгновенный EMM-слот остаётся необязательным QS6.
|
|
||||||
|
|
||||||
**Критерий приёмки:** сохранить, выйти по `ESC`, запустить roomtest заново,
|
|
||||||
загрузить — и оказаться там же.
|
|
||||||
|
|
||||||
### <a id="l1-speed"></a>L1-SPEED. Игра идёт быстрее оригинала (найдено 2026-08-01) — ЗАКРЫТА 2026-08-19
|
### <a id="l1-speed"></a>L1-SPEED. Игра идёт быстрее оригинала (найдено 2026-08-01) — ЗАКРЫТА 2026-08-19
|
||||||
|
|
||||||
|
|||||||
@@ -20,10 +20,7 @@ extern uint8_t kdat_blk; /* EMM-блок страницы kid_data.bin */
|
|||||||
extern uint8_t kdat_page; /* её номер (маппится в W0 на время play_seq) */
|
extern uint8_t kdat_page; /* её номер (маппится в W0 на время play_seq) */
|
||||||
extern uint8_t kdat_ok; /* данные загружены */
|
extern uint8_t kdat_ok; /* данные загружены */
|
||||||
|
|
||||||
/* ПРАВИЛО ОКНА W3: банковый модуль исполняется из W3 и переключать это окно
|
/* Файловый обмен с W3 делает общий резидентный bank_load_file(), поэтому
|
||||||
* не может (следующая инструкция придёт уже из чужой страницы). Поэтому
|
* оркестратор загрузки kid_data.bin целиком живёт в холодном банке 8. */
|
||||||
* ЧТЕНИЕ kid_data.bin — pop_kid_data_load — осталось в резиденте, а в банк
|
|
||||||
* уехало только то, что читает через W0 или вообще не трогает окна.
|
|
||||||
* То же правило и в _pop_level.h. */
|
|
||||||
|
|
||||||
#endif /* _POP_KID_H */
|
#endif /* _POP_KID_H */
|
||||||
|
|||||||
@@ -69,13 +69,9 @@ static const uint8_t *room_bg_ptr(uint8_t r)
|
|||||||
return (const uint8_t *)(LVL_DATA_OFF + BP_BG) + (uint16_t)(r - 1) * ROOM_TILES;
|
return (const uint8_t *)(LVL_DATA_OFF + BP_BG) + (uint16_t)(r - 1) * ROOM_TILES;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ПРАВИЛО ОКНА W3. Банковый модуль исполняется ИЗ W3, поэтому сам он
|
/* ПРАВИЛО ОКНА W3. Банковый модуль исполняется ИЗ W3 и сам менять окно
|
||||||
* менять это окно НЕ МОЖЕТ: после sprinter_page_w3(другая_страница)
|
* не может. Файлы в EMM читает резидентный bank_load_file(), после его
|
||||||
* следующая инструкция читается уже из чужой страницы. Всё чтение файлов
|
* возврата холодный код продолжает подготовку страницы через W0. */
|
||||||
* (оно грузит данные по 0xC000) обязано жить в резиденте — отсюда
|
|
||||||
* pop_level_read_file здесь, а не в pop_level_cold.c. Через W0
|
|
||||||
* (gfx_w0_map) банкам работать можно: это другое окно. */
|
|
||||||
int pop_level_read_file(const char *path);
|
|
||||||
|
|
||||||
/* Живая копия стражей из данных уровня (загрузка/рестарт) — в холодной. */
|
/* Живая копия стражей из данных уровня (загрузка/рестарт) — в холодной. */
|
||||||
void pop_gstate_init(void) __banked;
|
void pop_gstate_init(void) __banked;
|
||||||
|
|||||||
@@ -19,8 +19,4 @@ extern uint8_t pop_snd_ok;
|
|||||||
* cbl_open() холодной половиной. */
|
* cbl_open() холодной половиной. */
|
||||||
int pop_sfx_fill(uint16_t n);
|
int pop_sfx_fill(uint16_t n);
|
||||||
|
|
||||||
/* Прочитать файл целиком в ФИЗИЧЕСКУЮ страницу (через W3). Резидентный:
|
|
||||||
* банковый код, переключив W3, вырезал бы сам себя. */
|
|
||||||
int pop_page_read(const char *path, uint8_t phys, uint16_t want);
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
* _pop_tile.h — внутренний контракт между половинами общих листьев слоя
|
||||||
|
* фона (НЕ публичный, публичный — pop_tile.h). Горячая половина живёт в
|
||||||
|
* резиденте (pop_tile.c), холодная — в банке 5 (pop_tile_cold.c);
|
||||||
|
* критерий разреза и замеры — в шапке pop_tile_cold.c.
|
||||||
|
*/
|
||||||
|
#ifndef _POP_TILE_INTERNAL_H
|
||||||
|
#define _POP_TILE_INTERNAL_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/* Клипованный путь блита — статик горячей половины, ставший общим: его
|
||||||
|
* зовёт pop_mem_b из холодной. Переносить его следом НЕЛЬЗЯ (его основной
|
||||||
|
* вызывающий pop_blit_b делает 184 вызова на кадр редрава), а вызов
|
||||||
|
* банк -> резидент и так прямой: W1/W2 замаплены всегда. */
|
||||||
|
void blit_b_clip(const uint8_t *img, int x, int top, uint8_t w, uint8_t h);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -265,6 +265,12 @@ void pop_check_mouse(void) __banked
|
|||||||
static uint8_t demo_time; /* кадров с начала записи (0xFE = стоп) */
|
static uint8_t demo_time; /* кадров с начала записи (0xFE = стоп) */
|
||||||
static uint8_t demo_index; /* текущая запись */
|
static uint8_t demo_index; /* текущая запись */
|
||||||
|
|
||||||
|
void pop_guard_ai_qsave(pop_qs_io_t *io) __banked
|
||||||
|
{
|
||||||
|
pop_qs_u8(io, &demo_time);
|
||||||
|
pop_qs_u8(io, &demo_index);
|
||||||
|
}
|
||||||
|
|
||||||
#define SHADOW_SKILL 3
|
#define SHADOW_SKILL 3
|
||||||
#define SHADOW_HP 4
|
#define SHADOW_HP 4
|
||||||
#define SEQ_2_STAND 2
|
#define SEQ_2_STAND 2
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
Уровень 4, комната 22 - страж идет навстречу Киду и попадает сам в челюсти - должен быть инстинк самосохранения
|
||||||
|
|
||||||
|
Перед решеткой Кид делает лишний мелкий шаг.
|
||||||
|
|
||||||
|
Уровень 5, комната 2 - с разбега чуть раньше - иногда попадаем в стену (некорректное отображение падения)
|
||||||
|
|
||||||
|
Уровень 7, комната 9 - падает плита -1,1 - звук потери HP у Кида (причем раз даже когда Кид просто бъет по плите стоя на 0,0)
|
||||||
|
|
||||||
|
Уровень 10, комната 1 - когда Кид стоя на кнопке 1,8 роняет плиту 0,8 на кнопку 1,8 - кнопка ломается (превращается в щебень)
|
||||||
|
но двери которые она должна открыть остаются закрытыми
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include "pop_char.h" /* pop_char_t — общий проход отрисовки поверх Char */
|
#include "pop_char.h" /* pop_char_t — общий проход отрисовки поверх Char */
|
||||||
|
#include "pop_qsave.h"
|
||||||
|
|
||||||
/* ---- Вертикальный layout (PoP-экран 320×200 в нашем 320×256) --------- *
|
/* ---- Вертикальный layout (PoP-экран 320×200 в нашем 320×256) --------- *
|
||||||
* Игровой экран 200px центрируем по высоте: сдвиг всего рисунка вниз на
|
* Игровой экран 200px центрируем по высоте: сдвиг всего рисунка вниз на
|
||||||
@@ -64,12 +65,12 @@ void pop_room_draw(uint8_t room_num,
|
|||||||
* [10] — тайл (0,9) комнаты СНИЗУ-СЛЕВА (сосед угла (2,0); см.
|
* [10] — тайл (0,9) комнаты СНИЗУ-СЛЕВА (сосед угла (2,0); см.
|
||||||
* load_rowbelow, seg008:368). 0 — нет комнаты снизу. */
|
* load_rowbelow, seg008:368). 0 — нет комнаты снизу. */
|
||||||
/* НЕ __banked: тело в резиденте (pop_tile.c) — из банка зовётся прямым call. */
|
/* НЕ __banked: тело в резиденте (pop_tile.c) — из банка зовётся прямым call. */
|
||||||
void pop_room_set_below(const uint8_t *below_row0_fg);
|
void pop_room_set_below(const uint8_t *below_row0_fg) __banked;
|
||||||
|
|
||||||
/* Задать ряд 2 (fg-коды + модификаторы, 10 шт.) комнаты СВЕРХУ — для полосы
|
/* Задать ряд 2 (fg-коды + модификаторы, 10 шт.) комнаты СВЕРХУ — для полосы
|
||||||
* кладки у потолка (seg008 draw_room доп. верхний ряд из room_A). 0 — нет
|
* кладки у потолка (seg008 draw_room доп. верхний ряд из room_A). 0 — нет
|
||||||
* комнаты сверху (кромка уровня → пол). */
|
* комнаты сверху (кромка уровня → пол). */
|
||||||
void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod);
|
void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod) __banked;
|
||||||
|
|
||||||
/* Перерисовать передний слой (fore) и оверлеи тайлов ПОВЕРХ персонажа —
|
/* Перерисовать передний слой (fore) и оверлеи тайлов ПОВЕРХ персонажа —
|
||||||
* ОДИН проход на всех Char (Кид, страж, скелет, дальше тень/визирь): в
|
* ОДИН проход на всех Char (Кид, страж, скелет, дальше тень/визирь): в
|
||||||
@@ -178,6 +179,7 @@ void pop_loose_mob_spawn(int row, int col) __banked;
|
|||||||
void pop_loose_mob_spawn_at(uint8_t room, int row, int col) __banked;
|
void pop_loose_mob_spawn_at(uint8_t room, int row, int col) __banked;
|
||||||
void pop_loose_mob_tick(void) __banked;
|
void pop_loose_mob_tick(void) __banked;
|
||||||
void pop_loose_mob_reset(void) __banked; /* убрать все куски (СТАРТ УРОВНЯ, mobs_count=0) */
|
void pop_loose_mob_reset(void) __banked; /* убрать все куски (СТАРТ УРОВНЯ, mobs_count=0) */
|
||||||
|
void pop_loose_mob_qsave(pop_qs_io_t *io) __banked;
|
||||||
|
|
||||||
/* Забыть, на какой странице сделана первая запечка тайлов (см. bake_copy в
|
/* Забыть, на какой странице сделана первая запечка тайлов (см. bake_copy в
|
||||||
* pop_room.c). Звать при смене комнаты/уровня: номера тайлов повторяются, и
|
* pop_room.c). Звать при смене комнаты/уровня: номера тайлов повторяются, и
|
||||||
|
|||||||
@@ -128,12 +128,12 @@ extern uint8_t pop_cd_dirty; /* биты страниц: ме
|
|||||||
extern uint8_t pop_cd_ymin[2][10], pop_cd_ymax[2][10];
|
extern uint8_t pop_cd_ymin[2][10], pop_cd_ymax[2][10];
|
||||||
/* Задели ли прямоугольник (ЭКРАННЫЕ координаты, x1/y1 включительно) то,
|
/* Задели ли прямоугольник (ЭКРАННЫЕ координаты, x1/y1 включительно) то,
|
||||||
* что трогали на странице p. Резидент (pop_tile.c). */
|
* что трогали на странице p. Резидент (pop_tile.c). */
|
||||||
uint8_t pop_cd_hit(uint8_t p, int x0, int y0, int x1, int y1);
|
uint8_t pop_cd_hit(uint8_t p, int x0, int y0, int x1, int y1) __banked;
|
||||||
/* То же для СЛОТА персонажа: спрайт и накладной, координаты изнутри pop_cd
|
/* То же для СЛОТА персонажа: спрайт и накладной, координаты изнутри pop_cd
|
||||||
* (см. тело — там про цену пяти аргументов). */
|
* (см. тело — там про цену пяти аргументов). */
|
||||||
uint8_t pop_cd_hit_slot(uint8_t who, uint8_t p);
|
uint8_t pop_cd_hit_slot(uint8_t who, uint8_t p) __banked;
|
||||||
void pop_cd_clear(uint8_t p); /* снять метку страницы целиком */
|
void pop_cd_clear(uint8_t p) __banked; /* снять метку страницы целиком */
|
||||||
void pop_cd_init(void); /* пустые диапазоны на обеих страницах */
|
void pop_cd_init(void) __banked; /* пустые диапазоны на обеих страницах */
|
||||||
void pop_cd_touch(int x, int y, int w, int h);
|
void pop_cd_touch(int x, int y, int w, int h);
|
||||||
#define POP_CD_TOUCH_ALL() pop_cd_touch(0, 0, 320, 256)
|
#define POP_CD_TOUCH_ALL() pop_cd_touch(0, 0, 320, 256)
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
#include "kid_data.h" /* kframe */
|
#include "kid_data.h" /* kframe */
|
||||||
#include "pop_char.h"
|
#include "pop_char.h"
|
||||||
#include "pop_geom.h" /* pop_rnd_t — сид бросков боёвки */
|
#include "pop_geom.h" /* pop_rnd_t — сид бросков боёвки */
|
||||||
|
#include "pop_qsave.h"
|
||||||
|
|
||||||
/* seqids (types.h:1052+) — последовательности боя и стойки. */
|
/* seqids (types.h:1052+) — последовательности боя и стойки. */
|
||||||
#define SEQ_55_DRAW_SWORD 55
|
#define SEQ_55_DRAW_SWORD 55
|
||||||
@@ -303,6 +304,7 @@ void pop_check_sword_hurt(void) __banked;
|
|||||||
/* Текущий кадр стража (порт cur_frame для Char): заполняет логика в W1/W2,
|
/* Текущий кадр стража (порт cur_frame для Char): заполняет логика в W1/W2,
|
||||||
* читает отрисовка в резиденте W3. image == 255 — рисовать нечего. */
|
* читает отрисовка в резиденте W3. image == 255 — рисовать нечего. */
|
||||||
extern kframe pop_gframe;
|
extern kframe pop_gframe;
|
||||||
|
void pop_guard_ai_qsave(pop_qs_io_t *io) __banked;
|
||||||
void pop_guard_load_frame(void);
|
void pop_guard_load_frame(void);
|
||||||
|
|
||||||
/* check_guard_fallout (seg002:0241): убрать персонажа слота Guard, если он
|
/* check_guard_fallout (seg002:0241): убрать персонажа слота Guard, если он
|
||||||
|
|||||||
@@ -17,11 +17,6 @@
|
|||||||
#include "pop_vflip.h"
|
#include "pop_vflip.h"
|
||||||
#include "pop_guard.h" /* pop_guard_vflip_load — зеркала соперника */
|
#include "pop_guard.h" /* pop_guard_vflip_load — зеркала соперника */
|
||||||
|
|
||||||
/* ISR-стаб W0-страницы (тот же приём, что atlas_load): при прерывании с
|
|
||||||
* замапленной в W0 страницей CPU прыгает на 0x0038 → там обязан быть JP на
|
|
||||||
* _gfx_w0_isr (восстановит окно). */
|
|
||||||
extern void _gfx_w0_isr(void);
|
|
||||||
|
|
||||||
#define KID_MAXPAGES 28
|
#define KID_MAXPAGES 28
|
||||||
|
|
||||||
int pop_kid_load(uint8_t npages) __banked
|
int pop_kid_load(uint8_t npages) __banked
|
||||||
@@ -50,9 +45,21 @@ void pop_kid_free(void) __banked
|
|||||||
kid_npages = 0;
|
kid_npages = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ЗАГРУЗКА kid_data.bin (pop_kid_data_load) осталась в РЕЗИДЕНТЕ: она
|
int pop_kid_data_load(const char *path) __banked
|
||||||
* маппит страницу в W3 на время чтения, а этот модуль сам в W3 — см.
|
{
|
||||||
* то же правило в _pop_kid.h. */
|
uint8_t blk, page;
|
||||||
|
int n;
|
||||||
|
blk=mem_alloc_pages(1);
|
||||||
|
if (!blk) return -1;
|
||||||
|
page=mem_get_page(blk,0);
|
||||||
|
n=bank_load_file(page,KD_DATA_OFF,path,16384-KD_DATA_OFF);
|
||||||
|
if (n < (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) {
|
||||||
|
mem_free_block(blk); return -1;
|
||||||
|
}
|
||||||
|
gfx_w0_page_prepare(page);
|
||||||
|
kdat_blk=blk; kdat_page=page; kdat_ok=1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
void pop_kid_data_free(void) __banked
|
void pop_kid_data_free(void) __banked
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -61,8 +61,6 @@ static const uint16_t kid_seq_off[KID_NSEQ] = KID_SEQ_OFF_INIT;
|
|||||||
* Это ровно устройство оригинала: load_frame (seg006) заполняет cur_frame,
|
* Это ровно устройство оригинала: load_frame (seg006) заполняет cur_frame,
|
||||||
* и коллизия/отрисовка работают с ним, а не с таблицей.
|
* и коллизия/отрисовка работают с ним, а не с таблицей.
|
||||||
* Страница адресуется как у pop_level: данные с 0x100, ниже — ISR-стаб. */
|
* Страница адресуется как у pop_level: данные с 0x100, ниже — ISR-стаб. */
|
||||||
extern void _gfx_w0_isr(void);
|
|
||||||
|
|
||||||
/* Адрес кадра i в замапленной странице. Считаем В uint16_t и кастуем ОДИН
|
/* Адрес кадра i в замапленной странице. Считаем В uint16_t и кастуем ОДИН
|
||||||
* раз: запись `(const uint8_t *)CONST + (uint16_t)i * 5u` SDCC 4.5 собирает
|
* раз: запись `(const uint8_t *)CONST + (uint16_t)i * 5u` SDCC 4.5 собирает
|
||||||
* НЕВЕРНО — умножение делает в 16 битах (add hl,hl / add hl,bc), а потом
|
* НЕВЕРНО — умножение делает в 16 битах (add hl,hl / add hl,bc), а потом
|
||||||
@@ -449,35 +447,3 @@ void kid_tick(void)
|
|||||||
* W1/W2 — самый дефицитный ресурс. Теперь они лежат в отдельной EMM-
|
* W1/W2 — самый дефицитный ресурс. Теперь они лежат в отдельной EMM-
|
||||||
* странице, которая маппится в W0 РОВНО на время play_seq (раз за тик).
|
* странице, которая маппится в W0 РОВНО на время play_seq (раз за тик).
|
||||||
* Страница адресуется как у pop_level: данные с 0x100, ниже — ISR-стаб. */
|
* Страница адресуется как у pop_level: данные с 0x100, ниже — ISR-стаб. */
|
||||||
/* ЖИВЁТ В РЕЗИДЕНТЕ, хотя зовётся раз за игру: маппит страницу в W3 на
|
|
||||||
* время чтения, а банковый код сам лежит в W3 (см. _pop_kid.h). */
|
|
||||||
int pop_kid_data_load(const char *path)
|
|
||||||
{
|
|
||||||
uint8_t *pg = (uint8_t *)0xC000; /* страница мапится в W3 на время чтения */
|
|
||||||
uint8_t saved_w3, blk;
|
|
||||||
uint16_t stub;
|
|
||||||
int fd, n;
|
|
||||||
|
|
||||||
fd = open(path, O_RDONLY);
|
|
||||||
if (fd < 0) return -1;
|
|
||||||
blk = mem_alloc_pages(1);
|
|
||||||
if (!blk) { close(fd); return -1; }
|
|
||||||
kdat_blk = blk;
|
|
||||||
kdat_page = mem_get_page(blk, 0);
|
|
||||||
|
|
||||||
saved_w3 = _io_page_w3;
|
|
||||||
sprinter_page_w3(kdat_page);
|
|
||||||
n = read(fd, pg + KD_DATA_OFF, 16384 - KD_DATA_OFF);
|
|
||||||
if (n >= (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) {
|
|
||||||
stub = (uint16_t)&_gfx_w0_isr; /* ISR-стаб W0-страницы (как pop_level) */
|
|
||||||
pg[0x38] = 0xC3;
|
|
||||||
pg[0x39] = (uint8_t)(stub & 0xFF);
|
|
||||||
pg[0x3A] = (uint8_t)(stub >> 8);
|
|
||||||
pg[0x66] = 0xED; pg[0x67] = 0x45; /* RETN */
|
|
||||||
}
|
|
||||||
sprinter_page_w3(saved_w3);
|
|
||||||
close(fd);
|
|
||||||
if (n < (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) { mem_free_block(blk); return -1; }
|
|
||||||
kdat_ok = 1;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ int pop_kid_load(uint8_t npages) __banked;
|
|||||||
* (kid_data.bin): в _CODE окна W1/W2 они занимали 3.5 КБ. Грузить ДО
|
* (kid_data.bin): в _CODE окна W1/W2 они занимали 3.5 КБ. Грузить ДО
|
||||||
* первого kid_init/play_seq. 0 — OK, -1 — ошибка (без них Kid не оживёт).
|
* первого kid_init/play_seq. 0 — OK, -1 — ошибка (без них Kid не оживёт).
|
||||||
* Страница маппится в W0 внутри play_seq, раз за тик. */
|
* Страница маппится в W0 внутри play_seq, раз за тик. */
|
||||||
int pop_kid_data_load(const char *path);
|
int pop_kid_data_load(const char *path) __banked;
|
||||||
|
|
||||||
/* Прочитать кадр из таблицы страницы данных: tbl_off — KID_BIN_FRAMES_OFF
|
/* Прочитать кадр из таблицы страницы данных: tbl_off — KID_BIN_FRAMES_OFF
|
||||||
* (Kid) или KID_BIN_GFRAMES_OFF (страж, своя таблица frame_tbl_guard).
|
* (Kid) или KID_BIN_GFRAMES_OFF (страж, своя таблица frame_tbl_guard).
|
||||||
|
|||||||
@@ -17,10 +17,6 @@
|
|||||||
#include <sprinter_mem.h>
|
#include <sprinter_mem.h>
|
||||||
#include <sprite.h> /* gfx_w0_map / gfx_w0_unmap */
|
#include <sprite.h> /* gfx_w0_map / gfx_w0_unmap */
|
||||||
|
|
||||||
/* ISR-стаб W0-страницы (тот же приём, что atlas_load): при прерывании с
|
|
||||||
* замапленной в W0 страницей CPU прыгает на 0x0038 → там обязан быть JP на
|
|
||||||
* _gfx_w0_isr (восстановит окно). */
|
|
||||||
extern void _gfx_w0_isr(void);
|
|
||||||
#include "pop_level.h"
|
#include "pop_level.h"
|
||||||
#include "_pop_level.h"
|
#include "_pop_level.h"
|
||||||
|
|
||||||
@@ -138,53 +134,3 @@ uint8_t pop_guard_state_x(uint8_t room)
|
|||||||
if (room < 1 || room > 24) return 0;
|
if (room < 1 || room > 24) return 0;
|
||||||
return pop_gstate[(uint16_t)(room - 1) * 6 + GS_X];
|
return pop_gstate[(uint16_t)(room - 1) * 6 + GS_X];
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Чтение файла уровня в свежую EMM-страницу. ЖИВЁТ В РЕЗИДЕНТЕ, хотя
|
|
||||||
* зовётся раз на уровень: функция маппит страницу в W3 на время read(), а
|
|
||||||
* весь холодный код лежит В ЭТОМ ЖЕ окне — переключив его оттуда, банк
|
|
||||||
* вырезает из-под себя собственные инструкции (первый же вызов улетал в
|
|
||||||
* halt по мусору, поймано в MAME 2026-08-12). Правило — в _pop_level.h.
|
|
||||||
* Наружу не публичная в pop_level.h: уровень грузят по НОМЕРУ. */
|
|
||||||
int pop_level_read_file(const char *path)
|
|
||||||
{
|
|
||||||
uint8_t *pg = (uint8_t *)0xC000; /* страница мапится в W3 на время чтения */
|
|
||||||
uint8_t saved_w3, blk;
|
|
||||||
uint16_t stub;
|
|
||||||
int fd, n;
|
|
||||||
|
|
||||||
fd = open(path, O_RDONLY);
|
|
||||||
if (fd < 0) return -1;
|
|
||||||
|
|
||||||
blk = mem_alloc_pages(1);
|
|
||||||
if (!blk) { close(fd); return -1; }
|
|
||||||
pop_lvl_blk = blk;
|
|
||||||
pop_lvl_page = mem_get_page(blk, 0);
|
|
||||||
|
|
||||||
saved_w3 = _io_page_w3;
|
|
||||||
sprinter_page_w3(pop_lvl_page);
|
|
||||||
/* Файл целиком в offset 0x100 (ESTEX READ пишет в W3 — как atlas_load). */
|
|
||||||
n = read(fd, pg + LVL_DATA_OFF, 16384 - LVL_DATA_OFF);
|
|
||||||
if (n >= (int)MIN_SIZE) { /* пропатчить ISR-стаб */
|
|
||||||
uint16_t i;
|
|
||||||
stub = (uint16_t)&_gfx_w0_isr;
|
|
||||||
pg[0x38] = 0xC3; /* JP _gfx_w0_isr */
|
|
||||||
pg[0x39] = (uint8_t)(stub & 0xFF);
|
|
||||||
pg[0x3A] = (uint8_t)(stub >> 8);
|
|
||||||
pg[0x66] = 0xED; pg[0x67] = 0x45; /* RETN (NMI-хвост) */
|
|
||||||
/* Скопировать таблицы дверных связей в W2 (страница ещё в W3). */
|
|
||||||
for (i = 0; i < 256; i++) {
|
|
||||||
pop_dl1[i] = pg[LVL_DATA_OFF + BP_LINKLOC + i];
|
|
||||||
pop_dl2[i] = pg[LVL_DATA_OFF + BP_LINKMAP + i];
|
|
||||||
}
|
|
||||||
/* Эталон foretable — для рестарта уровня (см. LVL_PRISTINE_OFF). */
|
|
||||||
for (i = 0; i < 24u * ROOM_TILES; i++)
|
|
||||||
pg[LVL_PRISTINE_OFF + i] = pg[LVL_DATA_OFF + BP_FG + i];
|
|
||||||
}
|
|
||||||
sprinter_page_w3(saved_w3);
|
|
||||||
close(fd);
|
|
||||||
|
|
||||||
if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; }
|
|
||||||
pop_lvl_ok = 1;
|
|
||||||
pop_gstate_init(); /* живая копия состояния стражей — из уровня */
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
#ifndef POP_LEVEL_H
|
#ifndef POP_LEVEL_H
|
||||||
#define POP_LEVEL_H
|
#define POP_LEVEL_H
|
||||||
|
|
||||||
|
#include "pop_qsave.h"
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
/* Загрузить уровень из файла в EMM-страницу. 0 — OK, -1 — ошибка
|
/* Загрузить уровень из файла в EMM-страницу. 0 — OK, -1 — ошибка
|
||||||
@@ -32,6 +34,7 @@ void pop_level_free(void) __banked;
|
|||||||
* level). Уровень 1..15; 0 — демо-уровень, в скоуп не входит. Читают
|
* level). Уровень 1..15; 0 — демо-уровень, в скоуп не входит. Читают
|
||||||
* потабличные функции ниже, стражи (HP/тип) и стартовая логика. */
|
* потабличные функции ниже, стражи (HP/тип) и стартовая логика. */
|
||||||
extern uint8_t pop_current_level;
|
extern uint8_t pop_current_level;
|
||||||
|
void pop_level_qsave(pop_qs_io_t *io);
|
||||||
|
|
||||||
/* Загрузить уровень НОМЕРОМ: освободить страницу прошлого, взять
|
/* Загрузить уровень НОМЕРОМ: освободить страницу прошлого, взять
|
||||||
* `LEVELS\res20NN.bin` (fallback `a:\res20NN.bin`) и выставить
|
* `LEVELS\res20NN.bin` (fallback `a:\res20NN.bin`) и выставить
|
||||||
|
|||||||
@@ -18,16 +18,8 @@
|
|||||||
#include "_pop_level.h"
|
#include "_pop_level.h"
|
||||||
#include "pop_geom.h" /* pop_x_bump / FIRST_ONSCREEN_COLUMN — pos_guards */
|
#include "pop_geom.h" /* pop_x_bump / FIRST_ONSCREEN_COLUMN — pos_guards */
|
||||||
|
|
||||||
/* ISR-стаб W0-страницы (тот же приём, что atlas_load): при прерывании с
|
/* Низкоуровневое чтение через W3 выполняет резидентный bank_load_file();
|
||||||
* замапленной в W0 страницей CPU прыгает на 0x0038 → там обязан быть JP на
|
* остальная одноразовая подготовка уровня безопасно остаётся в банке 8. */
|
||||||
* _gfx_w0_isr (восстановит окно). Символ внутренний для libbgi, но
|
|
||||||
* стабильный — линкер тянет его из bgi256.lib (графика уже подключена). */
|
|
||||||
extern void _gfx_w0_isr(void);
|
|
||||||
|
|
||||||
/* ЧТЕНИЕ ФАЙЛА уровня живёт в РЕЗИДЕНТЕ (pop_level.c, pop_level_read_file):
|
|
||||||
* оно маппит страницу в W3 на время read(), а этот модуль сам лежит в W3 —
|
|
||||||
* переключив окно, он вырезал бы из-под себя собственный код (проверено:
|
|
||||||
* первый же вызов улетал в halt по мусору). Правило записано в _pop_level.h. */
|
|
||||||
|
|
||||||
void pop_level_free(void) __banked
|
void pop_level_free(void) __banked
|
||||||
{
|
{
|
||||||
@@ -58,6 +50,31 @@ int8_t pop_level_guard_type(uint8_t n) __banked { return n < 16 ? tbl_guard_typ
|
|||||||
uint8_t pop_level_tileset(uint8_t n) __banked { return n < 16 ? tbl_level_type[n] : 0; }
|
uint8_t pop_level_tileset(uint8_t n) __banked { return n < 16 ? tbl_level_type[n] : 0; }
|
||||||
uint8_t pop_level_type(void) __banked { return tbl_level_type[pop_current_level & 15]; }
|
uint8_t pop_level_type(void) __banked { return tbl_level_type[pop_current_level & 15]; }
|
||||||
|
|
||||||
|
static int level_read_file(const char *path)
|
||||||
|
{
|
||||||
|
uint8_t blk, page;
|
||||||
|
uint16_t i;
|
||||||
|
int n;
|
||||||
|
blk=mem_alloc_pages(1);
|
||||||
|
if (!blk) return -1;
|
||||||
|
page=mem_get_page(blk,0);
|
||||||
|
n=bank_load_file(page,LVL_DATA_OFF,path,16384-LVL_DATA_OFF);
|
||||||
|
if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; }
|
||||||
|
gfx_w0_page_prepare(page);
|
||||||
|
gfx_w0_map(page);
|
||||||
|
for (i=0; i<256; i++) {
|
||||||
|
pop_dl1[i]=((const uint8_t *)LVL_DATA_OFF)[BP_LINKLOC+i];
|
||||||
|
pop_dl2[i]=((const uint8_t *)LVL_DATA_OFF)[BP_LINKMAP+i];
|
||||||
|
}
|
||||||
|
for (i=0; i<24u*ROOM_TILES; i++)
|
||||||
|
((uint8_t *)LVL_PRISTINE_OFF)[i]=
|
||||||
|
((const uint8_t *)(LVL_DATA_OFF+BP_FG))[i];
|
||||||
|
gfx_w0_unmap();
|
||||||
|
pop_lvl_blk=blk; pop_lvl_page=page; pop_lvl_ok=1;
|
||||||
|
pop_gstate_init();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
int pop_level_load_num(uint8_t n) __banked
|
int pop_level_load_num(uint8_t n) __banked
|
||||||
{
|
{
|
||||||
/* Имена патчим на месте, а не через sprintf: printf-семейство тянет в
|
/* Имена патчим на месте, а не через sprintf: printf-семейство тянет в
|
||||||
@@ -71,10 +88,10 @@ int pop_level_load_num(uint8_t n) __banked
|
|||||||
pri[13] = alt[9] = (char)('0' + n % 10);
|
pri[13] = alt[9] = (char)('0' + n % 10);
|
||||||
|
|
||||||
/* Старую страницу отпускаем ТОЛЬКО после успешной загрузки новой:
|
/* Старую страницу отпускаем ТОЛЬКО после успешной загрузки новой:
|
||||||
* pop_level_read_file сам аллоцирует страницу, и если файла нет — остаёмся
|
* level_read_file сам аллоцирует страницу, и если файла нет — остаёмся
|
||||||
* на текущем уровне вместо падения в пустой уровень. */
|
* на текущем уровне вместо падения в пустой уровень. */
|
||||||
pop_lvl_ok = 0; /* чтобы level_load_path не мешался */
|
pop_lvl_ok = 0; /* чтобы level_load_path не мешался */
|
||||||
if (pop_level_read_file(pri) != 0 && pop_level_read_file(alt) != 0) {
|
if (level_read_file(pri) != 0 && level_read_file(alt) != 0) {
|
||||||
pop_lvl_blk = saved_blk; pop_lvl_page = saved_page; pop_lvl_ok = saved_ok;
|
pop_lvl_blk = saved_blk; pop_lvl_page = saved_page; pop_lvl_ok = saved_ok;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Сериализация уровня для QuickSave (банк 8).
|
||||||
|
*
|
||||||
|
* Код выполняется только по F6/F9 и вынесен из горячего резидентного
|
||||||
|
* pop_level.c. Данные уровня и снимка используют одно окно W0, поэтому
|
||||||
|
* foretable переносится небольшими блоками через стековый буфер W2.
|
||||||
|
*/
|
||||||
|
#include <sprite.h>
|
||||||
|
#include "pop_level.h"
|
||||||
|
#include "_pop_level.h"
|
||||||
|
|
||||||
|
void pop_level_qsave(pop_qs_io_t *io)
|
||||||
|
{
|
||||||
|
uint8_t tmp[30];
|
||||||
|
uint16_t off;
|
||||||
|
pop_qs_bytes(io, pop_dl1, sizeof(pop_dl1));
|
||||||
|
pop_qs_bytes(io, pop_dl2, sizeof(pop_dl2));
|
||||||
|
pop_qs_bytes(io, pop_gstate, sizeof(pop_gstate));
|
||||||
|
if (!pop_lvl_ok) { io->failed = 1; return; }
|
||||||
|
/* W0 одновременно не может держать страницу уровня и страницу снимка. */
|
||||||
|
for (off = 0; off < 24u * ROOM_TILES; off += sizeof(tmp)) {
|
||||||
|
uint8_t i;
|
||||||
|
if (!io->reading || io->apply) {
|
||||||
|
gfx_w0_map(pop_lvl_page);
|
||||||
|
if (!io->reading)
|
||||||
|
for (i=0; i<sizeof(tmp); i++)
|
||||||
|
tmp[i]=((uint8_t *)(LVL_DATA_OFF+BP_FG))[off+i];
|
||||||
|
gfx_w0_unmap();
|
||||||
|
}
|
||||||
|
pop_qs_bytes(io,tmp,sizeof(tmp));
|
||||||
|
if (io->reading && io->apply && !io->failed) {
|
||||||
|
gfx_w0_map(pop_lvl_page);
|
||||||
|
for (i=0; i<sizeof(tmp); i++)
|
||||||
|
((uint8_t *)(LVL_DATA_OFF+BP_FG))[off+i]=tmp[i];
|
||||||
|
gfx_w0_unmap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3394,3 +3394,16 @@ void pop_check_mirror(void) __banked
|
|||||||
/* Char остался ОТРАЖЁННЫМ — как в оригинале: обратно он не сохраняется,
|
/* Char остался ОТРАЖЁННЫМ — как в оригинале: обратно он не сохраняется,
|
||||||
* а следующий тик перезагружает окно из Kid. */
|
* а следующий тик перезагружает окно из Kid. */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Закрытое состояние физики, которое влияет на следующие кадры. Таблицы
|
||||||
|
* коллизии и кэши соседей намеренно не входят: их пересчитает вход в комнату. */
|
||||||
|
void pop_map_qsave(pop_qs_io_t *io) __banked
|
||||||
|
{
|
||||||
|
pop_qs_u8(io, &is_screaming);
|
||||||
|
pop_qs_u8(io, &grab_timer);
|
||||||
|
pop_qs_i8(io, &pickup_obj_type);
|
||||||
|
pop_qs_u16(io, &loose_seed.lo);
|
||||||
|
pop_qs_u16(io, &loose_seed.hi);
|
||||||
|
pop_qs_u8(io, &last_loose_snd);
|
||||||
|
if (io->reading && io->apply) pop_coll_invalidate();
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include "pop_char.h" /* pop_char_t — окклюзия ворот спрашивает про персонажа */
|
#include "pop_char.h" /* pop_char_t — окклюзия ворот спрашивает про персонажа */
|
||||||
|
#include "pop_qsave.h"
|
||||||
|
|
||||||
/* Задать карту текущей комнаты: fg[30] = коды тайлов (row*10+col, 3×10).
|
/* Задать карту текущей комнаты: fg[30] = коды тайлов (row*10+col, 3×10).
|
||||||
* col<0/>9 или row<0/>2 трактуются как стена (край уровня). Массив
|
* col<0/>9 или row<0/>2 трактуются как стена (край уровня). Массив
|
||||||
@@ -197,6 +198,7 @@ void pop_check_knock(void) __banked;
|
|||||||
* links.down — сменить комнату (pop_room_load+перерисовка) и продолжить;
|
* links.down — сменить комнату (pop_room_load+перерисовка) и продолжить;
|
||||||
* иначе — выпал из уровня (респавн). Снять флаг после обработки. */
|
* иначе — выпал из уровня (респавн). Снять флаг после обработки. */
|
||||||
extern uint8_t pop_fell_out;
|
extern uint8_t pop_fell_out;
|
||||||
|
void pop_map_qsave(pop_qs_io_t *io) __banked;
|
||||||
|
|
||||||
/* Пер-кадровая физика ПОСЛЕ kid_tick(play_seq): fall_accel + fall_speed +
|
/* Пер-кадровая физика ПОСЛЕ kid_tick(play_seq): fall_accel + fall_speed +
|
||||||
* determine_col + check_action (триггер падения / do_fall / приземление).
|
* determine_col + check_action (триггер падения / do_fall / приземление).
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
/*
|
||||||
|
* pop_qsave.c — файловая транзакция и схема игрового снимка.
|
||||||
|
* Живёт в холодном банке: в обычном кадре исполняется только проверка
|
||||||
|
* резидентного флага запроса, вся тяжёлая работа бывает по F6/F9.
|
||||||
|
*/
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <stdio.h> /* rename */
|
||||||
|
#include <sprinter_mem.h>
|
||||||
|
#include <sprite.h>
|
||||||
|
#include "pop_qsave.h"
|
||||||
|
#include "pop_kid.h"
|
||||||
|
#include "pop_guard.h"
|
||||||
|
#include "pop_map.h"
|
||||||
|
#include "pop_level.h"
|
||||||
|
#include "pop_trob.h"
|
||||||
|
#include "pop_bg.h"
|
||||||
|
#include "pop_state.h"
|
||||||
|
#include "pop_tile.h"
|
||||||
|
#include "pop_sfx.h"
|
||||||
|
#include "pop_pace.h"
|
||||||
|
#include "roomtest_cold.h"
|
||||||
|
|
||||||
|
#define QS_VERSION 3
|
||||||
|
#define QS_SAVE 1
|
||||||
|
#define QS_LOAD 2
|
||||||
|
#define QS_LIMIT 4096
|
||||||
|
#define QS_BASE 0x100
|
||||||
|
|
||||||
|
extern uint8_t pop_qsave_request;
|
||||||
|
|
||||||
|
static void qs_char(pop_qs_io_t *io, pop_char_t *c)
|
||||||
|
{
|
||||||
|
pop_qs_u8(io, &c->frame); pop_qs_u8(io, &c->x); pop_qs_u8(io, &c->y);
|
||||||
|
pop_qs_i8(io, &c->direction); pop_qs_i8(io, &c->curr_col);
|
||||||
|
pop_qs_i8(io, &c->curr_row); pop_qs_u8(io, &c->action);
|
||||||
|
pop_qs_i8(io, &c->fall_x); pop_qs_i8(io, &c->fall_y);
|
||||||
|
pop_qs_u8(io, &c->room); pop_qs_u8(io, &c->repeat);
|
||||||
|
pop_qs_u8(io, &c->charid); pop_qs_u8(io, &c->sword);
|
||||||
|
pop_qs_i8(io, &c->alive); pop_qs_u16(io, &c->curr_seq);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void qs_payload(pop_qs_io_t *io)
|
||||||
|
{
|
||||||
|
uint8_t speed=pop_speed_mode, immortal=pop_immortal, sound=pop_snd_want;
|
||||||
|
|
||||||
|
/* Пользовательские параметры входят в снимок, а не остаются свойством
|
||||||
|
* текущего запуска. Читаем сначала во временные байты: повреждённые
|
||||||
|
* значения не должны частично применить остальное игровое состояние. */
|
||||||
|
pop_qs_u8(io, &speed); pop_qs_u8(io, &immortal); pop_qs_u8(io, &sound);
|
||||||
|
if (io->reading && io->apply) {
|
||||||
|
if (speed >= POP_SPEED_MODES || immortal > 2 || sound > 1) {
|
||||||
|
io->failed=1; return;
|
||||||
|
}
|
||||||
|
pop_speed_mode=speed; pop_immortal=immortal; pop_snd_want=sound;
|
||||||
|
}
|
||||||
|
|
||||||
|
qs_char(io, &Kid); qs_char(io, &Guard);
|
||||||
|
pop_qs_u8(io, &cur_room); pop_qs_u8(io, &kid_room);
|
||||||
|
pop_qs_u8(io, &pop_checkpoint); pop_qs_u8(io, &pop_next_level);
|
||||||
|
pop_qs_i8(io, &knock);
|
||||||
|
|
||||||
|
pop_qs_u8(io, &hitp_curr); pop_qs_u8(io, &hitp_max);
|
||||||
|
pop_qs_u8(io, &hitp_beg_lev); pop_qs_i8(io, &hitp_delta);
|
||||||
|
pop_qs_u8(io, &guardhp_curr); pop_qs_u8(io, &guardhp_max);
|
||||||
|
pop_qs_i8(io, &guardhp_delta); pop_qs_u8(io, &guard_skill);
|
||||||
|
pop_qs_i8(io, &guard_refrac); pop_qs_i8(io, &justblocked);
|
||||||
|
pop_qs_i8(io, &kid_sword_strike); pop_qs_i8(io, &offguard);
|
||||||
|
pop_qs_i8(io, &holding_sword); pop_qs_u8(io, &pop_guard_hurt);
|
||||||
|
pop_qs_i8(io, &can_guard_see_kid); pop_qs_i8(io, &is_guard_notice);
|
||||||
|
pop_qs_i8(io, &pop_united_shadow); pop_qs_u8(io, &pop_shadow_init);
|
||||||
|
pop_qs_i8(io, &pop_guard_notice_timer);
|
||||||
|
pop_qs_u16(io, &pop_fight_seed.lo); pop_qs_u16(io, &pop_fight_seed.hi);
|
||||||
|
|
||||||
|
pop_qs_u8(io, &pop_kid_dead); pop_qs_u8(io, &pop_kid_hurt);
|
||||||
|
pop_qs_u8(io, &pop_have_sword); pop_qs_u8(io, &pop_item_taken);
|
||||||
|
pop_qs_u8(io, &pop_flash_time); pop_qs_u8(io, &pop_flash_color);
|
||||||
|
pop_qs_u8(io, &pop_feather); pop_qs_u8(io, &pop_upside);
|
||||||
|
pop_qs_u8(io, &pop_upside_want); pop_qs_u8(io, &pop_upside_dirty);
|
||||||
|
pop_qs_u8(io, &pop_leave_dir); pop_qs_u8(io, &pop_leave_timer);
|
||||||
|
pop_qs_u8(io, &pop_loose_fell); pop_qs_u8(io, &pop_ceil_fell);
|
||||||
|
pop_qs_u8(io, &pop_debris_at); pop_qs_u8(io, &pop_fell_out);
|
||||||
|
pop_qs_bytes(io, pop_loose_modif, sizeof(pop_loose_modif));
|
||||||
|
pop_qs_bytes(io, pop_ceil_modif, sizeof(pop_ceil_modif));
|
||||||
|
|
||||||
|
pop_qs_u8(io, &pop_loose_landed); pop_qs_u8(io, &pop_loose_exit);
|
||||||
|
pop_qs_u8(io, &pop_loose_exit_room); pop_qs_u8(io, &pop_droppedout);
|
||||||
|
pop_qs_u16(io, &pop_leveldoor_open); pop_qs_i8(io, &pop_jumped_mirror);
|
||||||
|
pop_qs_u8(io, &pop_seamless); pop_qs_u8(io, &pop_nav_hold);
|
||||||
|
|
||||||
|
pop_level_qsave(io);
|
||||||
|
pop_trob_qsave(io);
|
||||||
|
pop_loose_mob_qsave(io);
|
||||||
|
pop_map_qsave(io);
|
||||||
|
pop_guard_ai_qsave(io);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void qs_init(pop_qs_io_t *io, uint8_t page, uint16_t limit,
|
||||||
|
uint8_t reading, uint8_t apply)
|
||||||
|
{
|
||||||
|
io->page=page; io->pos=QS_BASE; io->limit=(uint16_t)(QS_BASE+limit);
|
||||||
|
io->checksum=0;
|
||||||
|
io->count=0; io->reading=reading; io->apply=apply; io->failed=0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t qs_header_read(pop_qs_io_t *io, uint8_t *level)
|
||||||
|
{
|
||||||
|
uint8_t h[7];
|
||||||
|
if (!pop_qs_raw_get(io,h,sizeof(h))) return 0;
|
||||||
|
if (h[0]!='P' || h[1]!='O' || h[2]!='P' || h[3]!='Q' ||
|
||||||
|
h[4] != QS_VERSION || h[5] < 1 || h[5] > 15 || h[6] != 0) return 0;
|
||||||
|
*level = h[5]; return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t qs_validate_page(uint8_t page, uint16_t size,
|
||||||
|
uint8_t *level, uint16_t *payload_size)
|
||||||
|
{
|
||||||
|
pop_qs_io_t io;
|
||||||
|
uint8_t tail[4];
|
||||||
|
uint16_t n, sum;
|
||||||
|
if (size < 11) return 0;
|
||||||
|
qs_init(&io,page,size,1,0);
|
||||||
|
if (!qs_header_read(&io,level)) return 0;
|
||||||
|
io.pos=(uint16_t)(QS_BASE+size-4);
|
||||||
|
if (!pop_qs_raw_get(&io,tail,4)) return 0;
|
||||||
|
n = (uint16_t)(tail[0] | ((uint16_t)tail[1] << 8));
|
||||||
|
sum = (uint16_t)(tail[2] | ((uint16_t)tail[3] << 8));
|
||||||
|
if (n != (uint16_t)(size-11) ||
|
||||||
|
sum != pop_qs_checksum_page(page,QS_BASE+7,n)) return 0;
|
||||||
|
*payload_size=n; return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t qs_apply(uint8_t page, uint16_t size, uint16_t payload_size,
|
||||||
|
uint8_t *level)
|
||||||
|
{
|
||||||
|
pop_qs_io_t io;
|
||||||
|
qs_init(&io,page,size,1,1);
|
||||||
|
if (!qs_header_read(&io,level)) return 0;
|
||||||
|
qs_payload(&io);
|
||||||
|
return (uint8_t)(!io.failed && io.count == payload_size &&
|
||||||
|
io.pos == (uint16_t)(QS_BASE+7+payload_size));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int8_t qs_save(uint8_t page)
|
||||||
|
{
|
||||||
|
pop_qs_io_t io;
|
||||||
|
uint8_t tail[4];
|
||||||
|
const uint8_t h[7]={'P','O','P','Q',QS_VERSION,pop_current_level,0};
|
||||||
|
uint16_t size;
|
||||||
|
qs_init(&io,page,QS_LIMIT,0,0);
|
||||||
|
pop_qs_raw_put(&io,h,sizeof(h));
|
||||||
|
qs_payload(&io);
|
||||||
|
tail[0]=(uint8_t)io.count; tail[1]=(uint8_t)(io.count >> 8);
|
||||||
|
io.checksum=pop_qs_checksum_page(page,QS_BASE+7,io.count);
|
||||||
|
tail[2]=(uint8_t)io.checksum; tail[3]=(uint8_t)(io.checksum >> 8);
|
||||||
|
pop_qs_raw_put(&io,tail,4); size=(uint16_t)(io.pos-QS_BASE);
|
||||||
|
if (io.failed) return -1;
|
||||||
|
unlink("POP.NEW");
|
||||||
|
if (bank_save_file(page,QS_BASE,"POP.NEW",size) != size) {
|
||||||
|
unlink("POP.NEW"); return -1;
|
||||||
|
}
|
||||||
|
unlink("POP.BAK");
|
||||||
|
(void)rename("POP.SAV", "POP.BAK");
|
||||||
|
if (rename("POP.NEW", "POP.SAV") != 0) {
|
||||||
|
(void)rename("POP.BAK", "POP.SAV"); return -1;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int8_t qs_load_one(const char *name, uint8_t page)
|
||||||
|
{
|
||||||
|
uint8_t level;
|
||||||
|
uint16_t payload_size;
|
||||||
|
int size=bank_load_file(page,QS_BASE,name,QS_LIMIT);
|
||||||
|
if (size <= 0 || !qs_validate_page(page,(uint16_t)size,&level,
|
||||||
|
&payload_size)) return -1;
|
||||||
|
if (level != pop_current_level) {
|
||||||
|
pop_next_level = level;
|
||||||
|
if (pop_level_switch() != 0 || pop_current_level != level) return -1;
|
||||||
|
}
|
||||||
|
if (!qs_apply(page,(uint16_t)size,payload_size,&level)) return -1;
|
||||||
|
Char = Kid; Opp = Guard;
|
||||||
|
pop_qsave_restore_room(cur_room);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int8_t pop_qsave_process(void) __banked
|
||||||
|
{
|
||||||
|
uint8_t request = pop_qsave_request;
|
||||||
|
uint8_t blk,page;
|
||||||
|
int8_t rc=-1;
|
||||||
|
pop_qsave_request = 0;
|
||||||
|
if (!request) return 0;
|
||||||
|
blk=mem_alloc_pages(1);
|
||||||
|
if (!blk) return -1;
|
||||||
|
page=mem_get_page(blk,0);
|
||||||
|
gfx_w0_page_prepare(page);
|
||||||
|
pop_sfx_pause();
|
||||||
|
if (request == QS_SAVE) rc = qs_save(page);
|
||||||
|
else {
|
||||||
|
rc = qs_load_one("POP.SAV",page);
|
||||||
|
if (rc < 0) rc = qs_load_one("POP.BAK",page);
|
||||||
|
}
|
||||||
|
pop_sfx_start();
|
||||||
|
mem_free_block(blk);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* pop_qsave.h — потоковый QuickSave/QuickLoad одного игрового слота.
|
||||||
|
*
|
||||||
|
* На диске используются POP.NEW, POP.SAV и POP.BAK. Формат не содержит
|
||||||
|
* указателей, файловых дескрипторов и графических кэшей; многобайтные поля
|
||||||
|
* записываются явно little-endian. Внутренние функции pop_qs_* нужны
|
||||||
|
* модулям-владельцам закрытого состояния.
|
||||||
|
*/
|
||||||
|
#ifndef POP_QSAVE_H
|
||||||
|
#define POP_QSAVE_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t page;
|
||||||
|
uint16_t pos;
|
||||||
|
uint16_t limit;
|
||||||
|
uint16_t checksum;
|
||||||
|
uint16_t count;
|
||||||
|
uint8_t reading;
|
||||||
|
uint8_t apply;
|
||||||
|
uint8_t failed;
|
||||||
|
} pop_qs_io_t;
|
||||||
|
|
||||||
|
void pop_qs_bytes(pop_qs_io_t *io, void *data, uint16_t size);
|
||||||
|
void pop_qs_u8(pop_qs_io_t *io, uint8_t *value);
|
||||||
|
void pop_qs_i8(pop_qs_io_t *io, int8_t *value);
|
||||||
|
void pop_qs_u16(pop_qs_io_t *io, uint16_t *value);
|
||||||
|
void pop_qs_i16(pop_qs_io_t *io, int *value);
|
||||||
|
void pop_qs_raw_put(pop_qs_io_t *io, const uint8_t *data, uint8_t size);
|
||||||
|
uint8_t pop_qs_raw_get(pop_qs_io_t *io, uint8_t *data, uint8_t size);
|
||||||
|
uint16_t pop_qs_checksum_page(uint8_t page, uint16_t pos, uint16_t size);
|
||||||
|
|
||||||
|
/* Вызывается в безопасной границе кадра. Возвращает: 0 — запроса нет,
|
||||||
|
* 1 — сохранено/загружено, -1 — ошибка. */
|
||||||
|
int8_t pop_qsave_process(void) __banked;
|
||||||
|
void pop_qsave_request_save(void);
|
||||||
|
void pop_qsave_request_load(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Холодная часть ввода-вывода QuickSave (банк 8).
|
||||||
|
*
|
||||||
|
* Эти функции вызываются только банковым оркестратором pop_qsave.c, поэтому
|
||||||
|
* им не место в дефицитном резидентном _CODE. Здесь только доступ через W0:
|
||||||
|
* переключающие W3 DSS-операции обязаны оставаться в pop_qsave_io.c.
|
||||||
|
*/
|
||||||
|
#include "pop_qsave.h"
|
||||||
|
#include <sprite.h>
|
||||||
|
|
||||||
|
void pop_qs_raw_put(pop_qs_io_t *io, const uint8_t *data, uint8_t size)
|
||||||
|
{
|
||||||
|
uint8_t *q, i;
|
||||||
|
if (io->pos + size > io->limit) { io->failed=1; return; }
|
||||||
|
gfx_w0_map(io->page); q=(uint8_t *)io->pos;
|
||||||
|
for (i=0; i<size; i++) q[i]=data[i];
|
||||||
|
gfx_w0_unmap(); io->pos += size;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t pop_qs_raw_get(pop_qs_io_t *io, uint8_t *data, uint8_t size)
|
||||||
|
{
|
||||||
|
uint8_t *q, i;
|
||||||
|
if (io->pos + size > io->limit) return 0;
|
||||||
|
gfx_w0_map(io->page); q=(uint8_t *)io->pos;
|
||||||
|
for (i=0; i<size; i++) data[i]=q[i];
|
||||||
|
gfx_w0_unmap(); io->pos += size; return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* XOR8 достаточно для обнаружения случайной порчи транзакционного файла;
|
||||||
|
* размер payload проверяется отдельно, старший байт содержит инверсию XOR. */
|
||||||
|
uint16_t pop_qs_checksum_page(uint8_t page, uint16_t pos, uint16_t size)
|
||||||
|
{
|
||||||
|
uint8_t *p, sum=0;
|
||||||
|
gfx_w0_map(page); p=(uint8_t *)pos;
|
||||||
|
while(size--) sum ^= *p++;
|
||||||
|
gfx_w0_unmap();
|
||||||
|
return (uint16_t)(sum | ((uint16_t)(uint8_t)~sum << 8));
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/* Резидентная часть QuickSave: часто вызываемые межбанковые примитивы
|
||||||
|
* потока и флаг запроса. Файловый обмен с W3 теперь общий в libc. */
|
||||||
|
#include "pop_qsave.h"
|
||||||
|
#include <sprite.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
uint8_t pop_qsave_request;
|
||||||
|
void pop_qsave_request_save(void) { pop_qsave_request=1; }
|
||||||
|
void pop_qsave_request_load(void) { pop_qsave_request=2; }
|
||||||
|
|
||||||
|
void pop_qs_bytes(pop_qs_io_t *io, void *data, uint16_t size)
|
||||||
|
{
|
||||||
|
uint8_t *p=(uint8_t *)data, *q;
|
||||||
|
if (io->failed || io->pos + size > io->limit) { io->failed=1; return; }
|
||||||
|
gfx_w0_map(io->page); q=(uint8_t *)io->pos;
|
||||||
|
if (!io->reading) memcpy(q,p,size);
|
||||||
|
else if (io->apply) memcpy(p,q,size);
|
||||||
|
gfx_w0_unmap(); io->pos += size; io->count += size;
|
||||||
|
}
|
||||||
|
void pop_qs_u8(pop_qs_io_t *io, uint8_t *v) { pop_qs_bytes(io,v,1); }
|
||||||
|
void pop_qs_i8(pop_qs_io_t *io, int8_t *v) { pop_qs_bytes(io,v,1); }
|
||||||
|
void pop_qs_u16(pop_qs_io_t *io, uint16_t *value)
|
||||||
|
{
|
||||||
|
uint8_t b[2]; b[0]=(uint8_t)*value; b[1]=(uint8_t)(*value >> 8);
|
||||||
|
pop_qs_bytes(io,b,2);
|
||||||
|
if (io->reading && io->apply && !io->failed)
|
||||||
|
*value=(uint16_t)(b[0] | ((uint16_t)b[1] << 8));
|
||||||
|
}
|
||||||
|
void pop_qs_i16(pop_qs_io_t *io, int *value)
|
||||||
|
{
|
||||||
|
uint16_t v=(uint16_t)*value; pop_qs_u16(io,&v);
|
||||||
|
if (io->reading && io->apply && !io->failed) *value=(int)v;
|
||||||
|
}
|
||||||
@@ -1350,6 +1350,28 @@ void pop_loose_mob_reset(void) __banked
|
|||||||
* мапит и размапливает W0. Инвалидируется сменой комнаты. */
|
* мапит и размапливает W0. Инвалидируется сменой комнаты. */
|
||||||
static uint8_t mob_link_room, mob_link_up, mob_link_dn;
|
static uint8_t mob_link_room, mob_link_up, mob_link_dn;
|
||||||
|
|
||||||
|
void pop_loose_mob_qsave(pop_qs_io_t *io) __banked
|
||||||
|
{
|
||||||
|
uint8_t i, live = 0;
|
||||||
|
for (i = 0; i < MOB_MAX; i++) {
|
||||||
|
mob_t *m = &mobs[i];
|
||||||
|
pop_qs_u8(io, &m->active);
|
||||||
|
pop_qs_i16(io, &m->x); pop_qs_i16(io, &m->y);
|
||||||
|
pop_qs_i8(io, &m->speed); pop_qs_i8(io, &m->row);
|
||||||
|
pop_qs_i8(io, &m->col); pop_qs_u8(io, &m->room);
|
||||||
|
if (io->reading && io->apply) {
|
||||||
|
m->clean = m->defer = 0;
|
||||||
|
m->draw_y = m->prev_y[0] = m->prev_y[1] = MOB_Y_NONE;
|
||||||
|
if (m->active) live++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (io->reading && io->apply) {
|
||||||
|
mobs_live = live;
|
||||||
|
pop_mob_busy = (uint8_t)(live != 0);
|
||||||
|
mob_link_room = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void mob_links(void)
|
static void mob_links(void)
|
||||||
{
|
{
|
||||||
if (mob_link_room == pop_t_room) return;
|
if (mob_link_room == pop_t_room) return;
|
||||||
|
|||||||
@@ -9,9 +9,7 @@
|
|||||||
* ставит базовую страницу W1, значит хендлер обязан быть в базе;
|
* ставит базовую страницу W1, значит хендлер обязан быть в базе;
|
||||||
* - pop_sfx_play дёргается из горячих мест (play_seq на каждый шаг
|
* - pop_sfx_play дёргается из горячих мест (play_seq на каждый шаг
|
||||||
* Кида, физика, боёвка) — трамплин там был бы чистой потерей;
|
* Кида, физика, боёвка) — трамплин там был бы чистой потерей;
|
||||||
* - pop_page_read маппит W3 на время чтения, а банковый код сам живёт
|
* Файловое чтение страниц выполняет общий резидентный bank_load_file().
|
||||||
* в W3 и вырезал бы себя из-под ног (та же причина, по которой
|
|
||||||
* резидентен pop_level_read_file, см. _pop_level.h).
|
|
||||||
* Холодная половина (аллокация страниц, cbl_open) — в pop_sfx_cold.c.
|
* Холодная половина (аллокация страниц, cbl_open) — в pop_sfx_cold.c.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -198,21 +196,3 @@ void pop_sfx_stop(void)
|
|||||||
sfx_left = 0;
|
sfx_left = 0;
|
||||||
IRQ_ENABLE();
|
IRQ_ENABLE();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- чтение файла в ФИЗИЧЕСКУЮ страницу ------------------------------ *
|
|
||||||
* Резидентно по причине из шапки: маппим W3, читаем, возвращаем окно.
|
|
||||||
* Ровно то же делает atlas_load, поэтому вызов из банка безопасен.
|
|
||||||
* 0 — прочитано ожидаемое число байт, -1 — нет. */
|
|
||||||
int pop_page_read(const char *path, uint8_t phys, uint16_t want)
|
|
||||||
{
|
|
||||||
uint8_t saved_w3;
|
|
||||||
int fd, n;
|
|
||||||
fd = open(path, O_RDONLY);
|
|
||||||
if (fd < 0) return -1;
|
|
||||||
saved_w3 = _io_page_w3;
|
|
||||||
sprinter_page_w3(phys);
|
|
||||||
n = read(fd, (uint8_t *)0xC000, want);
|
|
||||||
_io_page_w3 = saved_w3;
|
|
||||||
close(fd);
|
|
||||||
return (n == (int)want) ? 0 : -1;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ int pop_sfx_init(void) __banked
|
|||||||
for (i = 0; i < POP_SND_PAGES; i++) {
|
for (i = 0; i < POP_SND_PAGES; i++) {
|
||||||
pop_snd_page[i] = mem_get_page(blk, i);
|
pop_snd_page[i] = mem_get_page(blk, i);
|
||||||
if (!pop_snd_page[i] ||
|
if (!pop_snd_page[i] ||
|
||||||
pop_page_read(pop_snd_files[i], pop_snd_page[i], 16384) != 0) {
|
bank_load_file(pop_snd_page[i],0,pop_snd_files[i],16384) != 16384) {
|
||||||
mem_free_block(blk);
|
mem_free_block(blk);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -48,16 +48,17 @@ int pop_sfx_init(void) __banked
|
|||||||
*
|
*
|
||||||
* Параметры: 10 937,5 Гц, 8 бит моно — формат данных один в один
|
* Параметры: 10 937,5 Гц, 8 бит моно — формат данных один в один
|
||||||
* (sound_plan.md). OTIR, а не акселератор: тот нужен только для 16 бит.
|
* (sound_plan.md). OTIR, а не акселератор: тот нужен только для 16 бит.
|
||||||
* UNDERRUN_APP, а не SILENCE: тишину насос льёт СВОЮ (первый блок
|
* cbl_open, а не cbl_open_silence: тишину насос льёт СВОЮ (первый блок
|
||||||
* набора), и просить у libc буфер через malloc не надо — куча в
|
* набора), и просить у libc буфер через malloc не надо — куча в резиденте
|
||||||
* резиденте W2 тесная, и её отказ ронял бы весь звук. */
|
* W2 тесная, и её отказ ронял бы весь звук. Заодно так malloc вообще не
|
||||||
|
* приезжает в резидент (275 Б, см. libc/cbl/_cbl_open_raw.c). */
|
||||||
int pop_sfx_start(void) __banked
|
int pop_sfx_start(void) __banked
|
||||||
{
|
{
|
||||||
if (pop_snd_ok) return 0;
|
if (pop_snd_ok) return 0;
|
||||||
if (!pop_snd_want) return 0; /* выключено пользователем (Ctrl+S) */
|
if (!pop_snd_want) return 0; /* выключено пользователем (Ctrl+S) */
|
||||||
if (!snd_blk) return -1; /* набор не загружен */
|
if (!snd_blk) return -1; /* набор не загружен */
|
||||||
if (cbl_open(CBL_FREQ_10K9, CBL_FMT_MONO8, CBL_PUMP_OTIR,
|
if (cbl_open(CBL_FREQ_10K9, CBL_FMT_MONO8, CBL_PUMP_OTIR,
|
||||||
CBL_UNDERRUN_APP, pop_sfx_fill) != 0)
|
pop_sfx_fill) != 0)
|
||||||
return -1;
|
return -1;
|
||||||
pop_snd_ok = 1;
|
pop_snd_ok = 1;
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
#include "pop_pace.h" /* pop_beam_sample — счёт кадров по лучу */
|
#include "pop_pace.h" /* pop_beam_sample — счёт кадров по лучу */
|
||||||
#include "pop_level.h" /* pop_doorlink2 — подстановка нажатой кнопки */
|
#include "pop_level.h" /* pop_doorlink2 — подстановка нажатой кнопки */
|
||||||
#include "_pop_draw.h" /* pop_heal_fast */
|
#include "_pop_draw.h" /* pop_heal_fast */
|
||||||
|
#include "_pop_tile.h" /* контракт с холодной половиной (pop_tile_cold.c) */
|
||||||
|
|
||||||
/* ---- Состояние (писучее, _DATA/W2 — видно из любого банка) ---------- */
|
/* ---- Состояние (писучее, _DATA/W2 — видно из любого банка) ---------- */
|
||||||
atlas_t pop_env[POP_ENV_PAGES], pop_wall_at, pop_fore_at, pop_pot_at;
|
atlas_t pop_env[POP_ENV_PAGES], pop_wall_at, pop_fore_at, pop_pot_at;
|
||||||
@@ -307,89 +308,6 @@ void pop_cd_touch(int x, int y, int w, int h)
|
|||||||
/* Задел ли прямоугольник (ЭКРАННЫЕ координаты, x1/y1 ВКЛЮЧИТЕЛЬНО) то, что
|
/* Задел ли прямоугольник (ЭКРАННЫЕ координаты, x1/y1 ВКЛЮЧИТЕЛЬНО) то, что
|
||||||
* трогали на странице p. Резидент: зовёт pop_cdraw.c из банка 4 прямым
|
* трогали на странице p. Резидент: зовёт pop_cdraw.c из банка 4 прямым
|
||||||
* вызовом, без трамплина. */
|
* вызовом, без трамплина. */
|
||||||
/* Прямоугольник слота против метки — БЕЗ передачи пяти аргументов.
|
|
||||||
*
|
|
||||||
* pop_cd_hit принимает (p, x0, y0, x1, y1): три последних идут стеком, и
|
|
||||||
* функция целиком уезжает в IX-фрейм — 45 % её тактов уходит на `-n(ix)`
|
|
||||||
* (замер asm 2026-08-19). А зовут её из cd_quiet дважды на слот, то есть
|
|
||||||
* до восьми раз за кадр. Здесь координаты берутся прямо из pop_cd, и
|
|
||||||
* аргументов остаётся два.
|
|
||||||
*
|
|
||||||
* Спрайт и накладной проверяются РАЗДЕЛЬНО — объединённый bbox ловит
|
|
||||||
* касания углом, которых нет (разбор в cd_quiet, pop_cdraw.c). */
|
|
||||||
static int hit_x, hit_y; /* аргументы hit_rect — file-scope, не стек */
|
|
||||||
static uint16_t hit_w, hit_h;
|
|
||||||
static uint8_t hit_p;
|
|
||||||
|
|
||||||
static uint8_t hit_rect(void)
|
|
||||||
{
|
|
||||||
int8_t c0, c1, c;
|
|
||||||
uint8_t ya, yb;
|
|
||||||
int x1 = hit_x + (int)hit_w - 1;
|
|
||||||
int y1 = hit_y + (int)hit_h - 1;
|
|
||||||
if (!hit_w || !hit_h) return 0;
|
|
||||||
if (x1 < 0 || hit_x > 319) return 0;
|
|
||||||
if (y1 < 0 || hit_y > 255) return 0;
|
|
||||||
ya = (uint8_t)(hit_y < 0 ? 0 : hit_y);
|
|
||||||
yb = (uint8_t)(y1 > 255 ? 255 : y1);
|
|
||||||
c0 = (int8_t)((hit_x < 0 ? 0 : hit_x) >> 5);
|
|
||||||
c1 = (int8_t)((x1 > 319 ? 319 : x1) >> 5);
|
|
||||||
for (c = c0; c <= c1; c++)
|
|
||||||
if (ya <= pop_cd_ymax[hit_p][c] && yb >= pop_cd_ymin[hit_p][c]) return 1;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t pop_cd_hit_slot(uint8_t who, uint8_t p)
|
|
||||||
{
|
|
||||||
const pop_cdraw_t *s = &pop_cd[who];
|
|
||||||
if (!(pop_cd_dirty & (1 << p))) return 0;
|
|
||||||
hit_p = p;
|
|
||||||
hit_x = s->x[p]; hit_y = s->y[p] + POP_YOFF;
|
|
||||||
hit_w = s->w[p]; hit_h = s->h[p];
|
|
||||||
if (hit_rect()) return 1;
|
|
||||||
if (!s->ovalid[p]) return 0;
|
|
||||||
hit_x = s->ox[p]; hit_y = s->oy[p] + POP_YOFF;
|
|
||||||
hit_w = s->ow[p]; hit_h = s->oh[p];
|
|
||||||
return hit_rect();
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t pop_cd_hit(uint8_t p, int x0, int y0, int x1, int y1)
|
|
||||||
{
|
|
||||||
int8_t c0, c1, c;
|
|
||||||
uint8_t ya, yb;
|
|
||||||
if (!(pop_cd_dirty & (1 << p))) return 0;
|
|
||||||
if (x1 < 0 || x0 > 319) return 0;
|
|
||||||
if (x0 < 0) x0 = 0;
|
|
||||||
if (x1 > 319) x1 = 319;
|
|
||||||
if (y1 < 0 || y0 > 255) return 0;
|
|
||||||
ya = (uint8_t)(y0 < 0 ? 0 : y0);
|
|
||||||
yb = (uint8_t)(y1 > 255 ? 255 : y1);
|
|
||||||
c0 = (int8_t)(x0 >> 5);
|
|
||||||
c1 = (int8_t)(x1 >> 5);
|
|
||||||
/* Пересечение отрезков [ya,yb] и [ymin,ymax] хотя бы в одной колонке.
|
|
||||||
* Пустая колонка держит ymin = 255, ymax = 0 — условие ниже её отсеет
|
|
||||||
* само, отдельной проверки «пусто» не нужно. */
|
|
||||||
for (c = c0; c <= c1; c++)
|
|
||||||
if (ya <= pop_cd_ymax[p][c] && yb >= pop_cd_ymin[p][c]) return 1;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Страница приведена в порядок — снять с неё метку целиком. */
|
|
||||||
/* Начальное состояние обеих страниц — «ничего не трогали». ЯВНО, а не
|
|
||||||
* расчётом на обнуление _DATA: пустая колонка обозначается ymin = 255,
|
|
||||||
* ymax = 0, а нули от crt0 читались бы как «затронута строка 0». */
|
|
||||||
void pop_cd_init(void)
|
|
||||||
{
|
|
||||||
pop_cd_clear(0);
|
|
||||||
pop_cd_clear(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void pop_cd_clear(uint8_t p)
|
|
||||||
{
|
|
||||||
uint8_t c;
|
|
||||||
for (c = 0; c < 10; c++) { pop_cd_ymin[p][c] = 255; pop_cd_ymax[p][c] = 0; }
|
|
||||||
pop_cd_dirty = (uint8_t)(pop_cd_dirty & ~(1 << p));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- Блит куска атласа ---------------------------------------------- */
|
/* ---- Блит куска атласа ---------------------------------------------- */
|
||||||
/* Клипованный путь — ОТДЕЛЬНОЙ функцией. У него девять 16-битных локалей,
|
/* Клипованный путь — ОТДЕЛЬНОЙ функцией. У него девять 16-битных локалей,
|
||||||
@@ -399,27 +317,6 @@ void pop_cd_clear(uint8_t p)
|
|||||||
* перерисовка тайлов — идёт с pop_t_fclip_on == 0. */
|
* перерисовка тайлов — идёт с pop_t_fclip_on == 0. */
|
||||||
#define FLIP_TOP(top, h) POP_FLIP_TOP(top, h) /* формула — в pop_bg.h */
|
#define FLIP_TOP(top, h) POP_FLIP_TOP(top, h) /* формула — в pop_bg.h */
|
||||||
|
|
||||||
/* Окно под точечную перерисовку/запечку — контракт в шапке pop_tile.h.
|
|
||||||
* Логическая пара нужна для отсева тайлов (он считается из ряда комнаты),
|
|
||||||
* экранная — для клипа самого блита; при перевороте они разные. */
|
|
||||||
void pop_t_win_set(int x, int ytop, int w, int h)
|
|
||||||
{
|
|
||||||
pop_t_fclip_x0 = x;
|
|
||||||
pop_t_fclip_x1 = x + w;
|
|
||||||
pop_t_fclip_ly0 = ytop;
|
|
||||||
pop_t_fclip_ly1 = ytop + h;
|
|
||||||
if (pop_upside) {
|
|
||||||
int t = FLIP_TOP(ytop, h);
|
|
||||||
pop_t_fclip_y0 = t;
|
|
||||||
pop_t_fclip_y1 = t + h;
|
|
||||||
} else {
|
|
||||||
pop_t_fclip_y0 = ytop;
|
|
||||||
pop_t_fclip_y1 = ytop + h;
|
|
||||||
}
|
|
||||||
pop_t_fclip_on = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void pop_t_win_clear(void) { pop_t_fclip_on = 0; }
|
|
||||||
|
|
||||||
/* Клипованный путь блита. Габарит и смещения внутри куска — БАЙТЫ, а вся
|
/* Клипованный путь блита. Габарит и смещения внутри куска — БАЙТЫ, а вся
|
||||||
* рабочая переменная — file-scope, НЕ локали.
|
* рабочая переменная — file-scope, НЕ локали.
|
||||||
@@ -447,7 +344,7 @@ static int bc_dx, bc_dy;
|
|||||||
static int16_t bc_d;
|
static int16_t bc_d;
|
||||||
static uint8_t bc_w, bc_h, bc_dw, bc_dh, bc_sx, bc_sy;
|
static uint8_t bc_w, bc_h, bc_dw, bc_dh, bc_sx, bc_sy;
|
||||||
|
|
||||||
static void blit_b_clip(const uint8_t *img, int x, int top, uint8_t w, uint8_t h)
|
void blit_b_clip(const uint8_t *img, int x, int top, uint8_t w, uint8_t h)
|
||||||
{
|
{
|
||||||
bc_img = img; bc_w = w; bc_h = h;
|
bc_img = img; bc_w = w; bc_h = h;
|
||||||
bc_dx = x; bc_dy = top;
|
bc_dx = x; bc_dy = top;
|
||||||
@@ -653,59 +550,12 @@ void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom)
|
|||||||
gfx_w0_unmap();
|
gfx_w0_unmap();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Блит getimage-блоба из ОБЫЧНОЙ памяти (W2), а не из EMM-страницы атласа:
|
|
||||||
* те же клип полосы у потолка и окно перерисовки, что у pop_blit_b, но без
|
|
||||||
* atlas_image и без gfx_w0_map/unmap (это ещё ~1 350 такта). Заведено под
|
|
||||||
* композитный спрайт падающего куска (pop_room.c: три части сложены в один
|
|
||||||
* блоб, один вызов блита вместо трёх). */
|
|
||||||
void pop_mem_b(const uint8_t *img, int x, int ybottom)
|
|
||||||
{
|
|
||||||
uint8_t w = img[0], h = img[2];
|
|
||||||
int top;
|
|
||||||
if (pop_t_fclip_on &&
|
|
||||||
(x >= pop_t_fclip_x1 || x + 64 <= pop_t_fclip_x0 ||
|
|
||||||
ybottom + POP_YOFF + 1 <= pop_t_fclip_ly0 ||
|
|
||||||
ybottom + POP_YOFF - 64 >= pop_t_fclip_ly1))
|
|
||||||
return;
|
|
||||||
if (!w || !h) return;
|
|
||||||
top = ybottom - (int)h + 1 + POP_YOFF;
|
|
||||||
if (pop_upside) top = FLIP_TOP(top, h);
|
|
||||||
if (!pop_t_fclip_on && !pop_t_clip_top &&
|
|
||||||
!(pop_upside && (top < POP_YOFF || top + (int)h > POP_CLIP_BOTTOM)) &&
|
|
||||||
x >= 0 && top >= 0 && x + (int)w <= 320 && top + (int)h <= 256) {
|
|
||||||
if (pop_upside) gfx_blit_noclip_vflip(x, top, img);
|
|
||||||
else gfx_blit_noclip(x, top, img);
|
|
||||||
pop_cd_touch(x, top, (int)w, (int)h);
|
|
||||||
} else {
|
|
||||||
blit_b_clip(img, x, top, w, h);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void pop_env_b(uint8_t id, int x, int yb) { pop_blit_b(&pop_env[id >> POP_ENV_SHIFT], (uint8_t)(id & POP_ENV_MASK), x, yb); }
|
void pop_env_b(uint8_t id, int x, int yb) { pop_blit_b(&pop_env[id >> POP_ENV_SHIFT], (uint8_t)(id & POP_ENV_MASK), x, yb); }
|
||||||
void pop_wall_b(uint8_t id, int x, int yb) { pop_blit_b(&pop_wall_at, id, x, yb); }
|
void pop_wall_b(uint8_t id, int x, int yb) { pop_blit_b(&pop_wall_at, id, x, yb); }
|
||||||
void pop_fore_b(uint8_t id, int x, int yb) { pop_blit_b(&pop_fore_at, id, x, yb); }
|
void pop_fore_b(uint8_t id, int x, int yb) { pop_blit_b(&pop_fore_at, id, x, yb); }
|
||||||
void pop_pot_b(uint8_t id, int x, int yb) { pop_blit_b(&pop_pot_at, id, x, yb); }
|
void pop_pot_b(uint8_t id, int x, int yb) { pop_blit_b(&pop_pot_at, id, x, yb); }
|
||||||
|
|
||||||
/* Heal по КОМНАТНОЙ координате (её и знают точечные перерисовки: чомпер,
|
|
||||||
* пики, loose, ворота, полоса потолка). При перевороте прямоугольник
|
|
||||||
* обязан отразиться ровно так же, как сам тайл, — иначе heal чистит одно
|
|
||||||
* место, а рисует в другое: именно так «ломалась анимация чомперов»
|
|
||||||
* (найдено пользователем на первом же прогоне зеркального фона). */
|
|
||||||
void pop_bar_black(int x, int top, int w, int h)
|
|
||||||
{
|
|
||||||
if (pop_upside) top = FLIP_TOP(top, h);
|
|
||||||
gfx_set_bank(GFX_BANK_NORMAL);
|
|
||||||
setfillstyle(SOLID_FILL, BLACK);
|
|
||||||
bar(x, top, x + w - 1, top + h - 1);
|
|
||||||
pop_cd_touch(x, top, w, h); /* bar идёт мимо pop_blit_b (DRAW-COST) */
|
|
||||||
}
|
|
||||||
|
|
||||||
void pop_heal_off(int x, int y, int w, int h)
|
|
||||||
{
|
|
||||||
int top = y + POP_YOFF;
|
|
||||||
if (pop_upside) top = FLIP_TOP(top, h);
|
|
||||||
pop_heal_fast(x, top, w, h);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- Чтение тайла отрисованной комнаты ------------------------------ */
|
/* ---- Чтение тайла отрисованной комнаты ------------------------------ */
|
||||||
uint8_t pop_tile_code(int row, int col)
|
uint8_t pop_tile_code(int row, int col)
|
||||||
@@ -757,25 +607,6 @@ uint8_t pop_wall_modifier(int row, int col)
|
|||||||
return 0; /* SWS */
|
return 0; /* SWS */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Зелье: склянка (tiles_10_potion, seg008 draw_tile_fore) --------- *
|
|
||||||
* Тип = (modif & 0xF8) >> 3: 0 = пусто (ничего не рисуем), 2..4 — «большая»
|
|
||||||
* склянка (id 13), остальные — маленькая (id 12). Спрайты из chtab_1 (атлас
|
|
||||||
* pop_pot), поэтому НЕ из env: раньше рисовали chtab_6 id 12 (res212),
|
|
||||||
* которого в VDUNGEON нет — оттуда и «мусорный объект». Позиция оригинала:
|
|
||||||
* xh = fore_x + draw_xh, xl = 6, ybottom = fore_y + draw_main_y.
|
|
||||||
*
|
|
||||||
* Здесь, а не в банке: склянку рисует и полная отрисовка тайла (холодная
|
|
||||||
* половина), и fore-проход поверх персонажа (горячая). */
|
|
||||||
void pop_potion_flask(int row, int col, uint8_t modif)
|
|
||||||
{
|
|
||||||
uint8_t type = (uint8_t)((modif & 0xF8) >> 3);
|
|
||||||
uint8_t id = 12;
|
|
||||||
if (type == 0) return;
|
|
||||||
if (type >= 2 && type < 5) id = 13; /* большая склянка */
|
|
||||||
if (pop_palace) id += 2; /* seg008:747: во дворце склянки другие */
|
|
||||||
pop_pot_b(id, (POT_FORE_X + POP_COL_XH[col]) * 8 + 6,
|
|
||||||
POT_FORE_Y + 63 * row + 62);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Окно fore-клипа: ставит тот, кто нарисовал персонажа (pop_cdraw, банк 4),
|
/* Окно fore-клипа: ставит тот, кто нарисовал персонажа (pop_cdraw, банк 4),
|
||||||
* читает blit_b. Резидент — чтобы вызов из банка шёл без трамплина. */
|
* читает blit_b. Резидент — чтобы вызов из банка шёл без трамплина. */
|
||||||
@@ -791,13 +622,3 @@ void pop_fore_set_clip(int x, int y, int w, int h)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ряд 0 комнаты СНИЗУ (fg-коды) — нужен draw_tile_topright/floorright, когда
|
|
||||||
* тайл нижнего ряда стал видимым (empty после падения loose): «стена вниз» =
|
|
||||||
* топрайт тайла room_B(0, col-1). 0 — нет комнаты снизу (кромка = стена). */
|
|
||||||
void pop_room_set_below(const uint8_t *below_row0_fg) { pop_t_below = below_row0_fg; }
|
|
||||||
|
|
||||||
/* Ряд 2 (fg-коды + модификаторы) комнаты СВЕРХУ — для полосы кладки у
|
|
||||||
* потолка (seg008 draw_room: доп. верхний ряд из room_A, draw_tile_aboveroom
|
|
||||||
* при draw_main_y=-1). 0 — нет комнаты сверху (кромка уровня → пол). */
|
|
||||||
void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod)
|
|
||||||
{ pop_t_above = above_row2_fg; pop_t_abovemod = above_row2_mod; }
|
|
||||||
|
|||||||
@@ -93,8 +93,8 @@ extern uint8_t pop_t_fclip_on;
|
|||||||
* ВАЖНО: пока окно стоит, pop_blit_b НЕ ставит пометку «фон трогали» (признак
|
* ВАЖНО: пока окно стоит, pop_blit_b НЕ ставит пометку «фон трогали» (признак
|
||||||
* fore-прохода) — вызывающий обязан пометить прямоугольник САМ (pop_cd_touch
|
* fore-прохода) — вызывающий обязан пометить прямоугольник САМ (pop_cd_touch
|
||||||
* или pop_bar_black, который метит сам). */
|
* или pop_bar_black, который метит сам). */
|
||||||
void pop_t_win_set(int x, int ytop, int w, int h);
|
void pop_t_win_set(int x, int ytop, int w, int h) __banked;
|
||||||
void pop_t_win_clear(void);
|
void pop_t_win_clear(void) __banked;
|
||||||
|
|
||||||
/* ---- Таблица тайлов (piece, seg008.c:27) ---------------------------- */
|
/* ---- Таблица тайлов (piece, seg008.c:27) ---------------------------- */
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@@ -154,7 +154,7 @@ void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom);
|
|||||||
/* Блит getimage-блоба из обычной памяти (W2) — без atlas_image и без
|
/* Блит getimage-блоба из обычной памяти (W2) — без atlas_image и без
|
||||||
* маппинга страницы; клип полосы у потолка и окно перерисовки те же, что у
|
* маппинга страницы; клип полосы у потолка и окно перерисовки те же, что у
|
||||||
* pop_blit_b. Для композитных спрайтов, собранных в рантайме. */
|
* pop_blit_b. Для композитных спрайтов, собранных в рантайме. */
|
||||||
void pop_mem_b(const uint8_t *img, int x, int ybottom);
|
void pop_mem_b(const uint8_t *img, int x, int ybottom) __banked;
|
||||||
|
|
||||||
void pop_env_b(uint8_t id, int x, int yb);
|
void pop_env_b(uint8_t id, int x, int yb);
|
||||||
void pop_wall_b(uint8_t id, int x, int yb);
|
void pop_wall_b(uint8_t id, int x, int yb);
|
||||||
@@ -162,14 +162,14 @@ void pop_fore_b(uint8_t id, int x, int yb);
|
|||||||
void pop_pot_b(uint8_t id, int x, int yb);
|
void pop_pot_b(uint8_t id, int x, int yb);
|
||||||
|
|
||||||
/* heal в room-space (со сдвигом POP_YOFF) — все heal слоя фона/loose. */
|
/* heal в room-space (со сдвигом POP_YOFF) — все heal слоя фона/loose. */
|
||||||
void pop_heal_off(int x, int y, int w, int h);
|
void pop_heal_off(int x, int y, int w, int h) __banked;
|
||||||
|
|
||||||
/* Чёрная плита по ЭКРАННЫМ координатам, с учётом переворота (POP_FLIP_TOP).
|
/* Чёрная плита по ЭКРАННЫМ координатам, с учётом переворота (POP_FLIP_TOP).
|
||||||
* Пишет банком 0x50 — и в видео-ОЗУ, и в ОЗУ-копию, иначе heal вернёт
|
* Пишет банком 0x50 — и в видео-ОЗУ, и в ОЗУ-копию, иначе heal вернёт
|
||||||
* стёртое. ВСЕ стирания слоя фона обязаны идти через неё, а не через голый
|
* стёртое. ВСЕ стирания слоя фона обязаны идти через неё, а не через голый
|
||||||
* bar: иначе при перевороте плита ложится в неотражённое место (так
|
* bar: иначе при перевороте плита ложится в неотражённое место (так
|
||||||
* ломались решётка и плита-потолок). */
|
* ломались решётка и плита-потолок). */
|
||||||
void pop_bar_black(int x, int top, int w, int h);
|
void pop_bar_black(int x, int top, int w, int h) __banked;
|
||||||
|
|
||||||
/* ---- Чтение тайла отрисованной комнаты ------------------------------ */
|
/* ---- Чтение тайла отрисованной комнаты ------------------------------ */
|
||||||
uint8_t pop_tile_code(int row, int col);
|
uint8_t pop_tile_code(int row, int col);
|
||||||
@@ -183,6 +183,6 @@ uint8_t pop_wall_modifier(int row, int col);
|
|||||||
|
|
||||||
/* Склянка зелья (draw_tile_fore, chtab_1): нужна и полной отрисовке тайла, и
|
/* Склянка зелья (draw_tile_fore, chtab_1): нужна и полной отрисовке тайла, и
|
||||||
* fore-проходу поверх персонажа. */
|
* fore-проходу поверх персонажа. */
|
||||||
void pop_potion_flask(int row, int col, uint8_t modif);
|
void pop_potion_flask(int row, int col, uint8_t modif) __banked;
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
/*
|
||||||
|
* pop_tile_cold.c — ХОЛОДНАЯ половина общих листьев слоя фона (банк 5).
|
||||||
|
*
|
||||||
|
* ЗАЧЕМ РАЗРЕЗ. pop_tile.c был самым крупным жильцом резидента (5 972 Б
|
||||||
|
* кода), а резидент huge-режима — это всё окно 0x4100..0xBB00, и место в нём
|
||||||
|
* нужно под музыку, заставки и UI. Но целиком pop_tile не уедет: его
|
||||||
|
* const-таблицы (POP_TILE_DIV/MOD, pop_tile_table, таблицы кадров) читают
|
||||||
|
* банки 2, 3, 7 и 8, а таблица в чужом банке не видна.
|
||||||
|
*
|
||||||
|
* КРИТЕРИЙ ОТБОРА — ЗАМЕР, а не догадка. Каждая функция pop_tile была
|
||||||
|
* посчитана брейкпоинтом-счётчиком в MAME: сколько вызовов в кадре покоя и
|
||||||
|
* сколько в кадре ПОЛНОЙ перерисовки комнаты (её форсирует чит +/-). Сюда
|
||||||
|
* переехало только то, у чего пик <= 3 вызовов на кадр — и проверено это
|
||||||
|
* на ДВУХ тайлсетах: подземелье (уровень 1) и дворец (уровень 4), потому
|
||||||
|
* что pop_mem_b рисует композитный кусок и мог оказаться дворцовым.
|
||||||
|
* Замер: pop_mem_b 0, pop_cd_hit 0, pop_t_win_set 0..1, pop_heal_off 0..1,
|
||||||
|
* pop_room_set_above 1, pop_bar_black 3, pop_potion_flask 0..1,
|
||||||
|
* pop_cd_hit_slot 2..3 — при канарейке pop_blit_b 184..214 в том же кадре.
|
||||||
|
*
|
||||||
|
* ЦЕНА. Трамплин замерен: 252 такта пролог + 84 эпилог + ~50 на стороне
|
||||||
|
* вызывающего = ~410 тактов на вызов. На кадр покоя выходит ~1 000 тактов
|
||||||
|
* (0,2 % работы кадра), на кадр редрава ~3 700 (0,009 растра).
|
||||||
|
*
|
||||||
|
* ЧТО ОСТАЛОСЬ В РЕЗИДЕНТЕ и почему: pop_blit_b (184 вызова на редрав) со
|
||||||
|
* своими статиками blit_b_clip/blit_b_oversize, pop_cd_touch (198),
|
||||||
|
* pop_tile_code (296), pop_wall_modifier (101), pop_env_b (73),
|
||||||
|
* pop_tile_mod (70), pop_cd_batch_end (40) — и все const-таблицы.
|
||||||
|
* pop_fore_set_clip оставлен намеренно: 88 Б не стоят отказа от прямого
|
||||||
|
* вызова из банка 4, ради которого он и заводился.
|
||||||
|
*
|
||||||
|
* ВЫЗОВЫ ОТСЮДА В РЕЗИДЕНТ — прямые, без трамплина: W1/W2 замаплены всегда.
|
||||||
|
* Поэтому blit_b_clip (статик горячей половины) просто перестал быть static
|
||||||
|
* и объявлен в _pop_tile.h, а не переехал следом.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <graphics.h>
|
||||||
|
#include <gfx.h>
|
||||||
|
#include <sprite.h>
|
||||||
|
#include "pop_bg.h" /* POP_YOFF, POP_PLAYFIELD_H */
|
||||||
|
#include "pop_map.h" /* pop_upside — переворот (зелье инверсии) */
|
||||||
|
#include "pop_cdraw.h" /* pop_cd, pop_cd_dirty, pop_cd_ymin/ymax */
|
||||||
|
#include "pop_tile.h"
|
||||||
|
#include "_pop_draw.h" /* pop_heal_fast */
|
||||||
|
#include "_pop_tile.h" /* blit_b_clip — из горячей половины */
|
||||||
|
|
||||||
|
#define FLIP_TOP(top, h) POP_FLIP_TOP(top, h) /* формула — в pop_bg.h */
|
||||||
|
|
||||||
|
/* Смещение склянки внутри тайла (seg008 draw_tile_fore): те же числа, что в
|
||||||
|
* tile_table горячей половины — дублируем, потому что там они локальные
|
||||||
|
* define'ы, а тащить их в публичный заголовок ради одной функции незачем. */
|
||||||
|
#define POT_FORE_X 2
|
||||||
|
#define POT_FORE_Y (-3)
|
||||||
|
|
||||||
|
/* Прямоугольник слота против метки — БЕЗ передачи пяти аргументов.
|
||||||
|
*
|
||||||
|
* pop_cd_hit принимает (p, x0, y0, x1, y1): три последних идут стеком, и
|
||||||
|
* функция целиком уезжает в IX-фрейм — 45 % её тактов уходит на `-n(ix)`
|
||||||
|
* (замер asm 2026-08-19). А зовут её из cd_quiet дважды на слот, то есть
|
||||||
|
* до восьми раз за кадр. Здесь координаты берутся прямо из pop_cd, и
|
||||||
|
* аргументов остаётся два.
|
||||||
|
*
|
||||||
|
* Спрайт и накладной проверяются РАЗДЕЛЬНО — объединённый bbox ловит
|
||||||
|
* касания углом, которых нет (разбор в cd_quiet, pop_cdraw.c). */
|
||||||
|
static int hit_x, hit_y; /* аргументы hit_rect — file-scope, не стек */
|
||||||
|
static uint16_t hit_w, hit_h;
|
||||||
|
static uint8_t hit_p;
|
||||||
|
|
||||||
|
static uint8_t hit_rect(void)
|
||||||
|
{
|
||||||
|
int8_t c0, c1, c;
|
||||||
|
uint8_t ya, yb;
|
||||||
|
int x1 = hit_x + (int)hit_w - 1;
|
||||||
|
int y1 = hit_y + (int)hit_h - 1;
|
||||||
|
if (!hit_w || !hit_h) return 0;
|
||||||
|
if (x1 < 0 || hit_x > 319) return 0;
|
||||||
|
if (y1 < 0 || hit_y > 255) return 0;
|
||||||
|
ya = (uint8_t)(hit_y < 0 ? 0 : hit_y);
|
||||||
|
yb = (uint8_t)(y1 > 255 ? 255 : y1);
|
||||||
|
c0 = (int8_t)((hit_x < 0 ? 0 : hit_x) >> 5);
|
||||||
|
c1 = (int8_t)((x1 > 319 ? 319 : x1) >> 5);
|
||||||
|
for (c = c0; c <= c1; c++)
|
||||||
|
if (ya <= pop_cd_ymax[hit_p][c] && yb >= pop_cd_ymin[hit_p][c]) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t pop_cd_hit_slot(uint8_t who, uint8_t p) __banked
|
||||||
|
{
|
||||||
|
const pop_cdraw_t *s = &pop_cd[who];
|
||||||
|
if (!(pop_cd_dirty & (1 << p))) return 0;
|
||||||
|
hit_p = p;
|
||||||
|
hit_x = s->x[p]; hit_y = s->y[p] + POP_YOFF;
|
||||||
|
hit_w = s->w[p]; hit_h = s->h[p];
|
||||||
|
if (hit_rect()) return 1;
|
||||||
|
if (!s->ovalid[p]) return 0;
|
||||||
|
hit_x = s->ox[p]; hit_y = s->oy[p] + POP_YOFF;
|
||||||
|
hit_w = s->ow[p]; hit_h = s->oh[p];
|
||||||
|
return hit_rect();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t pop_cd_hit(uint8_t p, int x0, int y0, int x1, int y1) __banked
|
||||||
|
{
|
||||||
|
int8_t c0, c1, c;
|
||||||
|
uint8_t ya, yb;
|
||||||
|
if (!(pop_cd_dirty & (1 << p))) return 0;
|
||||||
|
if (x1 < 0 || x0 > 319) return 0;
|
||||||
|
if (x0 < 0) x0 = 0;
|
||||||
|
if (x1 > 319) x1 = 319;
|
||||||
|
if (y1 < 0 || y0 > 255) return 0;
|
||||||
|
ya = (uint8_t)(y0 < 0 ? 0 : y0);
|
||||||
|
yb = (uint8_t)(y1 > 255 ? 255 : y1);
|
||||||
|
c0 = (int8_t)(x0 >> 5);
|
||||||
|
c1 = (int8_t)(x1 >> 5);
|
||||||
|
/* Пересечение отрезков [ya,yb] и [ymin,ymax] хотя бы в одной колонке.
|
||||||
|
* Пустая колонка держит ymin = 255, ymax = 0 — условие ниже её отсеет
|
||||||
|
* само, отдельной проверки «пусто» не нужно. */
|
||||||
|
for (c = c0; c <= c1; c++)
|
||||||
|
if (ya <= pop_cd_ymax[p][c] && yb >= pop_cd_ymin[p][c]) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Страница приведена в порядок — снять с неё метку целиком. */
|
||||||
|
/* Начальное состояние обеих страниц — «ничего не трогали». ЯВНО, а не
|
||||||
|
* расчётом на обнуление _DATA: пустая колонка обозначается ymin = 255,
|
||||||
|
* ymax = 0, а нули от crt0 читались бы как «затронута строка 0». */
|
||||||
|
void pop_cd_init(void) __banked
|
||||||
|
{
|
||||||
|
pop_cd_clear(0);
|
||||||
|
pop_cd_clear(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void pop_cd_clear(uint8_t p) __banked
|
||||||
|
{
|
||||||
|
uint8_t c;
|
||||||
|
for (c = 0; c < 10; c++) { pop_cd_ymin[p][c] = 255; pop_cd_ymax[p][c] = 0; }
|
||||||
|
pop_cd_dirty = (uint8_t)(pop_cd_dirty & ~(1 << p));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Окно под точечную перерисовку/запечку — контракт в шапке pop_tile.h.
|
||||||
|
* Логическая пара нужна для отсева тайлов (он считается из ряда комнаты),
|
||||||
|
* экранная — для клипа самого блита; при перевороте они разные. */
|
||||||
|
void pop_t_win_set(int x, int ytop, int w, int h) __banked
|
||||||
|
{
|
||||||
|
pop_t_fclip_x0 = x;
|
||||||
|
pop_t_fclip_x1 = x + w;
|
||||||
|
pop_t_fclip_ly0 = ytop;
|
||||||
|
pop_t_fclip_ly1 = ytop + h;
|
||||||
|
if (pop_upside) {
|
||||||
|
int t = FLIP_TOP(ytop, h);
|
||||||
|
pop_t_fclip_y0 = t;
|
||||||
|
pop_t_fclip_y1 = t + h;
|
||||||
|
} else {
|
||||||
|
pop_t_fclip_y0 = ytop;
|
||||||
|
pop_t_fclip_y1 = ytop + h;
|
||||||
|
}
|
||||||
|
pop_t_fclip_on = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pop_t_win_clear(void) __banked
|
||||||
|
{ pop_t_fclip_on = 0; }
|
||||||
|
|
||||||
|
/* Блит getimage-блоба из ОБЫЧНОЙ памяти (W2), а не из EMM-страницы атласа:
|
||||||
|
* те же клип полосы у потолка и окно перерисовки, что у pop_blit_b, но без
|
||||||
|
* atlas_image и без gfx_w0_map/unmap (это ещё ~1 350 такта). Заведено под
|
||||||
|
* композитный спрайт падающего куска (pop_room.c: три части сложены в один
|
||||||
|
* блоб, один вызов блита вместо трёх). */
|
||||||
|
void pop_mem_b(const uint8_t *img, int x, int ybottom) __banked
|
||||||
|
{
|
||||||
|
uint8_t w = img[0], h = img[2];
|
||||||
|
int top;
|
||||||
|
if (pop_t_fclip_on &&
|
||||||
|
(x >= pop_t_fclip_x1 || x + 64 <= pop_t_fclip_x0 ||
|
||||||
|
ybottom + POP_YOFF + 1 <= pop_t_fclip_ly0 ||
|
||||||
|
ybottom + POP_YOFF - 64 >= pop_t_fclip_ly1))
|
||||||
|
return;
|
||||||
|
if (!w || !h) return;
|
||||||
|
top = ybottom - (int)h + 1 + POP_YOFF;
|
||||||
|
if (pop_upside) top = FLIP_TOP(top, h);
|
||||||
|
if (!pop_t_fclip_on && !pop_t_clip_top &&
|
||||||
|
!(pop_upside && (top < POP_YOFF || top + (int)h > POP_CLIP_BOTTOM)) &&
|
||||||
|
x >= 0 && top >= 0 && x + (int)w <= 320 && top + (int)h <= 256) {
|
||||||
|
if (pop_upside) gfx_blit_noclip_vflip(x, top, img);
|
||||||
|
else gfx_blit_noclip(x, top, img);
|
||||||
|
pop_cd_touch(x, top, (int)w, (int)h);
|
||||||
|
} else {
|
||||||
|
blit_b_clip(img, x, top, w, h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Heal по КОМНАТНОЙ координате (её и знают точечные перерисовки: чомпер,
|
||||||
|
* пики, loose, ворота, полоса потолка). При перевороте прямоугольник
|
||||||
|
* обязан отразиться ровно так же, как сам тайл, — иначе heal чистит одно
|
||||||
|
* место, а рисует в другое: именно так «ломалась анимация чомперов»
|
||||||
|
* (найдено пользователем на первом же прогоне зеркального фона). */
|
||||||
|
void pop_bar_black(int x, int top, int w, int h) __banked
|
||||||
|
{
|
||||||
|
if (pop_upside) top = FLIP_TOP(top, h);
|
||||||
|
gfx_set_bank(GFX_BANK_NORMAL);
|
||||||
|
setfillstyle(SOLID_FILL, BLACK);
|
||||||
|
bar(x, top, x + w - 1, top + h - 1);
|
||||||
|
pop_cd_touch(x, top, w, h); /* bar идёт мимо pop_blit_b (DRAW-COST) */
|
||||||
|
}
|
||||||
|
|
||||||
|
void pop_heal_off(int x, int y, int w, int h) __banked
|
||||||
|
{
|
||||||
|
int top = y + POP_YOFF;
|
||||||
|
if (pop_upside) top = FLIP_TOP(top, h);
|
||||||
|
pop_heal_fast(x, top, w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Зелье: склянка (tiles_10_potion, seg008 draw_tile_fore) --------- *
|
||||||
|
* Тип = (modif & 0xF8) >> 3: 0 = пусто (ничего не рисуем), 2..4 — «большая»
|
||||||
|
* склянка (id 13), остальные — маленькая (id 12). Спрайты из chtab_1 (атлас
|
||||||
|
* pop_pot), поэтому НЕ из env: раньше рисовали chtab_6 id 12 (res212),
|
||||||
|
* которого в VDUNGEON нет — оттуда и «мусорный объект». Позиция оригинала:
|
||||||
|
* xh = fore_x + draw_xh, xl = 6, ybottom = fore_y + draw_main_y.
|
||||||
|
*
|
||||||
|
* Здесь, а не в банке: склянку рисует и полная отрисовка тайла (холодная
|
||||||
|
* половина), и fore-проход поверх персонажа (горячая). */
|
||||||
|
void pop_potion_flask(int row, int col, uint8_t modif) __banked
|
||||||
|
{
|
||||||
|
uint8_t type = (uint8_t)((modif & 0xF8) >> 3);
|
||||||
|
uint8_t id = 12;
|
||||||
|
if (type == 0) return;
|
||||||
|
if (type >= 2 && type < 5) id = 13; /* большая склянка */
|
||||||
|
if (pop_palace) id += 2; /* seg008:747: во дворце склянки другие */
|
||||||
|
pop_pot_b(id, (POT_FORE_X + POP_COL_XH[col]) * 8 + 6,
|
||||||
|
POT_FORE_Y + 63 * row + 62);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ряд 0 комнаты СНИЗУ (fg-коды) — нужен draw_tile_topright/floorright, когда
|
||||||
|
* тайл нижнего ряда стал видимым (empty после падения loose): «стена вниз» =
|
||||||
|
* топрайт тайла room_B(0, col-1). 0 — нет комнаты снизу (кромка = стена). */
|
||||||
|
void pop_room_set_below(const uint8_t *below_row0_fg) __banked
|
||||||
|
{ pop_t_below = below_row0_fg; }
|
||||||
|
|
||||||
|
/* Ряд 2 (fg-коды + модификаторы) комнаты СВЕРХУ — для полосы кладки у
|
||||||
|
* потолка (seg008 draw_room: доп. верхний ряд из room_A, draw_tile_aboveroom
|
||||||
|
* при draw_main_y=-1). 0 — нет комнаты сверху (кромка уровня → пол). */
|
||||||
|
void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod) __banked
|
||||||
|
{ pop_t_above = above_row2_fg; pop_t_abovemod = above_row2_mod; }
|
||||||
@@ -651,13 +651,34 @@ uint8_t pop_trob_room_active(uint8_t room) __banked
|
|||||||
* анимацию или нет). */
|
* анимацию или нет). */
|
||||||
#define TORCH_ANIM_DIV 1
|
#define TORCH_ANIM_DIV 1
|
||||||
|
|
||||||
#if TORCH_ANIM_DIV > 1
|
|
||||||
static uint8_t torch_tick;
|
static uint8_t torch_tick;
|
||||||
|
#if TORCH_ANIM_DIV > 1
|
||||||
#define TORCH_ANIM_NOW ((uint8_t)(torch_tick % TORCH_ANIM_DIV) == 0)
|
#define TORCH_ANIM_NOW ((uint8_t)(torch_tick % TORCH_ANIM_DIV) == 0)
|
||||||
#else
|
#else
|
||||||
#define TORCH_ANIM_NOW 1
|
#define TORCH_ANIM_NOW 1
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
void pop_trob_qsave(pop_qs_io_t *io) __banked
|
||||||
|
{
|
||||||
|
uint8_t i;
|
||||||
|
pop_qs_bytes(io, room_modif, sizeof(room_modif));
|
||||||
|
pop_qs_bytes(io, room_seen, sizeof(room_seen));
|
||||||
|
pop_qs_u8(io, &trobs_count);
|
||||||
|
for (i = 0; i < TROBS_MAX; i++) {
|
||||||
|
pop_qs_u8(io, &trobs[i].room);
|
||||||
|
pop_qs_u8(io, &trobs[i].tilepos);
|
||||||
|
pop_qs_i8(io, &trobs[i].type);
|
||||||
|
}
|
||||||
|
pop_qs_u16(io, &trob_seed.lo);
|
||||||
|
pop_qs_u16(io, &trob_seed.hi);
|
||||||
|
pop_qs_u8(io, &torch_tick);
|
||||||
|
if (io->reading && io->apply) {
|
||||||
|
if (trobs_count > TROBS_MAX) { io->failed = 1; return; }
|
||||||
|
for (i = 0; i < POP_ROOMTILES; i++) trob_drawn[i] = 0;
|
||||||
|
pop_redraw_reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void pop_process_trobs(uint8_t cur_room) __banked
|
void pop_process_trobs(uint8_t cur_room) __banked
|
||||||
{
|
{
|
||||||
uint8_t i, w = 0;
|
uint8_t i, w = 0;
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
#ifndef POP_TROB_H
|
#ifndef POP_TROB_H
|
||||||
#define POP_TROB_H
|
#define POP_TROB_H
|
||||||
|
|
||||||
|
#include "pop_qsave.h"
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
#define POP_NROOMS 24
|
#define POP_NROOMS 24
|
||||||
@@ -26,6 +28,7 @@
|
|||||||
/* Сброс всего состояния (новый уровень / рестарт): забыть все комнаты и
|
/* Сброс всего состояния (новый уровень / рестарт): забыть все комнаты и
|
||||||
* очистить список trob. */
|
* очистить список trob. */
|
||||||
void pop_trob_reset(void) __banked;
|
void pop_trob_reset(void) __banked;
|
||||||
|
void pop_trob_qsave(pop_qs_io_t *io) __banked;
|
||||||
|
|
||||||
/* Указатель на persistent modifier-массив комнаты room (1..24), 30 байт.
|
/* Указатель на persistent modifier-массив комнаты room (1..24), 30 байт.
|
||||||
* При ПЕРВОМ обращении лениво инициализируется из backtable уровня
|
* При ПЕРВОМ обращении лениво инициализируется из backtable уровня
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
#include "pop_vflip.h" /* зеркальный набор персонажей (зелье инверсии) */
|
#include "pop_vflip.h" /* зеркальный набор персонажей (зелье инверсии) */
|
||||||
#include "pop_pace.h" /* фиксированный логический кадр по лучу */
|
#include "pop_pace.h" /* фиксированный логический кадр по лучу */
|
||||||
#include "roomtest_cold.h" /* холодная половина цикла — в банке */
|
#include "roomtest_cold.h" /* холодная половина цикла — в банке */
|
||||||
|
#include "pop_qsave.h"
|
||||||
|
|
||||||
__sfr __at (0xFE) io_border;
|
__sfr __at (0xFE) io_border;
|
||||||
|
|
||||||
@@ -62,8 +63,8 @@ __sfr __at (0xFE) io_border;
|
|||||||
#define PROF(n) ((void)0)
|
#define PROF(n) ((void)0)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/* Сколько банков грузит crt0_banked (читает его стартовый код).
|
/* Раскладка банков; их число crt0_banked получает автоматически из
|
||||||
* 1 = guards (ИИ/боёвка), 2 = pop_bg (ГОРЯЧАЯ половина фона: fore-проход,
|
* --bank флагов: 1 = guards (ИИ/боёвка), 2 = pop_bg (ГОРЯЧАЯ половина фона: fore-проход,
|
||||||
* оверлеи, кладка), 3 = pop_map (коллизия/физика), 4 = pop_cdraw
|
* оверлеи, кладка), 3 = pop_map (коллизия/физика), 4 = pop_cdraw
|
||||||
* (отрисовка персонажей + полоса HP), 5 = pop_ctrl (диспетчер управления),
|
* (отрисовка персонажей + полоса HP), 5 = pop_ctrl (диспетчер управления),
|
||||||
* 6 = pop_trob (ловушки/анимация тайлов), 7 = pop_room (ХОЛОДНАЯ половина
|
* 6 = pop_trob (ловушки/анимация тайлов), 7 = pop_room (ХОЛОДНАЯ половина
|
||||||
@@ -72,14 +73,7 @@ __sfr __at (0xFE) io_border;
|
|||||||
* отладочная навигация — см. roomtest_cold.h).
|
* отладочная навигация — см. roomtest_cold.h).
|
||||||
* Отрисовка персонажей и холодная половина фона вынесены из банка 2
|
* Отрисовка персонажей и холодная половина фона вынесены из банка 2
|
||||||
* отдельно: тот дважды подходил к 16 КБ вплотную, а расти ему ещё есть
|
* отдельно: тот дважды подходил к 16 КБ вплотную, а расти ему ещё есть
|
||||||
* куда — тайлы поздних уровней, чомперы, зеркало.
|
* куда — тайлы поздних уровней, чомперы, зеркало. */
|
||||||
*
|
|
||||||
* ДЕРЖАТЬ В СОГЛАСИИ с числом --bank в Makefile. Забыть — это не ошибка
|
|
||||||
* сборки, а зависание: crt0 выделит меньше страниц, _bank_pages[N] так и
|
|
||||||
* останется нулём, и трамплин прыгнет по 0xC000 в мусор (см. шапку
|
|
||||||
* runtime/bank.s). Проверено 2026-08-05: с n_banks=4 и пятым банком игра
|
|
||||||
* доходила до конца загрузки ресурсов и вставала намертво в DSS. */
|
|
||||||
const uint8_t n_banks = 8;
|
|
||||||
|
|
||||||
/* Режим читов (pop_cheat.h): на время разработки включаем в main. */
|
/* Режим читов (pop_cheat.h): на время разработки включаем в main. */
|
||||||
uint8_t pop_cheats;
|
uint8_t pop_cheats;
|
||||||
@@ -115,6 +109,8 @@ static void kbd_idle(void)
|
|||||||
#define SEQ_STAND 2
|
#define SEQ_STAND 2
|
||||||
#define ROOM_1 1
|
#define ROOM_1 1
|
||||||
#define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */
|
#define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */
|
||||||
|
#define KBD_QUICKSAVE 0x0B /* F6, PS/2 set 2 */
|
||||||
|
#define KBD_QUICKLOAD 0x01 /* F9, PS/2 set 2 */
|
||||||
|
|
||||||
#define RESPAWN_DELAY 400 /* авто-респавн (фолбэк), если не нажали ↑ */
|
#define RESPAWN_DELAY 400 /* авто-респавн (фолбэк), если не нажали ↑ */
|
||||||
#define RESPAWN_SETTLE 20 /* тиков до приёма ↑ (труп успел лечь) */
|
#define RESPAWN_SETTLE 20 /* тиков до приёма ↑ (труп успел лечь) */
|
||||||
@@ -250,6 +246,8 @@ int main(void)
|
|||||||
* железо), pop_pace_* молча откатываются на прежние gfx_wait_vsync. */
|
* железо), pop_pace_* молча откатываются на прежние gfx_wait_vsync. */
|
||||||
pop_pace_arm();
|
pop_pace_arm();
|
||||||
|
|
||||||
|
{
|
||||||
|
uint8_t qs_save_prev = 0, qs_load_prev = 0;
|
||||||
while (!kbd_raw_down(KBD_ESC)) {
|
while (!kbd_raw_down(KBD_ESC)) {
|
||||||
/* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один
|
/* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один
|
||||||
* переключ). Выключение возвращает показ на страницу 0. */
|
* переключ). Выключение возвращает показ на страницу 0. */
|
||||||
@@ -286,6 +284,18 @@ int main(void)
|
|||||||
/* Читы — В БАНКЕ (roomtest_cold.c): они дёргаются раз в кадр, а места
|
/* Читы — В БАНКЕ (roomtest_cold.c): они дёргаются раз в кадр, а места
|
||||||
* в резиденте W1/W2 нет (см. TASKS_OPEN, MEM). */
|
* в резиденте W1/W2 нет (см. TASKS_OPEN, MEM). */
|
||||||
pop_cheat_tick();
|
pop_cheat_tick();
|
||||||
|
/* F6/F9 только ставят запрос; файловая операция выполняется здесь,
|
||||||
|
* на границе кадров — вне окон Char и до heal/tick/draw. */
|
||||||
|
{
|
||||||
|
uint8_t s = kbd_raw_down(KBD_QUICKSAVE);
|
||||||
|
uint8_t l = kbd_raw_down(KBD_QUICKLOAD);
|
||||||
|
int8_t qrc;
|
||||||
|
if (s && !qs_save_prev) pop_qsave_request_save();
|
||||||
|
if (l && !qs_load_prev) pop_qsave_request_load();
|
||||||
|
qs_save_prev = s; qs_load_prev = l;
|
||||||
|
qrc = pop_qsave_process();
|
||||||
|
if (qrc > 0) dead_frames = 0;
|
||||||
|
}
|
||||||
if (frozen) {
|
if (frozen) {
|
||||||
PROF(0); /* чёрный: ждём vsync */
|
PROF(0); /* чёрный: ждём vsync */
|
||||||
/* Заморозка — тоже полноценный логический кадр: иначе якорь
|
/* Заморозка — тоже полноценный логический кадр: иначе якорь
|
||||||
@@ -733,6 +743,7 @@ int main(void)
|
|||||||
front = back;
|
front = back;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pop_shutdown(); /* снять хук, закрыть ввод/графику, отдать ресурсы */
|
pop_shutdown(); /* снять хук, закрыть ввод/графику, отдать ресурсы */
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -602,6 +602,69 @@ void enter_room_side(uint8_t room, uint8_t side) __banked
|
|||||||
* Страж такой смены не «видит» — он остаётся в своей комнате. */
|
* Страж такой смены не «видит» — он остаётся в своей комнате. */
|
||||||
void enter_room(uint8_t room) __banked { enter_room_side(room, 0xFF); }
|
void enter_room(uint8_t room) __banked { enter_room_side(room, 0xFF); }
|
||||||
|
|
||||||
|
void pop_qsave_restore_room(uint8_t room) __banked
|
||||||
|
{
|
||||||
|
uint8_t i, pg, *room_mod;
|
||||||
|
gfx_rect_t full;
|
||||||
|
uint8_t lft, rgt, up;
|
||||||
|
|
||||||
|
pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg,
|
||||||
|
rcol_fg, rcol_bg, below_fg);
|
||||||
|
pop_map_set(room_fg);
|
||||||
|
lft = pop_room_link(room, 0); rgt = pop_room_link(room, 1);
|
||||||
|
up = pop_room_link(room, 2);
|
||||||
|
pop_map_set_edges(lft, rgt, up, pop_room_link(room, 3));
|
||||||
|
pop_room_set_below(below_fg);
|
||||||
|
if (up) {
|
||||||
|
uint8_t *um = pop_trob_modif(up);
|
||||||
|
pop_level_access_begin();
|
||||||
|
for (i = 0; i < 10; i++)
|
||||||
|
above_fg[i] = pop_level_tile_raw(up, (uint8_t)(20 + i));
|
||||||
|
pop_level_access_end();
|
||||||
|
for (i = 0; i < 10; i++) above_mod[i] = um[20 + i];
|
||||||
|
pop_room_set_above(above_fg, above_mod);
|
||||||
|
pop_map_set_above(above_fg);
|
||||||
|
} else {
|
||||||
|
pop_room_set_above(0, 0); pop_map_set_above(0);
|
||||||
|
}
|
||||||
|
pop_map_set_room(room);
|
||||||
|
pop_loose_reset();
|
||||||
|
pop_trob_room_changed();
|
||||||
|
pop_bake_forget();
|
||||||
|
pop_loose_mob_room_changed();
|
||||||
|
room_mod = pop_trob_modif(room);
|
||||||
|
if (lft) {
|
||||||
|
uint8_t *m = pop_trob_modif(lft);
|
||||||
|
for (i = 0; i < 3; i++) {
|
||||||
|
lcol_bg[i] = m[9 + i * 10];
|
||||||
|
seam_sig[i] = seam_row_sig(m, lcol_fg, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rgt) {
|
||||||
|
uint8_t *m = pop_trob_modif(rgt);
|
||||||
|
rcol_bg[0] = m[0]; rcol_bg[1] = m[10]; rcol_bg[2] = m[20];
|
||||||
|
}
|
||||||
|
seam_redraw = seam_rows = 0;
|
||||||
|
pg = (uint8_t)(gfx_get_visible_page() ^ 1);
|
||||||
|
gfx_set_draw_page(pg);
|
||||||
|
setfillstyle(SOLID_FILL, BLACK); bar(0, 0, 319, 255);
|
||||||
|
pop_room_draw(room, room_fg, room_mod, lcol_fg, lcol_bg);
|
||||||
|
gfx_set_visible_page(pg);
|
||||||
|
gfx_set_draw_page((uint8_t)(pg ^ 1));
|
||||||
|
full.x = 0; full.y = 0; full.w = 320; full.h = 256;
|
||||||
|
gfx_copy_page(&full, GFX_COPY_DIRECT);
|
||||||
|
cur_room = room; kid_room = Kid.room;
|
||||||
|
/* Обе страницы теперь содержат только свежий фон. Старые габариты
|
||||||
|
* персонажей относятся к кадру до QuickLoad и не должны запускать heal
|
||||||
|
* поверх восстановленной комнаты. */
|
||||||
|
pop_cd[POP_CH_KID].valid[0] = pop_cd[POP_CH_KID].valid[1] = 0;
|
||||||
|
pop_cd[POP_CH_KID].ovalid[0] = pop_cd[POP_CH_KID].ovalid[1] = 0;
|
||||||
|
pop_cd[POP_CH_OPP].valid[0] = pop_cd[POP_CH_OPP].valid[1] = 0;
|
||||||
|
pop_cd[POP_CH_OPP].ovalid[0] = pop_cd[POP_CH_OPP].ovalid[1] = 0;
|
||||||
|
pop_cd_init();
|
||||||
|
pop_hp_invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
/* start_level (seg003:0055) + do_startpos (seg003:01A3) + set_start_pos
|
/* start_level (seg003:0055) + do_startpos (seg003:01A3) + set_start_pos
|
||||||
* (seg003:028A) — поставить Кида в стартовую позицию УРОВНЯ. Один вход и
|
* (seg003:028A) — поставить Кида в стартовую позицию УРОВНЯ. Один вход и
|
||||||
* для запуска, и для рестарта после смерти / выпадения из уровня: в
|
* для запуска, и для рестарта после смерти / выпадения из уровня: в
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ extern uint8_t pop_checkpoint; /* checkpoint (seg000:270) — ставит у
|
|||||||
* Обе в банке 8: зовутся раз на комнату, а кода там на полтора килобайта. */
|
* Обе в банке 8: зовутся раз на комнату, а кода там на полтора килобайта. */
|
||||||
void enter_room_side(uint8_t room, uint8_t side) __banked;
|
void enter_room_side(uint8_t room, uint8_t side) __banked;
|
||||||
void enter_room(uint8_t room) __banked;
|
void enter_room(uint8_t room) __banked;
|
||||||
|
/* Пересобрать только производные кэши и обе страницы после QuickLoad.
|
||||||
|
* Игровые таймеры, trobs, mobs и персонажей не изменяет. */
|
||||||
|
void pop_qsave_restore_room(uint8_t room) __banked;
|
||||||
|
|
||||||
/* ---- холодная половина ----------------------------------------------- */
|
/* ---- холодная половина ----------------------------------------------- */
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,17 @@
|
|||||||
#include <sprite.h>
|
#include <sprite.h>
|
||||||
#include <sprinter.h>
|
#include <sprinter.h>
|
||||||
#include <sprinter_mem.h>
|
#include <sprinter_mem.h>
|
||||||
|
#include "../pop_qsave.h"
|
||||||
|
|
||||||
|
/* Сериализаторы приватного состояния входят в те же крупные модули, что и
|
||||||
|
* физика/ИИ. Сами QuickSave-тесты работают на target с EMM; обычным
|
||||||
|
* ucsim-сценам нужны только символы, и вызывать их они не должны. */
|
||||||
|
void pop_qs_bytes(pop_qs_io_t *io, void *p, uint16_t n)
|
||||||
|
{ (void)io; (void)p; (void)n; }
|
||||||
|
void pop_qs_u8(pop_qs_io_t *io, uint8_t *p) { (void)io; (void)p; }
|
||||||
|
void pop_qs_i8(pop_qs_io_t *io, int8_t *p) { (void)io; (void)p; }
|
||||||
|
void pop_qs_u16(pop_qs_io_t *io, uint16_t *p) { (void)io; (void)p; }
|
||||||
|
void pop_qs_i16(pop_qs_io_t *io, int *p) { (void)io; (void)p; }
|
||||||
|
|
||||||
#include "pop_kid.h"
|
#include "pop_kid.h"
|
||||||
#include "pop_guard.h"
|
#include "pop_guard.h"
|
||||||
@@ -35,6 +46,8 @@
|
|||||||
#include "pop_trob.h"
|
#include "pop_trob.h"
|
||||||
#include "pop_level.h"
|
#include "pop_level.h"
|
||||||
#include "pop_redraw.h"
|
#include "pop_redraw.h"
|
||||||
|
#include "pop_pace.h"
|
||||||
|
#include "pop_sfx.h"
|
||||||
#include "_pop_draw.h"
|
#include "_pop_draw.h"
|
||||||
#include "pop_cdraw.h"
|
#include "pop_cdraw.h"
|
||||||
#include "stubs.h"
|
#include "stubs.h"
|
||||||
@@ -49,6 +62,7 @@ void _gfx_w0_isr(void) { }
|
|||||||
|
|
||||||
void gfx_w0_map(uint8_t page) { (void)page; }
|
void gfx_w0_map(uint8_t page) { (void)page; }
|
||||||
void gfx_w0_unmap(void) { }
|
void gfx_w0_unmap(void) { }
|
||||||
|
void gfx_w0_page_prepare(uint8_t page) { (void)page; }
|
||||||
|
|
||||||
void gfx_blit_cols(int x, int y, const void *img, uint8_t flip)
|
void gfx_blit_cols(int x, int y, const void *img, uint8_t flip)
|
||||||
{ (void)x; (void)y; (void)img; (void)flip; }
|
{ (void)x; (void)y; (void)img; (void)flip; }
|
||||||
@@ -95,6 +109,12 @@ int read(int fd, void *b, int n)
|
|||||||
|
|
||||||
int close(int fd) { (void)fd; return 0; }
|
int close(int fd) { (void)fd; return 0; }
|
||||||
|
|
||||||
|
int bank_load_file(uint8_t page, uint16_t off, const char *path, uint16_t max)
|
||||||
|
{
|
||||||
|
(void)page; (void)off; (void)path;
|
||||||
|
return kid_blob_size <= max ? (int)kid_blob_size : (int)max;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- клавиатура: управляемая тестом --------------------------------- */
|
/* ---- клавиатура: управляемая тестом --------------------------------- */
|
||||||
|
|
||||||
uint16_t tk_keys[TK_KEYS_MAX];
|
uint16_t tk_keys[TK_KEYS_MAX];
|
||||||
@@ -124,6 +144,13 @@ void tk_key_down(uint16_t code)
|
|||||||
if (tk_keys[i] == 0) { tk_keys[i] = code; return; }
|
if (tk_keys[i] == 0) { tk_keys[i] = code; return; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Темп кадра и звук относятся к окружению реальной машины. Физика и
|
||||||
|
* управление только оставляют выборки луча и заявки эффектов; host-тесты
|
||||||
|
* проверяют их игровую логику, поэтому здесь достаточно no-op контрактов. */
|
||||||
|
void pop_beam_sample(void) __preserves_regs(b, c, d, e) { }
|
||||||
|
void pop_sfx_play(uint8_t id) { (void)id; }
|
||||||
|
void pop_sfx_stop(void) { }
|
||||||
|
|
||||||
/* ---- 2. соседние модули движка -------------------------------------- */
|
/* ---- 2. соседние модули движка -------------------------------------- */
|
||||||
|
|
||||||
/* room_modif текущей комнаты: пики (счётчик фазы), ворота (высота),
|
/* room_modif текущей комнаты: пики (счётчик фазы), ворота (высота),
|
||||||
@@ -167,8 +194,9 @@ void pop_start_chompers(uint8_t room, int8_t row) __banked
|
|||||||
{ (void)room; (void)row; }
|
{ (void)room; (void)row; }
|
||||||
|
|
||||||
void pop_trigger_button(uint8_t room, uint8_t tilepos,
|
void pop_trigger_button(uint8_t room, uint8_t tilepos,
|
||||||
uint8_t button_type, uint8_t modifier) __banked
|
uint8_t button_type, uint8_t modifier,
|
||||||
{ (void)modifier; log_put(TK_BUTTON, room, tilepos, button_type); }
|
uint8_t snd) __banked
|
||||||
|
{ (void)modifier; (void)snd; log_put(TK_BUTTON, room, tilepos, button_type); }
|
||||||
|
|
||||||
void pop_set_redraw(uint8_t tilepos, uint8_t kind, uint8_t pages) __banked
|
void pop_set_redraw(uint8_t tilepos, uint8_t kind, uint8_t pages) __banked
|
||||||
{ log_put(TK_REDRAW, tilepos, kind, pages); }
|
{ log_put(TK_REDRAW, tilepos, kind, pages); }
|
||||||
|
|||||||
@@ -84,19 +84,21 @@ TC_TEST(char_take_hp_goes_to_active)
|
|||||||
pop_saveshad();
|
pop_saveshad();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Чит бессмертия — только Киду: страж обязан гибнуть и с ним (иначе
|
/* Уровень 2 чита гасит только мелкий урон Киду. Мгновенная смерть
|
||||||
* отладочный режим ломает боёвку). */
|
* (count=100) проходит всегда, а на стража чит не действует вовсе. */
|
||||||
TC_TEST(char_immortal_covers_kid_only)
|
TC_TEST(char_immortal_level2_covers_kid_only)
|
||||||
{
|
{
|
||||||
sc_room(room_flat, 1);
|
sc_room(room_flat, 1);
|
||||||
sc_kid_at(3, 1, -1);
|
sc_kid_at(3, 1, -1);
|
||||||
sc_guard_at(CHARID_2_GUARD, 6, 1, -1, 3);
|
sc_guard_at(CHARID_2_GUARD, 6, 1, -1, 3);
|
||||||
pop_immortal = 1;
|
pop_immortal = 2;
|
||||||
|
|
||||||
pop_loadkid();
|
pop_loadkid();
|
||||||
hitp_delta = 0;
|
hitp_delta = 0;
|
||||||
tc_check(pop_take_hp(100), 0, "kid: бессмертен", __LINE__);
|
tc_check(pop_take_hp(1), 0, "kid: мелкий урон погашен", __LINE__);
|
||||||
tc_check(hitp_delta, 0, "kid: HP не тронуто", __LINE__);
|
tc_check(hitp_delta, 0, "kid: HP не тронуто", __LINE__);
|
||||||
|
tc_check(pop_take_hp(100), 1, "kid: мгновенная смерть проходит", __LINE__);
|
||||||
|
tc_check(hitp_delta, -3, "kid: смертельная дельта", __LINE__);
|
||||||
pop_savekid();
|
pop_savekid();
|
||||||
|
|
||||||
pop_loadshad();
|
pop_loadshad();
|
||||||
@@ -497,7 +499,7 @@ void main(void)
|
|||||||
{
|
{
|
||||||
sc_init();
|
sc_init();
|
||||||
TC_RUN(char_take_hp_goes_to_active);
|
TC_RUN(char_take_hp_goes_to_active);
|
||||||
TC_RUN(char_immortal_covers_kid_only);
|
TC_RUN(char_immortal_level2_covers_kid_only);
|
||||||
TC_RUN(char_delta_hp_applies_per_character);
|
TC_RUN(char_delta_hp_applies_per_character);
|
||||||
TC_RUN(char_kid_survives_two_row_fall);
|
TC_RUN(char_kid_survives_two_row_fall);
|
||||||
TC_RUN(char_guard_dies_from_two_row_fall);
|
TC_RUN(char_guard_dies_from_two_row_fall);
|
||||||
|
|||||||
+37
-10
@@ -163,6 +163,34 @@ done
|
|||||||
[[ -z "$OUT" ]] && { echo "sprinter-cc: -o NAME is required" >&2; exit 1; }
|
[[ -z "$OUT" ]] && { echo "sprinter-cc: -o NAME is required" >&2; exit 1; }
|
||||||
[[ ${#SOURCES[@]} -eq 0 ]] && { echo "sprinter-cc: no input files" >&2; exit 1; }
|
[[ ${#SOURCES[@]} -eq 0 ]] && { echo "sprinter-cc: no input files" >&2; exit 1; }
|
||||||
|
|
||||||
|
# ------- Validate bank layout and derive n_banks ----------------------------
|
||||||
|
# crt0_banked loads 16 KB images consecutively and fills _bank_pages[1..N],
|
||||||
|
# therefore N is the highest bank number and the numbering must have no gaps.
|
||||||
|
# Several source modules may share one bank, so counting --bank options would
|
||||||
|
# be incorrect.
|
||||||
|
BANK_MAX=0
|
||||||
|
BANK_SEEN=()
|
||||||
|
for spec in "${BANK_SPECS[@]}"; do
|
||||||
|
if [[ ! "$spec" =~ ^([0-9]+)=(.+)$ ]]; then
|
||||||
|
echo "sprinter-cc: bad --bank specification: '$spec' (expected N=FILE.c)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
bank_n="${BASH_REMATCH[1]}"
|
||||||
|
bank_n=$((10#$bank_n))
|
||||||
|
if (( bank_n < 1 || bank_n > 15 )); then
|
||||||
|
echo "sprinter-cc: bank number must be in range 1..15: '$spec'" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
BANK_SEEN[$bank_n]=1
|
||||||
|
(( bank_n > BANK_MAX )) && BANK_MAX=$bank_n
|
||||||
|
done
|
||||||
|
for ((bank_n = 1; bank_n <= BANK_MAX; bank_n++)); do
|
||||||
|
if [[ -z "${BANK_SEEN[$bank_n]:-}" ]]; then
|
||||||
|
echo "sprinter-cc: bank $bank_n is missing; banks must be consecutive from 1 to $BANK_MAX" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
# ------- --w3: резидентный код окна W3 (подход A) ----------------------------
|
# ------- --w3: резидентный код окна W3 (подход A) ----------------------------
|
||||||
# W3-модули едут в область W3CODE @0xC000 и вызываются напрямую (без
|
# W3-модули едут в область W3CODE @0xC000 и вызываются напрямую (без
|
||||||
# трамплинов). Работает поверх small-лейаута: main-код с 0x4100 (W1/W2),
|
# трамплинов). Работает поверх small-лейаута: main-код с 0x4100 (W1/W2),
|
||||||
@@ -399,8 +427,8 @@ fi
|
|||||||
|
|
||||||
# 3. bank infrastructure — required whenever crt0_banked is in play.
|
# 3. bank infrastructure — required whenever crt0_banked is in play.
|
||||||
# Always assemble bank.s for _bank_pages + the bcall/bjump trampolines.
|
# Always assemble bank.s for _bank_pages + the bcall/bjump trampolines.
|
||||||
# If the user did not pass any --bank, generate a tiny stub providing
|
# Generate n_banks from the validated --bank layout. crt0_banked.s imports
|
||||||
# n_banks = 0 (crt0_banked.s imports this symbol unconditionally).
|
# this symbol unconditionally, including banked memory modes with zero banks.
|
||||||
# In BIG mode banks live in W1 at low16 = 0x4000; in HUGE / default
|
# In BIG mode banks live in W1 at low16 = 0x4000; in HUGE / default
|
||||||
# mode they live in W3 at low16 = 0xC000.
|
# mode they live in W3 at low16 = 0xC000.
|
||||||
BANK_RELS=()
|
BANK_RELS=()
|
||||||
@@ -416,14 +444,13 @@ if [[ "$CRT0_TYPE" == "banked" ]]; then
|
|||||||
asm_runtime "$RUNTIME/bank.s" "$BANK_TRAMP_REL"
|
asm_runtime "$RUNTIME/bank.s" "$BANK_TRAMP_REL"
|
||||||
BANK_RELS+=("$BANK_TRAMP_REL")
|
BANK_RELS+=("$BANK_TRAMP_REL")
|
||||||
|
|
||||||
if [[ ${#BANK_SPECS[@]} -eq 0 ]]; then
|
BANK_COUNT_C="$WORK/_n_banks_auto.c"
|
||||||
# User has no banks — supply n_banks=0 so crt0_banked links.
|
BANK_COUNT_REL="$WORK/_n_banks_auto.rel"
|
||||||
STUB_C="$WORK/_n_banks_stub.c"
|
echo "const unsigned char n_banks = $BANK_MAX;" > "$BANK_COUNT_C"
|
||||||
STUB_REL="$WORK/_n_banks_stub.rel"
|
run "$SDCC" "${CC_FLAGS[@]}" -c -o "$BANK_COUNT_REL" "$BANK_COUNT_C"
|
||||||
echo "const unsigned char n_banks = 0;" > "$STUB_C"
|
BANK_RELS+=("$BANK_COUNT_REL")
|
||||||
run "$SDCC" "${CC_FLAGS[@]}" -c -o "$STUB_REL" "$STUB_C"
|
|
||||||
BANK_RELS+=("$STUB_REL")
|
if [[ ${#BANK_SPECS[@]} -gt 0 ]]; then
|
||||||
else
|
|
||||||
for spec in "${BANK_SPECS[@]}"; do
|
for spec in "${BANK_SPECS[@]}"; do
|
||||||
bank_n="${spec%%=*}"
|
bank_n="${spec%%=*}"
|
||||||
bank_src="${spec#*=}"
|
bank_src="${spec#*=}"
|
||||||
|
|||||||
+18
-20
@@ -5,6 +5,17 @@
|
|||||||
|
|
||||||
## Ближайшее
|
## Ближайшее
|
||||||
|
|
||||||
|
- [ ] **Перевести новые EMM/W0-функции libc/libbgi на Z80 asm:**
|
||||||
|
`bank_load_file()`, `bank_save_file()` и `gfx_w0_page_prepare()`.
|
||||||
|
Сейчас универсальная C-обвязка (четыре аргумента, IX-frame, проверки
|
||||||
|
диапазона и сохранение временных значений) съедает часть выигрыша от
|
||||||
|
унификации загрузчиков PoP. Сделать компактные `__naked`-реализации
|
||||||
|
по образцу остальных низкоуровневых функций библиотек, строго сохранив
|
||||||
|
`__sdcccall(1)`, IX, W3 до возврата и семантику `-1 + errno`; fast-
|
||||||
|
вариант может вырезать проверки диапазона, safe обязан их оставить.
|
||||||
|
После правки: `make -C libc`, `make -C libbgi`, `make size-check` и
|
||||||
|
MAME-проверка Atlas/Kid/Level/Sound + QuickSave/QuickLoad.
|
||||||
|
|
||||||
- [ ] **Модульные тесты libc и libbgi под ucsim_z80** — план:
|
- [ ] **Модульные тесты libc и libbgi под ucsim_z80** — план:
|
||||||
`docs/host-tests-plan.md`. Обвязка уже готова и обкатана
|
`docs/host-tests-plan.md`. Обвязка уже готова и обкатана
|
||||||
(`testkit/`, первый потребитель — PoP/roomtest). Не срочно, но
|
(`testkit/`, первый потребитель — PoP/roomtest). Не срочно, но
|
||||||
@@ -61,12 +72,10 @@
|
|||||||
|
|
||||||
## Auto-banking (memory/banking_roadmap.md)
|
## Auto-banking (memory/banking_roadmap.md)
|
||||||
|
|
||||||
- [ ] **Сверять `n_banks` с реальным числом банков — ошибкой сборки.**
|
- [x] **Генерировать `n_banks` из реальной раскладки банков.**
|
||||||
Сейчас число банков задаётся ДВАЖДЫ и вручную: флагами `--bank N=…`
|
`sprinter-cc` вычисляет максимальный N из `--bank N=…`, проверяет диапазон
|
||||||
у `sprinter-cc` и константой `const uint8_t n_banks = N;` в
|
1..15 и отсутствие дыр, затем линкует сгенерированный `_n_banks`.
|
||||||
приложении (её читает `crt0_banked` при старте, чтобы выделить
|
Раньше число задавалось ещё и вручную в приложении; рассогласование давало зависание в
|
||||||
EMM-страницы и загрузить в них образы банков). Рассогласование не
|
|
||||||
даёт ни ошибки компиляции, ни ошибки линковки — только зависание в
|
|
||||||
рантайме: `_bank_pages[N]` остаётся нулём, трамплин отображает в
|
рантайме: `_bank_pages[N]` остаётся нулём, трамплин отображает в
|
||||||
окно страницу 0 и прыгает по 0xC000 в мусор (предупреждение об этом
|
окно страницу 0 и прыгает по 0xC000 в мусор (предупреждение об этом
|
||||||
уже есть в шапке `runtime/bank.s`).
|
уже есть в шапке `runtime/bank.s`).
|
||||||
@@ -75,20 +84,9 @@
|
|||||||
загрузки ресурсов и вставала намертво в дисковом коде DSS. Диагноз
|
загрузки ресурсов и вставала намертво в дисковом коде DSS. Диагноз
|
||||||
занял заметно больше, чем сама правка: симптом (зависание в чужом
|
занял заметно больше, чем сама правка: симптом (зависание в чужом
|
||||||
коде) никак не указывает на причину.
|
коде) никак не указывает на причину.
|
||||||
Сверять надо не количество, а **максимальный индекс** банка: `--bank`
|
Берётся не число опций (в одном банке может быть несколько модулей), а
|
||||||
нумеруются с 1 и в принципе могут идти с дырами, а `crt0_banked`
|
**максимальный индекс**. Дыры запрещены, поскольку `crt0_banked` и `mkexe`
|
||||||
заполняет `_bank_pages[1..n_banks]` подряд.
|
обрабатывают `_bank_pages[1..n_banks]` подряд.
|
||||||
Варианты, от простого к правильному:
|
|
||||||
1. **Проверка после линковки** — вытащить значение `_n_banks` из
|
|
||||||
`.ihx`/`.map` (это `const` в `_CODE`, адрес известен из карты) и
|
|
||||||
сравнить с `max(N)` по флагам; не совпало — `sprinter-cc` падает
|
|
||||||
с внятным сообщением. Ничего не ломает в существующих
|
|
||||||
приложениях, ставится сразу.
|
|
||||||
2. **Генерировать самим `sprinter-cc`** — он уже так делает, когда
|
|
||||||
банков нет (`const unsigned char n_banks = 0;`). Правильнее по
|
|
||||||
сути (единственный источник истины), но требует убрать константу
|
|
||||||
из всех банковых приложений, иначе дубль символа: `tests/banked`,
|
|
||||||
`banktest`, `banklocl`, `bankedbg`, `applications/PoP/roomtest`.
|
|
||||||
|
|
||||||
Phase 1 — file-level bin-packing (`toolchain/auto_bank.py`) — когда
|
Phase 1 — file-level bin-packing (`toolchain/auto_bank.py`) — когда
|
||||||
проект перерастёт ~30 KB кода: парсинг размеров из .rel/.map,
|
проект перерастёт ~30 KB кода: парсинг размеров из .rel/.map,
|
||||||
|
|||||||
+12
-5
@@ -262,11 +262,14 @@ tests/bgi_img.
|
|||||||
Скролл региона из НЕактивной страницы в активную (<sprite.h>):
|
Скролл региона из НЕактивной страницы в активную (<sprite.h>):
|
||||||
`gfx_scroll_h(area, dx, dirty)` — горизонтальный (dx>0 = вправо; быстрый
|
`gfx_scroll_h(area, dx, dirty)` — горизонтальный (dx>0 = вправо; быстрый
|
||||||
построчный accel-скролл без страйдов, DI-банды по 16 строк),
|
построчный accel-скролл без страйдов, DI-банды по 16 строк),
|
||||||
`gfx_scroll_v(area, dy, dirty)` — вертикальный (dy>0 = вниз; через
|
`gfx_scroll_v(area, dy, dirty)` — вертикальный (dy>0 = вниз; прямой
|
||||||
строку-буфер на стеке, ПОКА не оптимален — Port_Y один на burst, разный
|
колоночный accel-проход: STOP между чтением и записью позволяет безопасно
|
||||||
read/write Y невозможен без ломающего accel OUT). Копия = скролл + heal
|
сменить Port_Y, промежуточный RAM-буфер не нужен). Копия = скролл + heal
|
||||||
цели (банк 0x50). Открывшуюся полосу |d| не заполняют — возвращают в
|
цели (банк 0x50). Открывшуюся полосу |d| не заполняют — возвращают в
|
||||||
*dirty (NULL = не нужно). Пример: examples/scroll.
|
*dirty (NULL = не нужно). Пример: examples/scroll.
|
||||||
|
Для произвольных EMM-данных, которые затем маппятся в W0,
|
||||||
|
`gfx_w0_page_prepare(page)` устанавливает IRQ/NMI-стабы в `0x38/0x66` и
|
||||||
|
запоминает штатную DSS-страницу. `atlas_load()` вызывает её автоматически.
|
||||||
Стиль линий (Ф2d): `setlinestyle(style,upattern,thick)`/`getlinesettings`
|
Стиль линий (Ф2d): `setlinestyle(style,upattern,thick)`/`getlinesettings`
|
||||||
(SOLID/DOTTED/CENTER/DASHED/USERBIT + NORM/THICK) — на line/rectangle/
|
(SOLID/DOTTED/CENTER/DASHED/USERBIT + NORM/THICK) — на line/rectangle/
|
||||||
drawpoly.
|
drawpoly.
|
||||||
@@ -395,7 +398,8 @@ applications/PoP/docs/PORT_PLAN.md §2 — пользователь подтве
|
|||||||
|
|
||||||
| Сигнатура | Описание |
|
| Сигнатура | Описание |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `int cbl_open(freq_code, fmt, pump_mode, underrun_mode, fill)` | включить CBL (`0x90\|fmt\|freq`), зарегистрировать callback; 0 / -1+errno (EBUSY повтор, EINVAL — freq/fmt/pump_mode/underrun_mode плохие или OTIR+16-бит, ENOMEM — буфер тишины) |
|
| `int cbl_open(freq_code, fmt, pump_mode, fill)` | включить CBL (`0x90\|fmt\|freq`), зарегистрировать callback; недолив — забота приложения; **malloc не тянет**; 0 / -1+errno (EBUSY повтор, EINVAL — freq/fmt/pump_mode плохие или OTIR+16-бит) |
|
||||||
|
| `int cbl_open_silence(freq_code, fmt, pump_mode, fill)` | то же, но тишину при недоливе шлёт libc из своего буфера; ТОЛЬКО этот вариант тянет malloc (+ENOMEM). Буфер не освобождается — живёт до выхода |
|
||||||
| `void cbl_close(void)` | выключить CBL, снять хук, освободить буфер тишины (если был); висит на atexit |
|
| `void cbl_close(void)` | выключить CBL, снять хук, освободить буфер тишины (если был); висит на atexit |
|
||||||
| `void cbl_push_otir(const void *src, uint16_t n)` | пропихнуть n байт через `otir` в порт 0x4F; звать ИЗ fill() |
|
| `void cbl_push_otir(const void *src, uint16_t n)` | пропихнуть n байт через `otir` в порт 0x4F; звать ИЗ fill() |
|
||||||
| `void cbl_push_accel(const void *src, uint16_t n)` | то же акселератором (страница EMM 0xFD@0xC000); единственный путь для 16-бит |
|
| `void cbl_push_accel(const void *src, uint16_t n)` | то же акселератором (страница EMM 0xFD@0xC000); единственный путь для 16-бит |
|
||||||
@@ -475,7 +479,10 @@ inline `sprinter_page_w0..w3(page)`; ENV: `getenv putenv sysenv`.
|
|||||||
mem_info(&total,&free)` (реализации _bios/_estex; макро-выбор
|
mem_info(&total,&free)` (реализации _bios/_estex; макро-выбор
|
||||||
MEM_MANAGE_MODE_*). HOME-резидентный доступ к чужим страницам:
|
MEM_MANAGE_MODE_*). HOME-резидентный доступ к чужим страницам:
|
||||||
`bank_load_byte/store_byte/read/write` (своп W3 внутри; *_w1 —
|
`bank_load_byte/store_byte/read/write` (своп W3 внутри; *_w1 —
|
||||||
вариант через окно W1).
|
вариант через окно W1). `bank_load_file(page,off,path,max_size)` читает
|
||||||
|
файл непосредственно в диапазон EMM-страницы, `bank_save_file` записывает
|
||||||
|
диапазон одним WRITE; обе функции возвращают фактический размер либо -1.
|
||||||
|
Они восстанавливают W3 до возврата и безопасны для вызова из huge-банка.
|
||||||
|
|
||||||
## <sprinter_exit.h>
|
## <sprinter_exit.h>
|
||||||
|
|
||||||
|
|||||||
@@ -9,15 +9,15 @@ banked 1056
|
|||||||
bankedbg 1067
|
bankedbg 1067
|
||||||
banklocl 4683
|
banklocl 4683
|
||||||
banktest 3767
|
banktest 3767
|
||||||
bgi_img 8451
|
bgi_img 8680
|
||||||
bgitest 3751
|
bgitest 3751
|
||||||
bios_text 4461
|
bios_text 4461
|
||||||
blitperf 5860
|
blitperf 5860
|
||||||
blitw 4939
|
blitw 4939
|
||||||
cat 913
|
cat 913
|
||||||
cblstream 6464
|
cblstream 5850
|
||||||
cbltest 6613
|
cbltest 5996
|
||||||
cblwav 6681
|
cblwav 6089
|
||||||
conio 4605
|
conio 4605
|
||||||
conio2 3929
|
conio2 3929
|
||||||
convbench 3508
|
convbench 3508
|
||||||
@@ -45,6 +45,7 @@ pageflip 6334
|
|||||||
palfile 5406
|
palfile 5406
|
||||||
ptime 5744
|
ptime 5744
|
||||||
rt_test 5187
|
rt_test 5187
|
||||||
|
scroll 6744
|
||||||
seek 4188
|
seek 4188
|
||||||
simple 955
|
simple 955
|
||||||
solidt 11531
|
solidt 11531
|
||||||
|
|||||||
|
+11
-35
@@ -2,58 +2,37 @@
|
|||||||
* atlas_load — загрузить .atl-файл в свежую EMM-страницу (§3.1/§9в
|
* atlas_load — загрузить .atl-файл в свежую EMM-страницу (§3.1/§9в
|
||||||
* дизайна, вариант II: файл-офсет == офсет страницы == W0-адрес).
|
* дизайна, вариант II: файл-офсет == офсет страницы == W0-адрес).
|
||||||
*
|
*
|
||||||
* Порядок: выделить страницу (mem_alloc_pages) → замапить в W3 →
|
* Порядок: выделить страницу → bank_load_file() → проверить магию 'SPA1'
|
||||||
* read() файла ЦЕЛИКОМ в 0xC000 (ESTEX READ пишет в W3 — проверено
|
* → gfx_w0_page_prepare(). Никакого пересчёта офсетов: каталог и ленты
|
||||||
* tests/w0page P1) → проверить магию 'SPA1' → пропатчить ISR-стаб
|
* уже лежат по своим W0-адресам.
|
||||||
* (0x38: JP _gfx_w0_isr; 0x66: RETN — место зарезервировано в
|
|
||||||
* заголовке файла) → вернуть W3. Никакого пересчёта офсетов: каталог
|
|
||||||
* и ленты уже лежат по своим W0-адресам.
|
|
||||||
*
|
*
|
||||||
* Вызывать ВНЕ рендера (W3 на время загрузки занята страницей атласа).
|
* Вызывать ВНЕ рендера (W3 на время загрузки занята страницей атласа).
|
||||||
* Ошибки: -1 + errno (open/read/alloc; EINVAL — не .atl).
|
* Ошибки: -1 + errno (open/read/alloc; EINVAL — не .atl).
|
||||||
*/
|
*/
|
||||||
#include <fcntl.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <sprinter.h>
|
|
||||||
#include <sprinter_mem.h>
|
#include <sprinter_mem.h>
|
||||||
#include <sprite.h>
|
#include <sprite.h>
|
||||||
#include "../_bgi.h"
|
#include "../_bgi.h"
|
||||||
|
|
||||||
int atlas_load(atlas_t *a, const char *path)
|
int atlas_load(atlas_t *a, const char *path)
|
||||||
{
|
{
|
||||||
uint8_t *pg = (uint8_t *)0xC000;
|
uint8_t blk, ok, count, h[5];
|
||||||
uint8_t saved_w3, blk, ok, count;
|
int n;
|
||||||
uint16_t stub;
|
|
||||||
int fd, n;
|
|
||||||
|
|
||||||
fd = open(path, O_RDONLY);
|
|
||||||
if (fd < 0) return -1;
|
|
||||||
|
|
||||||
blk = mem_alloc_pages(1);
|
blk = mem_alloc_pages(1);
|
||||||
if (!blk) { close(fd); return -1; }
|
if (!blk) return -1;
|
||||||
|
|
||||||
if (!_gfx_w0_dss)
|
|
||||||
_gfx_w0_dss = _io_page_w0; /* снять страницу ядра DSS */
|
|
||||||
|
|
||||||
a->blk = blk;
|
a->blk = blk;
|
||||||
a->page = mem_get_page(blk, 0);
|
a->page = mem_get_page(blk, 0);
|
||||||
|
|
||||||
saved_w3 = _io_page_w3;
|
n = bank_load_file(a->page, 0, path, 16384);
|
||||||
sprinter_page_w3(a->page);
|
if (n >= 5) bank_read(a->page, 0, h, sizeof(h));
|
||||||
|
|
||||||
n = read(fd, pg, 16384);
|
|
||||||
ok = (n >= (int)_ATL_DATA_OFF &&
|
ok = (n >= (int)_ATL_DATA_OFF &&
|
||||||
pg[0] == 'S' && pg[1] == 'P' && pg[2] == 'A' && pg[3] == '1');
|
h[0] == 'S' && h[1] == 'P' && h[2] == 'A' && h[3] == '1');
|
||||||
count = 0;
|
count = 0;
|
||||||
if (ok) {
|
if (ok) {
|
||||||
count = pg[4];
|
count = h[4];
|
||||||
stub = (uint16_t)&_gfx_w0_isr;
|
gfx_w0_page_prepare(a->page);
|
||||||
pg[0x38] = 0xC3; /* JP _gfx_w0_isr */
|
|
||||||
pg[0x39] = (uint8_t)(stub & 0xFF);
|
|
||||||
pg[0x3A] = (uint8_t)(stub >> 8);
|
|
||||||
pg[0x66] = 0xED; /* RETN (NMI) */
|
|
||||||
pg[0x67] = 0x45;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Восстановить W3 ДО записи в *a: если atlas_t лежит в банковской
|
/* Восстановить W3 ДО записи в *a: если atlas_t лежит в банковской
|
||||||
@@ -63,9 +42,6 @@ int atlas_load(atlas_t *a, const char *path)
|
|||||||
* a->count оставался мусором, room_draw читал пиксели не с того
|
* a->count оставался мусором, room_draw читал пиксели не с того
|
||||||
* оффсета). a->blk/a->page выше пишутся ДО ремапа — их это не
|
* оффсета). a->blk/a->page выше пишутся ДО ремапа — их это не
|
||||||
* касалось, только a->count. */
|
* касалось, только a->count. */
|
||||||
sprinter_page_w3(saved_w3);
|
|
||||||
close(fd);
|
|
||||||
|
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
mem_free_block(blk);
|
mem_free_block(blk);
|
||||||
a->page = 0;
|
a->page = 0;
|
||||||
|
|||||||
@@ -27,18 +27,73 @@
|
|||||||
#include "../_bgi.h"
|
#include "../_bgi.h"
|
||||||
|
|
||||||
void gfx_scroll_h(const gfx_rect_t *area, int16_t dx, gfx_rect_t *dirty) {
|
void gfx_scroll_h(const gfx_rect_t *area, int16_t dx, gfx_rect_t *dirty) {
|
||||||
|
#ifdef GFX_NOCHECK
|
||||||
uint8_t saved = gfx_get_bank();
|
uint8_t saved = gfx_get_bank();
|
||||||
uint16_t adx = (dx < 0) ? (uint16_t)(-dx) : (uint16_t)dx;
|
uint16_t adx = (dx < 0) ? (uint16_t)(-dx) : (uint16_t)dx;
|
||||||
/* src-строка стартует правее на |dx| при сдвиге влево, dst — правее
|
|
||||||
* на dx при сдвиге вправо; так оба края остаются внутри area. */
|
|
||||||
uint16_t src_x = area->x + ((dx < 0) ? adx : 0);
|
uint16_t src_x = area->x + ((dx < 0) ? adx : 0);
|
||||||
uint16_t dst_x = area->x + ((dx > 0) ? adx : 0);
|
uint16_t dst_x = area->x + ((dx > 0) ? adx : 0);
|
||||||
uint16_t w = area->w - adx; /* байт (пикселей) в строке */
|
uint16_t w = area->w - adx;
|
||||||
uint8_t h = (uint8_t)area->h; /* строк (256 => 0 = 256 у leaf) */
|
uint8_t h = (uint8_t)area->h;
|
||||||
uint16_t src = _gfx_addr_shadow_base + src_x;
|
uint16_t src = _gfx_addr_shadow_base + src_x;
|
||||||
uint16_t dst = _gfx_addr_base + dst_x;
|
uint16_t dst = _gfx_addr_base + dst_x;
|
||||||
|
|
||||||
|
gfx_set_bank(GFX_BANK_NORMAL);
|
||||||
|
_bgi_begin();
|
||||||
|
while (w) {
|
||||||
|
uint8_t cw = (w > 255) ? 255 : (uint8_t)w;
|
||||||
|
_bgi_scroll_rows_raw(src, dst, cw, h, (uint8_t)area->y);
|
||||||
|
src += cw; dst += cw; w -= cw;
|
||||||
|
}
|
||||||
|
_bgi_end();
|
||||||
|
gfx_set_bank(saved);
|
||||||
|
|
||||||
|
if (dirty) {
|
||||||
|
dirty->x = (dx >= 0) ? area->x : (int16_t)(area->x + area->w - adx);
|
||||||
|
dirty->y = area->y; dirty->w = adx; dirty->h = area->h;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
int16_t x, y;
|
||||||
|
uint16_t aw, ah, cut, adx;
|
||||||
|
uint8_t saved, h;
|
||||||
|
uint16_t src_x, dst_x, w, src, dst;
|
||||||
|
|
||||||
|
if (!area) return;
|
||||||
|
x = area->x; y = area->y; aw = area->w; ah = area->h;
|
||||||
|
if (!aw || !ah) goto empty;
|
||||||
|
|
||||||
|
/* Raw-leaf не клипит. Нормализуем область до экрана и не допускаем
|
||||||
|
* 16-битного underflow в area->w-|dx|. */
|
||||||
|
if (x < 0) {
|
||||||
|
cut = (uint16_t)(-x);
|
||||||
|
if (cut >= aw) goto empty;
|
||||||
|
aw -= cut; x = 0;
|
||||||
|
}
|
||||||
|
if (y < 0) {
|
||||||
|
cut = (uint16_t)(-y);
|
||||||
|
if (cut >= ah) goto empty;
|
||||||
|
ah -= cut; y = 0;
|
||||||
|
}
|
||||||
|
if (x >= GFX_WIDTH || y >= GFX_HEIGHT) goto empty;
|
||||||
|
if (aw > (uint16_t)(GFX_WIDTH - x)) aw = (uint16_t)(GFX_WIDTH - x);
|
||||||
|
if (ah > (uint16_t)(GFX_HEIGHT - y)) ah = (uint16_t)(GFX_HEIGHT - y);
|
||||||
|
|
||||||
|
/* Сначала сравнение со знаком: так не вычисляем -INT16_MIN. */
|
||||||
|
if (dx >= (int16_t)aw || dx <= -(int16_t)aw) {
|
||||||
|
if (dirty) { dirty->x=x; dirty->y=y; dirty->w=aw; dirty->h=ah; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
adx = (dx < 0) ? (uint16_t)(-dx) : (uint16_t)dx;
|
||||||
|
saved = gfx_get_bank();
|
||||||
|
/* src-строка стартует правее на |dx| при сдвиге влево, dst — правее
|
||||||
|
* на dx при сдвиге вправо; так оба края остаются внутри area. */
|
||||||
|
src_x = (uint16_t)x + ((dx < 0) ? adx : 0);
|
||||||
|
dst_x = (uint16_t)x + ((dx > 0) ? adx : 0);
|
||||||
|
w = aw - adx;
|
||||||
|
h = (uint8_t)ah; /* 256 => 0 у leaf */
|
||||||
|
src = _gfx_addr_shadow_base + src_x;
|
||||||
|
dst = _gfx_addr_base + dst_x;
|
||||||
|
|
||||||
gfx_set_bank(GFX_BANK_NORMAL);
|
gfx_set_bank(GFX_BANK_NORMAL);
|
||||||
_bgi_begin();
|
_bgi_begin();
|
||||||
|
|
||||||
@@ -54,7 +109,7 @@ void gfx_scroll_h(const gfx_rect_t *area, int16_t dx, gfx_rect_t *dirty) {
|
|||||||
// src += 256; dst += 256;
|
// src += 256; dst += 256;
|
||||||
// w -= (w > 256) ? 256 : w;
|
// w -= (w > 256) ? 256 : w;
|
||||||
uint8_t cw = (w > 255) ? 255 : (uint8_t)w; /* 0 => 256 */
|
uint8_t cw = (w > 255) ? 255 : (uint8_t)w; /* 0 => 256 */
|
||||||
_bgi_scroll_rows_raw(src, dst, cw, h, (uint8_t)area->y);
|
_bgi_scroll_rows_raw(src, dst, cw, h, (uint8_t)y);
|
||||||
src += cw; dst += cw; w -= cw;
|
src += cw; dst += cw; w -= cw;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,9 +117,14 @@ void gfx_scroll_h(const gfx_rect_t *area, int16_t dx, gfx_rect_t *dirty) {
|
|||||||
gfx_set_bank(saved);
|
gfx_set_bank(saved);
|
||||||
|
|
||||||
if (dirty) {
|
if (dirty) {
|
||||||
dirty->x = (dx >= 0) ? area->x : (int16_t)(area->x + area->w - adx);
|
dirty->x = (dx >= 0) ? x : (int16_t)(x + aw - adx);
|
||||||
dirty->y = area->y;
|
dirty->y = y;
|
||||||
dirty->w = adx;
|
dirty->w = adx;
|
||||||
dirty->h = area->h;
|
dirty->h = ah;
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
|
||||||
|
empty:
|
||||||
|
if (dirty) { dirty->x=0; dirty->y=0; dirty->w=0; dirty->h=0; }
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,20 +24,72 @@
|
|||||||
#include "../_bgi.h"
|
#include "../_bgi.h"
|
||||||
|
|
||||||
void gfx_scroll_v(const gfx_rect_t *area, int16_t dy, gfx_rect_t *dirty) {
|
void gfx_scroll_v(const gfx_rect_t *area, int16_t dy, gfx_rect_t *dirty) {
|
||||||
|
#ifdef GFX_NOCHECK
|
||||||
uint8_t saved = gfx_get_bank();
|
uint8_t saved = gfx_get_bank();
|
||||||
uint16_t ady = (dy < 0) ? (uint16_t)(-dy) : (uint16_t)dy;
|
uint16_t ady = (dy < 0) ? (uint16_t)(-dy) : (uint16_t)dy;
|
||||||
/* Строку чтения (ys) сдвигаем вниз на |dy| при сдвиге вверх, строку
|
|
||||||
* записи (yd) — вниз на dy при сдвиге вниз; края в пределах area. */
|
|
||||||
uint8_t ys = (uint8_t)(area->y + ((dy < 0) ? ady : 0));
|
uint8_t ys = (uint8_t)(area->y + ((dy < 0) ? ady : 0));
|
||||||
uint8_t yd = (uint8_t)(area->y + ((dy > 0) ? ady : 0));
|
uint8_t yd = (uint8_t)(area->y + ((dy > 0) ? ady : 0));
|
||||||
uint16_t hb = area->h - ady; /* строк к переносу (≤256) */
|
uint16_t hb = area->h - ady;
|
||||||
|
|
||||||
if (hb) { /* 0 = переносить нечего */
|
if (hb) {
|
||||||
uint8_t hblk = (uint8_t)hb; /* размер accel-блока: 256 => 0 */
|
uint8_t hblk = (uint8_t)hb;
|
||||||
uint16_t w = area->w;
|
uint16_t w = area->w;
|
||||||
uint16_t src = _gfx_addr_shadow_base + area->x;
|
uint16_t src = _gfx_addr_shadow_base + area->x;
|
||||||
uint16_t dst = _gfx_addr_base + area->x;
|
uint16_t dst = _gfx_addr_base + area->x;
|
||||||
|
gfx_set_bank(GFX_BANK_NORMAL);
|
||||||
|
_bgi_begin();
|
||||||
|
while (w) {
|
||||||
|
uint8_t cw = (w > 255) ? 255 : (uint8_t)w;
|
||||||
|
_bgi_scroll_cols_raw(src, dst, cw, hblk, ys, yd);
|
||||||
|
src += cw; dst += cw; w -= cw;
|
||||||
|
}
|
||||||
|
_bgi_end();
|
||||||
|
gfx_set_bank(saved);
|
||||||
|
}
|
||||||
|
if (dirty) {
|
||||||
|
dirty->x = area->x;
|
||||||
|
dirty->y = (dy >= 0) ? area->y : (int16_t)(area->y + area->h - ady);
|
||||||
|
dirty->w = area->w; dirty->h = ady;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
int16_t x, y;
|
||||||
|
uint16_t aw, ah, cut, ady, hb;
|
||||||
|
uint8_t saved, ys, yd;
|
||||||
|
|
||||||
|
if (!area) return;
|
||||||
|
x = area->x; y = area->y; aw = area->w; ah = area->h;
|
||||||
|
if (!aw || !ah) goto empty;
|
||||||
|
if (x < 0) {
|
||||||
|
cut = (uint16_t)(-x);
|
||||||
|
if (cut >= aw) goto empty;
|
||||||
|
aw -= cut; x = 0;
|
||||||
|
}
|
||||||
|
if (y < 0) {
|
||||||
|
cut = (uint16_t)(-y);
|
||||||
|
if (cut >= ah) goto empty;
|
||||||
|
ah -= cut; y = 0;
|
||||||
|
}
|
||||||
|
if (x >= GFX_WIDTH || y >= GFX_HEIGHT) goto empty;
|
||||||
|
if (aw > (uint16_t)(GFX_WIDTH - x)) aw = (uint16_t)(GFX_WIDTH - x);
|
||||||
|
if (ah > (uint16_t)(GFX_HEIGHT - y)) ah = (uint16_t)(GFX_HEIGHT - y);
|
||||||
|
if (dy >= (int16_t)ah || dy <= -(int16_t)ah) {
|
||||||
|
if (dirty) { dirty->x=x; dirty->y=y; dirty->w=aw; dirty->h=ah; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ady = (dy < 0) ? (uint16_t)(-dy) : (uint16_t)dy;
|
||||||
|
saved = gfx_get_bank();
|
||||||
|
/* Строку чтения (ys) сдвигаем вниз на |dy| при сдвиге вверх, строку
|
||||||
|
* записи (yd) — вниз на dy при сдвиге вниз; края в пределах area. */
|
||||||
|
ys = (uint8_t)(y + ((dy < 0) ? ady : 0));
|
||||||
|
yd = (uint8_t)(y + ((dy > 0) ? ady : 0));
|
||||||
|
hb = ah - ady;
|
||||||
|
|
||||||
|
if (hb) { /* 0 = переносить нечего */
|
||||||
|
uint8_t hblk = (uint8_t)hb; /* размер accel-блока: 256 => 0 */
|
||||||
|
uint16_t w = aw;
|
||||||
|
uint16_t src = _gfx_addr_shadow_base + (uint16_t)x;
|
||||||
|
uint16_t dst = _gfx_addr_base + (uint16_t)x;
|
||||||
|
|
||||||
gfx_set_bank(GFX_BANK_NORMAL);
|
gfx_set_bank(GFX_BANK_NORMAL);
|
||||||
_bgi_begin();
|
_bgi_begin();
|
||||||
@@ -55,9 +107,14 @@ void gfx_scroll_v(const gfx_rect_t *area, int16_t dy, gfx_rect_t *dirty) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dirty) {
|
if (dirty) {
|
||||||
dirty->x = area->x;
|
dirty->x = x;
|
||||||
dirty->y = (dy >= 0) ? area->y : (int16_t)(area->y + area->h - ady);
|
dirty->y = (dy >= 0) ? y : (int16_t)(y + ah - ady);
|
||||||
dirty->w = area->w;
|
dirty->w = aw;
|
||||||
dirty->h = ady;
|
dirty->h = ady;
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
|
||||||
|
empty:
|
||||||
|
if (dirty) { dirty->x=0; dirty->y=0; dirty->w=0; dirty->h=0; }
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* gfx_w0_page_prepare — сделать произвольную EMM-страницу безопасной в W0.
|
||||||
|
*
|
||||||
|
* При первом вызове запоминает штатную страницу DSS, затем ставит в целевой
|
||||||
|
* странице переход на общий IRQ-восстановитель и RETN для NMI. Функция
|
||||||
|
* резидентна: она сама временно переключает W3.
|
||||||
|
*/
|
||||||
|
#include <sprinter.h>
|
||||||
|
#include <sprite.h>
|
||||||
|
#include "../_bgi.h"
|
||||||
|
|
||||||
|
void gfx_w0_page_prepare(uint8_t page)
|
||||||
|
{
|
||||||
|
uint8_t saved=_io_page_w3, *p=(uint8_t *)0xC000;
|
||||||
|
uint16_t stub=(uint16_t)&_gfx_w0_isr;
|
||||||
|
if (!_gfx_w0_dss) _gfx_w0_dss=_io_page_w0;
|
||||||
|
sprinter_page_w3(page);
|
||||||
|
p[0x38]=0xC3; p[0x39]=(uint8_t)stub; p[0x3A]=(uint8_t)(stub >> 8);
|
||||||
|
p[0x66]=0xED; p[0x67]=0x45;
|
||||||
|
sprinter_page_w3(saved);
|
||||||
|
}
|
||||||
@@ -290,6 +290,10 @@ typedef struct {
|
|||||||
* 0 — OK; -1 + errno (файл/память/формат). */
|
* 0 — OK; -1 + errno (файл/память/формат). */
|
||||||
int atlas_load(atlas_t *a, const char *path);
|
int atlas_load(atlas_t *a, const char *path);
|
||||||
|
|
||||||
|
/* Подготовить уже выделенную EMM-страницу для безопасного маппинга в W0:
|
||||||
|
* установить IRQ/NMI-стабы и запомнить штатную страницу DSS. */
|
||||||
|
void gfx_w0_page_prepare(uint8_t page);
|
||||||
|
|
||||||
/* Освободить EMM-страницу атласа (все sprite_t с его лентами должны
|
/* Освободить EMM-страницу атласа (все sprite_t с его лентами должны
|
||||||
* быть скрыты/переинициализированы ДО вызова). */
|
* быть скрыты/переинициализированы ДО вызова). */
|
||||||
void atlas_free(atlas_t *a);
|
void atlas_free(atlas_t *a);
|
||||||
|
|||||||
@@ -55,6 +55,12 @@ extern uint8_t _cbl_port_refs; /* держатели bit7 помимо
|
|||||||
/* Запись в 16-битный порт управления 0x004E. */
|
/* Запись в 16-битный порт управления 0x004E. */
|
||||||
void _cbl_ctrl(uint8_t v);
|
void _cbl_ctrl(uint8_t v);
|
||||||
|
|
||||||
|
/* Общее тело обеих публичных точек входа (cbl_open / cbl_open_silence).
|
||||||
|
* Буфер тишины не трогает — им владеет cbl_open_silence.c; так malloc не
|
||||||
|
* приезжает в резидент приложениям, которые льют тишину сами. */
|
||||||
|
int _cbl_open_raw(uint8_t freq_code, uint8_t fmt, uint8_t pump_mode,
|
||||||
|
uint8_t underrun_mode, cbl_fill_fn fill);
|
||||||
|
|
||||||
/* Залить весь аппаратный буфер (256 слотов) байтом тишины сразу после
|
/* Залить весь аппаратный буфер (256 слотов) байтом тишины сразу после
|
||||||
* включения — иначе первые 23 мс играет то, что лежало в буфере раньше.
|
* включения — иначе первые 23 мс играет то, что лежало в буфере раньше.
|
||||||
* Подробный разбор — в шапке _cbl_prime.c. */
|
* Подробный разбор — в шапке _cbl_prime.c. */
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* _cbl_open_raw — общее тело обеих точек входа CBL (cbl_open и
|
||||||
|
* cbl_open_silence). Проверяет параметры, вешает насос и включает железо.
|
||||||
|
*
|
||||||
|
* ПОЧЕМУ ТОЧЕК ВХОДА ДВЕ, а не один параметр underrun_mode. Линкер тянет
|
||||||
|
* .rel ЦЕЛИКОМ, поэтому malloc/free, стоящие в ветке CBL_UNDERRUN_SILENCE,
|
||||||
|
* приезжали в резидент КАЖДОМУ приложению — даже тому, которое льёт тишину
|
||||||
|
* само и кучей не пользуется вовсе. Это 275 Б кода плюс вся область кучи;
|
||||||
|
* в huge-режиме, где под резидент отведено 0x4100..0xBB00, такая мелочь
|
||||||
|
* заметна (у PoP оставалось 129 Б свободных). Разведя аллокацию в отдельный
|
||||||
|
* публичный модуль, мы отдаём выбор линкеру: зовёшь cbl_open — malloc не
|
||||||
|
* приезжает.
|
||||||
|
*
|
||||||
|
* Указатель на буфер тишины (_cbl_silence) здесь НЕ ТРОГАЕТСЯ: им владеет
|
||||||
|
* cbl_open_silence.c, который его и аллоцирует. В режиме APP насос на него
|
||||||
|
* не смотрит вовсе (условие в _cbl_pump_*), а обнуляй мы его тут — cbl_open
|
||||||
|
* после cbl_open_silence терял бы уже выделенный буфер, и следующая
|
||||||
|
* silence-сессия аллоцировала бы второй.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <sprinter_exit.h>
|
||||||
|
#include "_cbl.h"
|
||||||
|
#include "../irq/_irq.h"
|
||||||
|
|
||||||
|
int _cbl_open_raw(uint8_t freq_code, uint8_t fmt, uint8_t pump_mode,
|
||||||
|
uint8_t underrun_mode, cbl_fill_fn fill)
|
||||||
|
{
|
||||||
|
if (_irq_cbl_hook) { errno = EBUSY; return -1; }
|
||||||
|
if (freq_code < CBL_FREQ_7K8 || freq_code > CBL_FREQ_109K) {
|
||||||
|
errno = EINVAL;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (fmt & (uint8_t)~(CBL_FMT_MONO16 | CBL_FMT_STEREO8)) {
|
||||||
|
errno = EINVAL; /* биты вне 5/6 — неизвестный формат */
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (pump_mode != CBL_PUMP_OTIR && pump_mode != CBL_PUMP_ACCEL) {
|
||||||
|
errno = EINVAL;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
/* ЗАПРЕТ CBL_PUMP_OTIR + 16-бит (подтверждено чтением исходника MAME
|
||||||
|
* sprinter.cpp): порт данных 0x4F ВСЕГДА кладёт байт как есть в один
|
||||||
|
* слот, не собирая пару байт в 16-бит значение и не сверяясь с
|
||||||
|
* cbl_mode16() вообще — это умеет только акселераторный путь (спец-
|
||||||
|
* страница 0xFD, do_accel_block). Комбинация принципиально не может
|
||||||
|
* звучать правильно, поэтому запрещена. */
|
||||||
|
if (pump_mode == CBL_PUMP_OTIR && (fmt & CBL_FMT_MONO16)) {
|
||||||
|
errno = EINVAL;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_irq_table_ref() != 0) return -1;
|
||||||
|
|
||||||
|
_cbl_block = (fmt & CBL_FMT_MONO16) ? 256u : 128u;
|
||||||
|
_cbl_reqs = 0;
|
||||||
|
_cbl_undr = 0;
|
||||||
|
_cbl_fill = fill;
|
||||||
|
_cbl_underrun_mode = underrun_mode;
|
||||||
|
|
||||||
|
static uint8_t atexit_armed;
|
||||||
|
if (!atexit_armed) {
|
||||||
|
atexit_armed = 1;
|
||||||
|
atexit(cbl_close); /* CBL обязан умолкнуть до шелла */
|
||||||
|
}
|
||||||
|
|
||||||
|
IRQ_DISABLE();
|
||||||
|
_irq_cbl_hook = (pump_mode == CBL_PUMP_ACCEL) ? _cbl_pump_accel : _cbl_pump_otir;
|
||||||
|
_cbl_ctrl((uint8_t)(0x90 | fmt | freq_code)); /* on + int + формат + частота */
|
||||||
|
/* ...и НЕМЕДЛЕННО забить буфер тишиной: железо пускает воспроизведение
|
||||||
|
* с нулевого слота той же записью в порт управления, а буфер хранит
|
||||||
|
* старое содержимое — без этого первые 23 мс уходит мусор. Заливка
|
||||||
|
* обязана быть внутри той же DI-скобки и сразу за включением (разбор —
|
||||||
|
* _cbl_prime.c). */
|
||||||
|
_cbl_prime((uint8_t)((fmt & CBL_FMT_MONO16) ? 0x00 : 0x80));
|
||||||
|
IRQ_ENABLE();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
/*
|
/*
|
||||||
* cbl_close — выключить CBL, снять хук насоса, освободить буфер
|
* cbl_close — выключить CBL, снять хук насоса и отпустить IM2-таблицу.
|
||||||
* тишины (если был аллоцирован под CBL_UNDERRUN_SILENCE) и отпустить
|
* Идемпотентно; вызывается и из atexit.
|
||||||
* IM2-таблицу. Идемпотентно; вызывается и из atexit.
|
*
|
||||||
|
* БУФЕР ТИШИНЫ НЕ ОСВОБОЖДАЕТСЯ. Раньше здесь стоял free(_cbl_silence), и
|
||||||
|
* он один тянул malloc/free в резидент КАЖДОМУ приложению — включая те, что
|
||||||
|
* cbl_open_silence() не зовут вовсе. Буфер живёт до выхода из программы и
|
||||||
|
* переиспользуется при повторном открытии (см. cbl_open_silence.c).
|
||||||
*
|
*
|
||||||
* Порт 0x004E не гасится напрямую — вместо этого _cbl_port_sync()
|
* Порт 0x004E не гасится напрямую — вместо этого _cbl_port_sync()
|
||||||
* пересчитывает нужное значение: если gfx_wait_vsync() ещё держит
|
* пересчитывает нужное значение: если gfx_wait_vsync() ещё держит
|
||||||
@@ -9,7 +13,6 @@
|
|||||||
* синхронизации; иначе порт гасится полностью.
|
* синхронизации; иначе порт гасится полностью.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include "_cbl.h"
|
#include "_cbl.h"
|
||||||
#include "../irq/_irq.h"
|
#include "../irq/_irq.h"
|
||||||
|
|
||||||
@@ -20,7 +23,5 @@ void cbl_close(void)
|
|||||||
_irq_cbl_hook = 0;
|
_irq_cbl_hook = 0;
|
||||||
IRQ_ENABLE();
|
IRQ_ENABLE();
|
||||||
_cbl_port_sync();
|
_cbl_port_sync();
|
||||||
free(_cbl_silence);
|
|
||||||
_cbl_silence = 0;
|
|
||||||
_irq_table_unref();
|
_irq_table_unref();
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-80
@@ -1,89 +1,15 @@
|
|||||||
/*
|
/*
|
||||||
* cbl_open — включить CBL, зарегистрировать fill()-callback и один
|
* cbl_open — включить CBL, когда тишину при недоливе льёт САМО ПРИЛОЖЕНИЕ
|
||||||
* из ISR-насосов.
|
* (режим CBL_UNDERRUN_APP). Буфера не просит, поэтому НЕ ТЯНЕТ malloc —
|
||||||
|
* ради этого точки входа и разведены (см. шапку _cbl_open_raw.c).
|
||||||
*
|
*
|
||||||
* Управляющее слово: bit7 (CBL on) + bit4 (int enable) + fmt (bit6
|
* Если тишина нужна от libc — cbl_open_silence() в соседнем модуле.
|
||||||
* stereo, bit5 16-бит) + freq_code. Размер блока/буфер тишины
|
|
||||||
* зависят от fmt и underrun_mode (см. cbl.h). cbl_close вешается на
|
|
||||||
* atexit.
|
|
||||||
*
|
|
||||||
* ЗАПРЕТ CBL_PUMP_OTIR + 16-бит (подтверждено чтением исходника MAME
|
|
||||||
* sprinter.cpp): порт данных 0x4F ВСЕГДА кладёт байт как есть в один
|
|
||||||
* слот, не собирая пару байт в 16-бит значение и не сверяясь с
|
|
||||||
* cbl_mode16() вообще — это умеет только акселераторный путь (спец-
|
|
||||||
* страница 0xFD, do_accel_block). Комбинация принципиально не может
|
|
||||||
* звучать правильно, поэтому запрещена (EINVAL). Для 16-бит —
|
|
||||||
* CBL_PUMP_ACCEL.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <errno.h>
|
|
||||||
#include <sprinter_exit.h>
|
|
||||||
#include "_cbl.h"
|
#include "_cbl.h"
|
||||||
#include "../irq/_irq.h"
|
|
||||||
|
|
||||||
int cbl_open(uint8_t freq_code, uint8_t fmt, uint8_t pump_mode,
|
int cbl_open(uint8_t freq_code, uint8_t fmt, uint8_t pump_mode,
|
||||||
uint8_t underrun_mode, cbl_fill_fn fill)
|
cbl_fill_fn fill)
|
||||||
{
|
{
|
||||||
if (_irq_cbl_hook) { errno = EBUSY; return -1; }
|
return _cbl_open_raw(freq_code, fmt, pump_mode, CBL_UNDERRUN_APP, fill);
|
||||||
if (freq_code < CBL_FREQ_7K8 || freq_code > CBL_FREQ_109K) {
|
|
||||||
errno = EINVAL;
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (fmt & (uint8_t)~(CBL_FMT_MONO16 | CBL_FMT_STEREO8)) {
|
|
||||||
errno = EINVAL; /* биты вне 5/6 — неизвестный формат */
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (pump_mode != CBL_PUMP_OTIR && pump_mode != CBL_PUMP_ACCEL) {
|
|
||||||
errno = EINVAL;
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (pump_mode == CBL_PUMP_OTIR && (fmt & CBL_FMT_MONO16)) {
|
|
||||||
errno = EINVAL; /* OTIR не может собрать 16-бит сэмпл — только ACCEL */
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (underrun_mode != CBL_UNDERRUN_APP && underrun_mode != CBL_UNDERRUN_SILENCE) {
|
|
||||||
errno = EINVAL;
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t block = (fmt & CBL_FMT_MONO16) ? 256u : 128u;
|
|
||||||
|
|
||||||
uint8_t *silence = 0;
|
|
||||||
if (underrun_mode == CBL_UNDERRUN_SILENCE) {
|
|
||||||
silence = malloc(block);
|
|
||||||
if (!silence) { errno = ENOMEM; return -1; }
|
|
||||||
memset(silence, (fmt & CBL_FMT_MONO16) ? 0x00 : 0x80, block);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_irq_table_ref() != 0) {
|
|
||||||
free(silence);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
_cbl_block = block;
|
|
||||||
_cbl_reqs = 0;
|
|
||||||
_cbl_undr = 0;
|
|
||||||
_cbl_fill = fill;
|
|
||||||
_cbl_underrun_mode = underrun_mode;
|
|
||||||
_cbl_silence = silence;
|
|
||||||
|
|
||||||
static uint8_t atexit_armed;
|
|
||||||
if (!atexit_armed) {
|
|
||||||
atexit_armed = 1;
|
|
||||||
atexit(cbl_close); /* CBL обязан умолкнуть до шелла */
|
|
||||||
}
|
|
||||||
|
|
||||||
IRQ_DISABLE();
|
|
||||||
_irq_cbl_hook = (pump_mode == CBL_PUMP_ACCEL) ? _cbl_pump_accel : _cbl_pump_otir;
|
|
||||||
_cbl_ctrl((uint8_t)(0x90 | fmt | freq_code)); /* on + int + формат + частота */
|
|
||||||
/* ...и НЕМЕДЛЕННО забить буфер тишиной: железо пускает воспроизведение
|
|
||||||
* с нулевого слота той же записью в порт управления, а буфер хранит
|
|
||||||
* старое содержимое — без этого первые 23 мс уходит мусор. Заливка
|
|
||||||
* обязана быть внутри той же DI-скобки и сразу за включением (разбор —
|
|
||||||
* _cbl_prime.c). */
|
|
||||||
_cbl_prime((uint8_t)((fmt & CBL_FMT_MONO16) ? 0x00 : 0x80));
|
|
||||||
IRQ_ENABLE();
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* cbl_open_silence — то же, что cbl_open, но при недоливе тишину шлёт САМА
|
||||||
|
* libc: буфер под неё аллоцируется здесь. Именно этот модуль тянет malloc,
|
||||||
|
* поэтому приложения на cbl_open() за него не платят.
|
||||||
|
*
|
||||||
|
* БУФЕР НЕ ОСВОБОЖДАЕТСЯ. cbl_close() его не трогает намеренно: free()
|
||||||
|
* в cbl_close вернул бы malloc в резидент всем подряд — ровно то, от чего
|
||||||
|
* уходили. Потеря невелика (128 или 256 байт), а при повторном открытии
|
||||||
|
* буфер переиспользуется; окончательно память возвращает выход из
|
||||||
|
* программы.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include "_cbl.h"
|
||||||
|
|
||||||
|
/* Сколько байт под буфером сейчас лежит. Помнить обязательно: у 8-бит
|
||||||
|
* формата блок 128, у 16-бит — 256, и открытие 16-бит ПОСЛЕ 8-бит писало бы
|
||||||
|
* memset'ом мимо выделенного куска. */
|
||||||
|
static uint16_t silence_cap;
|
||||||
|
|
||||||
|
int cbl_open_silence(uint8_t freq_code, uint8_t fmt, uint8_t pump_mode,
|
||||||
|
cbl_fill_fn fill)
|
||||||
|
{
|
||||||
|
uint16_t block = (fmt & CBL_FMT_MONO16) ? 256u : 128u;
|
||||||
|
uint8_t quiet = (fmt & CBL_FMT_MONO16) ? 0x00 : 0x80;
|
||||||
|
|
||||||
|
if (silence_cap < block) {
|
||||||
|
uint8_t *p = malloc(block);
|
||||||
|
if (!p) { errno = ENOMEM; return -1; }
|
||||||
|
_cbl_silence = p; /* прежний кусок не освобождаем — см. шапку */
|
||||||
|
silence_cap = block;
|
||||||
|
}
|
||||||
|
memset(_cbl_silence, quiet, block);
|
||||||
|
return _cbl_open_raw(freq_code, fmt, pump_mode, CBL_UNDERRUN_SILENCE, fill);
|
||||||
|
}
|
||||||
+29
-13
@@ -44,13 +44,14 @@
|
|||||||
* кладёт байт как есть в один слот, не собирая пару байт в 16-бит
|
* кладёт байт как есть в один слот, не собирая пару байт в 16-бит
|
||||||
* значение — это умеет только акселераторный путь (страница 0xFD).
|
* значение — это умеет только акселераторный путь (страница 0xFD).
|
||||||
*
|
*
|
||||||
* Поведение при недоливе — CBL_UNDERRUN_* (4-й параметр cbl_open):
|
* Поведение при недоливе задаётся ВЫБОРОМ ТОЧКИ ВХОДА (cbl_open /
|
||||||
|
* cbl_open_silence), а не параметром:
|
||||||
* CBL_UNDERRUN_APP (по умолчанию, 0) — недолив не наша забота;
|
* CBL_UNDERRUN_APP (по умолчанию, 0) — недолив не наша забота;
|
||||||
* в CBL доигрывает то, что уже лежит в его буфере (никакого
|
* в CBL доигрывает то, что уже лежит в его буфере (никакого
|
||||||
* буфера тишины не аллоцируется).
|
* буфера тишины не аллоцируется).
|
||||||
* CBL_UNDERRUN_SILENCE (1) — насос сам пропихивает тишину при
|
* CBL_UNDERRUN_SILENCE (1) — насос сам пропихивает тишину при
|
||||||
* недоливе; буфер тишины (128/256 Б по формату) аллоцируется
|
* недоливе; буфер тишины (128/256 Б по формату) аллоцируется
|
||||||
* malloc'ом ВНУТРИ cbl_open(), только в этом режиме.
|
* malloc'ом внутри cbl_open_silence(), только в этом режиме.
|
||||||
* cbl_underruns() считает недоливы в ОБОИХ режимах — это диагностика,
|
* cbl_underruns() считает недоливы в ОБОИХ режимах — это диагностика,
|
||||||
* поведение не меняет.
|
* поведение не меняет.
|
||||||
*
|
*
|
||||||
@@ -89,8 +90,10 @@
|
|||||||
#define CBL_PUMP_ACCEL 1 /* cbl_push_accel() — единственный путь для 16-бит */
|
#define CBL_PUMP_ACCEL 1 /* cbl_push_accel() — единственный путь для 16-бит */
|
||||||
|
|
||||||
/* Поведение при недоливе (fill() вернул 0 или не задан). */
|
/* Поведение при недоливе (fill() вернул 0 или не задан). */
|
||||||
#define CBL_UNDERRUN_APP 0 /* по умолчанию: не наша забота, буфер тишины не аллоцируется */
|
/* Внутренние коды режима недолива: приложение их не передаёт (режим задаёт
|
||||||
#define CBL_UNDERRUN_SILENCE 1 /* насос сам шлёт тишину; буфер аллоцируется в cbl_open() */
|
* выбор cbl_open / cbl_open_silence), но насос по ним и различает поведение. */
|
||||||
|
#define CBL_UNDERRUN_APP 0 /* не наша забота, буфер тишины не аллоцируется */
|
||||||
|
#define CBL_UNDERRUN_SILENCE 1 /* насос сам шлёт тишину из буфера libc */
|
||||||
|
|
||||||
/* fill(n): пропихнуть n байт (через cbl_push_otir/accel) прямо из
|
/* fill(n): пропихнуть n байт (через cbl_push_otir/accel) прямо из
|
||||||
* данных приложения. Вызывается ИЗ ISR — обязан быть быстрым (без
|
* данных приложения. Вызывается ИЗ ISR — обязан быть быстрым (без
|
||||||
@@ -98,17 +101,30 @@
|
|||||||
* данных сейчас (недолив). */
|
* данных сейчас (недолив). */
|
||||||
typedef int (*cbl_fill_fn)(uint16_t n);
|
typedef int (*cbl_fill_fn)(uint16_t n);
|
||||||
|
|
||||||
/* Включить CBL с частотой CBL_FREQ_*, форматом CBL_FMT_*, способом
|
/* Включить CBL с частотой CBL_FREQ_*, форматом CBL_FMT_* и способом выдачи
|
||||||
* выдачи CBL_PUMP_* и поведением при недоливе CBL_UNDERRUN_*; fill —
|
* CBL_PUMP_*; fill — callback, зовущийся из ISR за новым блоком (может быть
|
||||||
* callback, зовущийся из ISR за новым блоком (может быть NULL, тогда
|
* NULL, тогда недолив — на каждом запросе). Недолив НЕ наша забота
|
||||||
* недолив — на каждом запросе). 0 или -1 + errno (EBUSY — уже
|
* (CBL_UNDERRUN_APP): железо доиграет по кругу неактивную половину, и если
|
||||||
* открыт; EINVAL — плохой код частоты/формат/pump/underrun_mode, или
|
* это слышно — приложение обязано отдавать тишину само.
|
||||||
* не-W2 режим; ENOMEM — не хватило памяти под буфер тишины). */
|
*
|
||||||
|
* 0 или -1 + errno (EBUSY — уже открыт; EINVAL — плохой код частоты, формат,
|
||||||
|
* pump, или не-W2 режим).
|
||||||
|
*
|
||||||
|
* ТОЧКИ ВХОДА ДВЕ, и это не косметика: cbl_open() не аллоцирует ничего и
|
||||||
|
* потому НЕ ТЯНЕТ malloc в резидент. За тишиной от libc — cbl_open_silence()
|
||||||
|
* ниже, и malloc приезжает только тому, кто её позвал. */
|
||||||
int cbl_open(uint8_t freq_code, uint8_t fmt, uint8_t pump_mode,
|
int cbl_open(uint8_t freq_code, uint8_t fmt, uint8_t pump_mode,
|
||||||
uint8_t underrun_mode, cbl_fill_fn fill);
|
cbl_fill_fn fill);
|
||||||
|
|
||||||
/* Выключить CBL, отпустить IM2-таблицу, освободить буфер тишины (если
|
/* То же, но при недоливе тишину шлёт САМА libc (CBL_UNDERRUN_SILENCE):
|
||||||
* был). Идемпотентно. */
|
* буфер под неё аллоцируется здесь. Добавляет ENOMEM к кодам ошибок.
|
||||||
|
* Буфер живёт до конца программы и переиспользуется при повторном открытии
|
||||||
|
* — cbl_close() его не освобождает намеренно (иначе free вернул бы malloc
|
||||||
|
* в резидент всем подряд). */
|
||||||
|
int cbl_open_silence(uint8_t freq_code, uint8_t fmt, uint8_t pump_mode,
|
||||||
|
cbl_fill_fn fill);
|
||||||
|
|
||||||
|
/* Выключить CBL и отпустить IM2-таблицу. Идемпотентно. */
|
||||||
void cbl_close(void);
|
void cbl_close(void);
|
||||||
|
|
||||||
/* Пропихнуть n байт из src прямо в CBL (звать ИЗ fill()). */
|
/* Пропихнуть n байт из src прямо в CBL (звать ИЗ fill()). */
|
||||||
|
|||||||
@@ -110,6 +110,15 @@ void bank_read(uint8_t phys_page, uint16_t off, void *dst, uint16_t n);
|
|||||||
/* Copy `n` bytes from near buffer `src` into phys_page[off..off+n-1]. */
|
/* Copy `n` bytes from near buffer `src` into phys_page[off..off+n-1]. */
|
||||||
void bank_write(uint8_t phys_page, uint16_t off, const void *src, uint16_t n);
|
void bank_write(uint8_t phys_page, uint16_t off, const void *src, uint16_t n);
|
||||||
|
|
||||||
|
/* Файловый обмен непосредственно с диапазоном EMM-страницы через W3.
|
||||||
|
* В отличие от bank_read/bank_write безопасен для вызова из huge-банка:
|
||||||
|
* path используется до переключения W3, а окно восстановлено до возврата.
|
||||||
|
* Возвращает фактический размер либо -1 + errno. */
|
||||||
|
int bank_load_file(uint8_t phys_page, uint16_t off, const char *path,
|
||||||
|
uint16_t max_size);
|
||||||
|
int bank_save_file(uint8_t phys_page, uint16_t off, const char *path,
|
||||||
|
uint16_t size);
|
||||||
|
|
||||||
/* ===================================================================
|
/* ===================================================================
|
||||||
* Far-page accessors via window 1 (base 0x4000, port 0xA2)
|
* Far-page accessors via window 1 (base 0x4000, port 0xA2)
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* bank_load_file — прочитать файл непосредственно в физическую EMM-страницу.
|
||||||
|
*
|
||||||
|
* ESTEX принимает файловый буфер только в W1..W3; используем W3, сохраняя и
|
||||||
|
* восстанавливая его раскладку. Функция резидентна, поэтому её безопасно
|
||||||
|
* вызывать из банкового кода huge: после open() указатель path больше не
|
||||||
|
* читается, пока W3 временно занят целевой страницей.
|
||||||
|
*
|
||||||
|
* Возврат: фактически прочитанный размер либо -1 + errno.
|
||||||
|
*/
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <sprinter.h>
|
||||||
|
#include <sprinter_mem.h>
|
||||||
|
|
||||||
|
int bank_load_file(uint8_t phys_page, uint16_t off, const char *path,
|
||||||
|
uint16_t max_size)
|
||||||
|
{
|
||||||
|
uint8_t saved;
|
||||||
|
int fd, n;
|
||||||
|
if (off > 0x3FFFu || max_size > (uint16_t)(0x4000u - off)) {
|
||||||
|
errno=EINVAL; return -1;
|
||||||
|
}
|
||||||
|
fd = open(path, O_RDONLY);
|
||||||
|
if (fd < 0) return -1;
|
||||||
|
saved = _io_page_w3;
|
||||||
|
sprinter_page_w3(phys_page);
|
||||||
|
n = read(fd, (void *)(0xC000u + off), max_size);
|
||||||
|
sprinter_page_w3(saved);
|
||||||
|
close(fd);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* bank_save_file — записать диапазон физической EMM-страницы в файл.
|
||||||
|
*
|
||||||
|
* Файл создаётся заново (O_CREAT|O_TRUNC), данные передаются одним WRITE.
|
||||||
|
* W3 восстанавливается до close(), поэтому функцию можно вызывать из
|
||||||
|
* банкового кода huge. Возврат: записанный размер либо -1 + errno.
|
||||||
|
*/
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <sprinter.h>
|
||||||
|
#include <sprinter_mem.h>
|
||||||
|
|
||||||
|
int bank_save_file(uint8_t phys_page, uint16_t off, const char *path,
|
||||||
|
uint16_t size)
|
||||||
|
{
|
||||||
|
uint8_t saved;
|
||||||
|
int fd, n;
|
||||||
|
if (off > 0x3FFFu || size > (uint16_t)(0x4000u - off)) {
|
||||||
|
errno=EINVAL; return -1;
|
||||||
|
}
|
||||||
|
fd = open(path, O_WRONLY | O_CREAT | O_TRUNC);
|
||||||
|
if (fd < 0) return -1;
|
||||||
|
saved = _io_page_w3;
|
||||||
|
sprinter_page_w3(phys_page);
|
||||||
|
n = write(fd, (const void *)(0xC000u + off), size);
|
||||||
|
sprinter_page_w3(saved);
|
||||||
|
if (close(fd) != 0) return -1;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ sprinter-cc -o OUT.exe SRC.c [more.c ...] [options]
|
|||||||
|
|
||||||
| Flag | Description |
|
| Flag | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `--bank N=FILE.c` | Compile FILE.c into bank N (1..15). Repeatable. Functions in banked files need the `__banked` qualifier. |
|
| `--bank N=FILE.c` | Compile FILE.c into bank N (1..15). Numbers must be consecutive; crt0's bank count is generated automatically. Repeatable; functions need `__banked`. |
|
||||||
| `--crt0=TYPE` | Override startup file: `default` / `minimal` / `banked` / `small`. Normally chosen automatically by the memory mode. |
|
| `--crt0=TYPE` | Override startup file: `default` / `minimal` / `banked` / `small`. Normally chosen automatically by the memory mode. |
|
||||||
|
|
||||||
### Diagnostics
|
### Diagnostics
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ sprinter-cc -o OUT.exe SRC.c [more.c ...] [options]
|
|||||||
|
|
||||||
| Флаг | Описание |
|
| Флаг | Описание |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `--bank N=FILE.c` | Компилировать FILE.c в банк N (1..15). Повторяемый. Функции в banked-файлах нужны с квалификатором `__banked`. |
|
| `--bank N=FILE.c` | Компилировать FILE.c в банк N (1..15). Номера без пропусков; число банков crt0 получает автоматически. Повторяемый; функции нужны с `__banked`. |
|
||||||
| `--crt0=TYPE` | Переопределение startup-файла: `default` / `minimal` / `banked` / `small`. Обычно выбирается автоматически по memory mode. |
|
| `--crt0=TYPE` | Переопределение startup-файла: `default` / `minimal` / `banked` / `small`. Обычно выбирается автоматически по memory mode. |
|
||||||
|
|
||||||
### Диагностика
|
### Диагностика
|
||||||
|
|||||||
@@ -20,11 +20,9 @@
|
|||||||
;; 6. CLOSE file
|
;; 6. CLOSE file
|
||||||
;; 7. Standard gsinit + call _main + ESTEX EXIT
|
;; 7. Standard gsinit + call _main + ESTEX EXIT
|
||||||
;;
|
;;
|
||||||
;; _n_banks MUST be defined as `const uint8_t n_banks = N;` — we read it
|
;; _n_banks is generated by sprinter-cc from the highest --bank number.
|
||||||
;; before gsinit, so a plain initialized `uint8_t` would still hold 0
|
;; It is emitted as const because we read it before gsinit; a normal
|
||||||
;; (the initializer hasn't been copied yet) and bank loading would be
|
;; initialized variable would still contain 0 at this point.
|
||||||
;; silently skipped, leaving window 3 mapped to a garbage page.
|
|
||||||
;; For zero banks, link crt0.s instead.
|
|
||||||
;; ----------------------------------------------------------------------
|
;; ----------------------------------------------------------------------
|
||||||
|
|
||||||
.module crt0_banked
|
.module crt0_banked
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <sprinter.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(int x) __banked;
|
void bank1_func(int x) __banked;
|
||||||
void bank2_func(int x) __banked;
|
void bank2_func(int x) __banked;
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,6 @@
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <sprinter.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(int x) __banked;
|
void bank1_func(int x) __banked;
|
||||||
void bank2_func(int x) __banked;
|
void bank2_func(int x) __banked;
|
||||||
|
|
||||||
|
|||||||
@@ -18,11 +18,6 @@ extern void bank1_map_print(void) __banked;
|
|||||||
extern uint8_t *bank1_alloc_and_fill(int n, uint8_t pattern) __banked;
|
extern uint8_t *bank1_alloc_and_fill(int n, uint8_t pattern) __banked;
|
||||||
extern void bank1_free (void *p) __banked;
|
extern void bank1_free (void *p) __banked;
|
||||||
|
|
||||||
/* MUST be `const`: crt0_banked reads _n_banks BEFORE gsinit, so
|
|
||||||
* non-const initialized variables haven't been copied yet and would
|
|
||||||
* read 0 → bank loading is skipped → garbage in window 3. */
|
|
||||||
const uint8_t n_banks = 1;
|
|
||||||
|
|
||||||
uint8_t local0_buf[16];
|
uint8_t local0_buf[16];
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <sprinter.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 bank1_func(void) __banked;
|
||||||
void bank2_func(void) __banked;
|
void bank2_func(void) __banked;
|
||||||
void bank1_func2(uint16_t) __banked;
|
void bank1_func2(uint16_t) __banked;
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ int main(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cbl_open(CBL_FREQ_21K9, CBL_FMT_MONO8, CBL_PUMP_OTIR,
|
if (cbl_open(CBL_FREQ_21K9, CBL_FMT_MONO8, CBL_PUMP_OTIR,
|
||||||
CBL_UNDERRUN_APP, fill_stream) != 0) {
|
fill_stream) != 0) {
|
||||||
printf("cbl_open failed: errno=%d\n", errno);
|
printf("cbl_open failed: errno=%d\n", errno);
|
||||||
close(fd);
|
close(fd);
|
||||||
return 1;
|
return 1;
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ static void play_combo(uint16_t idx, uint16_t total,
|
|||||||
gen_chunk(bits, stereo, want_l, want_r);
|
gen_chunk(bits, stereo, want_l, want_r);
|
||||||
current_pump = pump_mode;
|
current_pump = pump_mode;
|
||||||
|
|
||||||
if (cbl_open(freq_code, fmt, pump_mode, CBL_UNDERRUN_APP, fill_tone) != 0) {
|
if (cbl_open(freq_code, fmt, pump_mode, fill_tone) != 0) {
|
||||||
printf("[%2u/%u] %s %s %s: cbl_open failed errno=%d\n",
|
printf("[%2u/%u] %s %s %s: cbl_open failed errno=%d\n",
|
||||||
idx, total, pump_name, fmt_name, freq_name, errno);
|
idx, total, pump_name, fmt_name, freq_name, errno);
|
||||||
return;
|
return;
|
||||||
@@ -136,7 +136,7 @@ int main(void)
|
|||||||
for (uint8_t q = 0; q < 4; q++) {
|
for (uint8_t q = 0; q < 4; q++) {
|
||||||
idx++;
|
idx++;
|
||||||
/* OTIR принципиально не может собрать 16-бит сэмпл
|
/* OTIR принципиально не может собрать 16-бит сэмпл
|
||||||
* (см. cbl_open.c) — cbl_open вернёт EINVAL; не тратим
|
* (см. _cbl_open_raw.c) — cbl_open вернёт EINVAL; не тратим
|
||||||
* время на заведомо запрещённую комбинацию. */
|
* время на заведомо запрещённую комбинацию. */
|
||||||
if (pumps[p] == CBL_PUMP_OTIR && fmt_bits[f] == 16) {
|
if (pumps[p] == CBL_PUMP_OTIR && fmt_bits[f] == 16) {
|
||||||
printf("[%2u/%u] %s %s %s : skip (OTIR can't do 16-bit)\n",
|
printf("[%2u/%u] %s %s %s : skip (OTIR can't do 16-bit)\n",
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ int main(void)
|
|||||||
done = 0;
|
done = 0;
|
||||||
|
|
||||||
if (cbl_open(CBL_FREQ_21K9, CBL_FMT_MONO8, CBL_PUMP_OTIR,
|
if (cbl_open(CBL_FREQ_21K9, CBL_FMT_MONO8, CBL_PUMP_OTIR,
|
||||||
CBL_UNDERRUN_APP, fill_speech) != 0) {
|
fill_speech) != 0) {
|
||||||
printf("cbl_open failed: errno=%d\n", errno);
|
printf("cbl_open failed: errno=%d\n", errno);
|
||||||
mem_free_block(blk);
|
mem_free_block(blk);
|
||||||
return 1;
|
return 1;
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||||
|
EXAMPLE := scroll
|
||||||
|
EXTRA_FLAGS ?= --gfx 256 --safe
|
||||||
|
include $(PROJ_ROOT)/app.mk
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
/* Побайтовый регресс gfx_scroll_h/gfx_scroll_v.
|
||||||
|
* Источник — неактивная page 1, приёмник — draw page 0. */
|
||||||
|
#include <graphics.h>
|
||||||
|
#include <gfx.h>
|
||||||
|
#include <sprite.h>
|
||||||
|
|
||||||
|
#define AX 10
|
||||||
|
#define AY 24
|
||||||
|
#define AW 300 /* проверяет разбиение ширины 255+45 */
|
||||||
|
#define AH 160
|
||||||
|
#define DIRT 0x31
|
||||||
|
|
||||||
|
static uint8_t pattern(int x, int y)
|
||||||
|
{
|
||||||
|
return (uint8_t)(0x40 + ((x * 3 + y * 5) & 0x3f));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void draw_source(void)
|
||||||
|
{
|
||||||
|
int x, y;
|
||||||
|
for (y = AY; y < AY + AH; y++)
|
||||||
|
for (x = AX; x < AX + AW; x++)
|
||||||
|
putpixel(x, y, pattern(x, y));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void clear_target(void)
|
||||||
|
{
|
||||||
|
setfillstyle(SOLID_FILL, DIRT);
|
||||||
|
bar(0, 0, 319, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t rect_eq(const gfx_rect_t *r, int x, int y, int w, int h)
|
||||||
|
{
|
||||||
|
return r->x == x && r->y == y && r->w == w && r->h == h;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t check_h(int dx)
|
||||||
|
{
|
||||||
|
gfx_rect_t a = { AX, AY, AW, AH }, d;
|
||||||
|
int x, y, sx;
|
||||||
|
|
||||||
|
clear_target();
|
||||||
|
gfx_scroll_h(&a, dx, &d);
|
||||||
|
if (dx > 0) {
|
||||||
|
if (!rect_eq(&d, AX, AY, dx, AH)) return 0;
|
||||||
|
} else if (dx < 0) {
|
||||||
|
if (!rect_eq(&d, AX + AW + dx, AY, -dx, AH)) return 0;
|
||||||
|
} else if (!rect_eq(&d, AX, AY, 0, AH)) return 0;
|
||||||
|
|
||||||
|
for (y = AY; y < AY + AH; y++)
|
||||||
|
for (x = AX; x < AX + AW; x++) {
|
||||||
|
sx = x - dx;
|
||||||
|
if (sx >= AX && sx < AX + AW) {
|
||||||
|
if ((uint8_t)getpixel(x, y) != pattern(sx, y)) return 0;
|
||||||
|
} else if ((uint8_t)getpixel(x, y) != DIRT) return 0;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t check_v(int dy)
|
||||||
|
{
|
||||||
|
gfx_rect_t a = { AX, AY, AW, AH }, d;
|
||||||
|
int x, y, sy;
|
||||||
|
|
||||||
|
clear_target();
|
||||||
|
gfx_scroll_v(&a, dy, &d);
|
||||||
|
if (dy > 0) {
|
||||||
|
if (!rect_eq(&d, AX, AY, AW, dy)) return 0;
|
||||||
|
} else if (!rect_eq(&d, AX, AY + AH + dy, AW, -dy)) return 0;
|
||||||
|
|
||||||
|
for (y = AY; y < AY + AH; y++)
|
||||||
|
for (x = AX; x < AX + AW; x++) {
|
||||||
|
sy = y - dy;
|
||||||
|
if (sy >= AY && sy < AY + AH) {
|
||||||
|
if ((uint8_t)getpixel(x, y) != pattern(x, sy)) return 0;
|
||||||
|
} else if ((uint8_t)getpixel(x, y) != DIRT) return 0;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t check_limits(void)
|
||||||
|
{
|
||||||
|
gfx_rect_t a = { -5, -7, 25, 27 }, d;
|
||||||
|
gfx_scroll_h(&a, 40, &d);
|
||||||
|
if (!rect_eq(&d, 0, 0, 20, 20)) return 0;
|
||||||
|
gfx_scroll_v(&a, -40, &d);
|
||||||
|
return rect_eq(&d, 0, 0, 20, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t check_source(void)
|
||||||
|
{
|
||||||
|
int x, y;
|
||||||
|
gfx_set_draw_page(1);
|
||||||
|
for (y = AY; y < AY + AH; y++)
|
||||||
|
for (x = AX; x < AX + AW; x++)
|
||||||
|
if ((uint8_t)getpixel(x, y) != pattern(x, y)) {
|
||||||
|
gfx_set_draw_page(0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
gfx_set_draw_page(0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void say(int y, const char *name, uint8_t ok)
|
||||||
|
{
|
||||||
|
setcolor(WHITE); outtextxy(8, y, (char *)name);
|
||||||
|
setcolor(ok ? GREEN : RED); outtextxy(232, y, ok ? (char *)"PASS" : (char *)"FAIL");
|
||||||
|
}
|
||||||
|
|
||||||
|
void main(void)
|
||||||
|
{
|
||||||
|
uint8_t t[7], i;
|
||||||
|
|
||||||
|
initgraph();
|
||||||
|
gfx_set_bank(GFX_BANK_NORMAL);
|
||||||
|
gfx_set_draw_page(1); draw_source();
|
||||||
|
gfx_set_draw_page(0);
|
||||||
|
|
||||||
|
t[0] = check_h(8);
|
||||||
|
t[1] = check_h(-13);
|
||||||
|
t[2] = check_v(9);
|
||||||
|
t[3] = check_v(-11);
|
||||||
|
t[4] = check_limits();
|
||||||
|
t[5] = check_h(0);
|
||||||
|
t[6] = check_source();
|
||||||
|
|
||||||
|
clear_target();
|
||||||
|
say(40, "H +8, wide", t[0]);
|
||||||
|
say(56, "H -13, wide", t[1]);
|
||||||
|
say(72, "V +9, wide", t[2]);
|
||||||
|
say(88, "V -11, wide", t[3]);
|
||||||
|
say(104, "clip/full dirty", t[4]);
|
||||||
|
say(120, "H zero/full copy", t[5]);
|
||||||
|
say(136, "source intact", t[6]);
|
||||||
|
for (i = 0; i < 7 && t[i]; i++) { }
|
||||||
|
setcolor(WHITE);
|
||||||
|
outtextxy(8, 160, i == 7 ? (char *)"ALL PASS" : (char *)"SOME FAILED");
|
||||||
|
for (;;) { }
|
||||||
|
}
|
||||||
@@ -15,8 +15,6 @@
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <sprinter.h>
|
#include <sprinter.h>
|
||||||
|
|
||||||
const uint8_t n_banks = 1; /* читает crt0_banked */
|
|
||||||
|
|
||||||
extern uint8_t res_page_seen;
|
extern uint8_t res_page_seen;
|
||||||
uint8_t bank_page_before, bank_page_after, bank_page_after_home, bank_alive;
|
uint8_t bank_page_before, bank_page_after, bank_page_after_home, bank_alive;
|
||||||
extern uint8_t bank_pages[];
|
extern uint8_t bank_pages[];
|
||||||
|
|||||||
@@ -7,9 +7,6 @@
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <sprinter.h>
|
#include <sprinter.h>
|
||||||
|
|
||||||
/* crt0_banked читает это ДО gsinit — только `const`. */
|
|
||||||
const uint8_t n_banks = 1;
|
|
||||||
|
|
||||||
void bigbank_func(void) __banked; /* W1-банк, трамплин */
|
void bigbank_func(void) __banked; /* W1-банк, трамплин */
|
||||||
extern uint8_t w3_probe(uint8_t x); /* W3-резидент, прямо */
|
extern uint8_t w3_probe(uint8_t x); /* W3-резидент, прямо */
|
||||||
extern void w3_show(void);
|
extern void w3_show(void);
|
||||||
|
|||||||
@@ -12,8 +12,6 @@
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <sprinter.h>
|
#include <sprinter.h>
|
||||||
|
|
||||||
const uint8_t n_banks = 1; /* один свапаемый W3-банк */
|
|
||||||
|
|
||||||
void hugebank_func(void) __banked; /* W3-банк, трамплин */
|
void hugebank_func(void) __banked; /* W3-банк, трамплин */
|
||||||
extern uint8_t w3_probe(uint8_t x); /* W3-резидент, прямой вызов */
|
extern uint8_t w3_probe(uint8_t x); /* W3-резидент, прямой вызов */
|
||||||
extern void w3_show(void); /* W3-резидент, прямой вызов */
|
extern void w3_show(void); /* W3-резидент, прямой вызов */
|
||||||
|
|||||||
Reference in New Issue
Block a user