Compare commits
114 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a8d0692eb2 | |||
| 259c782190 | |||
| e8189b137a | |||
| 958ad0ae0f | |||
| 113d468c48 | |||
| 0ade3b4821 | |||
| 5dc7998324 | |||
| a43e7bda89 | |||
| 619c4c9af5 | |||
| 12667c865a | |||
| 7970a27c98 | |||
| 9ad5a018a2 | |||
| addc00a0f3 | |||
| f91d296476 | |||
| 60762a7c0d | |||
| 0a71f94240 | |||
| d25f28e1cd | |||
| 4ee45f0ace | |||
| a849a7b71d | |||
| 2e7ffd64a4 | |||
| 0e74aaa7ee | |||
| 5323b168a7 | |||
| e4695b8281 | |||
| 50c6e56b7b | |||
| 8e389c03f8 | |||
| 05bcd8197e | |||
| 25abf8ea36 | |||
| f206a4cca6 | |||
| aaa480d0f2 | |||
| 8548aa9132 | |||
| 400f5cba63 | |||
| cb995bf0fc | |||
| 38fb3c03bb | |||
| 879f2bae31 | |||
| b3f9a7430c | |||
| fee3bdb354 | |||
| 6a97124e0d | |||
| 8610a8c178 | |||
| 81e8ed4676 | |||
| 8490288d79 | |||
| 31d0075090 | |||
| 6176f6da31 | |||
| 7377bf6a2c | |||
| b34997073e | |||
| 63bfd997a9 | |||
| decbec79de | |||
| b0e7130d0b | |||
| 4e12aa50d1 | |||
| c218e8b983 | |||
| a3aaa30e53 | |||
| 90e304f071 | |||
| b55d4d11e3 | |||
| ca67895667 | |||
| a5252b1c60 | |||
| 3a0e847353 | |||
| df5071a967 | |||
| 2349481b86 | |||
| 589894c50d | |||
| ea8efdb0fd | |||
| 623199337e | |||
| 602c3a20fa | |||
| 4ac3584bf9 | |||
| 3bf28da8ee | |||
| 770b946a36 | |||
| 89e9663753 | |||
| ea07a8d7b0 | |||
| d23126983e | |||
| 50570881f5 | |||
| f25ed37d85 | |||
| 3449f6f8c9 | |||
| 6dabe9b4b1 | |||
| a05970cd36 | |||
| 2176c12cc5 | |||
| 4e43890fce | |||
| 25b2db8b0b | |||
| f76914849f | |||
| 6cc8611d03 | |||
| 659071838d | |||
| f8e96c0495 | |||
| 76f02e76db | |||
| c71981fdf9 | |||
| 808c2a5349 | |||
| 31b82661eb | |||
| 4b74478d19 | |||
| 747783c422 | |||
| a3c5c600da | |||
| faec9a7d3a | |||
| 5d912f0f3f | |||
| 4086dde1f1 | |||
| 0b15ed7b8f | |||
| f66fd0e1b6 | |||
| e5179af9d8 | |||
| 06a1772011 | |||
| 5ede8b9045 | |||
| 0db4f94707 | |||
| 4a939e42f8 | |||
| 0086ac80c2 | |||
| 7e6b38cca6 | |||
| 65797af44c | |||
| d86adc54c4 | |||
| 16d3262340 | |||
| 4327ac88b9 | |||
| ba3aca07bf | |||
| 34ba5f71d5 | |||
| ffb59b3470 | |||
| 5756101d29 | |||
| 47a4b084c4 | |||
| 7fd7f28ffc | |||
| f4b4852d51 | |||
| b6699b3aef | |||
| 536c60d14c | |||
| e2730f78e0 | |||
| 3ea4546656 | |||
| 8458ec65f4 |
@@ -0,0 +1,29 @@
|
|||||||
|
# Настройки Codex
|
||||||
|
|
||||||
|
`config.toml` и запускной скрипт `toolchain/run-mame-mcp.sh` хранятся в Git.
|
||||||
|
Запускайте Codex из корня репозитория или любой вложенной папки.
|
||||||
|
Корень определяется через `git rev-parse --show-toplevel`.
|
||||||
|
|
||||||
|
На каждом компьютере установите `uv` и `pyenv`. Используется локальный
|
||||||
|
pyenv Python 3.12 из `.python-version`; uv ставит только пакет `mcp<2`,
|
||||||
|
не управляет версией Python. По умолчанию сервер расположен в соседнем
|
||||||
|
`../MAME/src/mame_mcp.py`. При иной раскладке задайте явный путь.
|
||||||
|
|
||||||
|
Если расположение отличается, создайте `.codex/mame.local.env` (игнорируется Git):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
MAME_UV="/opt/homebrew/bin/uv"
|
||||||
|
MAME_MCP_SCRIPT="/путь/к/MAME/src/mame_mcp.py"
|
||||||
|
MAME_PYTHON="/путь/к/pyenv/versions/3.12/bin/python"
|
||||||
|
```
|
||||||
|
|
||||||
|
Путь к серверу может быть абсолютным или относительно корня проекта.
|
||||||
|
Пути к uv/Python могут быть абсолютными или именами из PATH; если
|
||||||
|
`MAME_PYTHON` не задан, скрипт вызывает `pyenv which python`.
|
||||||
|
Переменные можно также передать через окружение Codex; локальный файл имеет
|
||||||
|
приоритет. Файл читается как shell-код запускным скриптом, а не самим Codex.
|
||||||
|
Не храните в нём секреты и не добавляйте его в Git.
|
||||||
|
|
||||||
|
После изменения настроек перезапустите подключение MCP или Codex.
|
||||||
|
Инициализация MCP не требует запущенного эмулятора; для команд отладки нужен
|
||||||
|
MAME с `mame_bridge.lua` (см. `docs/mame-autotest.md`).
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
[mcp_servers.mame-z80]
|
||||||
|
command = "sh"
|
||||||
|
args = [
|
||||||
|
"-c",
|
||||||
|
'root=$(git rev-parse --show-toplevel) || exit; exec sh "$root/toolchain/run-mame-mcp.sh"',
|
||||||
|
]
|
||||||
|
startup_timeout_sec = 60
|
||||||
|
|
||||||
|
[mcp_servers.mame-z80.tools.clear_breakpoint]
|
||||||
|
approval_mode = "approve"
|
||||||
|
|
||||||
|
[mcp_servers.mame-z80.tools.list_breakpoints]
|
||||||
|
approval_mode = "approve"
|
||||||
|
|
||||||
|
[mcp_servers.mame-z80.tools.press_key]
|
||||||
|
approval_mode = "approve"
|
||||||
|
|
||||||
|
[mcp_servers.mame-z80.tools.step_out]
|
||||||
|
approval_mode = "approve"
|
||||||
|
|
||||||
|
[mcp_servers.mame-z80.tools.debugger_command]
|
||||||
|
approval_mode = "approve"
|
||||||
|
|
||||||
|
[mcp_servers.mame-z80.tools.pause]
|
||||||
|
approval_mode = "approve"
|
||||||
|
|
||||||
|
[mcp_servers.mame-z80.tools.status]
|
||||||
|
approval_mode = "approve"
|
||||||
+9
-45
@@ -8,25 +8,9 @@ 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 SDK tests.
|
||||||
# (real apps under examples/ and libc feature tests under tests/).
|
|
||||||
examples/*/*.exe
|
|
||||||
examples/*/*.asm
|
|
||||||
examples/*/*.lst
|
|
||||||
examples/*/*.lk
|
|
||||||
examples/*/*.ihx
|
|
||||||
examples/*/*.noi
|
|
||||||
examples/*/*.sym
|
|
||||||
examples/*/*.map
|
|
||||||
examples/*/*.rel
|
|
||||||
examples/*/*.cdb
|
|
||||||
examples/*/*.mem
|
|
||||||
examples/*/*.rst
|
|
||||||
|
|
||||||
# Temporary build directory for floppy disk image preparation
|
|
||||||
examples/*/.disk_tmp/
|
|
||||||
|
|
||||||
tests/*/*.exe
|
tests/*/*.exe
|
||||||
tests/*/*.asm
|
tests/*/*.asm
|
||||||
tests/*/*.lst
|
tests/*/*.lst
|
||||||
@@ -40,30 +24,6 @@ tests/*/*.cdb
|
|||||||
tests/*/*.mem
|
tests/*/*.mem
|
||||||
tests/*/*.rst
|
tests/*/*.rst
|
||||||
|
|
||||||
# applications/<app>/<prog>/ — реальные приложения (та же схема, что
|
|
||||||
# examples/ и tests/, но на уровень глубже: applications/PoP/roomtest/...)
|
|
||||||
applications/*/*/*.exe
|
|
||||||
applications/*/*/*.asm
|
|
||||||
applications/*/*/*.lst
|
|
||||||
applications/*/*/*.lk
|
|
||||||
applications/*/*/*.ihx
|
|
||||||
applications/*/*/*.noi
|
|
||||||
applications/*/*/*.sym
|
|
||||||
applications/*/*/*.map
|
|
||||||
applications/*/*/*.rel
|
|
||||||
applications/*/*/*.cdb
|
|
||||||
applications/*/*/*.mem
|
|
||||||
applications/*/*/*.rst
|
|
||||||
|
|
||||||
# PoP-порт: внешние референс-репозитории (собственные git-клоны — НЕ
|
|
||||||
# часть этого репозитория) + оригинальные game-данные (копирайт, только
|
|
||||||
# для реверса форматов на этой машине).
|
|
||||||
applications/PoP/SDLPoP/
|
|
||||||
applications/PoP/mininim/
|
|
||||||
applications/PoP/PR/
|
|
||||||
applications/PoP/Prince-of-Persia-Apple-II/
|
|
||||||
applications/PoP/MSDOS/
|
|
||||||
|
|
||||||
# libc + libbgi archives (built by libc/Makefile + libbgi/Makefile)
|
# libc + libbgi archives (built by libc/Makefile + libbgi/Makefile)
|
||||||
lib/*.lib
|
lib/*.lib
|
||||||
|
|
||||||
@@ -77,6 +37,10 @@ toolchain/mkexe/tests/*.actual
|
|||||||
*.obj
|
*.obj
|
||||||
*.dSYM/
|
*.dSYM/
|
||||||
|
|
||||||
|
# Python host-tools: bytecode всегда воспроизводим и не входит в исходники.
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# Vendored / downloaded
|
# Vendored / downloaded
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
@@ -88,9 +52,6 @@ third_party/sdcc-*/
|
|||||||
third_party/*.tar.bz2
|
third_party/*.tar.bz2
|
||||||
third_party/*.tar.gz
|
third_party/*.tar.gz
|
||||||
|
|
||||||
# MAME emulator install — ~1 GB binary + ROMs + CHDs
|
|
||||||
mame/
|
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# OS / editor / AI assistant
|
# OS / editor / AI assistant
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
@@ -120,3 +81,6 @@ mame/
|
|||||||
# .git внутри (в коммите стали бы битыми gitlink-ссылками).
|
# .git внутри (в коммите стали бы битыми gitlink-ссылками).
|
||||||
docs/extra/
|
docs/extra/
|
||||||
docs/sources/
|
docs/sources/
|
||||||
|
|
||||||
|
# Локальные пути MCP; общая конфигурация .codex остаётся в Git.
|
||||||
|
/.codex/mame.local.env
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
3.12
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# Sprinter C-Compiler — правила проекта
|
||||||
|
|
||||||
|
Target-слой SDCC 4.5 (z80) для компьютера Sprinter Sp2000: crt0,
|
||||||
|
линковка, libc, mkexe. Общение и комментарии — на русском.
|
||||||
|
|
||||||
|
## Сборка и проверка
|
||||||
|
|
||||||
|
```
|
||||||
|
make # tools + lib + libbgi + все тесты SDK
|
||||||
|
make -C libc # только libc → lib/sprinter.lib (fast) + sprinter_safe.lib
|
||||||
|
make -C libbgi # только BGI → lib/bgi256.lib (fast) + bgi256_safe.lib
|
||||||
|
make floppy # упаковать тесты в build/media/toolkit-tests.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`,
|
||||||
|
снимает скриншоты, выводит пути). Для сложных сценариев (диалог, несколько
|
||||||
|
шагов ввода) — прямой вызов:
|
||||||
|
`pyenv exec python toolchain/mame_interactive.py tests/<имя>/<имя>.exe --snap T1,T2 --timeout T`.
|
||||||
|
Скриншоты лежат в `build/mame-autotest/<session>/sprinter/`.
|
||||||
|
Подробности: `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 (обновлять при добавлении
|
||||||
|
функций).
|
||||||
|
|
||||||
|
## Документация source debugger
|
||||||
|
|
||||||
|
При изменении `<sdbg.h>`, извлечения/форматирования logMessage,
|
||||||
|
поддержанных типов/регистров, чтения указателей или маршрутов MAME/DAP
|
||||||
|
одновременно обновлять `docs/sdbg-log-macros.md` и проверяемые примеры;
|
||||||
|
ссылки и краткий статус синхронизировать с `docs/mame-source-debug.md`,
|
||||||
|
`docs/vscode-sprinter-debug.md`, `docs/mame-source-debug-status.md` и
|
||||||
|
`docs/libc-reference.md`. Финальные задачи по hex и разыменованию
|
||||||
|
указателей пока только в плане, не считать их рабочим API.
|
||||||
|
|
||||||
|
## 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`, приложения — в независимых репозиториях `../Applications/*`
|
||||||
|
- `docs/` — дизайн-доки; `docs/TODO.md` — roadmap
|
||||||
|
- `third_party/solid-c/` — нативный Sprinter C (референс, CP866;
|
||||||
|
их ABI несовместим — только как образец)
|
||||||
@@ -30,10 +30,10 @@ Vendored third-party components are under their own licenses:
|
|||||||
for compatibility. Licence not specified upstream;
|
for compatibility. Licence not specified upstream;
|
||||||
treat as reference material, do not redistribute
|
treat as reference material, do not redistribute
|
||||||
original binaries.
|
original binaries.
|
||||||
mame/v306/ — MAME (https://www.mamedev.org), GPL v2+.
|
|
||||||
mame/v306/IMG/*.img *.chd *.iso — Sprinter ROM / DSS / sample disk images
|
MAME and its Sprinter ROM/DSS/CHD installation are held in a separate
|
||||||
from Peters Plus. Redistribution policy: see
|
project and are not distributed with this SDK. Their own licenses and
|
||||||
Peters Plus documentation.
|
redistribution terms apply.
|
||||||
|
|
||||||
Documentation in docs/converted/, docs/reference/, docs/samples/, and
|
Documentation in docs/converted/, docs/reference/, docs/samples/, and
|
||||||
docs/memory management/ contains material originally published by Peters Plus
|
docs/memory management/ contains material originally published by Peters Plus
|
||||||
|
|||||||
@@ -4,12 +4,7 @@
|
|||||||
# make tools build only host tools (mkexe)
|
# make tools build only host tools (mkexe)
|
||||||
# make lib build lib/sprinter.lib (libc) + lib/bgi256.lib (libbgi)
|
# make lib build lib/sprinter.lib (libc) + lib/bgi256.lib (libbgi)
|
||||||
# make tests build all libc feature tests under tests/
|
# make tests build all libc feature tests under tests/
|
||||||
# make examples build all real applications under examples/ (НЕ входит
|
# make floppy пакет всех SDK-тестов в build/media/toolkit-tests.img
|
||||||
# в `make all`: это регрессная сборка, а examples/ —
|
|
||||||
# крупные приложения, которые её только замедляют
|
|
||||||
# (mdview компилируется минутами) и ничего нового про
|
|
||||||
# libc не показывают. Собирать явно перед `make floppy`.)
|
|
||||||
# make floppy package every .exe + test fixtures into mame/v306/IMG/mc.img
|
|
||||||
# make check run mkexe unit tests
|
# make check run mkexe unit tests
|
||||||
# make clean remove all build artefacts
|
# make clean remove all build artefacts
|
||||||
# make sdcc download/extract vendored SDCC
|
# make sdcc download/extract vendored SDCC
|
||||||
@@ -25,25 +20,24 @@ TESTS := hello hello2 simple banked bankedbg banktest strtest cat seek \
|
|||||||
gfx_demo gfx_dbuf bgitest bgi_img accfill
|
gfx_demo gfx_dbuf bgitest bgi_img accfill
|
||||||
# gfx_d16 / gfx_text / gfx_mous — 16-цветные; убраны до Фазы 2 (bgi16.lib
|
# gfx_d16 / gfx_text / gfx_mous — 16-цветные; убраны до Фазы 2 (bgi16.lib
|
||||||
# ещё не собирается). Вернуть мигрированными на BGI --gfx 16.
|
# ещё не собирается). Вернуть мигрированными на BGI --gfx 16.
|
||||||
# Larger end-user applications under examples/.
|
FLOPPY_IMG := build/media/toolkit-tests.img
|
||||||
APPS := mdview mdview2
|
MAKE_DISK := toolchain/make_disk.py
|
||||||
|
MAME_HOME ?=
|
||||||
MAME_DIR := mame/v306
|
PYTHON ?= $(shell pyenv which python)
|
||||||
FLOPPY_IMG := $(MAME_DIR)/IMG/mc.img
|
ifeq ($(strip $(PYTHON)),)
|
||||||
MAKE_DISK := $(MAME_DIR)/make_disk.py
|
$(error pyenv Python 3.12 не найден; задайте PYTHON=/путь/к/python3.12)
|
||||||
|
endif
|
||||||
|
|
||||||
TEST_EXES := $(foreach t,$(TESTS),tests/$(t)/$(t).exe)
|
TEST_EXES := $(foreach t,$(TESTS),tests/$(t)/$(t).exe)
|
||||||
APP_EXES := $(foreach a,$(APPS),examples/$(a)/$(a).exe)
|
ALL_EXES := $(TEST_EXES)
|
||||||
ALL_EXES := $(TEST_EXES) $(APP_EXES)
|
|
||||||
|
|
||||||
DATA_FILES := \
|
DATA_FILES := \
|
||||||
tests/cat/test.txt \
|
tests/cat/test.txt \
|
||||||
tests/seek/big.txt \
|
tests/seek/big.txt \
|
||||||
tests/cblwav/speech.pcm \
|
tests/cblwav/speech.pcm
|
||||||
examples/mdview/SAMPLE.MD
|
|
||||||
|
|
||||||
.PHONY: all tools lib tests examples check clean sdcc floppy \
|
.PHONY: all tools lib tests check clean sdcc floppy run \
|
||||||
size-check size-baseline host-tests $(TESTS) $(APPS)
|
size-check size-baseline host-tests sdbg-tests $(TESTS)
|
||||||
|
|
||||||
all: tools lib tests
|
all: tools lib tests
|
||||||
|
|
||||||
@@ -58,47 +52,52 @@ check: tools
|
|||||||
$(MAKE) -C toolchain/mkexe check
|
$(MAKE) -C toolchain/mkexe check
|
||||||
|
|
||||||
tests: $(TESTS)
|
tests: $(TESTS)
|
||||||
examples: $(APPS)
|
|
||||||
|
|
||||||
$(TESTS): tools lib
|
$(TESTS): tools lib
|
||||||
$(MAKE) -C tests/$@
|
$(MAKE) -C tests/$@
|
||||||
|
|
||||||
$(APPS): tools lib
|
|
||||||
$(MAKE) -C examples/$@
|
|
||||||
|
|
||||||
# Generate big.txt if missing (gen_bigfile.py creates 100 KB marker file).
|
# Generate big.txt if missing (gen_bigfile.py creates 100 KB marker file).
|
||||||
tests/seek/big.txt:
|
tests/seek/big.txt:
|
||||||
cd tests/seek && python3 gen_bigfile.py big.txt 102400
|
cd tests/seek && $(PYTHON) gen_bigfile.py big.txt 102400
|
||||||
|
|
||||||
# Re-pack the MAME floppy image with every built exe + needed data files.
|
# Собственный SDK-носитель: не изменяет диск MAME или приложения.
|
||||||
floppy: tests examples tests/seek/big.txt
|
floppy: tests tests/seek/big.txt
|
||||||
python3 $(MAKE_DISK) $(FLOPPY_IMG) $(ALL_EXES) $(DATA_FILES)
|
$(PYTHON) $(MAKE_DISK) $(FLOPPY_IMG) $(ALL_EXES) $(DATA_FILES)
|
||||||
@echo
|
@echo
|
||||||
@echo "Floppy ready: $(FLOPPY_IMG)"
|
@echo "Floppy ready: $(FLOPPY_IMG)"
|
||||||
@echo "Run: cd $(MAME_DIR) && ./run_mame.sh"
|
@echo "Run: make run MAME_HOME=/путь/к/MAME/runtime"
|
||||||
|
|
||||||
|
run: floppy
|
||||||
|
$(PYTHON) toolchain/run_sprinter_mame.py --mame-home "$(MAME_HOME)" --floppy "$(FLOPPY_IMG)"
|
||||||
|
|
||||||
# Модульные тесты под ucsim_z80. Обвязка — testkit/, сами наборы лежат
|
# Модульные тесты под ucsim_z80. Обвязка — testkit/, сами наборы лежат
|
||||||
# рядом с кодом, который проверяют. MAME не нужна, идут за секунды;
|
# рядом с кодом, который проверяют. MAME не нужна, идут за секунды;
|
||||||
# ucsim идёт в комплекте нашего SDCC.
|
# ucsim идёт в комплекте нашего SDCC.
|
||||||
HOST_TEST_DIRS := testkit applications/PoP/roomtest/tests-host
|
# Тесты продуктов запускаются в собственных репозиториях.
|
||||||
|
HOST_TEST_DIRS := testkit
|
||||||
|
|
||||||
host-tests:
|
host-tests:
|
||||||
@for d in $(HOST_TEST_DIRS); do $(MAKE) -C $$d || exit 1; done
|
@for d in $(HOST_TEST_DIRS); do $(MAKE) -C $$d || exit 1; done
|
||||||
|
|
||||||
|
# Отладочная карта: реальные SDCC/linker и протокол транспорта без MAME.
|
||||||
|
# Для local pyenv: pyenv exec make sdbg-tests.
|
||||||
|
sdbg-tests: tools lib
|
||||||
|
$(PYTHON) -m unittest discover -s tests/sdbg -v
|
||||||
|
|
||||||
# Размерный регресс: сверить _CODE всех программ с docs/size_baseline.tsv.
|
# Размерный регресс: сверить _CODE всех программ с docs/size_baseline.tsv.
|
||||||
size-check:
|
size-check: tests
|
||||||
python3 toolchain/size_check.py
|
$(PYTHON) toolchain/size_check.py --require $(TESTS)
|
||||||
|
|
||||||
# Принять текущие размеры как эталон (после осознанных изменений).
|
# Принять текущие размеры как эталон (после осознанных изменений).
|
||||||
size-baseline:
|
size-baseline: tests
|
||||||
python3 toolchain/size_check.py --update
|
$(PYTHON) toolchain/size_check.py --update --require $(TESTS)
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
$(MAKE) -C toolchain/mkexe clean
|
$(MAKE) -C toolchain/mkexe clean
|
||||||
$(MAKE) -C libc clean
|
$(MAKE) -C libc clean
|
||||||
$(MAKE) -C libbgi clean
|
$(MAKE) -C libbgi clean
|
||||||
@for t in $(TESTS); do $(MAKE) -C tests/$$t clean; done
|
@for t in $(TESTS); do $(MAKE) -C tests/$$t clean; done
|
||||||
@for a in $(APPS); do $(MAKE) -C examples/$$a clean; done
|
|
||||||
|
|
||||||
sdcc:
|
sdcc:
|
||||||
bash third_party/setup-sdcc.sh
|
bash third_party/setup-sdcc.sh
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ banked-call trampolines, graphics & accelerator API, mouse driver wrappers, and
|
|||||||
git clone <this repo> sprinter-c
|
git clone <this repo> sprinter-c
|
||||||
cd sprinter-c
|
cd sprinter-c
|
||||||
make sdcc # one-time: fetch SDCC 4.5 binary (~25 MB)
|
make sdcc # one-time: fetch SDCC 4.5 binary (~25 MB)
|
||||||
make all # build mkexe + libsprinter.lib + 27 examples
|
make all # build tools, libraries and SDK tests
|
||||||
make floppy # pack everything into mame/v306/IMG/mc.img
|
make floppy # pack tests into build/media/toolkit-tests.img
|
||||||
cd mame/v306 && ./run_mame.sh # boot Sprinter in MAME
|
MAME_HOME=/путь/к/MAME/runtime make run
|
||||||
```
|
```
|
||||||
|
|
||||||
Compile a single program:
|
Compile a single program:
|
||||||
@@ -70,7 +70,11 @@ Banked functions are declared with `__banked`:
|
|||||||
void engine_tick(int dt) __banked; // lives in BANK1, automatically swapped
|
void engine_tick(int dt) __banked; // lives in BANK1, automatically swapped
|
||||||
```
|
```
|
||||||
|
|
||||||
## Examples (27 total)
|
## SDK tests and separate examples
|
||||||
|
|
||||||
|
Programs listed below live in `tests/` and validate the SDK. Demonstration
|
||||||
|
applications live in the separate `Examples` repository. Its Makefiles use
|
||||||
|
`SPRINTER_ROOT` to find this SDK.
|
||||||
|
|
||||||
| Example | What it demonstrates |
|
| Example | What it demonstrates |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -128,8 +132,9 @@ Sprinter-specific:
|
|||||||
## Toolchain commands
|
## Toolchain commands
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
make all # build mkexe + lib + every example
|
make all # build mkexe + libraries + SDK tests
|
||||||
make floppy # repack mame/v306/IMG/mc.img with all .exe files
|
make floppy # pack SDK tests into build/media/toolkit-tests.img
|
||||||
|
MAME_HOME=/путь/к/MAME/runtime make run
|
||||||
make check # 17 mkexe unit-tests
|
make check # 17 mkexe unit-tests
|
||||||
make clean # remove all build artefacts
|
make clean # remove all build artefacts
|
||||||
make sdcc # one-time: fetch SDCC 4.5 binary
|
make sdcc # one-time: fetch SDCC 4.5 binary
|
||||||
@@ -144,7 +149,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
|
||||||
@@ -197,8 +203,8 @@ runtime/ crt0 variants (default, minimal, small, banked)
|
|||||||
libc/include/ headers
|
libc/include/ headers
|
||||||
libc/io|stdio|mem|gfx/ C and asm sources for libsprinter.lib
|
libc/io|stdio|mem|gfx/ C and asm sources for libsprinter.lib
|
||||||
lib/ Makefile that archives libsprinter.lib via sdar
|
lib/ Makefile that archives libsprinter.lib via sdar
|
||||||
examples/ 27 example programs
|
tests/ SDK regression programs
|
||||||
mame/v306/ MAME binary + Sprinter ROM/HDD images + floppy script
|
app.mk shared rules for independent applications
|
||||||
third_party/sdcc/ vendored SDCC 4.5 (fetched via `make sdcc`)
|
third_party/sdcc/ vendored SDCC 4.5 (fetched via `make sdcc`)
|
||||||
third_party/solid-c/ reference: original Sprinter native C (for compat target)
|
third_party/solid-c/ reference: original Sprinter native C (for compat target)
|
||||||
docs/ documentation
|
docs/ documentation
|
||||||
@@ -207,7 +213,7 @@ docs/ documentation
|
|||||||
## License
|
## License
|
||||||
|
|
||||||
This repository contains:
|
This repository contains:
|
||||||
* Original code in `bin/`, `toolchain/`, `runtime/`, `libc/`, `lib/`, `examples/` —
|
* Original code in `bin/`, `toolchain/`, `runtime/`, `libc/`, `libbgi/`, `lib/` —
|
||||||
MIT-licensed.
|
MIT-licensed.
|
||||||
* `third_party/sdcc/` — SDCC 4.5 under GPLv2 with linking exception
|
* `third_party/sdcc/` — SDCC 4.5 under GPLv2 with linking exception
|
||||||
(see `third_party/sdcc/COPYING.txt`)
|
(see `third_party/sdcc/COPYING.txt`)
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
# app.mk — shared Makefile fragment for any standalone Sprinter ESTEX
|
# app.mk — shared Makefile fragment for any standalone Sprinter ESTEX
|
||||||
# program — used both by libc feature tests under tests/ and by real
|
# program. Приложение может лежать вне репозитория тулкита.
|
||||||
# applications under examples/.
|
|
||||||
#
|
#
|
||||||
# Usage in a per-program Makefile:
|
# Usage in a per-program Makefile:
|
||||||
#
|
#
|
||||||
# PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
# SPRINTER_ROOT := /путь/к/C-Compiler
|
||||||
# EXAMPLE := my_program # base name (matches my_program.c)
|
# EXAMPLE := my_program # base name (matches my_program.c)
|
||||||
#
|
#
|
||||||
# # Optional overrides (any combination):
|
# # Optional overrides (any combination):
|
||||||
@@ -12,7 +11,10 @@
|
|||||||
# # STACK_SIZE := 2048 # bytes reserved for the stack
|
# # STACK_SIZE := 2048 # bytes reserved for the stack
|
||||||
# # EXTRA_SRCS := helper.c util.c # additional .c files in this dir
|
# # EXTRA_SRCS := helper.c util.c # additional .c files in this dir
|
||||||
# # EXTRA_FLAGS := --crt0=minimal # passed through to sprinter-cc
|
# # EXTRA_FLAGS := --crt0=minimal # passed through to sprinter-cc
|
||||||
|
# # SRC_DEBUG := 1 # карта C/asm и проверенный debug-пакет
|
||||||
|
# # SRC_DEBUG_FILES := helper.c # либо карта только выбранных TU
|
||||||
# # EXTRA_DATA := test.txt # extra files to add to `make floppy`
|
# # EXTRA_DATA := test.txt # extra files to add to `make floppy`
|
||||||
|
# # HDD_DEST_DIR := games/myapp # общий каталог файлов в `make hdd`
|
||||||
#
|
#
|
||||||
# include $(PROJ_ROOT)/app.mk
|
# include $(PROJ_ROOT)/app.mk
|
||||||
#
|
#
|
||||||
@@ -25,41 +27,131 @@
|
|||||||
# all build $(EXAMPLE).exe (default)
|
# all build $(EXAMPLE).exe (default)
|
||||||
# clean remove build artefacts
|
# clean remove build artefacts
|
||||||
# floppy build the example and pack it (alone, plus EXTRA_DATA) into
|
# floppy build the example and pack it (alone, plus EXTRA_DATA) into
|
||||||
# mame/v306/IMG/mc.img — useful for trying a single program
|
# build/media/$(EXAMPLE).img внутри приложения.
|
||||||
# without rebuilding every example. Top-level `make floppy`
|
|
||||||
# (in the repo root) still packs all examples.
|
|
||||||
# run floppy + launch MAME
|
# run floppy + launch MAME
|
||||||
|
|
||||||
|
ifeq ($(strip $(PROJ_ROOT)$(SPRINTER_ROOT)),)
|
||||||
|
$(error Задайте SPRINTER_ROOT — путь к установленному Sprinter-CC)
|
||||||
|
endif
|
||||||
|
ifeq ($(strip $(PROJ_ROOT)),)
|
||||||
|
PROJ_ROOT := $(SPRINTER_ROOT)
|
||||||
|
endif
|
||||||
|
ifeq ($(strip $(SPRINTER_ROOT)),)
|
||||||
|
SPRINTER_ROOT := $(PROJ_ROOT)
|
||||||
|
endif
|
||||||
|
|
||||||
|
# SPRINTER_PYTHON в sprinter-cc — один исполняемый файл, а не shell-команда.
|
||||||
|
# pyenv which выбирает local .python-version текущего приложения.
|
||||||
|
PYTHON ?= $(shell pyenv which python)
|
||||||
|
ifeq ($(strip $(PYTHON)),)
|
||||||
|
$(error pyenv Python не найден; задайте PYTHON=/путь/к/python3.12)
|
||||||
|
endif
|
||||||
SPRINTER_CC := $(PROJ_ROOT)/bin/sprinter-cc
|
SPRINTER_CC := $(PROJ_ROOT)/bin/sprinter-cc
|
||||||
MKEXE := $(PROJ_ROOT)/toolchain/mkexe/mkexe
|
MKEXE := $(PROJ_ROOT)/toolchain/mkexe/mkexe
|
||||||
LIB := $(PROJ_ROOT)/lib/sprinter.lib
|
LIB := $(PROJ_ROOT)/lib/sprinter.lib
|
||||||
|
|
||||||
MAME_DIR := $(PROJ_ROOT)/mame/v306
|
ifeq ($(wildcard $(SPRINTER_CC)),)
|
||||||
FLOPPY_IMG := $(MAME_DIR)/IMG/mc.img
|
$(error SPRINTER_ROOT=$(SPRINTER_ROOT): bin/sprinter-cc не найден)
|
||||||
HDD_IMG := $(MAME_DIR)/IMG/test_hdd.chd
|
endif
|
||||||
MAKE_DISK := $(MAME_DIR)/make_disk.py
|
|
||||||
MAKE_HDD := $(PROJ_ROOT)/toolchain/make_hdd.sh
|
MAME_HOME ?=
|
||||||
RUN_MAME := $(MAME_DIR)/run_mame.sh
|
# Пустое значение выбирает MAME_HOME/sprinter или старый mame.arm в профиле.
|
||||||
|
MAME_BIN ?=
|
||||||
|
MAME_ROMPATH ?= $(if $(strip $(MAME_HOME)),$(MAME_HOME)/roms,)
|
||||||
|
MAME_DSS_IMAGE ?= $(if $(strip $(MAME_HOME)),$(MAME_HOME)/IMG/dss171u.img,)
|
||||||
|
MAME_SYSTEM_HDD_IMAGE ?= $(if $(strip $(MAME_HOME)),$(MAME_HOME)/IMG/sp_hdd_sys.chd,)
|
||||||
|
MAME_BIOS ?= v3.06
|
||||||
|
FLOPPY_IMG ?= $(CURDIR)/build/media/$(EXAMPLE).img
|
||||||
|
HDD_IMG ?= $(CURDIR)/build/hdd/$(EXAMPLE).chd
|
||||||
|
MAKE_DISK := $(PROJ_ROOT)/toolchain/make_disk.py
|
||||||
|
MAKE_HDD ?= $(PROJ_ROOT)/toolchain/make_hdd.sh
|
||||||
|
RUN_MAME := $(PROJ_ROOT)/toolchain/run_sprinter_mame.py
|
||||||
|
CHDMAN_BIN ?= chdman
|
||||||
|
MAME_PROFILE_ARGS = --mame-home "$(MAME_HOME)" --mame-bin "$(MAME_BIN)" \
|
||||||
|
--mame-rompath "$(MAME_ROMPATH)" \
|
||||||
|
--mame-dss-image "$(MAME_DSS_IMAGE)" \
|
||||||
|
--mame-system-hdd-image "$(MAME_SYSTEM_HDD_IMAGE)" \
|
||||||
|
--mame-bios "$(MAME_BIOS)"
|
||||||
|
|
||||||
# Optional knobs — see top of file.
|
# Optional knobs — see top of file.
|
||||||
MEMORY ?= tiny
|
MEMORY ?= tiny
|
||||||
SOURCES := $(EXAMPLE).c $(EXTRA_SRCS)
|
|
||||||
|
# SRC_DIR / BUILD_DIR — раскладка приложения, которое НЕ держит исходники и
|
||||||
|
# выхлоп в одной папке с Makefile (например, SprPoP: src/ и build/). По
|
||||||
|
# умолчанию обе пусты, то есть всё как было: ./$(EXAMPLE).c → ./$(EXAMPLE).exe.
|
||||||
|
# Пустое значение обрабатывается отдельной веткой намеренно: "./prog.exe" и
|
||||||
|
# "prog.exe" — разные имена целей, и склеивать префикс безусловно нельзя.
|
||||||
|
SRC_DIR ?=
|
||||||
|
BUILD_DIR ?=
|
||||||
|
ifeq ($(strip $(SRC_DIR)),)
|
||||||
|
MAIN_SRC := $(EXAMPLE).c
|
||||||
|
else
|
||||||
|
MAIN_SRC := $(SRC_DIR)/$(EXAMPLE).c
|
||||||
|
endif
|
||||||
|
ifeq ($(strip $(BUILD_DIR)),)
|
||||||
|
EXE := $(EXAMPLE).exe
|
||||||
|
else
|
||||||
|
EXE := $(BUILD_DIR)/$(EXAMPLE).exe
|
||||||
|
endif
|
||||||
|
SOURCES := $(MAIN_SRC) $(EXTRA_SRCS)
|
||||||
|
# Аргументы упаковщика HDD. Обычно это exe и EXTRA_DATA; приложение со
|
||||||
|
# своей раскладкой каталогов может переопределить переменную до include.
|
||||||
|
HDD_PACK_ARGS ?= $(EXE) $(EXTRA_DATA)
|
||||||
|
# Общий каталог назначения внутри HDD. Пустое значение сохраняет прежнюю
|
||||||
|
# укладку в корень; вложенные КАТАЛОГ:файл считаются относительно него.
|
||||||
|
HDD_DEST_DIR ?=
|
||||||
|
|
||||||
CC_FLAGS := --memory $(MEMORY)
|
CC_FLAGS := --memory $(MEMORY)
|
||||||
ifneq ($(STACK_SIZE),)
|
ifneq ($(STACK_SIZE),)
|
||||||
CC_FLAGS += --stack-size $(STACK_SIZE)
|
CC_FLAGS += --stack-size $(STACK_SIZE)
|
||||||
endif
|
endif
|
||||||
CC_FLAGS += $(EXTRA_FLAGS)
|
CC_FLAGS += $(EXTRA_FLAGS)
|
||||||
|
ifeq ($(SRC_DEBUG),1)
|
||||||
|
CC_FLAGS += --src-debug
|
||||||
|
endif
|
||||||
|
ifneq ($(strip $(SRC_DEBUG_FILES)),)
|
||||||
|
CC_FLAGS += $(foreach src,$(SRC_DEBUG_FILES),--src-debug-file $(src))
|
||||||
|
endif
|
||||||
|
|
||||||
all: $(EXAMPLE).exe
|
all: $(EXE)
|
||||||
|
|
||||||
# runtime/*.s (crt0-семейство, bank.s, heap.s) собираются per-build
|
# runtime/*.s (crt0-семейство, bank.s, heap.s) собираются per-build
|
||||||
# внутри sprinter-cc — без этой зависимости их правка не перелинкует
|
# внутри sprinter-cc — без этой зависимости их правка не перелинкует
|
||||||
# уже собранный exe (кусало: фикс bank.s не подхватился).
|
# уже собранный exe (кусало: фикс bank.s не подхватился).
|
||||||
RUNTIME_DEPS := $(wildcard $(PROJ_ROOT)/runtime/*.s)
|
RUNTIME_DEPS := $(wildcard $(PROJ_ROOT)/runtime/*.s)
|
||||||
|
|
||||||
$(EXAMPLE).exe: $(SOURCES) $(MKEXE) $(LIB) $(RUNTIME_DEPS)
|
# ПРОВЕРКА БАНКОВЫХ ВЫЗОВОВ — сразу после линковки, пока артефакты свежие.
|
||||||
$(SPRINTER_CC) $(CC_FLAGS) -o $@ $(SOURCES)
|
# Ловит прямой `call` в чужой банк: он собирается МОЛЧА и стреляет диким
|
||||||
|
# переходом в пустой хвост банка (разбор — в шапке скрипта). Запускается
|
||||||
|
# только если банки вообще есть, то есть по наличию каталога сборки с
|
||||||
|
# bankN_*.asm; обычным небанковым программам ничего не стоит.
|
||||||
|
BANK_CHECK := $(PROJ_ROOT)/toolchain/check_bank_calls.py
|
||||||
|
|
||||||
|
# ПРОВЕРКА ISR-СТАБА W0-СТРАНИЦ — там же, по свежей карте. Ловит стаб
|
||||||
|
# _gfx_w0_isr, оставшийся в W1: программа, кладущая свои страницы в W0
|
||||||
|
# (атласы спрайтов, gfx_w0_map), получает недетерминированные зависания,
|
||||||
|
# когда прерывание приходит во время вызова DSS и W1 перемаплен. Только
|
||||||
|
# предупреждение: страницы в W0 кладут не все. Разбор — в шапке скрипта.
|
||||||
|
W0ISR_CHECK := $(PROJ_ROOT)/toolchain/check_w0_isr.py
|
||||||
|
|
||||||
|
# Команда/зависимости карты участвуют в пересборке, включая смену режима.
|
||||||
|
SDBG_CONFIG := $(dir $(EXE)).resource-stamps/$(EXAMPLE)-config.json
|
||||||
|
SDBG_MANIFEST := $(dir $(EXE)).sprinter-cc-$(EXAMPLE)/manifest.json
|
||||||
|
.PHONY: sdbg-config-force
|
||||||
|
SDBG_CONFIG_ARGS = $(SDBG_CONFIG) $(SPRINTER_CC) $(CC_FLAGS) $(SOURCES) $(SDBG_MANIFEST)
|
||||||
|
|
||||||
|
# Проверка содержимого обязательна: mtime make может иметь точность в секунду.
|
||||||
|
# Сохранение fingerprint только после успешной сборки позволяет повторить сбой.
|
||||||
|
$(EXE): $(SOURCES) $(MKEXE) $(LIB) $(RUNTIME_DEPS) sdbg-config-force $(SPRINTER_CC) $(wildcard $(PROJ_ROOT)/toolchain/sdbg/*.py) $(wildcard $(PROJ_ROOT)/toolchain/sdbg_*.py)
|
||||||
|
@state=$$($(PYTHON) $(PROJ_ROOT)/toolchain/sdbg_config.py check $(SDBG_CONFIG_ARGS)) || exit $$?; \
|
||||||
|
if [ "$$state" = changed ] || [ -n "$(filter-out sdbg-config-force,$?)" ]; then \
|
||||||
|
mkdir -p $(dir $@); \
|
||||||
|
SPRINTER_PYTHON="$(PYTHON)" $(SPRINTER_CC) $(CC_FLAGS) -o $@ $(SOURCES) || exit $$?; \
|
||||||
|
d=$(dir $@).sprinter-cc-$(EXAMPLE); \
|
||||||
|
if ls $$d/bank*_*.asm >/dev/null 2>&1; then $(PYTHON) $(BANK_CHECK) $$d || exit $$?; fi; \
|
||||||
|
if ls $$d/*.map >/dev/null 2>&1; then $(PYTHON) $(W0ISR_CHECK) $$d || exit $$?; fi; \
|
||||||
|
$(PYTHON) $(PROJ_ROOT)/toolchain/sdbg_config.py save $(SDBG_CONFIG_ARGS); \
|
||||||
|
fi
|
||||||
|
|
||||||
$(MKEXE):
|
$(MKEXE):
|
||||||
$(MAKE) -C $(PROJ_ROOT)/toolchain/mkexe
|
$(MAKE) -C $(PROJ_ROOT)/toolchain/mkexe
|
||||||
@@ -73,28 +165,28 @@ $(LIB):
|
|||||||
$(MAKE) -C $(PROJ_ROOT)/libbgi
|
$(MAKE) -C $(PROJ_ROOT)/libbgi
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf .sprinter-cc-* $(EXAMPLE).exe
|
rm -rf $(if $(strip $(BUILD_DIR)),$(BUILD_DIR),.sprinter-cc-* $(EXE))
|
||||||
|
|
||||||
# `make floppy` packs ONLY this program (+ optional EXTRA_DATA files) into
|
# `make floppy` создаёт носитель только этого приложения, без записи в MAME.
|
||||||
# the MAME floppy image, replacing whatever was there. Handy for trying a
|
floppy: $(EXE)
|
||||||
# single program without rebuilding everything.
|
$(PYTHON) $(MAKE_DISK) "$(FLOPPY_IMG)" $(EXE) $(EXTRA_DATA)
|
||||||
floppy: $(EXAMPLE).exe
|
|
||||||
python3 $(MAKE_DISK) $(FLOPPY_IMG) $(EXAMPLE).exe $(EXTRA_DATA)
|
|
||||||
@echo
|
@echo
|
||||||
@echo "Floppy ready: $(FLOPPY_IMG) (with $(EXAMPLE).exe$(if $(EXTRA_DATA), + $(EXTRA_DATA))) "
|
@echo "Floppy ready: $(FLOPPY_IMG) (with $(EXAMPLE).exe$(if $(EXTRA_DATA), + $(EXTRA_DATA))) "
|
||||||
@echo "Run: cd $(MAME_DIR) && ./run_mame.sh"
|
@echo "Run: make run MAME_HOME=/путь/к/MAME/runtime"
|
||||||
|
|
||||||
run: floppy
|
run: floppy
|
||||||
cd $(MAME_DIR) && ./run_mame.sh
|
$(PYTHON) $(RUN_MAME) $(MAME_PROFILE_ARGS) --floppy "$(FLOPPY_IMG)"
|
||||||
|
|
||||||
# `make hdd` packs this program (+ optional EXTRA_DATA files) into the MAME
|
# `make hdd` кладёт носитель в build/ приложения. Для нового inode после
|
||||||
# HDD image mounted as disk D: (-hard2 test_hdd.chd). Гораздо быстрее FDD —
|
# пересборки образа запущенный MAME должен быть перезапущен.
|
||||||
# используется MCP-мостом к MAME (run_bridge.sh). После пересборки образа
|
hdd: $(EXE)
|
||||||
# MAME ОБЯЗАН полный рестарт (chdman -f = новый inode; см. memory).
|
mkdir -p "$(dir $(HDD_IMG))"
|
||||||
hdd: $(EXAMPLE).exe
|
CHDMAN_BIN="$(CHDMAN_BIN)" $(MAKE_HDD) $(if $(strip $(HDD_DEST_DIR)),--dest "$(HDD_DEST_DIR)") "$(HDD_IMG)" $(HDD_PACK_ARGS)
|
||||||
$(MAKE_HDD) $(HDD_IMG) $(EXAMPLE).exe $(EXTRA_DATA)
|
|
||||||
@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 — образ пересобран."
|
||||||
|
|
||||||
.PHONY: all clean floppy run hdd
|
run-hdd: hdd
|
||||||
|
$(PYTHON) $(RUN_MAME) $(MAME_PROFILE_ARGS) --hdd "$(HDD_IMG)"
|
||||||
|
|
||||||
|
.PHONY: all clean floppy run hdd run-hdd
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
# Prince of Persia → ZX Sprinter — правила подпроекта
|
|
||||||
|
|
||||||
Порт Prince of Persia (DOS/Apple II) на Sprinter Sp2000 поверх нашего
|
|
||||||
sprinter-cc / libc / libbgi. Действуют правила корневого
|
|
||||||
`CLAUDE.md` (сборка, libc, ABI, MAME-автотест); ниже — только специфика PoP.
|
|
||||||
Общение и комментарии — на русском.
|
|
||||||
|
|
||||||
## Главное правило: SDLPoP — источник истины. Сначала читай, потом кодь
|
|
||||||
|
|
||||||
**`SDLPoP/src/` (github.com/NagyD/SDLPoP, GPLv3) — ЕДИНСТВЕННЫЙ авторитетный
|
|
||||||
источник того, как оригинальный движок это делает.** Правило без исключений:
|
|
||||||
|
|
||||||
1. **Перед реализацией ЛЮБОЙ функции** (движение, коллизия, окклюзия,
|
|
||||||
падение, loose-полы, стражники, отрисовка, тайминги, любые числовые
|
|
||||||
константы) — СНАЧАЛА найди и прочитай соответствующий код в `SDLPoP/src/`,
|
|
||||||
и портируй по нему. Не пиши по памяти, не выводи логику «из общих
|
|
||||||
соображений», не угадывай значения — это источник багов, которые потом
|
|
||||||
ловятся в MAME часами.
|
|
||||||
2. **По любому вопросу «как в оригинале должно быть»** (что окклюдит что,
|
|
||||||
в каком порядке слои, когда меняется тайл, какая скорость/задержка,
|
|
||||||
что делает такой-то кадр анимации) — ответ ищи в `SDLPoP/src/`, а не
|
|
||||||
строй гипотезу. Если в SDLPoP не нашёл — это повод копать дальше в
|
|
||||||
исходнике, а не додумывать.
|
|
||||||
3. Расхождение нашей реализации с SDLPoP — по умолчанию **баг у нас**, пока
|
|
||||||
не доказано обратное (наша платформа/ABI требует отличия — тогда явно
|
|
||||||
зафиксировать почему в комментарии **и записью в `docs/impl_diff.md`**:
|
|
||||||
что делает оригинал, что делаем мы, чем платим, что проверять при
|
|
||||||
регрессе).
|
|
||||||
|
|
||||||
Карта сегментов: `seg005` control-диспетчер, `seg006` play_kid/коллизия/
|
|
||||||
seqtbl, `seg007` mob/loose/падающие объекты, `seg008` отрисовка тайлов/
|
|
||||||
слои/окклюзия, `seg009` чтение ресурсов. Слои окклюзии у нас = слои SDLPoP.
|
|
||||||
См. memory `pop_check_sdlpop_first`.
|
|
||||||
|
|
||||||
Вторичные референсы (когда в SDLPoP непонятно/нужен другой ракурс):
|
|
||||||
- `Prince-of-Persia-Apple-II/` — оригинальный 6502-исходник 1989 (Мехнер).
|
|
||||||
- `PR/` (github.com/NagyD/PR, GPLv2) — Princed Resources.
|
|
||||||
- `mininim/` — независимая реализация.
|
|
||||||
|
|
||||||
Все эти папки — **справочник логики/структур/констант и источник ассетов**,
|
|
||||||
но НЕ код для копирования (лицензии несовместимы, наш ABI другой): читаем
|
|
||||||
и переписываем под наш движок, а не вставляем куски.
|
|
||||||
|
|
||||||
## Ассеты
|
|
||||||
|
|
||||||
Готовые распакованные VGA-256 ассеты (то, что нужно под 320×256×256) —
|
|
||||||
`SDLPoP/data/` (`res<id>.png`/`.pal`/`.bin`). Брать оттуда, а НЕ писать свой
|
|
||||||
декодер DOS `.DAT`. Локальные `.DAT` — в `MSDOS/`.
|
|
||||||
|
|
||||||
**Каноническая спецификация форматов `.DAT` — `docs/POP-DAT-FormatSpecifications.pdf`**
|
|
||||||
(грепаемая копия — `docs/POP-DAT-FormatSpecifications.txt`): первоисточник
|
|
||||||
Princed для DAT v1.0 (контейнер/индекс/чек-сумма, кодеки RLE/LZG, палитры,
|
|
||||||
формат уровней, звук), на нём построены и SDLPoP, и Princed Resources. Наши
|
|
||||||
разборы (`docs/MSDOS_RESOURCE_FORMAT.md` / `docs/APPLEII_RESOURCE_FORMAT.md` /
|
|
||||||
`docs/README.md`) — практические заметки/сверки; при расхождении источник
|
|
||||||
истины — спецификация. Формат уровня почти идентичен в Apple II и DOS.
|
|
||||||
|
|
||||||
Упаковка ассетов под Sprinter (атласы `.atl`, палитра) — python-скрипты в
|
|
||||||
`toolchain/` (`render_room.py`, `pop_pack_bg.py`, `pop_pack_kid.py`,
|
|
||||||
`pop_extract_kid_data.py`). Их дёргают Makefile'ы тестов.
|
|
||||||
|
|
||||||
## Структура папки
|
|
||||||
|
|
||||||
- `docs/` — планы и форматы; **индекс с отметками актуальности —
|
|
||||||
`docs/README.md`**, начинать чтение оттуда. Ключевое:
|
|
||||||
`levels_plan.md` (следующий этап), `layout_plan_v2.md` (раскладка кода по
|
|
||||||
окнам/банкам + скорость отрисовки), `PORT_PLAN.md` (карта фаз со
|
|
||||||
статусами).
|
|
||||||
- `roomtest/` — **активная разработка**: уровень 1 целиком (Kid, стражи,
|
|
||||||
ловушки, ворота, loose-полы). Свой `CLAUDE.md`; текущие задачи —
|
|
||||||
`roomtest/TASKS_OPEN.md` (закрытые с протоколами —
|
|
||||||
`roomtest/TASKS_CLOSED.md`), открытые баги — `roomtest/BUGS_OPEN.md`,
|
|
||||||
закрытые с разбором корней — `roomtest/BUGS_CLOSED.md`.
|
|
||||||
- `poc/` — ранний proof-of-concept (снег/атлас/kbd_raw); ассеты в `poc/res/`.
|
|
||||||
- `bgtest/`, `coltest/` — отдельные проверки фона/коллизии.
|
|
||||||
- `toolchain/` — python-упаковщики ассетов + эталонные PNG (`1.1-2.png`).
|
|
||||||
- `SDLPoP/`, `PR/`, `Prince-of-Persia-Apple-II/`, `mininim/`, `MSDOS/` —
|
|
||||||
референсы/оригинальные данные (см. выше).
|
|
||||||
|
|
||||||
## Ключевые архитектурные решения (memory/)
|
|
||||||
|
|
||||||
- `pop_port_project` — общий статус порта.
|
|
||||||
- `pop_banking_architecture` — будущее: big+BANK_W1, графику нельзя в W3,
|
|
||||||
один файл = один банк = прямые вызовы, main резидентен.
|
|
||||||
- `pop_background_strategy` — фон = композиция тайлов в рантайме (вариант 3).
|
|
||||||
- `pop_kid_plan` / `pop_hang_state` / `pop_fore_layer` /
|
|
||||||
`pop_fall_debug_baseline` — этапы Kid.
|
|
||||||
- `kbd_raw_fifo_drain` — held-state клавиатуры (единственный принципиальный
|
|
||||||
пробел движка, закрыт `<kbd_raw.h>`): вычерпывать FIFO SIO циклом.
|
|
||||||
- `png_strip_padding_tradeoff`, `pop_tile_atlas_palette_merge` — квирки
|
|
||||||
упаковки ассетов.
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# Prince of Persia на ZX Sprinter
|
|
||||||
|
|
||||||
Порт Prince of Persia на компьютер Sprinter Sp2000 поверх нашего
|
|
||||||
target-слоя SDCC (sprinter-cc / libc / libbgi). Цель — 320×256×256 (режим
|
|
||||||
0x81), VGA-256 ассеты оригинала переносятся почти впрямую.
|
|
||||||
|
|
||||||
**Состояние (2026-08-01): играется весь уровень 1** — комнаты и переходы,
|
|
||||||
Kid со всем набором действий, ловушки, ворота, дверь уровня, меч и бой,
|
|
||||||
стражи с ИИ, HP и зелья. Нет: перехода на следующий уровень, звука,
|
|
||||||
таймера/HUD, сохранений.
|
|
||||||
|
|
||||||
- Что в работе прямо сейчас — [`roomtest/TASKS_OPEN.md`](roomtest/TASKS_OPEN.md).
|
|
||||||
- Следующий этап (уровни 2+) — [`docs/levels_plan.md`](docs/levels_plan.md).
|
|
||||||
- Общий план и статус фаз — [`docs/PORT_PLAN.md`](docs/PORT_PLAN.md).
|
|
||||||
- Правила работы для ИИ-сессий — [`CLAUDE.md`](CLAUDE.md).
|
|
||||||
|
|
||||||
## Что где
|
|
||||||
|
|
||||||
| Папка | Назначение |
|
|
||||||
|-------|-----------|
|
|
||||||
| `roomtest/` | **Активная разработка.** Уровень 1 целиком: фон композицией тайлов, Kid (seqtbl-анимация, ввод, коллизия, падение, зацеп, окклюзия), ловушки, ворота, стражи, бой. Свой README/CLAUDE/TASKS. |
|
|
||||||
| `docs/` | Планы и разбор форматов ресурсов Apple II / DOS — см. индекс в [`docs/README.md`](docs/README.md). |
|
|
||||||
| `toolchain/` | Python-упаковщики ассетов под Sprinter (рендер комнат, атласы тайлов/Kid, извлечение данных анимации) + эталонные скриншоты. |
|
|
||||||
| `poc/` | Ранний proof-of-concept (снег, атлас, raw-клавиатура). Ассеты в `poc/res/`. |
|
|
||||||
| `bgtest/`, `coltest/` | Точечные проверки фона и коллизии. |
|
|
||||||
| `SDLPoP/`, `PR/`, `mininim/` | Референсные реализации движка (GPL) — читаем логику/константы, НЕ копируем код. `SDLPoP/data/` — источник распакованных VGA-ассетов. |
|
|
||||||
| `Prince-of-Persia-Apple-II/` | Оригинальный 6502-исходник 1989 г. |
|
|
||||||
| `MSDOS/` | Локальные `.DAT`-ресурсы DOS-версии. |
|
|
||||||
|
|
||||||
## Референсы = только справочник
|
|
||||||
|
|
||||||
`SDLPoP/`, `PR/`, `mininim/`, `Prince-of-Persia-Apple-II/` используются как
|
|
||||||
справочник структур/логики и как источник готовых ассетов — их код НЕ
|
|
||||||
копируется в наш порт (лицензии несовместимы, ABI другой). Любая механика
|
|
||||||
сверяется с `SDLPoP/src/` **до** реализации.
|
|
||||||
|
|
||||||
## Форматы ресурсов
|
|
||||||
|
|
||||||
Формат уровня почти идентичен в Apple II и DOS (2304 / 2305 байт,
|
|
||||||
`blueprnt`). Графика различается принципиально, но брать распакованные PNG
|
|
||||||
из `SDLPoP/data/` практичнее, чем декодировать сырой `.DAT`. Подробности —
|
|
||||||
[`docs/README.md`](docs/README.md).
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
# bgtest — мини-тест атласов статического фона PoP (Шаг 2, до порта room.c).
|
|
||||||
# Проверяет загрузку .atl + палитру + прямую адресацию + прозрачность.
|
|
||||||
#
|
|
||||||
# --memory huge (как poc): atlas_load/gfx_blit трогают W3; huge кладёт
|
|
||||||
# CODE в W1, DATA/BSS в W2 — без банков. --gfx 256 подлинкует bgi256.
|
|
||||||
#
|
|
||||||
# Данные фона генерит toolchain/pop_pack_bg.py в ../poc/res/bg/.
|
|
||||||
|
|
||||||
PROJ_ROOT := $(abspath $(CURDIR)/../../..)
|
|
||||||
EXAMPLE := bgtest
|
|
||||||
MEMORY ?= huge
|
|
||||||
EXTRA_FLAGS ?= --gfx 256
|
|
||||||
|
|
||||||
BG_DIR := $(CURDIR)/../poc/res/bg
|
|
||||||
BG_DATA := $(BG_DIR)/pop_env0.atl $(BG_DIR)/pop_env1.atl $(BG_DIR)/pop_env2.atl \
|
|
||||||
$(BG_DIR)/pop_env3.atl $(BG_DIR)/pop_env4.atl \
|
|
||||||
$(BG_DIR)/pop_wall.atl $(BG_DIR)/pop_fore.atl $(BG_DIR)/pop_bg.pal
|
|
||||||
|
|
||||||
EXTRA_DATA := $(BG_DATA)
|
|
||||||
include $(PROJ_ROOT)/app.mk
|
|
||||||
|
|
||||||
# Ассеты фона: пересобрать пакером, если исходники поменялись.
|
|
||||||
$(BG_DATA): $(PROJ_ROOT)/applications/PoP/toolchain/pop_pack_bg.py \
|
|
||||||
$(PROJ_ROOT)/applications/PoP/toolchain/render_room.py
|
|
||||||
cd $(PROJ_ROOT)/applications/PoP/toolchain && python3 pop_pack_bg.py
|
|
||||||
|
|
||||||
$(EXAMPLE).exe: $(BG_DATA)
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
/*
|
|
||||||
* bgtest.c — мини-тест атласов статического фона PoP (Шаг 2 порта, ДО
|
|
||||||
* порта room.c). Проверяет на MAME: загрузку 7 .atl, палитру pop_bg.pal,
|
|
||||||
* ПРЯМУЮ адресацию (id -> страница/idx без remap-таблиц) и прозрачность
|
|
||||||
* (индекс 0xFF). Рисует сетку репрезентативных спрайтов на цветной
|
|
||||||
* заливке — прозрачные области должны показать фон.
|
|
||||||
*
|
|
||||||
* Раскладка из toolchain/pop_pack_bg.py (см. pop_bg_atlas.h):
|
|
||||||
* ENV фон id N -> env[N>>5], idx N&31 ; WALL/FORE idx = id.
|
|
||||||
*/
|
|
||||||
#include <graphics.h>
|
|
||||||
#include <gfx.h>
|
|
||||||
#include <sprite.h>
|
|
||||||
#include <conio.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
|
|
||||||
/* Имена файлов — плоская ФС диска (make_disk кладёт по basename). */
|
|
||||||
static const char *const ENV_ATL[5] = {
|
|
||||||
"pop_env0.atl", "pop_env1.atl", "pop_env2.atl",
|
|
||||||
"pop_env3.atl", "pop_env4.atl"
|
|
||||||
};
|
|
||||||
|
|
||||||
static atlas_t env[5];
|
|
||||||
static atlas_t wall_a;
|
|
||||||
static atlas_t fore_a;
|
|
||||||
|
|
||||||
/* Блит одного спрайта ленты idx атласа a в (x,y): страница атласа в W0,
|
|
||||||
* gfx_blit читает w/h из getimage-заголовка ленты (в W0). */
|
|
||||||
static void put(atlas_t *a, unsigned char idx, int x, int y)
|
|
||||||
{
|
|
||||||
const void *img = atlas_image(a, idx);
|
|
||||||
gfx_w0_map(a->page);
|
|
||||||
gfx_blit(x, y, img);
|
|
||||||
gfx_w0_unmap();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void put_env(unsigned char id, int x, int y)
|
|
||||||
{
|
|
||||||
put(&env[id >> 5], (unsigned char)(id & 31), x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
int i;
|
|
||||||
|
|
||||||
/* Загрузка всех 7 атласов (env0..4 + wall + fore). */
|
|
||||||
for (i = 0; i < 5; i++) {
|
|
||||||
if (atlas_load(&env[i], ENV_ATL[i]) != 0) {
|
|
||||||
printf("atlas_load %s failed\n", ENV_ATL[i]);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (atlas_load(&wall_a, "pop_wall.atl") != 0) { puts("wall atl fail"); return 1; }
|
|
||||||
if (atlas_load(&fore_a, "pop_fore.atl") != 0) { puts("fore atl fail"); return 1; }
|
|
||||||
|
|
||||||
initgraph();
|
|
||||||
gfx_set_draw_page(0);
|
|
||||||
gfx_set_visible_page(0);
|
|
||||||
if (gfx_pal_fload(0, "pop_bg.pal") < 0)
|
|
||||||
gfx_pal_fload(0, "a:\\pop_bg.pal");
|
|
||||||
/* Свой яркий фон-индекс (вне 0x50..0x6F, занятых графикой) — чтобы
|
|
||||||
* прозрачные (0xFF) области спрайтов были ЯВНО видны. */
|
|
||||||
#define BG_IDX 0x20
|
|
||||||
gfx_pal_set(0, BG_IDX, 120, 0, 90); /* r,g,b — тёмно-пурпурный */
|
|
||||||
gfx_pal_sync();
|
|
||||||
|
|
||||||
setfillstyle(SOLID_FILL, BG_IDX);
|
|
||||||
bar(0, 0, 319, 255);
|
|
||||||
setcolor(WHITE);
|
|
||||||
outtextxy(2, 2, "PoP bg atlas test");
|
|
||||||
|
|
||||||
/* Прозрачный блит: банк не пишет 0xFF (фон проступает). */
|
|
||||||
gfx_set_bank(GFX_BANK_TRANSPARENT);
|
|
||||||
|
|
||||||
/* --- Стены (chtab_7): нижние грани 7/9/5/3, основные 8/10/6/4 --- */
|
|
||||||
put(&wall_a, 3, 4, 20); put(&wall_a, 5, 40, 20);
|
|
||||||
put(&wall_a, 7, 76, 20); put(&wall_a, 9, 112, 20);
|
|
||||||
put(&wall_a, 4, 4, 90); put(&wall_a, 6, 40, 90);
|
|
||||||
put(&wall_a, 8, 76, 90); put(&wall_a, 10, 112, 90);
|
|
||||||
/* декали-марки стен 14..17 */
|
|
||||||
put(&wall_a, 14, 150, 20); put(&wall_a, 15, 168, 20);
|
|
||||||
put(&wall_a, 16, 186, 20); put(&wall_a, 17, 204, 20);
|
|
||||||
|
|
||||||
/* --- Столб: база 92, боковая грань 93, фронт 95 (fore) --- */
|
|
||||||
put_env(92, 4, 160);
|
|
||||||
put_env(93, 40, 160);
|
|
||||||
put(&fore_a, 95, 76, 160);
|
|
||||||
|
|
||||||
/* --- Пол: база 41, правый треуг. 42, низ 43; силуэт 44/45 --- */
|
|
||||||
put_env(41, 150, 120);
|
|
||||||
put_env(42, 190, 120);
|
|
||||||
put_env(44, 230, 120);
|
|
||||||
put_env(45, 260, 120);
|
|
||||||
|
|
||||||
/* --- Решётка-окно 126, дебрис 97/98, слайсы ворот 52/53 --- */
|
|
||||||
put_env(126, 150, 160);
|
|
||||||
put_env(97, 190, 160);
|
|
||||||
put_env(52, 230, 160);
|
|
||||||
put_env(53, 250, 160);
|
|
||||||
|
|
||||||
gfx_set_bank(GFX_BANK_NORMAL);
|
|
||||||
/* Держим картинку на экране до нажатия (не полагаемся на блокирующий
|
|
||||||
* getch — в автотесте клавиш нет, крутимся до таймаута MAME). */
|
|
||||||
while (!kbhit())
|
|
||||||
gfx_wait_vsync();
|
|
||||||
|
|
||||||
closegraph();
|
|
||||||
for (i = 0; i < 5; i++) atlas_free(&env[i]);
|
|
||||||
atlas_free(&wall_a);
|
|
||||||
atlas_free(&fore_a);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
# coltest — прототип вертикального accel-copy (gfx_blit_cols) + флип.
|
|
||||||
PROJ_ROOT := $(abspath $(CURDIR)/../../..)
|
|
||||||
EXAMPLE := coltest
|
|
||||||
MEMORY ?= huge
|
|
||||||
EXTRA_FLAGS ?= --gfx 256
|
|
||||||
include $(PROJ_ROOT)/app.mk
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
/*
|
|
||||||
* coltest.c — прототип K2a: проверка вертикального accel-COPY
|
|
||||||
* (gfx_blit_cols) + прозрачности 0xFF + горизонтального флипа.
|
|
||||||
*
|
|
||||||
* Спрайт 16x24 column-major, АСИММЕТРИЧНЫЙ:
|
|
||||||
* левые 8 колонок: верх (row<12) = ПРОЗРАЧНО (0xFF), низ = БЕЛЫЙ (1)
|
|
||||||
* правые 8 колонок: КРАСНЫЙ (2)
|
|
||||||
* На синем фоне (3). Ожидаем на MAME:
|
|
||||||
* normal @ (50,100): слева бело-снизу+прозрачно-сверху, справа красный;
|
|
||||||
* flip @(120,100): ЗЕРКАЛО — слева красный, справа бело+прозрачно-сверху;
|
|
||||||
* в прозрачных местах виден СИНИЙ фон.
|
|
||||||
*/
|
|
||||||
#include <graphics.h>
|
|
||||||
#include <gfx.h>
|
|
||||||
#include <conio.h>
|
|
||||||
|
|
||||||
#define W 16
|
|
||||||
#define H 24
|
|
||||||
static unsigned char spr[4 + W * H];
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
int col, row;
|
|
||||||
|
|
||||||
spr[0] = W; spr[1] = 0;
|
|
||||||
spr[2] = H; spr[3] = 0;
|
|
||||||
for (col = 0; col < W; col++)
|
|
||||||
for (row = 0; row < H; row++) {
|
|
||||||
/* ФИНАЛ: АСИММЕТРИЧНЫЙ — лево верх прозрачно / низ белый,
|
|
||||||
* право красное. normal: лево бело+прозрач-верх, право красн;
|
|
||||||
* flip: ЗЕРКАЛО (лево красн, право бело+прозрач-верх). */
|
|
||||||
unsigned char v;
|
|
||||||
if (col < 8)
|
|
||||||
v = (row < 12) ? 0xFF : 1;
|
|
||||||
else
|
|
||||||
v = 2;
|
|
||||||
spr[4 + col * H + row] = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
initgraph();
|
|
||||||
gfx_set_draw_page(0);
|
|
||||||
gfx_set_visible_page(0);
|
|
||||||
gfx_pal_set(0, 1, 255, 255, 255); /* белый */
|
|
||||||
gfx_pal_set(0, 2, 224, 32, 32); /* красный */
|
|
||||||
gfx_pal_set(0, 3, 32, 48, 200); /* синий фон */
|
|
||||||
gfx_pal_set(0, 255, 0, 224, 0); /* ДИАГ: 0xFF как ЗЕЛЁНЫЙ цвет (bank NORMAL) */
|
|
||||||
gfx_pal_sync();
|
|
||||||
|
|
||||||
setfillstyle(SOLID_FILL, 3);
|
|
||||||
bar(0, 0, 319, 255);
|
|
||||||
setcolor(1);
|
|
||||||
outtextxy(40, 80, "normal");
|
|
||||||
outtextxy(110, 80, "flip");
|
|
||||||
|
|
||||||
gfx_set_bank(GFX_BANK_TRANSPARENT); /* ДИАГ: 0x58 подавление 0xFF (с тенью) */
|
|
||||||
gfx_blit_cols(50, 100, spr, 0); /* обычный */
|
|
||||||
gfx_blit_cols(120, 100, spr, 1); /* зеркало */
|
|
||||||
gfx_set_bank(GFX_BANK_NORMAL);
|
|
||||||
|
|
||||||
while (!kbhit())
|
|
||||||
gfx_wait_vsync();
|
|
||||||
closegraph();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -1,283 +0,0 @@
|
|||||||
# Формат ресурсов Prince of Persia (Apple II, оригинальные исходники 1989)
|
|
||||||
|
|
||||||
Источник — официально опубликованные Джорданом Мехнером исходники
|
|
||||||
(`Prince-of-Persia-Apple-II/`, 6502-ассемблер). В отличие от DOS-версии, здесь
|
|
||||||
формат восстановлен **напрямую по коду**, а не по догадкам о байтах —
|
|
||||||
уверенность высокая везде, где указана ссылка на конкретный файл/строки.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Формат уровня (`01 POP Source/Levels/LEVEL0`…`LEVEL14`, 2304 байта)
|
|
||||||
|
|
||||||
Файлы уровня — это побайтовый дамп структуры `blueprnt`, которая грузится по
|
|
||||||
фиксированному адресу `$b700` (`EQ.S:28`) и объявлена как `dum blueprnt` в
|
|
||||||
`EQ.S:258-266`. Никакого отдельного заголовка файла нет — это чистый образ
|
|
||||||
структуры в памяти:
|
|
||||||
|
|
||||||
| Поле | Размер | Смещение в файле | Описание |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `BLUETYPE` | 720 Б | 0–719 | 24 экрана × 30 тайлов: тип объекта/тайла |
|
|
||||||
| `BLUESPEC` | 720 Б | 720–1439 | 24 экрана × 30 тайлов: доп. байт состояния объекта |
|
|
||||||
| `LINKLOC` | 256 Б | 1440–1695 | Таблица связей нажимных плит/дверей, часть 1 |
|
|
||||||
| `LINKMAP` | 256 Б | 1696–1951 | Таблица связей, часть 2 |
|
|
||||||
| `MAP` | 96 Б | 1952–2047 | 24 экрана × 4 байта: граф соседних экранов |
|
|
||||||
| `INFO` | 256 Б | 2048–2303 | Метаданные уровня: старт Кида, стражников и т.д. |
|
|
||||||
|
|
||||||
Сумма: 720+720+256+256+96+256 = **2304** — точно совпадает с размером файла,
|
|
||||||
что подтверждает: это чистый дамп структуры, без обёртки.
|
|
||||||
|
|
||||||
### 1.1 Сетка тайлов (`BLUETYPE` / `BLUESPEC`)
|
|
||||||
|
|
||||||
Каждый экран — ровно **30 тайлов** (10 столбцов × 3 ряда): подтверждено
|
|
||||||
таблицами `BlockTable`/`BlockEdge` (`TABLES.S:74-154`) и логикой перехода
|
|
||||||
между экранами в `CTRLSUBS.S:218-234` (при переходе через край экрана
|
|
||||||
`tempblockx` меняется на ±10, `tempblocky` — на ±3).
|
|
||||||
|
|
||||||
Функция `CALCBLUE` (`GRAFIX.S:1757-1784`) вычисляет для экрана 1–24:
|
|
||||||
`BlueType = blueprnt + (screen-1)*30`, `BlueSpec = BlueType + 24*30`,
|
|
||||||
используя таблицу `Mult30` (`TABLES.S:131-140`).
|
|
||||||
|
|
||||||
Байт `BLUETYPE` упакован битовыми полями (`EQ.S:484-486`):
|
|
||||||
|
|
||||||
```
|
|
||||||
бит 7-6: secmask (%11000000) — назначение не установлено по доступному коду
|
|
||||||
(возможно, служебное поле редактора)
|
|
||||||
бит 5: reqmask (%00100000) — флаг "необходимая опорная плитка"
|
|
||||||
(проверяется в BREAKLOOSE, MOVER.S:395-397)
|
|
||||||
бит 4-0: idmask (%00011111) — тип тайла/объекта, 0-29
|
|
||||||
```
|
|
||||||
|
|
||||||
Перечень 30 типов объектов (`MOVEDATA.S:8-37`):
|
|
||||||
|
|
||||||
```
|
|
||||||
0 space 8 pillarbottom 16 exit 24 window2
|
|
||||||
1 floor 9 pillartop 17 exit2 25 archbot
|
|
||||||
2 spikes 10 flask 18 slicer 26 archtop1
|
|
||||||
3 posts 11 loose 19 torch 27 archtop2
|
|
||||||
4 gate 12 panelwof 20 block 28 archtop3
|
|
||||||
5 dpressplate 13 mirror 21 bones 29 archtop4
|
|
||||||
6 pressplate 14 rubble 22 sword
|
|
||||||
7 panelwif 15 upressplate 23 window
|
|
||||||
```
|
|
||||||
|
|
||||||
Проверено вручную на дампе начала `LEVEL1` (`00 00 00 21 01 21 21 21 34 34
|
|
||||||
33 33 21 23 00 34 14 14 14 34 14 34 34 2e 23 0b 01 21 34`) — например,
|
|
||||||
`0x33 → id=0x13=19 (torch)`+reqmask, `0x34 → id=20 (block)`+reqmask —
|
|
||||||
декодирование по таблице сходится чисто.
|
|
||||||
|
|
||||||
`BLUESPEC` — доп. байт, чья семантика зависит от типа тайла (единой схемы
|
|
||||||
нет, разбирается объект-специфичным кодом):
|
|
||||||
|
|
||||||
- **gate** (дверь, `FRAMEADV.S:2222-2234`): на диске — маленький enum (1 =
|
|
||||||
начинает открытой сверху, 2 = снизу, …), который через `initsettings`
|
|
||||||
(`FRAMEADV.S:22-23`, диапазон `gminval=0`..`gmaxval=188`, из
|
|
||||||
`MOVEDATA.S:56-57`) при инициализации уровня превращается в живой счётчик
|
|
||||||
"высоты двери" 0–188.
|
|
||||||
- **loose** (шаткая плитка, `FRAMEADV.S:2224-2237`): при инициализации всегда
|
|
||||||
принудительно обнуляется, независимо от значения на диске.
|
|
||||||
- **flask** (зелье, `FRAMEADV.S:2226,2239-2246`): значение×32 выбирает
|
|
||||||
цвет/тип зелья.
|
|
||||||
- **spikes** (шипы, `MOVER.S:365-382`, константы `spikeExt=5, spikeRet=9` в
|
|
||||||
`MOVEDATA.S:45-46`): 0 = безопасно/убраны, 1–8 — кадр анимации
|
|
||||||
выдвижения/втягивания, `$FF` = навсегда заклинило (тело наколото).
|
|
||||||
- **pressplate/upressplate** (нажимные плиты, `MOVER.S:425-464`,
|
|
||||||
`FRAMEADV.S:2059-2098`): значение — это **индекс в цепочке связей**
|
|
||||||
`LINKLOC`/`LINKMAP` (см. ниже); младшие 5 бит `LINKMAP` по этому индексу
|
|
||||||
одновременно служат счётчиком таймера плиты (0–31), определяющим
|
|
||||||
состояние "поднято/опущено".
|
|
||||||
|
|
||||||
### 1.2 `LINKLOC` / `LINKMAP` — граф триггеров (нажимные плиты → двери и т.п.)
|
|
||||||
|
|
||||||
Два параллельных массива по 256 байт кодируют цепочки "нажатие плиты X →
|
|
||||||
сработать объект на экране S, блок B". Восстановлено из `MOVER.S:506-537`
|
|
||||||
(цикл `trigger`) и `MOVER.S:1549-1581` (`gettimer/chgtimer/getloc/
|
|
||||||
getlastflag/getscrn`):
|
|
||||||
|
|
||||||
```
|
|
||||||
LINKLOC[i]: бит 7 = флаг "последнее звено цепочки"
|
|
||||||
биты 6-5 = младшие 2 бита номера целевого экрана
|
|
||||||
биты 4-0 = номер целевого блока (0-29); $FF = "никуда не привязано"
|
|
||||||
|
|
||||||
LINKMAP[i]: биты 7-5 = старшие 3 бита номера целевого экрана
|
|
||||||
(вместе с LINKLOC биты 6-5 → полный номер экрана 0-31)
|
|
||||||
биты 4-0 = таймер обратного отсчёта плиты (0-31, значим только
|
|
||||||
по индексу самой плиты)
|
|
||||||
```
|
|
||||||
|
|
||||||
`BLUESPEC` плиты хранит индекс `i` её *первого* звена; `getlastflag` идёт
|
|
||||||
вперёд (`inc linkindex`), пока не встретит бит 7 в `LINKLOC`. Сверено на
|
|
||||||
`LEVEL1`: байт по смещению 1440 (`0x89 = 10001001` → флаг конца цепочки,
|
|
||||||
целевой блок 9) и параллельно байт по смещению 1696 (`0x60 = 01100000` →
|
|
||||||
старшие биты номера экрана) — согласуется с этой раскладкой. Заполнены
|
|
||||||
реально используемые уровнем звенья, остальное — "мусорные" повторяющиеся
|
|
||||||
байты-заполнители.
|
|
||||||
|
|
||||||
### 1.3 `MAP` — граф соседних экранов
|
|
||||||
|
|
||||||
24 записи × 4 байта = 96 байт: для каждого экрана (1–24)
|
|
||||||
`MAP[(scrn-1)*4 + 0..3] = левый, правый, верхний, нижний соседние экраны`,
|
|
||||||
читается через `GETLEFT/GETRIGHT/GETUP/GETDOWN` (`CTRLSUBS.S:244-274`,
|
|
||||||
индексация `MAP-4..MAP-1,x` при `x = scrn*4`). Экран `0` зарезервирован как
|
|
||||||
"нет экрана" (проверка `beq ]rts` в этих же процедурах).
|
|
||||||
|
|
||||||
### 1.4 `INFO` — метаданные уровня (256 байт, база = смещение файла 2048)
|
|
||||||
|
|
||||||
Объявлено как `dum INFO` в `EQ.S:272-288`:
|
|
||||||
|
|
||||||
| Смещение (от начала INFO) | Поле | Размер |
|
|
||||||
|---|---|---|
|
|
||||||
| 0 | "число экранов + 1" (используется в `SETINITIALS`, `SUBS.S:1441-1445`) | 1 |
|
|
||||||
| 1–63 | резерв/не используется | 63 |
|
|
||||||
| 64 | `KidStartScrn` | 1 |
|
|
||||||
| 65 | `KidStartBlock` | 1 |
|
|
||||||
| 66 | `KidStartFace` (направление; при загрузке инвертируется XOR `$ff`, `SUBS.S:1516-1518`) | 1 |
|
|
||||||
| 67 | заполнитель | 1 |
|
|
||||||
| 68 | `SwStartScrn` (стартовый экран меча) | 1 |
|
|
||||||
| 69 | `SwStartBlock` | 1 |
|
|
||||||
| 70 | заполнитель | 1 |
|
|
||||||
| 71–94 | `GdStartBlock[1..24]` — стартовый блок стражника на экране; **≥30 = "стражника нет"** (`AUTO.S:1832-1834`, `SUBS.S:1677-1679`) | 24 |
|
|
||||||
| 95–118 | `GdStartFace[1..24]` (86 = "стражника нет", см. `ShadFace cmp #86` по всему `AUTO.S`) | 24 |
|
|
||||||
| 119–142 | `GdStartX[1..24]` — пересчитывается заново из блока при старте уровня, значение на диске почти не используется (`SUBS.S:1674-1690`) | 24 |
|
|
||||||
| 143–166 | `GdStartSeqL[1..24]` | 24 |
|
|
||||||
| 167–190 | `GdStartProg[1..24]` — "программа"/поведение ИИ стражника | 24 |
|
|
||||||
| 191–214 | `GdStartSeqH[1..24]` — обнуляется при старте (`SUBS.S:1685-1686`) | 24 |
|
|
||||||
| 215–255 | резерв/не используется | 41 |
|
|
||||||
|
|
||||||
Проверено на `LEVEL1`: байт по смещению файла 0x800 = `0x18`=24 (число
|
|
||||||
активных экранов = 23+1); по смещению 0x840 — `01 00 ff 00 00 00 ff 1e 1e
|
|
||||||
11 1e 1e ...` → `KidStartScrn=1, KidStartBlock=0, KidStartFace=$FF,
|
|
||||||
SwStartScrn=0, SwStartBlock=0`, далее 24 байта `GdStartBlock`, в основном
|
|
||||||
`0x1e`(30, "нет стражника"), с реальной расстановкой только на экране 3
|
|
||||||
(`0x11`=17) и экране 23 (`0x06`) — согласуется с уровнем, где всего два
|
|
||||||
стражника.
|
|
||||||
|
|
||||||
### 1.5 Как уровень попадает с диска (важно: имя файла — не игровой механизм)
|
|
||||||
|
|
||||||
В рантайме нет чтения "по имени файла LEVELn" — это чисто утилита для
|
|
||||||
экспорта в этом репозитории. Реально `LOADLEVELX` (`MISC.S:795-809`)
|
|
||||||
использует фиксированные таблицы по номеру уровня `bluepTRKlst`/
|
|
||||||
`bluepREGlst` (`MISC.S:776-787`), дающие физическую **дорожку (1-33)** и
|
|
||||||
**регион (0/1)**, затем `rdbluep` (`MASTER.S:598-616`) вызывает
|
|
||||||
низкоуровневое чтение `rw18` (`RdGrpErr`) 9 физических групп по 256 байт
|
|
||||||
(`$b7-$bf`) — 9×256=2304 байта — прямо в буфер blueprint; регион 0/1 выбирает
|
|
||||||
половину 18-секторной дорожки (два уровня делят одну дорожку). Файлы
|
|
||||||
`LEVELn` в этом репозитории — реконструкция этого сырого блока для удобства
|
|
||||||
работы с инструментами.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Формат изображений/спрайтов (`IMG.CHTAB1-7`, `IMG.BGTAB1/2.DUN/.PAL`)
|
|
||||||
|
|
||||||
Каждый такой файл грузится целиком по **фиксированному адресу**, заданному
|
|
||||||
константами `chtableN`/`bgtableN` (`GAMEEQ.S:9-18`):
|
|
||||||
|
|
||||||
```
|
|
||||||
chtable1=$6000 chtable2=$8400 chtable3=$0800 chtable4=$9600
|
|
||||||
chtable5=$a800 chtable6=$6000 chtable7=$9f00
|
|
||||||
bgtable1=$6000 bgtable2=$8400
|
|
||||||
```
|
|
||||||
— то есть смещения внутри файла один-в-один совпадают с адресами в памяти
|
|
||||||
после загрузки.
|
|
||||||
|
|
||||||
### 2.1 Раскладка контейнера
|
|
||||||
|
|
||||||
Восстановлено из заголовка-комментария "Image table format" в `HIRES.S:181-186`,
|
|
||||||
процедуры разрешения указателя `setimage` (`HIRES.S:263-277`) и
|
|
||||||
`GETWIDTH`/`PREPREP` (`HIRES.S:283-339`):
|
|
||||||
|
|
||||||
```
|
|
||||||
Смещение 0 : 1 байт — число изображений в таблице (максимум 127,
|
|
||||||
в образцах встречается 0x7f)
|
|
||||||
Смещение 1..254 : 127 × 2-байтных little-endian указателей
|
|
||||||
(указатель на изображение N — по смещению 1+(N-1)*2,
|
|
||||||
N=1..127) — АБСОЛЮТНЫЕ адреса в адресном пространстве
|
|
||||||
фиксированной загрузки этой таблицы, указывающие на
|
|
||||||
запись данных этого изображения
|
|
||||||
Смещение 255 : заполнитель (таблица указателей занимает ровно 256 байт)
|
|
||||||
Смещение 256 (база+0x100) и далее:
|
|
||||||
последовательно идущие записи данных изображений:
|
|
||||||
байт 0: ширина (в байтах на строку)
|
|
||||||
байт 1: высота (число строк)
|
|
||||||
байты 2..(2+ширина*высота-1): сырые байты пикселей,
|
|
||||||
слева направо, сверху вниз, БЕЗ сжатия
|
|
||||||
```
|
|
||||||
|
|
||||||
Проверено вручную на `IMG.BGTAB1.DUN`: с точной арифметикой индексов из
|
|
||||||
`setimage` (`Y = image*2 - 1`, `HIRES.S:264-267`) первые ~30 записей дают
|
|
||||||
строго возрастающую последовательность указателей `0x6101, 0x6133, 0x6159,
|
|
||||||
0x618b, 0x61c9, 0x61fb, 0x6221, 0x6313, 0x63c9, ...` — указатель
|
|
||||||
изображения #1 приходится ровно на `bgtable1 ($6000) + 0x100`, то есть точно
|
|
||||||
на конец 256-байтной таблицы указателей. Это независимо подтверждает и
|
|
||||||
размер таблицы, и семантику указателей.
|
|
||||||
|
|
||||||
**Важно: сжатия в этом формате нет.** RLE/дельта-упаковка (`SngExpand`/
|
|
||||||
`DblExpand`/`DeltaExpPop`/`DeltaExpWipe` в `01 POP Source/Source/UNPACK.S`)
|
|
||||||
применяется только к полноэкранным изображениям (титры/пролог/катсцены), но
|
|
||||||
не к CHTAB/BGTAB — спрайты и фоновые тайлы хранятся как чистые упакованные
|
|
||||||
байты hi-res/double-hi-res экрана Apple II, без какого-либо RLE или дельты.
|
|
||||||
|
|
||||||
### 2.2 Параметры отрисовки (не часть файла ресурса)
|
|
||||||
|
|
||||||
При выводе спрайта (`LAY`/`FASTLAY`/`PEEL` и т.д., `HIRES.S:658-1740`)
|
|
||||||
используются zero-page параметры `PAGE/XCO/YCO/OFFSET/IMAGE/OPACITY/TABLE/
|
|
||||||
BANK` (описаны в `HIRES.S:155-178`): `OFFSET` (0–6) — горизontальный сдвиг на
|
|
||||||
под-байтовый пиксель, `OPACITY` выбирает режим совмещения (AND/OR/STA/XOR/
|
|
||||||
маска-OR) плюс отдельный бит горизонтального зеркалирования (бит 7). Это
|
|
||||||
чисто рантайм-параметры отрисовки, не хранящиеся в файле ресурса. Точный
|
|
||||||
механизм барабанного сдвига для `OFFSET` (таблицы `HRTABLES.S`/`YLO`/`YHI`)
|
|
||||||
не прослежен до конца — при необходимости требует отдельного анализа.
|
|
||||||
|
|
||||||
### 2.3 Инструмент DRAZ (авторская утилита создания спрайтов)
|
|
||||||
|
|
||||||
В `04 Support/DRAZ` нет исходников самой утилиты DRAZ — только файлы данных
|
|
||||||
(`PAC.*` — позы персонажей, и уже скомпилированные `IMG.*`), поэтому
|
|
||||||
внутренний пайплайн DRAZ (как позы превращаются в CHTAB) напрямую не виден.
|
|
||||||
Формат контейнера выше выведен полностью из кода движка-потребителя, что
|
|
||||||
является надёжным, но косвенным источником.
|
|
||||||
|
|
||||||
Отдельно: в игровой логике списков объектов (`ADDBACK`, `GRAFIX.S:191-214`)
|
|
||||||
встречается **рантайм-упаковка ссылки на фоновую картинку** в один байт: бит
|
|
||||||
7 выбирает `bgtable1` или `bgtable2`, биты 0-6 — номер картинки в таблице
|
|
||||||
(0-63). Это соглашение для внутриигровых списков объектов (`bgIMG` и т.п.), а
|
|
||||||
не свойство самих файлов CHTAB/BGTAB на диске.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. "Главного индекса ресурсов" не существует
|
|
||||||
|
|
||||||
В отличие от DOS-версии (см. `docs/MSDOS_RESOURCE_FORMAT.md`), в рантайм-коде
|
|
||||||
Apple II **нет обобщённого справочника "имя ресурса → расположение на
|
|
||||||
диске"**. Расположение каждого ресурса зашито напрямую как таблицы
|
|
||||||
дорожка/группа-секторов прямо в коде загрузчика:
|
|
||||||
|
|
||||||
- Уровни: `bluepTRKlst`/`bluepREGlst` (`MISC.S:776-787`), используются
|
|
||||||
`LOADLEVELX`/`LOADLEVEL` (`MISC.S:795-809`, `MASTER.S:467-481`).
|
|
||||||
- Альтернативные наборы фонов/персонажей: `bg1trk`/`bg2trk`/`ch4trk`/`ch4off`
|
|
||||||
(`MASTER.S:522-528`).
|
|
||||||
- Массовая загрузка при старте (chtable1-7, bgtable1-2, seqtable и т.д.):
|
|
||||||
прямые вызовы `rw18`/`RdGrp`/`RdSeq` с литеральными hex-списками
|
|
||||||
групп-секторов в `MASTER.S:1250-1360` и `BOOT.S:100-118`.
|
|
||||||
|
|
||||||
Весь дисковый ввод-вывод идёт через нестандартный низкоуровневый драйвер
|
|
||||||
`rw18` (`rw18 = $d000`, `EQ.S:11-12`; папка `02 POP Disk Routines/RW1835`),
|
|
||||||
реализующий нестандартный формат **18 секторов/дорожку** (вместо 16 у
|
|
||||||
стандартного DOS 3.3) — этим объясняется, почему регионы уровня (9×256Б)
|
|
||||||
идут парами на одной физической дорожке. Символические имена
|
|
||||||
`chtableN`/`bgtableN` в `GAMEEQ.S` — ближайший аналог "индекса ресурсов", но
|
|
||||||
они связывают ресурс с **фиксированным адресом в ОЗУ**, а не с положением на
|
|
||||||
диске; связь с диском — отдельная, вручную сопровождаемая таблица,
|
|
||||||
сопоставленная с ресурсом лишь порядком вызовов загрузчика.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Что ещё не восстановлено (открытые вопросы)
|
|
||||||
|
|
||||||
- Точное назначение бит `secmask` (%11000000) в `BLUETYPE` — не встречено
|
|
||||||
использование в доступном игровом коде (возможно, поле только для
|
|
||||||
редактора уровней, не читается движком).
|
|
||||||
- Механизм барабанного сдвига `OFFSET` для суб-байтового позиционирования
|
|
||||||
спрайта по X (`HRTABLES.S`) — не прослежен в деталях.
|
|
||||||
- Внутренний формат авторских файлов `PAC.*` инструмента DRAZ (как позы
|
|
||||||
скелетной анимации превращаются в растровые кадры CHTAB) — исходники DRAZ
|
|
||||||
отсутствуют в репозитории, можно только косвенно восстановить по
|
|
||||||
результату (уже скомпилированным `IMG.*`).
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
# Prince of Persia — Kid (персонаж): анализ и план
|
|
||||||
|
|
||||||
> **Статус: РЕАЛИЗОВАНО (2026-08-01).** Kid играется целиком: интерпретатор
|
|
||||||
> `seqtbl` + `frame_table` (`roomtest/pop_kid.c`), диспетчер `control()`
|
|
||||||
> (`pop_ctrl.c`), коллизия/физика/зацеп (`pop_map.c`), бой и HP. Модель
|
|
||||||
> персонажа стала общей: `Char`-окно (`pop_state.c`) обслуживает и Кида, и
|
|
||||||
> стражей. Таблицы кадров и `seqtbl` уехали из `_CODE` в EMM-страницу
|
|
||||||
> (`kid_data.bin`, см. `layout_plan_v2.md` шаг 1).
|
|
||||||
>
|
|
||||||
> Отступление от §2.1 плана: выбран ПАДДИНГ кадров (общий канвас), а не
|
|
||||||
> per-frame offset — компромисс зафиксирован в `PORT_PLAN.md §6.1`.
|
|
||||||
>
|
|
||||||
> **Документ оставлен как СПРАВОЧНИК по модели персонажа** (`char_type`,
|
|
||||||
> категории `actions_*`, устройство `play_seq`, объём спрайтов) — он нужен
|
|
||||||
> при портировании остальных акторов (скелет, тень, визирь). Текущие
|
|
||||||
> задачи — `../roomtest/TASKS_OPEN.md`.
|
|
||||||
|
|
||||||
Составлен 2026-07-16. Опирается на разбор `SDLPoP/src/seg006.c`
|
|
||||||
(ядро физики/управления Kid), `seqtbl.c` (таблицы последовательностей),
|
|
||||||
`types.h` (char_type, seq_*, SEQ_*, actions_*), `SDLPoP/data/KID` (спрайты).
|
|
||||||
Фон уже готов и проверен на MAME (`applications/PoP/roomtest`, см.
|
|
||||||
`memory/pop_background_strategy`) — Kid развиваем в том же `roomtest` как
|
|
||||||
новый PoC (решение пользователя: старый `poc/` не трогаем).
|
|
||||||
|
|
||||||
**Копирайт:** спрайты Kid (`SDLPoP/data/KID`) — Broderbund/Ubisoft.
|
|
||||||
Использование настоящей графики Kid — сознательное решение пользователя
|
|
||||||
(в отличие от плейсхолдера в старом `poc/`, см. PORT_PLAN §5.1).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Как устроен персонаж в оригинале (что портируем)
|
|
||||||
|
|
||||||
### 1.1 Состояние — `char_type` (14 полей, types.h)
|
|
||||||
|
|
||||||
```
|
|
||||||
frame текущий номер кадра (индекс во frame_table_kid)
|
|
||||||
x, y позиция (byte; x — с учётом direction)
|
|
||||||
direction -1 влево / 0 вправо
|
|
||||||
curr_col, логическая клетка (тайл), где персонаж
|
|
||||||
curr_row
|
|
||||||
action КАТЕГОРИЯ действия (actions_*, см. 1.2)
|
|
||||||
fall_x, скорость падения (fall_y<22 = 1 ряд, <33 = 2 ряда)
|
|
||||||
fall_y
|
|
||||||
room комната
|
|
||||||
repeat счётчик для удержания-ввода (напр. повторный прыжок)
|
|
||||||
sword есть ли меч (бой — вне Фазы 1)
|
|
||||||
alive жив/мёртв
|
|
||||||
curr_seq УКАЗАТЕЛЬ в seqtbl (байткод текущей последовательности)
|
|
||||||
```
|
|
||||||
|
|
||||||
Состояние крошечное — легко живёт в W2.
|
|
||||||
|
|
||||||
### 1.2 Категории действия — `actions_*` (9 шт)
|
|
||||||
|
|
||||||
`0 stand`, `1 run_jump`, `2 hang_climb`, `3 in_midair`, `4 in_freefall`,
|
|
||||||
`5 bumped`, `6 hang_straight`, `7 turn`, `99 hurt`. `action` определяет,
|
|
||||||
как `check_action()`/`play_kid()` реагируют на ввод и физику каждый тик.
|
|
||||||
|
|
||||||
### 1.3 Движок анимации/движения — ГЛАВНОЕ
|
|
||||||
|
|
||||||
**Движение НЕ физика, а байткод + per-frame смещения** (подтверждает
|
|
||||||
PORT_PLAN §6). Три уровня:
|
|
||||||
|
|
||||||
1. **`seqtbl`** — байткод-программа на действие. Опкоды (types.h):
|
|
||||||
`SEQ_DX`(0xFB) сдвиг x на amount×direction, `SEQ_DY`(0xFA) сдвиг y,
|
|
||||||
`SEQ_FLIP`(0xFE) разворот, `SEQ_JMP`(0xFF)/`SEQ_JMP_IF_FEATHER`(0xF7),
|
|
||||||
`SEQ_UP`/`SEQ_DOWN`(0xFD/0xFC) смена ряда, `SEQ_ACTION`(0xF9) задать
|
|
||||||
`Char.action`, `SEQ_SET_FALL`(0xF8), `SEQ_KNOCK_UP/DOWN`, `SEQ_SOUND`,
|
|
||||||
`SEQ_DIE`/`SEQ_END_LEVEL`/`SEQ_GET_ITEM`. **Байт < 0xF0 = НОМЕР КАДРА**
|
|
||||||
→ ставит `Char.frame` и play_seq возвращается (один кадр за тик).
|
|
||||||
2. **`play_seq()`** (seg006.c:570) — интерпретатор: крутит опкоды из
|
|
||||||
`seqtbl + Char.curr_seq`, пока не встретит кадр. ~15 case — портируется
|
|
||||||
1-в-1. **Квирк:** seqtbl использует АБСОЛЮТНЫЕ DOS-адреса в JMP;
|
|
||||||
`SEQTBL_0 = seqtbl - SEQTBL_BASE(0x196E)` — при порте пересчитать
|
|
||||||
базу (JMP-адреса в наших данных).
|
|
||||||
3. **`frame_table_kid[]`** (seg006.c:127, ~180 кадров) — на КАЖДЫЙ кадр:
|
|
||||||
`{image, sword_flags, dx, dy, flags}`. `image` — индекс спрайта Kid;
|
|
||||||
`dx/dy` — смещение позиции ЭТОГО кадра; `flags`: 0x1F weight_x, 0x20
|
|
||||||
thin, 0x40 needs_floor, 0x80 even/odd-pixel (влияет на x-рендер).
|
|
||||||
|
|
||||||
**Тик персонажа:** `play_kid()` (диспетчер по action+вводу) → `play_seq()`
|
|
||||||
(двигает curr_seq, ставит кадр, применяет seq-dx/dy) → `frame_table[frame]`
|
|
||||||
даёт image+собственные dx/dy → позиция и спрайт. У нас это ложится на
|
|
||||||
`sprite_frame`+`sprite_move` (НЕ `sprite_anim`/`sprite_moveto` — см.
|
|
||||||
PORT_PLAN §6: авторские таблицы, не автопрогрессия).
|
|
||||||
|
|
||||||
### 1.4 Управление — `control_kid()`/`read_user_control()` (seg006.c)
|
|
||||||
|
|
||||||
Читает ввод (у нас — held-state `kbd_raw`, уже готово, §2 PORT_PLAN) и по
|
|
||||||
`Char.action` выбирает последовательность (`seqtbl_offset_char(seq_id)`).
|
|
||||||
Логика «что можно из какого состояния» — ядро ощущения PoP.
|
|
||||||
|
|
||||||
### 1.5 Взаимодействие с картой — collision (seg006.c)
|
|
||||||
|
|
||||||
`check_on_floor()`/`start_fall()` — пол под ногами / падение в яму;
|
|
||||||
`in_wall()` — упор в стену (сдвиг наружу); `check_grab()`/
|
|
||||||
`can_grab_front_above()` — зацеп за уступ; `fell_out()` — вывалиться из
|
|
||||||
комнаты; `check_spiked()`/loose — ловушки; `fall_accel()`/`fall_speed()` —
|
|
||||||
ускорение падения. Всё читает ТИП тайла (`get_tile`) — у нас это уже
|
|
||||||
разобранные `fg[]/bg[]` (level.h/room1_data.h).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Спрайты Kid (219 шт, 16 цветов, 177 КБ)
|
|
||||||
|
|
||||||
- 219 PNG (`data/KID`), 16-цветные (палитра `res400.pal`, 16×RGB как env/
|
|
||||||
wall), макс кадр **53×35** — влезает в лимит движка 64×64. 177 КБ в
|
|
||||||
8bpp.
|
|
||||||
- `frame_table_kid` отображает кадр→`image` (индекс спрайта). Число
|
|
||||||
РАЗЛИЧНЫХ image — уточнить (≤219); паковать те, что реально используются
|
|
||||||
платформинг-последовательностями Фазы 1 (не все 219 — бой/катсцены
|
|
||||||
отдельно).
|
|
||||||
- **Палитра:** Kid 16 цветов → слоты Sprinter `0x70-0x7F` (env 0x50, wall
|
|
||||||
0x60 уже заняты; Kid не пересекается). Пиксель i: 0→0xFF, i→0x70+i.
|
|
||||||
Тот же пайплайн, что `pop_pack_bg.py`.
|
|
||||||
- **Атлас:** прямая адресация по номеру image (как фон): `kid[img>>5]`,
|
|
||||||
idx `img&31`; ~7 EMM-страниц (или SHIFT=4). Свой пакер `pop_pack_kid.py`
|
|
||||||
(переиспользовать код `pop_pack_bg.py`).
|
|
||||||
|
|
||||||
### 2.1 РЕШЕНИЕ ДО СТАРТА: per-frame offset vs padding
|
|
||||||
|
|
||||||
Кадры Kid — РАЗНОГО размера, а `sprite_t` рисует от угла фикс. w/h. Два
|
|
||||||
пути (см. PORT_PLAN §6.1, `memory/png_strip_padding_tradeoff`):
|
|
||||||
- **Padding** (bottom-center) — просто, но 219×53×35 ≈ 406 КБ (раздув ×2.3).
|
|
||||||
- **Per-frame offset** — хранить XCO/YCO кадра (у оригинала он и есть,
|
|
||||||
`APPLEII_RESOURCE_FORMAT §2.2`), рисовать `blit(x+xco, y+yco)`; паддинг не
|
|
||||||
нужен, память по факту (177 КБ). Требует лёгкого расширения хранения
|
|
||||||
(offset рядом с кадром) ИЛИ ручного смещения в коде рендера Kid.
|
|
||||||
**Рекомендация:** per-frame offset — оригинал так и делает (frame_table dx/dy
|
|
||||||
+ image XCO/YCO), даёт точное позиционирование И экономию. Хранить xco/yco
|
|
||||||
в нашей копии frame_table (добавить 2 байта/кадр — ~360 Б). Не тянуть
|
|
||||||
расширение `sprite.h` — рисовать Kid прямым `gfx_blit(x+xco, y+yco, img)`
|
|
||||||
(как фон), НЕ через retained `sprite_t`, раз позиция и кадр всё равно
|
|
||||||
задаются вручную каждый тик.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Данные для порта (объём)
|
|
||||||
|
|
||||||
- `frame_table_kid` → C-массив ~180×(5+2 offset) ≈ 1.3 КБ (const, ROM).
|
|
||||||
- `seqtbl` (нужные последовательности) → C-массив байт. Весь seqtbl ~1-2 КБ;
|
|
||||||
для Фазы 1 можно взять только платформинг-последовательности (вырезать
|
|
||||||
бой/гардов 55-92) — оценить после разметки. JMP-адреса пересчитать под
|
|
||||||
свою базу.
|
|
||||||
- Спрайты — атласы (EMM, не W2).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Фазы работы (по твоему списку, порядок по зависимостям)
|
|
||||||
|
|
||||||
**Фаза K0 — конвейер спрайтов + отрисовка одного кадра**
|
|
||||||
- `pop_pack_kid.py`: 219 (или подмножество) → `kid*.atl` + `kid.pal`
|
|
||||||
(слоты 0x70), таблица кадр→image + xco/yco.
|
|
||||||
- Отрисовать Kid ОДНИМ кадром (stand) в roomtest поверх фона на верном
|
|
||||||
тайле — проверить палитру/позицию/прозрачность на MAME.
|
|
||||||
- Артефакт-цель: Kid стоит на уступе комнаты 1 как в `1.1-2.png`.
|
|
||||||
|
|
||||||
**Фаза K1 — движок анимации (play_seq + frame_table)**
|
|
||||||
- Портировать `play_seq()` (интерпретатор) + `frame_table_kid` + минимальный
|
|
||||||
`seqtbl` (stand/run/turn).
|
|
||||||
- Прогнать несколько последовательностей вручную (stand→run→stop) —
|
|
||||||
проверить, что кадры и смещения совпадают с оригиналом (сверять с
|
|
||||||
SDLPoP/скриншотами, тайминг 50 Гц).
|
|
||||||
|
|
||||||
**Фаза K2 — управление на месте + ходьба (твои а, б)**
|
|
||||||
- `control_kid` подмножество: stand (2), run (1/84/13), turn (5/6),
|
|
||||||
standing_jump (3), crouch (50/49), safe_step (29-44 — аккуратный шаг).
|
|
||||||
- Held-state через `kbd_raw` (готово).
|
|
||||||
|
|
||||||
**Фаза K3 — коллизия с картой (твой п.3)**
|
|
||||||
- `check_on_floor`/`start_fall` — падение в ямы (тип тайла под ногами из
|
|
||||||
`fg[]`); `in_wall`/стоп у стены; `fell_out` (край экрана — пока без
|
|
||||||
перехода комнат).
|
|
||||||
- Падения/приземления (seq 7/17/19/20) + `fall_accel/fall_speed`.
|
|
||||||
|
|
||||||
**Фаза K4 — прыжки и повисание (твои а-прыжок, в)**
|
|
||||||
- run_jump (4), jump_up (28/14), grab (8/16/24), climb_up (10)/down (68),
|
|
||||||
hang (25/6), release (11/23). Это самый «PoP-овый» кусок — сверять
|
|
||||||
дистанции/тайминг с оригиналом (не на глаз).
|
|
||||||
|
|
||||||
**Фаза K5 — прочее (твой г)**
|
|
||||||
- drink (78), level_door (70), crouch_hop (79), spiked/loose/chomped
|
|
||||||
(ловушки, если тайлы есть в комнате), death (71).
|
|
||||||
|
|
||||||
Бой (меч, seq 55-92, стражники — seg005) — ВНЕ этого плана (отдельная фаза
|
|
||||||
полного приложения, PORT_PLAN §7 Фаза 3).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Риски/решения ДО кода (правило defer_unexplained_quirks)
|
|
||||||
|
|
||||||
1. **Per-frame offset** (§2.1) — решить до K0 (влияет на формат данных).
|
|
||||||
Рекомендация: xco/yco в frame_table, прямой blit.
|
|
||||||
2. **seqtbl rebasing** — JMP-адреса абсолютные (SEQTBL_BASE 0x196E); при
|
|
||||||
порте пересчитать в оффсеты своего массива. Проверить на 1-2 seq.
|
|
||||||
3. **Тайминг** — оригинал (DOS) фиксированный тик; наш 50 Гц. Если
|
|
||||||
логическая частота кадров иная — пересчёт dx/dy (PORT_PLAN §8.4).
|
|
||||||
Сверять дистанцию бега/прыжка с эталоном.
|
|
||||||
4. **Число реально нужных кадров/последовательностей** для Фазы 1 —
|
|
||||||
разметить (вырезать бой/катсцены/гардов), чтобы не тянуть все 219
|
|
||||||
спрайта и весь seqtbl.
|
|
||||||
5. **Копирайт графики Kid** — подтверждено решение пользователя (§вводная).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Что переиспользуем (готово)
|
|
||||||
|
|
||||||
- Фон комнаты (`pop_bg.c`) — Kid рисуется ПОВЕРХ (сейчас — прямым blit;
|
|
||||||
heal против фона — когда/если понадобится через RAM-копию, фон её уже
|
|
||||||
заполняет, `GFX_BANK_TRANSPARENT`).
|
|
||||||
- `kbd_raw` held-state (§2 PORT_PLAN) — готов и проверен.
|
|
||||||
- Пакер спрайтов/палитра (`pop_pack_bg.py`) — шаблон для `pop_pack_kid.py`.
|
|
||||||
- Разобранная карта комнаты (`fg[]/bg[]`, level.h) — для коллизий.
|
|
||||||
- `gfx_blit`/`gfx_w0_map` из W0-атласа — проверенный путь (bgtest/roomtest).
|
|
||||||
@@ -1,301 +0,0 @@
|
|||||||
# Формат ресурсов Prince of Persia (MS-DOS, каталог `MSDOS/`)
|
|
||||||
|
|
||||||
Документ описывает бинарный формат `*.DAT`-файлов ресурсов DOS-версии PoP.
|
|
||||||
Исходников для этой версии нет, поэтому всё, что ниже — результат
|
|
||||||
структурного (эмпирического) анализа реальных файлов из `MSDOS/`, а не чтения
|
|
||||||
кода. Уровень уверенности указан для каждого раздела. Все находки проверены
|
|
||||||
скриптами (Python), которые разбирают файл и валидируют согласованность
|
|
||||||
(например: смещение+размер последней записи таблицы точно совпадает с
|
|
||||||
началом самой таблицы — то есть данные и каталог стыкуются без дыр).
|
|
||||||
|
|
||||||
**Основной источник спецификации формата — `POP-DAT-FormatSpecifications.pdf`**
|
|
||||||
(и его текстовая конверсия `POP-DAT-FormatSpecifications.txt` в этой же папке,
|
|
||||||
для grep/цитирования): *«Prince of Persia — Specifications of File Formats»*,
|
|
||||||
Princed Development Team, 2008 — каноническая спецификация формата `DAT v1.0`,
|
|
||||||
на которой построен и SDLPoP, и Princed Resources. Разбирает контейнер, индекс,
|
|
||||||
чек-сумму, кодеки изображений (RLE / LZG), палитры, формат уровней (room
|
|
||||||
mapping, wall-drawing, room-linking, guards, start position, door events),
|
|
||||||
звук (digital waves / MIDI / PC speaker), бинарные файлы и Mac-варианты. При
|
|
||||||
любом расхождении между эмпирическими находками ниже и этим документом —
|
|
||||||
источником истины считать спецификацию (сверять §-номера: её §3.x).
|
|
||||||
|
|
||||||
Дополнительно как справка при реализации (порт на ZX Sprinter):
|
|
||||||
|
|
||||||
- **SDLPoP** (github.com/NagyD/SDLPoP, GPLv3) — open-source реализация
|
|
||||||
DOS-версии на основе дизассемблирования оригинального `PRINCE.EXE`. Содержит
|
|
||||||
рабочий код чтения `.DAT`-файлов и полный кодек изображений/уровней. Точные
|
|
||||||
структуры (`dat_table_type` и т.п.), процитированные ниже, получены через
|
|
||||||
автоматический пересказ содержимого файла третьей стороной, а не через
|
|
||||||
прямое чтение исходника — поэтому такие детали помечены как "требует сверки
|
|
||||||
при реализации", в отличие от эмпирически подтверждённых байтовых оффсетов.
|
|
||||||
- **Princed Resources / PR** (github.com/NagyD/PR, princed.org, GPLv2) — это
|
|
||||||
профильный инструмент именно для распаковки/запаковки `.DAT`-ресурсов PoP
|
|
||||||
(версии DAT 1 и 2), сделанный тем же автором. В его документации
|
|
||||||
(`doc/Dataformats.md`) официально описаны экспортные форматы ресурсов —
|
|
||||||
это подтверждает и уточняет часть находок ниже (см. §3–4), и является более
|
|
||||||
надёжным источником, чем самостоятельная догадка по байтам.
|
|
||||||
|
|
||||||
**Важная находка:** репозиторий SDLPoP в папке `data/` содержит не только
|
|
||||||
код движка, но и **реальные ресурсы игры** — как сырые `.DAT`-контейнеры, так
|
|
||||||
и уже распакованные поштучно файлы (PNG-кадры спрайтов, `.pal`-палитры,
|
|
||||||
`.bin`-дампы уровней), см. §7. Это готовый источник ассетов и одновременно
|
|
||||||
независимая проверка формата, описанного в этом документе.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Общий контейнер `.DAT` (уверенность: высокая, подтверждено на 28 файлах)
|
|
||||||
|
|
||||||
Каждый `*.DAT`-файл (кроме служебных `config.dat`/`setup.dat`, см. §5) — это
|
|
||||||
простой архив-контейнер: блок данных + оглавление (каталог ресурсов) в конце
|
|
||||||
файла.
|
|
||||||
|
|
||||||
### 1.1 Заголовок файла (6 байт, смещение 0x00)
|
|
||||||
|
|
||||||
| Смещение | Размер | Поле | Значение |
|
|
||||||
|----------|--------|--------------|----------|
|
|
||||||
| 0x00 | 4 | `tableOffset`| LE u32. Абсолютное смещение в файле, с которого начинается таблица оглавления. Совпадает с "концом данных". |
|
|
||||||
| 0x04 | 2 | `tableSize` | LE u16. Размер таблицы оглавления в байтах. |
|
|
||||||
|
|
||||||
Инвариант, подтверждённый на всех 28 `.dat`-файлах в каталоге:
|
|
||||||
|
|
||||||
```
|
|
||||||
tableOffset + tableSize == размер файла (без исключений)
|
|
||||||
```
|
|
||||||
|
|
||||||
Данные ресурсов идут сразу после заголовка, начиная с байта 0x06, и
|
|
||||||
заканчиваются на `tableOffset`.
|
|
||||||
|
|
||||||
### 1.2 Таблица оглавления (по смещению `tableOffset`, длиной `tableSize`)
|
|
||||||
|
|
||||||
Таблица — плоский массив записей по 8 байт. Количество записей:
|
|
||||||
`tableSize / 8` (округление вниз; в файле почти всегда остаётся 2 "лишних"
|
|
||||||
байта в хвосте таблицы — назначение не установлено, вероятно, служебное поле
|
|
||||||
инструмента-упаковщика или паддинг; на итоговый разбор не влияет).
|
|
||||||
|
|
||||||
Запись (8 байт):
|
|
||||||
|
|
||||||
| Смещение в записи | Размер | Поле | Описание |
|
|
||||||
|---|---|---|---|
|
|
||||||
| 0 | 2 | `size` | LE u16 — размер данных ресурса в байтах |
|
|
||||||
| 2 | 2 | `id` | LE u16 — идентификатор ресурса |
|
|
||||||
| 4 | 2 | `offset` | LE u16 — **абсолютное** смещение данных ресурса в файле (не относительное!) |
|
|
||||||
| 6 | 2 | `reserved` | во всех проверенных записях (сотни штук) всегда `0x0000` |
|
|
||||||
|
|
||||||
Проверено на `levels.dat`: 16 записей, `id`=2000..2015, и `offset[i] + size[i]
|
|
||||||
== offset[i+1]` для всех соседних записей, а последняя запись заканчивается
|
|
||||||
ровно на `tableOffset` — то есть данные абсолютно плотно упакованы, без
|
|
||||||
пробелов, для этого файла. В других файлах (например `guard.dat`) между
|
|
||||||
записями изредка есть небольшие зазоры в несколько байт (вероятно, выравнивание
|
|
||||||
или "мёртвые" байты от инструмента-компоновщика) — не является нарушением
|
|
||||||
формата.
|
|
||||||
|
|
||||||
### 1.3 Диапазоны `id` по типам файлов (собрано эмпирически)
|
|
||||||
|
|
||||||
Похоже, что числовые ID образуют условные "пространства имён" по типу
|
|
||||||
контента — вероятно, глобальные константы в оригинальном коде:
|
|
||||||
|
|
||||||
| Файл(ы) | Диапазон `id` | Кол-во записей | Предполагаемое содержимое |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `levels.dat` | 2000–2015 | 16 | id=2000 — служебный блок (16 байт, см. §3); id=2001..2015 — 15 уровней |
|
|
||||||
| `guard.dat`, `fat.dat`, `skel.dat`, `shadow.dat` | 750–784 (варьируется) | ~30–35 | id=751(750) — служебный блок; остальные — кадры анимации спрайта |
|
|
||||||
| `vizier.dat` | аналогично guard | — | кадры анимации визиря |
|
|
||||||
| `kid.dat` | ~400+ | 220 | кадры анимации игрока (намного больше — герой умеет гораздо больше действий) |
|
|
||||||
| `guard1.dat`, `guard2.dat` | 750 (1 запись) | 1 | вероятно, дополнительные/альтернативные кадры/варианты |
|
|
||||||
| `title.dat` | 40–55 | 12 | картинки титульного экрана/логотипов |
|
|
||||||
| `cpalace.dat`,`epalace.dat`,`vpalace.dat`,`cdungeon.dat`,`edungeon.dat`,`vdungeon.dat` | 200–1343 | 205–238 | фоновые тайлы дворца/подземелья, отдельно для CGA(`c*`)/EGA(`e*`)/VGA(`v*`) |
|
|
||||||
| `pv.dat` | 800–981 | 103 | доп. графика (возможно, "Prince/Vizier" катсцены) |
|
|
||||||
| `digisnd1/2/3.dat` | 10000+ | 20–44 | оцифрованный звук (Covox/Disney Sound Source) |
|
|
||||||
| `midisnd1/2.dat` | 10024+ / аналог | 16 | General MIDI музыка |
|
|
||||||
| `mt32snd1/2.dat` | 10000+ | 24/7 | музыка для Roland MT-32 |
|
|
||||||
| `ibm_snd1/2.dat` | 10000+ | 44 | музыка/эффекты через PC-спикер |
|
|
||||||
| `prince.dat` | — (1 крупный ресурс) | — | MIDI-тема (вероятно, финальная тема "Принц"/титры — см. текстовые события "The Princess awaits") |
|
|
||||||
|
|
||||||
Во всех файлах первая (наименьшая по `id`) запись — маленький "служебный"
|
|
||||||
ресурс (6–44 байта), стоящий перед основным контентом. Скорее всего это
|
|
||||||
локальная мини-таблица/палитра/список ссылок для данного набора ресурсов —
|
|
||||||
по аналогии с тем, что у уровней id=2000 отдельно от самих уровней (см. §3).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Формат уровня (`levels.dat`, id=2001..2015) — уверенность: высокая
|
|
||||||
|
|
||||||
Каждая запись уровня имеет размер **2305 байт** и по данным полностью
|
|
||||||
совпадает по объёму с уровнями из Apple II версии (`01 POP Source/Levels/LEVELn`
|
|
||||||
— ровно **2304 байта** каждый, см. `docs/APPLEII_RESOURCE_FORMAT.md`).
|
|
||||||
|
|
||||||
Вывод: формат карты уровня в DOS-версии, судя по всему, **унаследован
|
|
||||||
практически без изменений от оригинального Apple II формата** (Джордан
|
|
||||||
Мехнер писал игру на 6502 и данные уровней переносились как есть), с добавлением
|
|
||||||
одного лишнего байта в DOS-упаковке (2304+1=2305 — вероятно, контрольный байт/
|
|
||||||
маркер конца, добавленный DOS-упаковщиком ресурсов, а не часть игровых данных).
|
|
||||||
|
|
||||||
**Практическое следствие:** байтовая структура самого уровня (тайлы 3×10 на
|
|
||||||
экран, 24 экрана, таблицы стражников, дверей и т.д.) должна документироваться
|
|
||||||
один раз — по исходникам Apple II (см. соответствующий раздел), и напрямую
|
|
||||||
применяться к DOS `levels.dat`, отбросив 1 лишний байт в конце каждой записи.
|
|
||||||
Байтовые значения тайлов в дампе (в основном 0x00–0x39) визуально согласуются
|
|
||||||
с диапазоном небольших целых кодов тайлов, что для формата карты и ожидается.
|
|
||||||
|
|
||||||
Первая запись, id=2000, размер 16 байт — не уровень, а отдельный маленький
|
|
||||||
блок (возможно: количество уровней, начальный уровень, версия формата,
|
|
||||||
стартовые координаты игрока/охраны по умолчанию). Точное назначение не
|
|
||||||
установлено — требует сопоставления с дизассемблированным кодом загрузчика
|
|
||||||
уровней (в SDLPoP это, по всем признакам, отдельная процедура чтения
|
|
||||||
`level` ресурса).
|
|
||||||
|
|
||||||
**Сверка с независимой распаковкой SDLPoP (`data/LEVELS/`):** там лежат файлы
|
|
||||||
`res2000.bin`…`res2015.bin` (16 штук — количество совпадает). Байты
|
|
||||||
`res2001.bin` содержательно совпадают с тайловыми данными нашей записи
|
|
||||||
id=2001 (та же последовательность значений тайлов) — это подтверждает, что
|
|
||||||
нумерация id верна. Но есть нестыковка по размеру: у SDLPoP `res2000.bin` —
|
|
||||||
**2305 байт** (как и все остальные), тогда как в нашем локальном
|
|
||||||
`levels.dat` запись id=2000 — всего **16 байт**. Скорее всего, это разные
|
|
||||||
релизы/сборки игры (см. §7 — размеры некоторых `.dat` у SDLPoP и у нас уже
|
|
||||||
отличались), и в версии SDLPoP маленький служебный блок либо отсутствует,
|
|
||||||
либо пронумерован иначе. Это не меняет сам формат контейнера, но означает,
|
|
||||||
что **точную семантику 16-байтного блока id=2000 в нашей копии игры пока
|
|
||||||
нельзя проверить через данные SDLPoP** — открытый вопрос.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2.1 Кросс-подтверждение по исходникам Apple II
|
|
||||||
|
|
||||||
Фоновый анализ исходников Apple II (см. `docs/APPLEII_RESOURCE_FORMAT.md`)
|
|
||||||
подтверждает и объясняет структуру уровня напрямую по коду. Уровень на Apple
|
|
||||||
II — дамп структуры `blueprnt` (`EQ.S`): `BLUETYPE`(720Б, 24 экрана×30 тайлов)
|
|
||||||
+ `BLUESPEC`(720Б) + `LINKLOC`(256Б) + `LINKMAP`(256Б) + `MAP`(96Б, граф
|
|
||||||
соседних экранов) + `INFO`(256Б, метаданные/старт Кида/стражников) = ровно
|
|
||||||
2304 байта. Учитывая, что DOS-запись уровня — это ровно 2304+1 байт с
|
|
||||||
байтовыми значениями тайлов, укладывающимися в диапазон 0–29 (id тайла) плюс
|
|
||||||
служебные биты (аналогично `idmask=%00011111`, `reqmask=%00100000` из
|
|
||||||
`EQ.S:484-486`), можно с высокой уверенностью считать, что **DOS-версия
|
|
||||||
использует ту же самую раскладку `blueprnt`**, лишь с добавлением одного
|
|
||||||
байта (вероятно, контрольной суммы) в конце DOS-упаковки. Это снимает
|
|
||||||
необходимость отдельно реверсить формат уровня для DOS — таблица тайлов,
|
|
||||||
enum id (0=space...29=archtop4), формат `LINKLOC`/`LINKMAP` и `INFO` из
|
|
||||||
Apple II документа применимы напрямую.
|
|
||||||
|
|
||||||
## 3. Графика (спрайты и фоновые тайлы) — уверенность: средняя/низкая
|
|
||||||
|
|
||||||
Файлы `kid.dat`, `guard.dat`, `fat.dat`, `shadow.dat`, `skel.dat`,
|
|
||||||
`vizier.dat`, `title.dat`, `c/e/v-palace.dat`, `c/e/v-dungeon.dat`, `pv.dat`
|
|
||||||
хранят по контейнерному формату (§1) множество мелких чанков (десятки—сотни
|
|
||||||
байт каждый).
|
|
||||||
|
|
||||||
Что подтверждено:
|
|
||||||
- Наборы `shadow.dat`/`kid.dat` и `fat.dat`/`vizier.dat` содержат **побайтово
|
|
||||||
идентичные фрагменты** данных в начале файла — это ожидаемо: "Тень" (Shadow)
|
|
||||||
визуально копирует анимацию Кида, а "Толстый страж" (Fat guard, пасхалка)
|
|
||||||
переиспользует модель Визиря. Подтверждает, что персонажи одного "типа
|
|
||||||
тела" используют общий набор геометрии/анимации.
|
|
||||||
- Отдельные чанки *не* имеют очевидного унифицированного заголовка
|
|
||||||
(высота/ширина/палитра) фиксированного размера — попытка интерпретировать
|
|
||||||
первые байты чанка как `{height:u16, width:u16, flags:u16}` не подтвердилась
|
|
||||||
на реальных данных (получаются нереалистичные размеры для маленьких чанков).
|
|
||||||
Вероятно, как и в Apple II версии (см. `FRAMEDEF.S`/`SEQTABLE.S`), геометрия
|
|
||||||
кадра (ширина, высота, точка привязки) хранится **отдельно от самих
|
|
||||||
пиксельных данных** — в таблицах внутри `PRINCE.EXE`, а не в `.DAT`-чанке.
|
|
||||||
Сам чанк, вероятно, содержит только упакованные пиксельные данные
|
|
||||||
(RLE/дельта-упаковка, по аналогии с `UNPACK.S` в Apple II исходниках).
|
|
||||||
- Точный алгоритм упаковки пикселей **не восстановлен** в рамках этого
|
|
||||||
анализа по сырым байтам — байт-в-байт разбор распаковщика без
|
|
||||||
дизассемблирования `PRINCE.EXE` надёжно не сделать. **Но для практических
|
|
||||||
целей это не требуется**: см. §7 — в SDLPoP уже есть тот же самый набор
|
|
||||||
изображений в готовом, распакованном виде (PNG), которым можно пользоваться
|
|
||||||
напрямую как источником ассетов, не реализуя свой декодер `.DAT`-пикселей.
|
|
||||||
Писать собственный декодер имеет смысл только если понадобится читать
|
|
||||||
оригинальные `.DAT` "на лету" (например, для точной сверки контента именно
|
|
||||||
нашей копии игры) — тогда ориентир — исходник SDLPoP (`src/seg009.c`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Звук — уверенность: высокая (по структуре), низкая (по деталям кодека)
|
|
||||||
|
|
||||||
Обнаружено 4 параллельных набора звуковых ресурсов под разные звуковые
|
|
||||||
устройства DOS-эпохи — типично для игр начала 1990-х с "звуковым меню":
|
|
||||||
|
|
||||||
| Файл | Устройство | Формат чанка |
|
|
||||||
|---|---|---|
|
|
||||||
| `midisnd1.dat`, `midisnd2.dat` | General MIDI / MPU-401 | каждый чанк = 2-байтовый LE-префикс длины + встроенный Standard MIDI File (`MThd`...`MTrk`...) |
|
|
||||||
| `mt32snd1.dat`, `mt32snd2.dat` | Roland MT-32/CM-32L | тот же формат: префикс длины + `MThd`/`MTrk`, с MT-32-специфичными SysEx (видны строки `MT-32.mff`, текстовые мета-события вроде `"The Princess awaits"`) |
|
|
||||||
| `prince.dat` | (аналогично MIDI) | отдельный крупный музыкальный ресурс, тот же MIDI-контейнер — вероятно, финальная тема |
|
|
||||||
| `digisnd1/2/3.dat` | Covox / Disney Sound Source / Sound Blaster (оцифрованный звук) | чанк начинается с нескольких служебных байт, среди которых слово `0x2AF8` = 11000 — похоже на частоту дискретизации 11 кГц; далее — сырые 8-битные PCM-сэмплы (значения кластеризуются вокруг ~0x7A–0x90, типично для беззнакового 8-бит аудио, смещённого к середине шкалы) |
|
|
||||||
| `ibm_snd1.dat`, `ibm_snd2.dat` | PC Speaker | чанк — последовательность троек байт похожих на (длительность, делитель_частоты) — простой формат "бипера", отличный от MIDI |
|
|
||||||
|
|
||||||
Подтверждено разбором первых чанков в каждом файле (см. байтовые дампы,
|
|
||||||
проверялись скриптом). Точная семантика полей внутри `digisnd`/`ibm_snd`
|
|
||||||
(разрядность, порядок байт служебного заголовка) не выведена до конца — при
|
|
||||||
реализации порта достаточно распознавания по типу файла и (для MIDI-семейства)
|
|
||||||
можно напрямую воспроизводить встроенный Standard MIDI File, пропустив
|
|
||||||
2-байтовый префикс длины.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Готовые распакованные ассеты в SDLPoP (`data/`) — практический источник для порта
|
|
||||||
|
|
||||||
Репозиторий github.com/NagyD/SDLPoP содержит папку `data/`, где, помимо
|
|
||||||
самих `.DAT`-контейнеров, каждый ресурс **продублирован в виде отдельно
|
|
||||||
распакованного файла**, названного по его `id` из таблицы оглавления (§1.2).
|
|
||||||
Проверено через GitHub API (`api.github.com/repos/NagyD/SDLPoP/contents/...`):
|
|
||||||
|
|
||||||
| Подпапка/файл в `data/` | Содержимое | Соответствие нашему разбору |
|
|
||||||
|---|---|---|
|
|
||||||
| `GUARD.DAT`, `GUARD1.DAT`, `GUARD2.DAT` | сырые `.DAT` | размер **побайтово совпадает** с нашими локальными `guard.dat`/`guard1.dat`/`guard2.dat` (6950 / 117 / 117 байт) |
|
|
||||||
| `DIGISND1.DAT`, `MIDISND2.DAT` и др. | сырые `.DAT` | размер **не совпадает** с нашими локальными файлами (48545 vs 50101, 18408 vs 18958) — другой релиз/сборка игры |
|
|
||||||
| `GUARD/res751.png` … `res784.png` | готовые PNG, по одному на кадр анимации, имя = `res<id>.png` | id-диапазон (751-784) точно совпадает с нашим разбором `guard.dat` |
|
|
||||||
| `VPALACE/res200.pal`, `res201.png`, `res202.png`, … | палитра (JASC `.pal`) + PNG-кадры фонов дворца, **VGA-вариант (256 цветов)** | id-диапазон (200+) совпадает с `vpalace.dat` |
|
|
||||||
| `LEVELS/res2000.bin` … `res2015.bin` | сырые дампы уровней по 2304-2305 байт | id совпадает с `levels.dat`; содержимое `res2001.bin` **сверено побайтово** с нашим id=2001 — тайловые данные совпадают |
|
|
||||||
| `KID/`, `PRINCE/`, `SHADOW/`, `SKEL/`, `VIZIER/`, `FAT/`, `TITLE/`, `VDUNGEON/`, `PV/`, `IBM_SND1/`, `IBM_SND2/`, `font/`, `music/` | аналогичные наборы для остальных ресурсов | не проверялись по отдельности, но структура (папка на каждый `.dat`, файлы `res<id>.ext`) наблюдается одинаково |
|
|
||||||
|
|
||||||
**Вывод:** это данные из немного **другого релиза DOS-версии**, чем те, что
|
|
||||||
лежат у нас в `MSDOS/` (см. расхождение в размере `digisnd`/`midisnd`), но
|
|
||||||
формат контейнера и нумерация `id` — те же самые. Практически это значит:
|
|
||||||
|
|
||||||
1. Для получения играбельных PNG-спрайтов и VGA-фонов **не нужно
|
|
||||||
реализовывать декодер сжатия пикселей** — можно взять готовые файлы
|
|
||||||
`data/<ИМЯ>/res<id>.png` напрямую как исходный материал для конвертации
|
|
||||||
под видеорежим ZX Sprinter (в т.ч. `VPALACE`/`VDUNGEON` — уже
|
|
||||||
256-цветный VGA-арт, что прямо отвечает на вопрос про полноцветность).
|
|
||||||
2. Если в проекте важно использовать именно ту версию контента, что в наших
|
|
||||||
`MSDOS/*.dat` (а не версию из SDLPoP) — распаковку своих файлов всё же
|
|
||||||
придётся делать (кодек пикселей по-прежнему не восстановлен для сырых
|
|
||||||
`.DAT`, см. §3), либо принять решение работать с версией SDLPoP как
|
|
||||||
мастер-источником ассетов вместо своей.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Служебные не-ресурсные файлы
|
|
||||||
|
|
||||||
- `config.dat`, `setup.dat` — 28 байт, не являются ресурсными контейнерами
|
|
||||||
(не проходят проверку §1.1 — "размер" получается больше самого файла).
|
|
||||||
Скорее всего простые бинарные структуры настроек (звук/видеорежим,
|
|
||||||
выбранный на этапе `SETUP.EXE`/`INSTALL.EXE`), не связаны с игровым
|
|
||||||
контентом.
|
|
||||||
- `desktopd.cfg`, `setup.cfg` — текстовые/бинарные конфиги DOS-инсталлятора,
|
|
||||||
вне скоупа игровых ресурсов.
|
|
||||||
- `PRINCE.EXE` / `PRINCE.REM` — почти идентичны (отличие в единичных байтах
|
|
||||||
в районе смещения ~0x4ED0), похоже на кряк/патч одного байта проверки —
|
|
||||||
не относится к формату ресурсов.
|
|
||||||
- `old-games.nfo` — ASCII-арт NFO релиз-группы (old-games.ru), не игровые
|
|
||||||
данные.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Итоговая таблица уверенности
|
|
||||||
|
|
||||||
| Раздел | Уверенность | Как подтверждено |
|
|
||||||
|---|---|---|
|
|
||||||
| Контейнер `.DAT` (заголовок + таблица) | Высокая | Проверено скриптом на всех 28 файлах, инвариант offset+size выполняется без исключений; независимо подтверждено именованием `res<id>.*` в SDLPoP `data/` |
|
|
||||||
| ID-пространства ресурсов | Средняя-высокая | Наблюдение по диапазонам + сверка с `res<id>` именами файлов SDLPoP и побайтовым содержимым `res2001.bin` |
|
|
||||||
| Формат уровня = формату Apple II | Высокая (по размеру и содержимому), служебный блок id=2000 — открытый вопрос | Совпадение размера (2304 vs 2305), тайловые байты сходятся с `res2001.bin` из SDLPoP |
|
|
||||||
| Формат изображений/спрайтов (сырой `.DAT`) | Низкая-средняя | Контейнер подтверждён, кодек пикселей — нет; но практически закрыто наличием готовых PNG в SDLPoP `data/` (§5) |
|
|
||||||
| Формат звука (тип контейнера) | Высокая для MIDI-семейств, средняя для digisnd/ibm_snd | Явные MIDI-сигнатуры `MThd`/`MTrk` видны в байтах |
|
|
||||||
|
|
||||||
**Рекомендация для дальнейшей работы:** для получения арт-ассетов (спрайты,
|
|
||||||
фоны, палитры) — использовать готовые распакованные файлы из
|
|
||||||
`github.com/NagyD/SDLPoP/tree/master/data` (§5), это быстрее и надёжнее
|
|
||||||
самостоятельной реализации декодера. Декодер сырого `.DAT`-формата
|
|
||||||
изображений и точную семантику служебных полей `digisnd`/`ibm_snd`
|
|
||||||
(§3, §4) стоит восстанавливать только если понадобится читать именно нашу
|
|
||||||
локальную копию `MSDOS/*.dat` "как есть" — тогда ориентир прежний: исходник
|
|
||||||
SDLPoP (`src/seg009.c`, `src/data.c`/`data.h`).
|
|
||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -1,555 +0,0 @@
|
|||||||
# Prince of Persia на ZX Sprinter — план порта
|
|
||||||
|
|
||||||
## СТАТУС (обновлено 2026-08-01)
|
|
||||||
|
|
||||||
Документ составлен 2026-07-15 как план «с нуля» и с тех пор во многом
|
|
||||||
исполнен. Читать его надо так:
|
|
||||||
|
|
||||||
| Раздел | Что с ним сейчас |
|
|
||||||
|--------|------------------|
|
|
||||||
| §1 возможности библиотек | актуально как обзор, но **спрайтовый движок `sprite.h` для персонажей НЕ используется**: Kid/страж рисуются прямыми блитами атласов (`gfx_blit_cols_part*`) с ручным heal — так требует модель оригинала (§6) |
|
|
||||||
| §2 held-state клавиатуры | **сделано** (`kbd_mod_state`, `<kbd_raw.h>`). Открытая проблема — потеря байт при аккордах Shift+стрелка; диагноз и план в `../roomtest/TASKS_CLOSED.md` (KBD-1) |
|
|
||||||
| §3 форматы данных | актуально; уровень читается живьём (`roomtest/pop_level.c`) |
|
|
||||||
| §4 стратегия фона | **сделано** — тайловый рендерер в рантайме (`roomtest/pop_bg.c`) |
|
|
||||||
| §5 PoC | **закрыт и превзойдён.** `poc/` (плейсхолдер-персонаж) — история; активная разработка ушла в `roomtest/` с настоящей графикой |
|
|
||||||
| §6 модель движения | **сделано**: `play_seq` + `frame_table` оригинала, не физика с нуля |
|
|
||||||
| §7 фазы | см. отметки статуса прямо в разделе |
|
|
||||||
| §8 риски | п.1 закрыт, п.3 закрыт (28 страниц-атласов Кида), п.2/п.4 — см. отметки в разделе |
|
|
||||||
| §10 режим памяти | **сделано и переросло план**: `huge` + четыре банка кода; актуальная раскладка — `layout_plan_v2.md` |
|
|
||||||
|
|
||||||
**Где смотреть текущее состояние, а не план:** `../roomtest/README.md`
|
|
||||||
(что играется), `../roomtest/TASKS_OPEN.md` (что в работе), `levels_plan.md`
|
|
||||||
(следующие уровни), `layout_plan_v2.md` (раскладка кода по окнам и банкам).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Опирается на
|
|
||||||
`APPLEII_RESOURCE_FORMAT.md` / `MSDOS_RESOURCE_FORMAT.md` / `README.md` в
|
|
||||||
этой папке, на текущий sprinter-cc/libc/libbgi (см. §1) и на локальные копии
|
|
||||||
`applications/PoP/SDLPoP` (github.com/NagyD/SDLPoP, GPLv3) и
|
|
||||||
`applications/PoP/PR` (github.com/NagyD/PR, GPLv2) — используются только как
|
|
||||||
справочник по структурам/константам оригинального движка и как источник
|
|
||||||
готовых распакованных ассетов (`SDLPoP/data/`), не как код для копирования.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Что уже есть в sprinter-cc и библиотеках (используем как есть)
|
|
||||||
|
|
||||||
Собрано из `docs/TODO.md`, `docs/libc-reference.md`, `docs/sprite-api-design.md`,
|
|
||||||
`libbgi/include/{gfx.h,sprite.h,graphics.h}`, `examples/rpgwalk`.
|
|
||||||
|
|
||||||
- **Графика 320×256×256** (`GFX_MODE_320x256x256`, режим 0x81) — разрешение и
|
|
||||||
глубина цвета совпадают почти впрямую с VGA-ассетами оригинала
|
|
||||||
(`SDLPoP/data/VPALACE`, `VDUNGEON` — уже 256-цветные PNG). Не нужно ужимать
|
|
||||||
в EGA/CGA палитру.
|
|
||||||
- **BGI-слой** (`graphics.h`) — примитивы, палитра, текст, `getimage/putimage`
|
|
||||||
— Фазы 1-2d готовы и проверены в MAME.
|
|
||||||
- **Спрайтовый движок v2** (`sprite.h`, ветка `sprite-engine-v2`) — ровно то,
|
|
||||||
что нужно персонажам PoP:
|
|
||||||
- retained-модель (`sprite_update`/`sprite_flip`, double-buffer, dirty-биты,
|
|
||||||
heal+blit за один проход);
|
|
||||||
- кадровая анимация по ленте (`sprite_anim`, LOOP/PINGPONG/ONCE,
|
|
||||||
горизонтальная/вертикальная лента) и tween-перемещение
|
|
||||||
(`sprite_moveto`, DDA без knowledge-heavy арифметики);
|
|
||||||
- Y-сортировка слоями (`gfx_sprite_ysort`, `layer`) — то, что нужно для
|
|
||||||
«Кид перед/за стражником» без ручной пересортировки;
|
|
||||||
- атласы в EMM-страницах (`atlas_t`/`atlas_load`) — на восьмерых
|
|
||||||
персонажей в `rpgwalk` уже работает: прямой прецедент для Кида/стражника;
|
|
||||||
- ограничение кадра ≤ 64×64 — с запасом (см. §3: кадры Кида в оригинале
|
|
||||||
~12-30 × 39-42 px).
|
|
||||||
- **Frame pacing** (`gfx_set_fps_div`) + цепочка кадровых IRQ — стабильный
|
|
||||||
логический тик независимо от рендер-нагрузки экрана (проверено MAME).
|
|
||||||
- **EMM-бюджет**: ~3.3 МБ свободно на старте (`memory/sprinter_emm_budget`) —
|
|
||||||
с большим запасом на все спрайт-атласы и предрендеренные фоны комнат (см.
|
|
||||||
§4) даже без выгрузки неиспользуемых уровней.
|
|
||||||
- **Файловый ввод-вывод** (FILE* v2, `fopen/fread/...`) — для загрузки
|
|
||||||
уровней/атласов/палитр с дискеты, по образцу `rpgwalk` (`atlas_load`,
|
|
||||||
`gfx_pal_fload`).
|
|
||||||
- **Клавиатура (событийная)** — `kbhit/getch/getkey` (ASCII + `KEY_*` скан-код
|
|
||||||
для стрелок), см. §2 — это НЕ то, что нужно для управления Кидом один в
|
|
||||||
один (см. ниже).
|
|
||||||
- **Звук** — `cbl.h` (потоковый CBL/COVOX, callback-модель, verified MAME) —
|
|
||||||
подходит для оцифрованных эффектов (`digisnd*.dat` — PC-звук
|
|
||||||
~11 кГц 8-бит, см. `MSDOS_RESOURCE_FORMAT.md` §4).
|
|
||||||
|
|
||||||
Вывод: **движок отрисовки и анимации почти не требует нового кода** —
|
|
||||||
самый близкий по духу пример (`rpgwalk`: атласы, анимация, tween, дабл-буфер,
|
|
||||||
FPS-делитель) переносится на PoP почти без изменений архитектуры.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Единственный принципиальный пробел: удержание клавиш
|
|
||||||
|
|
||||||
**Спайк проведён (2026-07-15), вопрос закрыт артефактами — не догадкой.**
|
|
||||||
|
|
||||||
`getch`/`getkey` — это события ESTEX WAITKEY/SCANKEY (по нажатию), без чёткой
|
|
||||||
информации о СОСТОЯНИИ (что зажато прямо сейчас, несколько клавиш
|
|
||||||
одновременно). Prince of Persia на управлении требует именно состояния:
|
|
||||||
держать направление (бег) + одновременно нажать вверх (прыжок вперёд), держать
|
|
||||||
Shift (модификатор) + направление и т.д.
|
|
||||||
|
|
||||||
### 2.1 Находки
|
|
||||||
|
|
||||||
1. **`docs/converted/ProgrammerManual.txt` документирует функцию, которую мы
|
|
||||||
раньше пропустили: `CTRLKEY` (ESTEX $33h)** — «Получить состояние
|
|
||||||
клавиатуры». Дословно: «данные берутся не из буфера клавиатуры (как в
|
|
||||||
остальных функциях), а непосредственно из результатов ПОСЛЕДНЕГО
|
|
||||||
сканирования» — то есть это НАСТОЯЩЕЕ live-state, не событие. Но
|
|
||||||
покрывает только модификаторы: Left/Right Shift, Ctrl, Alt,
|
|
||||||
Rus/Lat, Num/Scroll/Caps Lock, Insert (не обычные клавиши вроде стрелок).
|
|
||||||
Готовое решение для «держать Shift = бежать» — тривиальная обёртка,
|
|
||||||
без архитектурных рисков.
|
|
||||||
2. Для ОБЫЧНЫХ клавиш (стрелки, буквы) такого live-state нет нигде в ESTEX —
|
|
||||||
`WAITKEY`/`SCANKEY`/`TESTKEY` ($30/$31/$37h) — все три отдают ОДИНАКОВЫЙ
|
|
||||||
формат «очередное нажатие», без release. `TESTKEY` не удаляет событие из
|
|
||||||
буфера (полезно для «подсмотреть, не потребляя»), но это тоже разовое
|
|
||||||
нажатие, не состояние.
|
|
||||||
3. Автоповтор клавиатуры (typematic) не годится как замена held-state:
|
|
||||||
`MAME_MCP_GUIDE.md` фиксирует задержку до первого повтора ~1 секунда
|
|
||||||
(типично для PS/2) — на порядок медленнее кадра (20 мс), не подходит для
|
|
||||||
платформера.
|
|
||||||
4. **Решающий артефакт — `libc/irq/_irq_tramp.c` (сам трамплин прерывания,
|
|
||||||
не гипотеза):** вектор 0xFF общий для кадра/клавиатуры/CBL. Ветка
|
|
||||||
клавиатуры (бит 0 порта 0x19 = SIO-A RR0 «байт принят») делает буквально
|
|
||||||
`jp 0x0038` (прямиком в DSS) **до какого-либо чтения порта данных 0x18 И
|
|
||||||
до нашей кадровой цепочки (`_irq_chain`)** — наш `irq_chain_add`
|
|
||||||
вообще не видит клавиатурные прерывания, они физически не доходят до
|
|
||||||
цепочки (см. `tr_notkbd`/`tr_frame` разбор в файле). Значит текущая
|
|
||||||
инфраструктура (тот же механизм, что несёт FPS-делитель) НЕ дает
|
|
||||||
зацепки для клавиатуры без правки самого трамплина.
|
|
||||||
5. Регистр данных SIO (порт 0x18) — аппаратный приёмный буфer, чтение
|
|
||||||
деструктивно (дёргает байт из очереди); кто прочитал первым, тот и
|
|
||||||
владеет байтом. Значит «подглядеть, не мешая DSS» технически
|
|
||||||
невозможно — необходимо либо совсем не трогать этот путь (статус-кво),
|
|
||||||
либо взять его СЕБЕ полностью на время геймплея.
|
|
||||||
|
|
||||||
### 2.2 Рекомендация (конкретная, не три равнозначных варианта)
|
|
||||||
|
|
||||||
**A. Тривиально, почти без риска — обернуть `CTRLKEY` ($33h)** отдельной
|
|
||||||
функцией (например `kbd_mod_state()` в `<conio.h>`) — даёт настоящий
|
|
||||||
held-state для Shift/Ctrl/Alt. Можно делать хоть сейчас, не архитектурное
|
|
||||||
решение.
|
|
||||||
|
|
||||||
**B. Для обычных клавиш (стрелки и т.д.) — по прецеденту CBL.** В
|
|
||||||
`_irq_tramp.c` уже есть пример «приватного» пути на том же векторе 0xFF,
|
|
||||||
который сознательно НЕ чейнится к DSS (CBL: бит 7 порта 0xFE, свой
|
|
||||||
хук `_irq_cbl_hook`, полный сейв, свой `reti`). Предлагаемый новый
|
|
||||||
компонент `<kbd_raw.h>` — симметричный: ветка по биту 0 порта 0x19 читает
|
|
||||||
порт 0x18 САМА (декодирует PS/2 make/break, `0xF0`-префикс — протокол
|
|
||||||
уже задокументирован в `docs/samples/sprinterKeybLib.asm`), ведёт битовую
|
|
||||||
карту «клавиша N зажата», и НЕ прыгает в DSS, пока путь активен —
|
|
||||||
жизненный цикл `kbd_raw_open()`/`kbd_raw_close()` один в один как у
|
|
||||||
`cbl_open`/`cbl_close`.
|
|
||||||
|
|
||||||
**Важное следствие (сообщить пользователю явно, не прятать):** пока
|
|
||||||
`kbd_raw_open()` активен, DSS вообще не получает клавиатурных байт —
|
|
||||||
`kbhit/getch/getkey/CTRLKEY` заведомо не будут работать, ESC для выхода
|
|
||||||
в DSS-смысле тоже (нужно проверять raw-битовую карту самим). Это
|
|
||||||
нормально для активной фазы геймплея (у самой игры и так свой цикл
|
|
||||||
ввода), но означает: экраны/паузы, которым нужен ESTEX-ввод (например,
|
|
||||||
диалог сохранения через `fopen`, если тот когда-либо потребует ввода
|
|
||||||
с консоли), должны на это время `kbd_raw_close()`.
|
|
||||||
|
|
||||||
**Не рекомендую вариант «таймаут-эвристика поверх SCANKEY»** — after
|
|
||||||
находки о typematic-задержке ~1с он не даёт нужной задержки для игры;
|
|
||||||
рекомендация A+B закрывает потребность без компромиссов.
|
|
||||||
|
|
||||||
**Статус: A+B РЕАЛИЗОВАНЫ (2026-07-15, по согласованию с пользователем).**
|
|
||||||
|
|
||||||
- A: `kbd_mod_state()` — `libc/conio/kbd_mod_state.c` + `<conio.h>`
|
|
||||||
(`KBD_MOD_*`).
|
|
||||||
- B: `<kbd_raw.h>` (`libc/kbd/`) + правка `libc/irq/_irq_tramp.c`
|
|
||||||
(новая ветка на бите 0 порта 0x19: raw активен → сама читает порт
|
|
||||||
0x18, декодирует make/break, НЕ чейнится к DSS; raw выключен —
|
|
||||||
поведение как раньше, без изменений). Трамплин вырос со 150 до
|
|
||||||
220 байт — `_IRQ_TRAMP_BUF_SIZE` поднят с 224 до 288 (было 4 байта
|
|
||||||
запаса, стало ≥60). `make -C libc` (fast+safe) — чисто.
|
|
||||||
- **Верификация в MAME** (`tests/kbdraw`, полный цикл open→держать→
|
|
||||||
отпустить→ESC-выход→close): `KBD_LEFT` (0x16B, расширенный код
|
|
||||||
E0 6B) — down на нажатие, up на отпускание, ТОЧНО совпало с
|
|
||||||
константой из `<kbd_raw.h>`; `KBD_ESC` (0x76, обычный код) —
|
|
||||||
корректно закрыл raw-канал и вернул DSS (`IM` вернулся в 1).
|
|
||||||
Побочно найдено и задокументировано в `docs/libc-reference.md`
|
|
||||||
(`<kbd_raw.h>`): MAME-мостовой `press_key` дёргает ОБЕ клавиатуры
|
|
||||||
(PC+ZX) одновременно и через ZX-путь давал паразitный незатухающий
|
|
||||||
бит — не относится к реальному сценарию (пользователь подтвердил:
|
|
||||||
матрица на Sprinter давно не используется), но означает, что
|
|
||||||
будущие MAME-тесты этой функции надо гонять через `:kbd:ms_naturl:*`
|
|
||||||
напрямую, не через удобный `press_key`. UP/DOWN/RIGHT/SPACE/SHIFT
|
|
||||||
константы — НЕ перепроверены поштучно (тот же общеизвестный
|
|
||||||
стандарт PS/2 Set 2, что и подтверждённые LEFT/ESC — проверить перед
|
|
||||||
использованием в PoC, если управление будет ощущаться неверно).
|
|
||||||
- На реальном железе — не проверено (только MAME).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Формат данных — что напрямую переносим из docs/*RESOURCE_FORMAT.md
|
|
||||||
|
|
||||||
- **Уровень** (`BLUETYPE`/`BLUESPEC`/`LINKLOC`/`LINKMAP`/`MAP`/`INFO`,
|
|
||||||
2304 байта, 24 экрана × 30 тайлов) — читаем один раз при загрузке уровня
|
|
||||||
в свою C-структуру (прямой memcpy дампа файла, поля читаем по офсетам
|
|
||||||
из `APPLEII_RESOURCE_FORMAT.md` §1). DOS `levels.dat` даёт то же самое
|
|
||||||
+1 байт в конце записи — отбросить.
|
|
||||||
- **Графика фона/спрайтов** — кодек сжатия DOS `.DAT` не восстановлен и
|
|
||||||
восстанавливать не будем: используем уже распакованные PNG из
|
|
||||||
`SDLPoP/data/{KID,GUARD,VPALACE,VDUNGEON,...}` (см.
|
|
||||||
`MSDOS_RESOURCE_FORMAT.md` §5, §7 — тот же контейнерный формат/нумерация,
|
|
||||||
просто другой релиз сборки данных). Измерено локально: кадры Кида —
|
|
||||||
~12×39 .. 30×42 px (P-режим, 4-бит палитра), фоновые тайлы подземелья —
|
|
||||||
32 px по ширине (10 колонок × 32 = 320 — сходится с шириной экрана), высота
|
|
||||||
тайла 20/60/62 px (неоднородные ряды пола/потолка/арок) — укладывается в
|
|
||||||
лимит спрайтового движка (кадр ≤ 64×64) без всяких изменений движка.
|
|
||||||
- **Звук** — `digisnd*.dat` (PC-звук 8-бит ~11 кГц) — конвертация в сырой
|
|
||||||
PCM и проигрывание через `cbl_open`/`cbl_push_*`; `ibm_snd*.dat` (PC-спикер
|
|
||||||
тройки «частота×2Б + длительность») — тривиальный бипер, не требует CBL.
|
|
||||||
MIDI-семейство (`midisnd`, `mt32snd`, `prince.dat`) — вне скоупа (нет
|
|
||||||
синтеза MIDI на платформе; не блокирует геймплей).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Стратегия фона — ПЕРЕСМОТРЕНО 2026-07-15: тайловый рендерер В РАНТАЙМЕ
|
|
||||||
|
|
||||||
**Было** (первая версия плана): офлайн-склейка каждой комнаты в готовую
|
|
||||||
растровую картинку 320×~193, `gfx_blit` целиком при входе — обоснование
|
|
||||||
было «ноль нового кода в libbgi». Пересчёт по факту наличия структурных
|
|
||||||
данных комнаты (§3.4 формата, `level.h`) показал: 16 уровней × 24 комнаты ×
|
|
||||||
~60-80 КБ/картинка — это **30+ МБ**, при том что одна и та же картинка
|
|
||||||
тайла (пол/стена/колонна) переиспользуется в десятках комнат — офлайн-
|
|
||||||
склейка печёт её заново в каждую копию.
|
|
||||||
|
|
||||||
**Стало**: тайлы — переиспользуемый набор картинок ОДИН на визуальный
|
|
||||||
стиль (не на комнату), структурные данные комнаты — компактные (60 байт:
|
|
||||||
30×foretable+30×backtable, все 16 уровней ≈ 37 КБ, см. `level.h`).
|
|
||||||
`room_draw()` (applications/PoP/poc/room.c) проходит 30 тайлов комнаты и
|
|
||||||
зовёт `gfx_blit` для каждого, читая картинку из таблицы по типу тайла
|
|
||||||
(`tile_images[TILE_TYPE]`). Итог: десятки-сотни КБ переиспользуемых
|
|
||||||
тайл-картинок на весь визуальный стиль + ~37 КБ структуры уровней —
|
|
||||||
вместо 30+ МБ.
|
|
||||||
|
|
||||||
**Почему это НЕ бьёт по бюджету кадра**: `room_draw()` зовётся ОДИН РАЗ
|
|
||||||
при входе в комнату (смена комнаты — не every-frame событие), не в
|
|
||||||
игровом цикле — это не `sprite_update`, тактовый бюджет кадра не
|
|
||||||
затронут.
|
|
||||||
|
|
||||||
Анимированные тайлы (факел, шипы, дверь-плита) по-прежнему рисуются как
|
|
||||||
отдельные `sprite_t` поверх фона — движок это уже умеет (Y-order/layers,
|
|
||||||
dirty-биты, heal против фона через ОЗУ-копию); `room_draw()` кладёт в
|
|
||||||
ОЗУ-копию именно статичную геометрию (пол/стены/колонны Фазы 1 — §5.2),
|
|
||||||
поверх неё heal спрайтов работает как обычно.
|
|
||||||
|
|
||||||
`toolchain/room_compose.py` (генерик-компоновщик тайлов в одну картинку,
|
|
||||||
§6.1) остаётся полезным ИНСТРУМЕНТОМ конвертации отдельных тайл-картинок
|
|
||||||
(PNG → getimage raw), просто теперь его выход — 32 маленьких файла
|
|
||||||
`tileNN.raw` (по одному на тип тайла), а не один большой файл на комнату;
|
|
||||||
сама раскладка/повторное использование по комнатам — в C-коде
|
|
||||||
(`room_draw`), не в офлайн-склейке.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Proof-of-Concept — цель: доказать, что порт вообще ощущается как PoP
|
|
||||||
|
|
||||||
> **Закрыт (исторический раздел).** PoC в `poc/` свою задачу выполнил и
|
|
||||||
> дальше не развивается: управление ощущается как PoP, held-state работает.
|
|
||||||
> Всё, что ниже про плейсхолдер-персонажа и приблизительную дугу прыжка,
|
|
||||||
> — уже неправда для активной ветки: в `roomtest/` стоит настоящая графика
|
|
||||||
> Кида и авторские таблицы кадров (§6). Раздел оставлен ради истории
|
|
||||||
> решений (в частности §5.1 — почему сначала был плейсхолдер).
|
|
||||||
|
|
||||||
**Объём**: одна комната (например Level 1, экран старта Кида), без
|
|
||||||
переходов между экранами, без стражников (стретч-цель, не обязательна).
|
|
||||||
|
|
||||||
**Что показываем**:
|
|
||||||
1. Кид на экране, с закреплённым офлайн-конвертированным набором кадров
|
|
||||||
(подмножество: idle, walk L/R, jump-начало/дуга/приземление, стоп-на-краю,
|
|
||||||
возможно повисание на краю) — атлас в W0-странице, по образцу `rpgwalk`.
|
|
||||||
2. Управление: держать влево/вправо — идёт; отпустил — тормозит/стоит;
|
|
||||||
нажатие вверх во время бега — прыжок вперёд (дуга по авторским таблицам
|
|
||||||
смещений, не по gravity-физике «с нуля» — см. §6). Здесь же проверяется
|
|
||||||
решение по §2 (реальный held-state).
|
|
||||||
3. Столкновения: пол/край экрана/провал — по факту чтения тайла из
|
|
||||||
`BLUETYPE` под ногами (без LINKLOC-триггеров пока).
|
|
||||||
4. Стабильный кадр 50 Гц через уже готовый `gfx_wait_vsync`/дабл-буфер
|
|
||||||
(без FPS-делителя — Кид анимируется каждый видеокадр, как в оригинале).
|
|
||||||
|
|
||||||
**Критерий успеха**: субъективно «прыжок ощущается как в PoP» (дистанция и
|
|
||||||
тайминг прыжка сверены с оригинальными таблицами, не подобраны на глаз —
|
|
||||||
см. §6), управление отзывчивое (не событийное с задержкой), сцена не мерцает
|
|
||||||
на стыке спрайт/фон.
|
|
||||||
|
|
||||||
**Не входит в PoC**: стражники/бой, звук, HUD/таймер, переходы между
|
|
||||||
комнатами, ловушки/триггеры, титры/меню, сохранения.
|
|
||||||
|
|
||||||
**Расположение**: `applications/PoP/poc/` (свой sprinter-cc проект + Python
|
|
||||||
конвертер ассетов, по структуре `examples/rpgwalk`).
|
|
||||||
|
|
||||||
### 5.1 Статус (2026-07-15) — первая итерация: управление + коллизия края
|
|
||||||
|
|
||||||
Сделано и проверено в MAME (`applications/PoP/poc/`, `make run`):
|
|
||||||
держать LEFT/RIGHT (`kbd_raw_down`, raw-канал из §2) — идёт непрерывно,
|
|
||||||
отпустил — стоит на месте (не событийно, реальный held-state);
|
|
||||||
столкновение с краями экрана (клип по `MINX`/`MAXX`); анимация
|
|
||||||
ходьбы/разворота лицом по направлению (`sprite_anim` пинг-понг);
|
|
||||||
дабл-буфер + `gfx_wait_vsync` — без видимого мерцания. Сборка —
|
|
||||||
`--memory huge` без `--bank` (§10, подтверждено рабочим).
|
|
||||||
|
|
||||||
**Важное отступление от плана (осознанно, не молча):** персонаж —
|
|
||||||
ВРЕМЕННАЯ заглушка (лицензированный спрайт-пак
|
|
||||||
`third_party/16x16-RPG-characters` через `tools/gen_kid_placeholder.py`,
|
|
||||||
тот же источник, что уже использует `examples/rpgwalk`), а НЕ
|
|
||||||
конвертированная графика оригинальной Prince of Persia. Причина:
|
|
||||||
исходный набор кадров Кида (`SDLPoP/data/KID`) — копирайт
|
|
||||||
Broderbund/Ubisoft; автоматический конвейер, который систематически
|
|
||||||
извлекает и переупаковывает его в новый формат, — это на практике
|
|
||||||
внутрипроектное решение, которое стоит принимать пользователю явно
|
|
||||||
для каждого шага, а не проводить асинхронно агентом без лишнего
|
|
||||||
подтверждения. Сама графика — не то, что проверяет PoC (§5 явно:
|
|
||||||
цель — ощущение управления/коллизий, не визуальная точность). Замена
|
|
||||||
на настоящую графику Кида — отдельный шаг, на усмотрение пользователя.
|
|
||||||
|
|
||||||
**Ещё не сделано** (следующие итерации §5): авторские таблицы
|
|
||||||
смещений кадров (§6 — движение при ходьбе линейное, px/кадр),
|
|
||||||
реальный уровень/фон по `BLUETYPE`/`LEVEL1` (сейчас — плейсхолдер:
|
|
||||||
плоский пол на весь экран, без ямы/выступа), `kbd_mod_state`/
|
|
||||||
Shift-бег не подключены к игровому циклу (обёртка готова с Фазы A).
|
|
||||||
|
|
||||||
**Прыжок/присед добавлены и ПРОВЕРЕНЫ (2026-07-15)**: состояние
|
|
||||||
`jumping`/`jump_t`/`crouching`, своя приблизительная дуга прыжка
|
|
||||||
(`jump_height[]`, 40 кадров) — не авторская таблица, см. §6.1.
|
|
||||||
HUD-текст статуса (нет отдельной позы).
|
|
||||||
|
|
||||||
Живое тестирование пользователем нашло реальный баг: держа UP чуть
|
|
||||||
дольше 0.8 с (длительность дуги), получали ДВА прыжка подряд — код
|
|
||||||
проверял `kbd_raw_down(KBD_UP)` как уровень (держится, пока клавиша
|
|
||||||
физически зажата), а не как фронт нажатия, поэтому в момент
|
|
||||||
приземления «UP всё ещё зажат» тут же триггерил новый прыжок.
|
|
||||||
Исправлено edge-detect'ом (`up_prev` — предыдущее состояние UP,
|
|
||||||
триггер только на переход 0→1). Проверено брейкпоинтом в отладчике
|
|
||||||
MAME на адресе входа в код прыжка: за одно длинное удержание UP
|
|
||||||
брейкпоинт срабатывает РОВНО ОДИН РАЗ — фикс подтверждён на уровне
|
|
||||||
кода, не только «на глаз».
|
|
||||||
|
|
||||||
Побочный урок (см. `docs/libc-reference.md` `<kbd_raw.h>`): моя
|
|
||||||
более ранняя попытка проверить UP/DOWN/RIGHT по скриншотам после
|
|
||||||
`press_key` ошибочно решила, что скрипт их не нажимает вообще —
|
|
||||||
на самом деле нажимает исправно, просто скриншот ловил случайный
|
|
||||||
момент дуги. Брейкпоинт/watchpoint на конкретный адрес кода —
|
|
||||||
надёжнее скриншота для таких проверок.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Модель движения: авторские таблицы кадров, не физика с нуля
|
|
||||||
|
|
||||||
Оригинальный движок PoP не считает прыжок как непрерывную физику
|
|
||||||
(gravity/velocity каждый тик) — движение персонажа задано таблицами кадров
|
|
||||||
анимации, где у части кадров зашито фиксированное смещение (dx, dy) для
|
|
||||||
ЭТОГО конкретного кадра последовательности (структура видна и в
|
|
||||||
исходниках Apple II — `SEQTABLE.S`/`MOVER.S`, и в SDLPoP `seg003.c`/`seq*`
|
|
||||||
таблицах). Практическое следствие для нашего движка:
|
|
||||||
- **Не использовать** `sprite_anim`/`sprite_moveto` для основного
|
|
||||||
персонажа как есть (они лианейно тянут по таймеру/тянут к линейной
|
|
||||||
цели) — вместо этого приложение само на каждый логический тик:
|
|
||||||
переключает кадр (`sprite_frame`, атлас как лента поз, не «прогрессия
|
|
||||||
первый..последний» автоматом) и одновременно применяет dx,dy ЭТОГО
|
|
||||||
кадра к позиции (`sprite_move`).
|
|
||||||
- Готовая автоматика движка (`sprite_anim`/`sprite_moveto`/tween,
|
|
||||||
Y-сортировка) остаётся полезной для декоративных/фоновых элементов
|
|
||||||
(факелы, патрулирующий стражник вне боя — почти один в один паттерн
|
|
||||||
`rpgwalk`).
|
|
||||||
- Источник таблиц смещений: переснять из `Prince-of-Persia-Apple-II/01 POP
|
|
||||||
Source/Source/{MOVER.S,SEQTABLE.S,FRAMEADV.S}` и/или
|
|
||||||
`SDLPoP/src/seq*.c` — задача Фазы 1 полной реализации (§7), не PoC
|
|
||||||
(для PoC можно взять урезанный набор смещений вручную по количеству
|
|
||||||
пикселей на кадр, посчитанному по видео/скриншотам оригинала, и уточнить
|
|
||||||
позже).
|
|
||||||
|
|
||||||
### 6.1 Инструмент конвертации кадров разного размера (`toolchain/png_strip.py`)
|
|
||||||
|
|
||||||
Кадры персонажа в оригинале — РАЗНОГО размера каждый (bbox зависит от
|
|
||||||
позы; `sprite_t` нашего движка (`libbgi/include/sprite.h`) хранит ОДИН
|
|
||||||
фиксированный w/h на весь спрайт и рисует от угла, без per-frame
|
|
||||||
смещения — в отличие от оригинала, где на каждый кадр было своё XCO/YCO
|
|
||||||
(`APPLEII_RESOURCE_FORMAT.md` §2.2). `toolchain/png_strip.py` (генерик,
|
|
||||||
не завязан на PoP — принимает произвольный список PNG) закрывает это
|
|
||||||
ПАДДИНГОМ: канвас = макс. w/h среди кадров ленты, якорь по умолчанию
|
|
||||||
bottom-center («ноги на месте»), остальное — прозрачность.
|
|
||||||
|
|
||||||
**Компромисс, не полноценное решение**: один сильно выбивающийся по
|
|
||||||
размеру кадр в ленте раздувает канвас (и память) ВСЕХ кадров этой же
|
|
||||||
ленты. Смягчается группировкой по похожим размерам в отдельные атласы
|
|
||||||
(не одна лента на все позы актора — так уже сделано для ходьбы отдельно
|
|
||||||
от прыжка).
|
|
||||||
|
|
||||||
**Полноценное решение (кандидат в будущее расширение библиотеки, НЕ
|
|
||||||
делать без предложения и подтверждения пользователя)**: per-frame
|
|
||||||
смещение в `sprite_t` (аналог XCO/YCO оригинала) — тогда паддинг
|
|
||||||
не нужен вообще, экономия памяти по полной. Делать только если память
|
|
||||||
станет РЕАЛЬНОЙ проблемой (не гипотетической) — тогда предложить как
|
|
||||||
отдельную правку `sprite.h`/движка. Подробности компромисса —
|
|
||||||
memory/png_strip_padding_tradeoff.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Полноценное приложение — фазы (после PoC)
|
|
||||||
|
|
||||||
Порядок — по риску и зависимостям, не по геймплейной важности.
|
|
||||||
**Отметки статуса — на 2026-08-01.**
|
|
||||||
|
|
||||||
**Фаза 0 — инфраструктура порта** — **СДЕЛАНА**, но иначе, чем задумано:
|
|
||||||
- Хелд-стейт клавиатуры по §2 — сделан.
|
|
||||||
- Конвертер уровней не понадобился: `res200N.bin` из `SDLPoP/data/LEVELS`
|
|
||||||
кладётся на образ как есть и читается по офсетам в рантайме
|
|
||||||
(`roomtest/pop_level.c`), уровень живёт в EMM-странице.
|
|
||||||
- Конвертер фона в растры **отменён осознанно** (§4): фон собирается
|
|
||||||
тайлами в рантайме. Спрайты — `toolchain/pop_pack_bg.py` /
|
|
||||||
`pop_pack_kid.py` / `pop_pack_guard.py` → атласы `.atl` (Kid — 28
|
|
||||||
страниц, риск §8 п.3 закрыт).
|
|
||||||
|
|
||||||
**Фаза 1 — Кид, полный набор действий** — **СДЕЛАНА**: стоять/идти/бежать/
|
|
||||||
тормозить/разворот/прыжки/повисание/подтягивание/спуск/приседание/
|
|
||||||
осторожный шаг/питьё зелья/смерть от провала и от пик; переходы между
|
|
||||||
комнатами во все четыре стороны. Осталось: **старт по данным уровня**
|
|
||||||
(`pop_level_start_*` реализованы, но не подключены) — задача L1-START в
|
|
||||||
`../roomtest/TASKS_OPEN.md`.
|
|
||||||
|
|
||||||
**Фаза 2 — мир и ловушки** — **СДЕЛАНА**: кнопки/ворота через
|
|
||||||
`LINKLOC`/`LINKMAP`, шипы, loose-полы (тряска, обрушение, щебень, пробой
|
|
||||||
потолка), зелья, дверь уровня (открывается), факелы. Подробности и
|
|
||||||
справочник — `gates_spikes_plan.md`.
|
|
||||||
|
|
||||||
**Фаза 3 — бой** — **СДЕЛАНА в объёме обычного стражника**: подбор и
|
|
||||||
выхватывание меча, стойка, удар/парирование, коллизия клинков, HP обеих
|
|
||||||
сторон, смерть; ИИ стража (замечает Кида, подходит, боевые ветки),
|
|
||||||
персистентность трупа между комнатами.
|
|
||||||
|
|
||||||
**Фаза 4 — разнообразие противников** — **НЕ НАЧАТА**. Скелет нужен на
|
|
||||||
уровне 3, толстый — на 6, тень — на 12, визирь — на 13; привязка
|
|
||||||
«уровень → тип стража» (`tbl_guard_type`) описана в `levels_plan.md` §1.
|
|
||||||
|
|
||||||
**Фаза 5 — звук** — **НЕ НАЧАТА**: CBL-эффекты (шаги, удары, двери,
|
|
||||||
падение) из `digisnd*.dat`→PCM; PC-спикер тройки (`ibm_snd*.dat`) как
|
|
||||||
опциональный дешёвый бипер без CBL, если формат подтвердится простым
|
|
||||||
парсингом. Опкод `SOUND` в `play_seq` пока просто съедает свой аргумент —
|
|
||||||
точки вызова уже на месте.
|
|
||||||
|
|
||||||
**Фаза 6 — оболочка** — **НЕ НАЧАТА**: титры, меню/выбор уровня, HUD
|
|
||||||
(таймер/жизни), сохранение прогресса (FILE*), финальные катсцены — по
|
|
||||||
минимуму, геймплейно не критично. Полоса HP — единственное, что уже есть.
|
|
||||||
|
|
||||||
**Между Фазами 4 и 5 вклинивается то, чего в этом плане не было:
|
|
||||||
переход между УРОВНЯМИ** (загрузка следующего уровня, второй тайлсет
|
|
||||||
palace, потабличные различия уровней). Отдельный документ —
|
|
||||||
`levels_plan.md`.
|
|
||||||
|
|
||||||
**Фаза 7 — стабилизация**: полный прогон всех 14 уровней в MAME
|
|
||||||
(`mame_interactive.py`), затем на реальном железе; профилирование бюджета
|
|
||||||
кадра по методике `sprite_engine_perf`/`sprite-api-design.md` §9д на самых
|
|
||||||
насыщенных экранах (несколько стражников + ловушки одновременно —
|
|
||||||
проверить лимит ~21 спрайт/кадр и Y-sort лимит 32); при необходимости —
|
|
||||||
банкинг (`--memory big/huge`) для кода/уровня, если размер вылезет за
|
|
||||||
tiny/small.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Риски, требующие спайка/артефакта до架构 решений
|
|
||||||
|
|
||||||
(по правилу `defer_unexplained_quirks` — не гадать, проверять)
|
|
||||||
|
|
||||||
1. ~~**Held-state клавиатуры** (§2)~~ — **закрыт** (`<kbd_raw.h>`). Открытый
|
|
||||||
остаток — не «есть ли held-state», а потеря байт при аккордах
|
|
||||||
Shift+стрелка: `../roomtest/TASKS_CLOSED.md`, KBD-1.
|
|
||||||
2. **Бюджет кадра** — риск подтвердился, но не в том виде, в каком ожидался:
|
|
||||||
спрайтовый движок для персонажей не используется, поэтому лимит
|
|
||||||
«~21 спрайт/кадр» неприменим. Реальный бюджет упирается в heal+блиты и
|
|
||||||
перерисовку тайлов; замер 2026-07-30 — типичный кадр ~371 К тактов
|
|
||||||
(~86 % периода). Инструмент замера уже в коде: полосы бордюра `PROF()`
|
|
||||||
в `roomtest.c`. План выжимания — `../roomtest/TASKS_CLOSED.md` (CLIP-1) и
|
|
||||||
`../roomtest/BUGS_OPEN.md` (T-1/T-2).
|
|
||||||
3. ~~**Ёмкость атласа на актора**~~ — **закрыт**: Kid разложен на 28
|
|
||||||
атласов-страниц по 8 спрайтов (`pop_pack_kid.py`), страж — на 5;
|
|
||||||
мульти-страничного формата `.atl` не потребовалось. Побочно
|
|
||||||
подтвердился компромисс паддинга (§6.1).
|
|
||||||
4. **Тайминг оригинала** — **ОТКРЫТ, и сверка 2026-08-01 показывает
|
|
||||||
расхождение.** Цифры оригинала (SDLPoP): базовый таймер `BASE_FPS = 60`
|
|
||||||
(`types.h:1373`), логический кадр игры — `base_speed = 5` тиков
|
|
||||||
(`data.h:869`), то есть **83.3 мс (12 лог. кадров/с)**; в бою
|
|
||||||
`fight_speed = 6` → **100 мс (10/с)**. У нас (`roomtest.c`) — три
|
|
||||||
ожидания `gfx_wait_vsync()` на итерацию, то есть **60 мс (16.7/с)** и
|
|
||||||
без отдельной скорости боя. Значит **игра идёт примерно на 39 %
|
|
||||||
быстрее эталона**. Точное соответствие даёт 4 ожидания vsync (80 мс
|
|
||||||
против 83.3) и 5 в бою (100 мс — совпадает точно).
|
|
||||||
Проверять не «на глаз», а секундомером по одинаковому отрезку
|
|
||||||
(SDLPoP рядом на том же экране), и только после того, как кадр
|
|
||||||
перестанет иногда вылезать за период (см. п.2) — иначе замедление
|
|
||||||
спрячет проблему бюджета вместо того, чтобы её показать.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. Режим памяти сборки
|
|
||||||
|
|
||||||
Пользователь предложил `huge` (горячий код в W1, данные в W2, редко
|
|
||||||
вызываемая логика — банками в W3) как целевой режим. Согласен, с уточнением
|
|
||||||
по срокам принятия решения.
|
|
||||||
|
|
||||||
**`huge` — правильная цель для ПОЛНОГО приложения**, но не то, с чего надо
|
|
||||||
стартовать:
|
|
||||||
|
|
||||||
- Layout `huge` (см. `memory_modes_implemented`): CODE_LOC=0x4100 (W1),
|
|
||||||
DATA_LOC=0x8000 (W2), банки — W3 (порт 0xE2), `crt0_banked` +
|
|
||||||
автодетект W2 (как `small`). Состояние приложения (структуры Кида,
|
|
||||||
уровня, массив `sprite_t`) остаётся в обычном W2-heap ДАЖЕ если код,
|
|
||||||
который его трогает, забанкован — `malloc` из банка возвращает
|
|
||||||
W2-указатель (`bank_local_data_pattern`), так что данные не привязаны к
|
|
||||||
конкретному банку.
|
|
||||||
- Оверхед `__banked`-вызова (trampoline: +3 байта на стеке между ret и
|
|
||||||
аргументами, виртуальный 24-битный адрес, см. `sdcc_banking`) — фиксированная
|
|
||||||
небольшая цена ЗА ВЫЗОВ, не за такт. Это не страшно для функций, которые
|
|
||||||
вызываются РЕДКО за кадр (AI одного стражника, диалог, переход между
|
|
||||||
комнатами) — страшно было бы забанковать что-то, что дёргается ВНУТРИ
|
|
||||||
горячего цикла отрисовки (там уже и так основной бюджет уходит на
|
|
||||||
`sprite_update`/блиты — см. `sprite_engine_perf`, ~19.5К тактов/спрайт).
|
|
||||||
Правило простое: **не банковать код на пути "раз в кадр на объект",
|
|
||||||
банковать код на пути "раз в кадр на комнату/раз в переход/раз в
|
|
||||||
редкое событие"**: логика ИИ стражника целиком, диалоги/катсцены, меню/
|
|
||||||
титры/выбор уровня, парсинг уровня при входе в комнату, сериализация
|
|
||||||
сохранений — хорошие кандидаты в банки; тик Кида, чтение столкновений,
|
|
||||||
вызов `sprite_update`/`gfx_wait_vsync`, обработка ввода — должны остаться
|
|
||||||
небанкованными (W1/W2).
|
|
||||||
- Гранулярность банкования — целый файл (`--bank N=FILE.c`), это уже
|
|
||||||
системный паттерн проекта (тот же принцип, что и «1 файл = 1 юнит DCE» в
|
|
||||||
libc) — значит выгодно с САМОГО начала Фазы 1 (не задним числом) резать
|
|
||||||
исходники приложения по границе «горячее/холодное» файл-в-файл: например
|
|
||||||
`kid_tick.c`/`collision.c`/`room.c`/`input.c` — неизменно вне банков;
|
|
||||||
`guard_ai_*.c`/`dialogue.c`/`menu.c`/`levelload.c`/`combat.c` — кандидаты
|
|
||||||
под `--bank`. Тогда переход на `huge` позже — это правка Makefile/
|
|
||||||
sprinter-cc-вызова (`--memory huge --bank N=file.c ...`), а не рефакторинг
|
|
||||||
логики.
|
|
||||||
|
|
||||||
**Уточнение (проверено в `bin/sprinter-cc`, строки ~342-350): можно сразу
|
|
||||||
собирать PoC на `--memory huge` без единого `--bank`.** Скрипт сам
|
|
||||||
подставляет стаб `const unsigned char n_banks = 0;`, когда `--bank` не
|
|
||||||
передан ни один раз — `crt0_banked` линкуется и корректно пропускает цикл
|
|
||||||
загрузки банков при старте. Layout при этом byte-в-byte совпадает с тем,
|
|
||||||
что делает `crt0_small` для режима `small` (CODE 0x4100/W1, DATA 0x8000/W2,
|
|
||||||
автодетект W2) — разница только в том, что попутно линкуется сам
|
|
||||||
`bank.s` (таблица `_bank_pages` + trampoline-инфраструктура), это
|
|
||||||
незначительный довесок к размеру, не к рантайм-цене. Значит **PoC можно
|
|
||||||
сразу собирать вызовом `sprinter-cc --memory huge` без `--bank`-флагов** —
|
|
||||||
и когда в полном приложении появятся первые «холодные» файлы, переход на
|
|
||||||
банкование — это просто добавление `--bank N=file.c`, без смены
|
|
||||||
`--memory`/адресов/crt0. Сборочная конфигурация не потребует миграции
|
|
||||||
между PoC и полным приложением.
|
|
||||||
|
|
||||||
Единственное, что стоит сделать уже в Фазе 1 полного приложения (не в
|
|
||||||
PoC) — планировать структуру исходников с расчётом на будущий файл-в-файл
|
|
||||||
сплит под банки (см. выше), раз гранулярность банкования — целый файл.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Что нужно от пользователя, прежде чем двигаться дальше
|
|
||||||
|
|
||||||
- Подтверждение направления по §2 (какой из трёх вариантов held-state
|
|
||||||
клавиатуры пробовать первым, или сначала спайк-эксперимент в MAME).
|
|
||||||
- Подтверждение объёма PoC (§5) — устраивает ли «одна комната без
|
|
||||||
стражников», или сразу закладывать хотя бы одного патрулирующего
|
|
||||||
стражника (это не архитектурно сложнее — Y-order и tween уже есть,
|
|
||||||
просто больше конвертации ассетов).
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
# `applications/PoP/docs` — индекс + сводка по форматам ресурсов
|
|
||||||
|
|
||||||
## Индекс документов (актуальность на 2026-08-01)
|
|
||||||
|
|
||||||
**Живые планы — читать перед работой:**
|
|
||||||
|
|
||||||
| Документ | О чём |
|
|
||||||
|----------|-------|
|
|
||||||
| [`../roomtest/TASKS_OPEN.md`](../roomtest/TASKS_OPEN.md) | **Что берётся в работу сейчас** (не в этой папке, но входная точка) |
|
|
||||||
| [`../roomtest/BUGS_OPEN.md`](../roomtest/BUGS_OPEN.md) | Открытые баги roomtest (закрытые — в `BUGS_CLOSED.md` рядом) |
|
|
||||||
| [`impl_diff.md`](impl_diff.md) | **Осознанные расхождения с SDLPoP**: где мы сделали не дословно и почему |
|
|
||||||
| [`perf_l13_room23.md`](perf_l13_room23.md) | **Сцена и метод замера кадра** (каскад плит, ур.13 к.23): как воспроизвести, зонды, канал `clog`, сводка по кадрам, габариты спрайтов и ответ про `uint8_t`. 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_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 |
|
|
||||||
| [`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 |
|
|
||||||
| [`midtable_analysis.md`](midtable_analysis.md) | **Слои отрисовки**: как устроены back/mid/fore и objtable в оригинале, чего стоит порт, развилки. 2026-08-13 |
|
|
||||||
| [`roomnav_skip.md`](roomnav_skip.md) | **Комнаты для отладочного телепорта**: какие пропускать и почему (посчитано по данным уровней). 2026-08-13 |
|
|
||||||
| [`layout_plan_v2.md`](layout_plan_v2.md) | Раскладка кода по окнам/банкам/страницам + замеры скорости отрисовки |
|
|
||||||
| [`room_model_plan.md`](room_model_plan.md) | `kid_room ≠ drawn_room` (straddle): сделан S1, остальное впереди |
|
|
||||||
| [`host_tests_plan.md`](host_tests_plan.md) | Модульные тесты движка под ucsim_z80: два шва, регрессии из `BUGS_CLOSED.md`, дифф против SDLPoP |
|
|
||||||
| [`shadow_render.md`](shadow_render.md) | **Вид Тени (OR+XOR)** — отложено: почему XOR несовместим с прозрачностью `#FF`, замер подготовки источника, четыре варианта |
|
|
||||||
| [`ideas_backlog.md`](ideas_backlog.md) | Осознанно отложенные гипотезы (мышь, PRNG) |
|
|
||||||
| [`prng_alternatives.md`](prng_alternatives.md) | Запасные генераторы, если упрёмся в бюджет кадра |
|
|
||||||
|
|
||||||
**Исполненные планы, оставленные как справочники:**
|
|
||||||
|
|
||||||
| Документ | Чем ещё полезен |
|
|
||||||
|----------|-----------------|
|
|
||||||
| [`PORT_PLAN.md`](PORT_PLAN.md) | Общая карта фаз со статусами; §6 (модель движения), §10 (режим памяти) |
|
|
||||||
| [`KID_PLAN.md`](KID_PLAN.md) | Модель персонажа: `char_type`, `actions_*`, устройство `play_seq` — нужна для скелета/тени/визиря |
|
|
||||||
| [`gates_spikes_plan.md`](gates_spikes_plan.md) | Раскладка объектов уровня 1 по комнатам, декод `LINKLOC`/`LINKMAP`, точные ссылки на seg-код |
|
|
||||||
|
|
||||||
**Форматы ресурсов** (ниже по этому файлу): `POP-DAT-FormatSpecifications.pdf`
|
|
||||||
/ `.txt` (первоисточник), `APPLEII_RESOURCE_FORMAT.md`,
|
|
||||||
`MSDOS_RESOURCE_FORMAT.md`.
|
|
||||||
|
|
||||||
Удалены 2026-08-01 как полностью исполненные и перекрытые кодом:
|
|
||||||
`clip_char_plan.md`, `double_buffer_plan.md`, `loose_floors_plan.md`,
|
|
||||||
`size_optimization_plan.md` (его §8 про скорость отрисовки перенесён в
|
|
||||||
`layout_plan_v2.md` §9). Ищутся в истории git, если понадобятся.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Форматы ресурсов — сводка
|
|
||||||
|
|
||||||
**Каноническая спецификация форматов** — `POP-DAT-FormatSpecifications.pdf`
|
|
||||||
(+ текстовая конверсия `POP-DAT-FormatSpecifications.txt` для grep/цитирования):
|
|
||||||
*«Prince of Persia — Specifications of File Formats»*, Princed Development Team,
|
|
||||||
2008. Это первоисточник формата `DAT v1.0` (контейнер, индекс, чек-сумма,
|
|
||||||
кодеки RLE/LZG, палитры, уровни, звук), на котором построены и SDLPoP, и
|
|
||||||
Princed Resources. Документы ниже — наши практические заметки/сверки; при
|
|
||||||
расхождении источником истины считать спецификацию.
|
|
||||||
|
|
||||||
Цель этих документов — подготовить почву для будущего порта Prince of Persia
|
|
||||||
на ZX Sprinter, разобрав, как устроены ресурсы игры в двух доступных нам
|
|
||||||
версиях:
|
|
||||||
|
|
||||||
- [`APPLEII_RESOURCE_FORMAT.md`](./APPLEII_RESOURCE_FORMAT.md) — формат
|
|
||||||
уровней и графики по официально опубликованным исходникам 1989 года
|
|
||||||
(6502-ассемблер). Уверенность высокая везде — восстановлено прямым чтением
|
|
||||||
кода движка, а не догадками.
|
|
||||||
- [`MSDOS_RESOURCE_FORMAT.md`](./MSDOS_RESOURCE_FORMAT.md) — формат `.DAT`
|
|
||||||
ресурсов DOS-версии (исходников нет). Восстановлено эмпирически (разбор
|
|
||||||
байтов + перепроверка скриптами) и сверено с документацией открытых
|
|
||||||
сторонних инструментов (SDLPoP, Princed Resources).
|
|
||||||
|
|
||||||
## Главный вывод
|
|
||||||
|
|
||||||
**Формат уровня практически идентичен в обеих версиях**: Apple II `LEVELn`
|
|
||||||
занимает ровно 2304 байта (структура `blueprnt` — тайлы, связи
|
|
||||||
плит/дверей, граф экранов, метаданные старта Кида/стражников), а запись
|
|
||||||
уровня в DOS `levels.dat` занимает 2305 байт с байтовыми значениями тайлов
|
|
||||||
того же диапазона. То есть Джордан Мехнер перенёс формат карты уровня в
|
|
||||||
DOS-порт практически без изменений (+1 байт, вероятно контрольная сумма от
|
|
||||||
DOS-упаковщика). Это значит: раскладку `BLUETYPE`/`BLUESPEC`/`LINKLOC`/
|
|
||||||
`LINKMAP`/`MAP`/`INFO`, задокументированную по Apple II исходникам, можно
|
|
||||||
применять напрямую и к DOS `levels.dat`.
|
|
||||||
|
|
||||||
Формат же **графики отличается принципиально**: на Apple II это простой
|
|
||||||
несжатый rowbyte-формат hi-res экрана с плоской таблицей указателей; в DOS —
|
|
||||||
контейнер с оглавлением ресурсов (id/size/offset), с отдельными вариантами
|
|
||||||
под CGA/EGA/VGA — точный кодек пикселей внутри сырого `.DAT`-чанка не
|
|
||||||
восстановлен ни для той, ни для другой версии до конца. **Но для DOS-графики
|
|
||||||
это не блокирует работу**: в репозитории github.com/NagyD/SDLPoP (папка
|
|
||||||
`data/`) уже лежат готовые распакованные PNG для каждого спрайта/фона
|
|
||||||
(включая VGA-256-цветный вариант `VPALACE`/`VDUNGEON` — то, что нужно под
|
|
||||||
320×256×256 Sprinter), см. §5 `MSDOS_RESOURCE_FORMAT.md`. Это другой
|
|
||||||
релиз/сборка данных, чем наш локальный `MSDOS/` (некоторые звуковые `.dat`
|
|
||||||
отличаются по размеру), но нумерация ресурсов и формат контейнера — те же,
|
|
||||||
что подтверждено побайтовой сверкой уровня `res2001.bin`.
|
|
||||||
|
|
||||||
## Общий контейнерный формат DOS `.DAT` (кратко)
|
|
||||||
|
|
||||||
```
|
|
||||||
[0x00] u32 LE tableOffset — смещение начала таблицы оглавления
|
|
||||||
[0x04] u16 LE tableSize — размер таблицы оглавления
|
|
||||||
[0x06..tableOffset) — данные ресурсов (конкатенация чанков)
|
|
||||||
[tableOffset..tableOffset+tableSize)
|
|
||||||
— массив записей по 8 байт:
|
|
||||||
u16 size, u16 id, u16 offset(абсолютный), u16 reserved(=0)
|
|
||||||
```
|
|
||||||
|
|
||||||
Инвариант `tableOffset + tableSize == размер файла` подтверждён на всех 28
|
|
||||||
`.dat`-файлах в `MSDOS/`, и независимо — именованием файлов `res<id>.*` в
|
|
||||||
`data/` репозитория SDLPoP.
|
|
||||||
|
|
||||||
## Готовые ассеты для порта (важно для практической работы)
|
|
||||||
|
|
||||||
`github.com/NagyD/SDLPoP/tree/master/data` содержит не только код движка, но
|
|
||||||
и сами ресурсы игры — как сырые `.DAT`, так и распакованные поштучно файлы
|
|
||||||
(`res<id>.png` для спрайтов/фонов, `res<id>.pal` для палитр, `res<id>.bin`
|
|
||||||
для уровней). Для арт-ассетов (в т.ч. нужного полноцветного VGA-варианта
|
|
||||||
дворца/подземелий) практичнее взять их оттуда напрямую, чем писать свой
|
|
||||||
декодер сжатия пикселей DOS `.DAT`.
|
|
||||||
|
|
||||||
## Что дальше по форматам (не сделано и пока не нужно)
|
|
||||||
|
|
||||||
Порт читает уровень напрямую из `res200N.bin` (`roomtest/pop_level.c`), а
|
|
||||||
графику берёт из распакованных PNG `SDLPoP/data/` — поэтому ни один пункт
|
|
||||||
ниже сейчас не блокирует работу.
|
|
||||||
|
|
||||||
1. Точный кодек сжатия пикселей спрайтов в сыром DOS `.DAT` (нужен только
|
|
||||||
если понадобится читать именно нашу локальную копию `MSDOS/*.dat`
|
|
||||||
"как есть", а не ассеты из SDLPoP `data/`) — сверка с исходником SDLPoP,
|
|
||||||
`src/seg009.c`.
|
|
||||||
2. Семантика служебных полей `digisnd*.dat`/`ibm_snd*.dat` перед сырыми
|
|
||||||
сэмплами/нотами (частично прояснено документацией Princed Resources —
|
|
||||||
PC speaker: 1 байт заголовка + повторяющиеся тройки байт "2 байта частоты
|
|
||||||
+ 1 байт длительности"; WAV: 8 бит, моно, unsigned, 11025 Гц).
|
|
||||||
3. Назначение бит `secmask` в `BLUETYPE` (Apple II) и служебного блока
|
|
||||||
`id=2000` в начале DOS `levels.dat` (16 байт в нашей копии, но 2305 байт
|
|
||||||
в версии SDLPoP — расхождение между релизами, не разобрано).
|
|
||||||
4. Оценка, какие видеорежимы/цветовые палитры ZX Sprinter реалистично
|
|
||||||
покрывают исходную графику (CGA/EGA/VGA варианты в DOS-ресурсах против
|
|
||||||
hi-res Apple II) — отдельная архитектурная задача порта, не формат
|
|
||||||
ресурсов как таковой.
|
|
||||||
@@ -1,603 +0,0 @@
|
|||||||
# Фиксированный логический кадр — разбор перед реализацией
|
|
||||||
|
|
||||||
Дата разбора: 2026-08-19. Отправная точка — тег `0.0.1-prealpha`.
|
|
||||||
Статус: **АНАЛИЗ, кода не трогали.**
|
|
||||||
|
|
||||||
Задача пользователя: перевести логический кадр на фиксированный размер,
|
|
||||||
не зависящий от длительности синей/зелёной/циан фаз. Инструмент —
|
|
||||||
`gfx_set_fps_div`, режимы FASTEST / FAST / NORMAL.
|
|
||||||
|
|
||||||
|
|
||||||
## 1. Что на самом деле меняется
|
|
||||||
|
|
||||||
Сейчас главный цикл (`roomtest.c`) после отрисовки ждёт **три**
|
|
||||||
`gfx_wait_vsync()` подряд. Каждый ждёт ближайший фронт луча, поэтому
|
|
||||||
период логического кадра равен
|
|
||||||
|
|
||||||
период = ceil(W) + 2 растровых кадра, W = работа в растрах
|
|
||||||
|
|
||||||
Первое ожидание доедает хвост текущего растра, два следующих — целые
|
|
||||||
растры. Отсюда наблюдаемое: `W <= 1` → период 3; `W = 1.1` → период
|
|
||||||
уже 4. То есть **реальный бюджет логического кадра сегодня — один
|
|
||||||
растр (430 080 тактов)**, всё сверх него стоит целого лишнего растра.
|
|
||||||
|
|
||||||
Нужное поведение:
|
|
||||||
|
|
||||||
период = max(n, ceil(W))
|
|
||||||
|
|
||||||
При `n = 3` бюджет становится **1 290 240 тактов** — втрое больше.
|
|
||||||
Замеренный максимум работы на 13/23 (911 862) укладывается туда с
|
|
||||||
запасом, а 11/15 (437 484 лёгкая / ~603 000 тяжёлая позиция) — вдвойне.
|
|
||||||
|
|
||||||
Это главный выигрыш, и он не про скорость игры, а про **исчезновение
|
|
||||||
скачков**: сегодня превышение растра на один такт стоит +33 % к периоду.
|
|
||||||
|
|
||||||
|
|
||||||
## 2. Три режима и их привязка к оригиналу
|
|
||||||
|
|
||||||
Растровый кадр Sprinter: 320 строк × 896 пикселей при 14 МГц =
|
|
||||||
**20,48 мс** (48,83 Гц). В тактах CPU (21 МГц) — **430 080**;
|
|
||||||
замерено на холостом DSS: 430 131 на прерывание.
|
|
||||||
|
|
||||||
Оригинал (`SDLPoP/src/seg003.c:363`): `BASE_FPS = 60`,
|
|
||||||
`base_speed = 5` тика = **83,3 мс**, `fight_speed = 6` = **100 мс**.
|
|
||||||
|
|
||||||
| режим | обычно | в бою | мс обычно | мс в бою | к оригиналу |
|
|
||||||
|---|---:|---:|---:|---:|---|
|
|
||||||
| FASTEST | 3 | 3 | 61,4 | 61,4 | +36 % скорости |
|
|
||||||
| FAST | 3 | 4 | 61,4 | 81,9 | +36 % / точно |
|
|
||||||
| NORMAL | 4 | 5 | 81,9 | 102,4 | точно (83,3 / 100) |
|
|
||||||
|
|
||||||
NORMAL воспроизводит оригинал с точностью 1,7 % и 2,4 % — расхождение
|
|
||||||
только из-за 48,83 Гц против 60 Гц, целыми делителями точнее не выйдет.
|
|
||||||
|
|
||||||
**Условие «бой» берём у оригинала буквально** — оно проще, чем кажется:
|
|
||||||
|
|
||||||
```c
|
|
||||||
if (Kid.sword == sword_2_drawn) set_timer_length(timer_1, fight_speed);
|
|
||||||
else set_timer_length(timer_1, base_speed);
|
|
||||||
```
|
|
||||||
|
|
||||||
Это не «идёт бой» и не «есть страж рядом», а **только «у Кида вынут
|
|
||||||
меч»**, и проверяется в самом верху главного цикла, до `play_frame()`.
|
|
||||||
У нас поле есть (`Kid.sword`, `SWORD_2_DRAWN` — `guards.c`), так что
|
|
||||||
переключение — одна строка в том же месте цикла.
|
|
||||||
|
|
||||||
Это закрывает и давнюю задачу **L1-SPEED** (`TASKS_OPEN.md`): сейчас мы
|
|
||||||
идём на 60 мс вместо 83,3 — примерно на 39 % быстрее эталона.
|
|
||||||
|
|
||||||
|
|
||||||
## 3. Как устроен темп сейчас и что мешает
|
|
||||||
|
|
||||||
`gfx_wait_vsync()` имеет две ветки (`libbgi/common/gfx_wait_vsync.c`):
|
|
||||||
|
|
||||||
- **`_gfx_fps_div <= 1`** — лучевой поллинг бита 5 порта `0xFE` в тесном
|
|
||||||
цикле, и в этом цикле зовётся **idle-хук** (`gfx_set_idle_hook`).
|
|
||||||
roomtest вешает туда `kbd_raw_poll` — это единственное, что делает
|
|
||||||
клавиатуру работоспособной (задача KBD-1: приёмный FIFO SIO 3 байта,
|
|
||||||
импульс запроса прерывания живёт 32 такта и теряется в DI-окнах
|
|
||||||
акселератора; лечится только плотным опросом раз в ~0,5 мс).
|
|
||||||
- **`_gfx_fps_div >= 2`** — счётчиковый путь: ждёт, пока фоновый ISR
|
|
||||||
(`_gfx_frame_isr`, слот кадровой цепочки) насчитает `n` фронтов, а
|
|
||||||
ожидание реализовано через **`ei; halt`**.
|
|
||||||
|
|
||||||
**И вот здесь блокер.** На счётчиковом пути idle-хук не зовётся вообще.
|
|
||||||
Включив `gfx_set_fps_div(3)` как есть, мы немедленно возвращаем KBD-1:
|
|
||||||
теряются нажатия при зажатом Shift, залипают клавиши. Это не мелочь и
|
|
||||||
не «потом поправим» — это единственная причина, по которой клавиатура
|
|
||||||
сейчас вообще работает.
|
|
||||||
|
|
||||||
|
|
||||||
## 4. Что измерено (MAME, 2026-08-19)
|
|
||||||
|
|
||||||
### Метод и его границы
|
|
||||||
|
|
||||||
Прерывания считались брейкпоинтами на резидентных адресах трамплина
|
|
||||||
(`_irq_tramp` = 0x88B4, ветка кадрового пути `tr_frame` = 0x8995).
|
|
||||||
|
|
||||||
**Счёт попаданий брейкпоинтом на этом драйвере недостоверен**: sprinter
|
|
||||||
дёргает `Z80_INPUT_LINE_WAIT` (`do_mem_wait`), инструкция пересчитывается,
|
|
||||||
и один и тот же PC срабатывает по нескольку раз. Наблюдалось
|
|
||||||
«попаданий больше, чем растровых кадров» и «попаданий в `tr_frame`
|
|
||||||
больше, чем входов в трамплин» — логически невозможные результаты.
|
|
||||||
|
|
||||||
Достоверен только **детектор разрыва**: брейкпоинт на следующей
|
|
||||||
инструкции пишет `temp3 = totalcycles`, брейкпоинт с условием
|
|
||||||
`(totalcycles - temp3) > 0x9D800` (1,5 растра) останавливает машину.
|
|
||||||
Дубли попаданий его не портят. Все числа ниже — этим методом.
|
|
||||||
|
|
||||||
Отдельная грабля: **литералы в отладчике MAME шестнадцатеричные**.
|
|
||||||
Первый прогон с порогом «700000» на деле проверял 0x700000 = 17 растров
|
|
||||||
и не срабатывал никогда.
|
|
||||||
|
|
||||||
### Факты
|
|
||||||
|
|
||||||
1. **Холостой DSS** — 799 прерываний на 343 674 880 тактов = 430 131 на
|
|
||||||
прерывание. Подтверждает константу растра и что потерь нет, когда
|
|
||||||
нечего рисовать.
|
|
||||||
|
|
||||||
2. **11/15, покой (чомпер + два факела + страж)** — потери ЕСТЬ:
|
|
||||||
разрыв ровно 860 129 тактов = два растра = одно потерянное
|
|
||||||
прерывание. Частота в «плохой» фазе: 12 потерь на 427 растровых
|
|
||||||
кадров = **2,8 %**; повтор — 12 на 495 (2,4 %) при бегущем Киде.
|
|
||||||
|
|
||||||
3. **Та же сцена после сдвига Кида** (`]`/`[`, попиксельно) —
|
|
||||||
**0 потерь на 3919 кадров**, и после возврата обратно **0 на 4010**.
|
|
||||||
|
|
||||||
4. **Полная перерисовка комнаты** (переход/`+`) — разрыв 1 720 289 =
|
|
||||||
ровно четыре растра = **три потерянных прерывания подряд**.
|
|
||||||
|
|
||||||
5. **13/23** — ни в покое, ни при беге потерь не поймано; на смене
|
|
||||||
комнаты — поймано.
|
|
||||||
|
|
||||||
6. **Клавиатура ни при чём**: с удержанной клавишей 3,1 %, без неё
|
|
||||||
2,8 % — в пределах разброса.
|
|
||||||
|
|
||||||
### Как это читать
|
|
||||||
|
|
||||||
Пункты 2 и 3 вместе — самое важное. Одна и та же сцена даёт то 2,8 %,
|
|
||||||
то ноль. Значит потеря определяется **не нагрузкой, а фазой**: попадает
|
|
||||||
ли момент кадрового прерывания внутрь DI-окна блита.
|
|
||||||
|
|
||||||
Механизм подтверждён исходником MAME (`sprinter.cpp`):
|
|
||||||
|
|
||||||
- `irq_on` поднимает линию и заводит `irq_off_timer` на **32 такта
|
|
||||||
неразогнанного клока (3,5 МГц) = 9,14 мкс**; `irq_off` гасит. Ядро
|
|
||||||
z80 в MAME уровневое (`m_irq_state` без защёлки) — импульс, пришедший
|
|
||||||
под `di`, теряется НАСОВСЕМ. Это же поведение у настоящего Spectrum
|
|
||||||
(INT 32 такта), так что это не эмуляторный артефакт.
|
|
||||||
- DI-окно у нас — один вызов `_bgi_blit_rows_raw`, а он по контракту
|
|
||||||
режется вызывающим на **чанки ≤16 строк** (≈6 200 тактов ≈ 0,29 мс).
|
|
||||||
То есть DI-окна короткие и с промежутками, отсюда и «то теряем, то
|
|
||||||
нет»: всё решает, куда попал 9-микросекундный импульс.
|
|
||||||
- `irqack_cb` гасит **все три** входа мержера (экран/клавиатура/CBL)
|
|
||||||
одним подтверждением. Плюс трамплин обслуживает за вход ровно один
|
|
||||||
источник и делает приватный RETI. Значит кадровое прерывание может
|
|
||||||
быть съедено клавиатурной веткой или (в будущем) CBL-веткой.
|
|
||||||
|
|
||||||
**Вывод по фазе.** Наш период сейчас 3 растра, но иногда 4 — и каждый
|
|
||||||
такой случай сдвигает фазу рендера относительно луча. Отсюда «полосы»:
|
|
||||||
десятки секунд без потерь, потом полоса с потерями. При жёстком
|
|
||||||
пейсинге период станет РОВНО n, фаза перестанет плавать — и сцена может
|
|
||||||
**залипнуть в плохой фазе надолго**. Это хуже случайных 3 %: систематическая
|
|
||||||
потеря по прерыванию на кадр превратит логический кадр из 3 растров в 4,
|
|
||||||
то есть даст ровные −25 % скорости, которые никак не проявятся в
|
|
||||||
профиле тактов.
|
|
||||||
|
|
||||||
|
|
||||||
## 5. Проблемы по убыванию риска
|
|
||||||
|
|
||||||
| # | проблема | риск |
|
|
||||||
|---|---|---|
|
|
||||||
| P1 | счётчиковый путь ждёт через `halt` → idle-хук не зовётся → возврат KBD-1 (потеря нажатий, залипание клавиш) | блокер |
|
|
||||||
| P2 | счётчик кадров теряет тики (фазозависимо, 0…3 %), при жёстком пейсинге может залипнуть в плохой фазе → ровный минус скорости | высокий |
|
|
||||||
| P3 | полноэкранная перерисовка (вход в комнату, старт уровня) теряет 3+ тика подряд → счётчик недосчитает, ожидание растянется сильнее самой работы | средний |
|
|
||||||
| P4 | звук через CBL (`_irq_cbl_hook`, реальный ISR в трамплине): (а) ещё один источник, крадущий кадровые тики приватным RETI; (б) обратно — любое подтверждение гасит ожидающий запрос CBL → underrun (счётчик `cbl_underruns()` уже есть); (в) ISR длинный (полный сейв обоих наборов + вызов в приложение) | средний, растёт |
|
|
||||||
| P5 | второй call-site `gfx_wait_vsync()` — ветка `frozen` (`roomtest.c:320`) ждёт один фронт; под делителем её смысл меняется | низкий |
|
|
||||||
| P6 | `IRQ_CHAIN_MAX = 4`; делитель занимает слот, звук — свой; запас есть, но конечный | низкий |
|
|
||||||
| P7 | бит 5 порта `0xFE` доступен только при включённом `cbl_mode`; сейчас его лениво занимает сам `gfx_wait_vsync` через `_cbl_port_ref`, при открытии реального звука владение переходит к CBL — переход уже спроектирован, но его надо проверить в связке | низкий |
|
|
||||||
|
|
||||||
Отдельно, не проблема а рычаг: по сообщению разработчиков
|
|
||||||
(IvanMak.txt:846–848) **новая прошивка позволяет акселератору работать с
|
|
||||||
EI** — по приходу прерывания он отключается, по `RETI` включается.
|
|
||||||
Если это подтвердится на железе и моделируется в MAME, P2 исчезает
|
|
||||||
полностью. Проверять отдельно; строить на этом нельзя (неизвестно, какая
|
|
||||||
прошивка у пользователя).
|
|
||||||
|
|
||||||
|
|
||||||
## 6. Варианты реализации
|
|
||||||
|
|
||||||
### A. Включить `gfx_set_fps_div(3)` как есть
|
|
||||||
Отвергается: P1 (убивает клавиатуру) — сразу, без вариантов.
|
|
||||||
|
|
||||||
### B. Свой `wait` в приложении: счётчик только на рендер, ожидание — лучом
|
|
||||||
Счётчик кадровых прерываний отвечает на один вопрос — «сколько фронтов
|
|
||||||
съел рендер», а само ожидание идёт **существующим лучевым поллингом с
|
|
||||||
idle-хуком**, то есть клавиатура работает ровно как сегодня.
|
|
||||||
|
|
||||||
```
|
|
||||||
k = tick - tick_at_frame_start; /* фронтов съел рендер */
|
|
||||||
if (k >= n) k = n - 1; /* опоздали — ждём хотя бы один */
|
|
||||||
повторить (n - k) раз: ждать фронт луча (поллинг + idle-хук)
|
|
||||||
tick_at_frame_start = tick; /* якорь на фактическом фронте */
|
|
||||||
```
|
|
||||||
|
|
||||||
Плюсы: минимальная правка, клавиатура нетронута, фаза переякоривается
|
|
||||||
каждый кадр (ошибка не копится). Минус: остаётся P2 — при залипании в
|
|
||||||
плохой фазе `k` систематически занижен на 1, и кадр ровно на растр
|
|
||||||
длиннее. Профилем это не видно.
|
|
||||||
|
|
||||||
### C. Программный счётчик кадров по лучу (без прерываний вообще)
|
|
||||||
Считать фронты **выборкой бита 5 в точках, которые мы и так проходим**.
|
|
||||||
Окно бланка (строки 272…319) длится **3,07 мс**, максимальное DI-окно —
|
|
||||||
0,29 мс, значит достаточно опрашивать чаще, чем раз в 3 мс.
|
|
||||||
|
|
||||||
Точки выборки: `pop_blit_b` (наша обёртка, зовётся на каждый блит —
|
|
||||||
в фазах рисования это плотнее 0,3 мс) плюс несколько точек в синей фазе
|
|
||||||
(она 149 106 тактов ≈ 6,9 мс без единого блита, нужно 3-4 точки).
|
|
||||||
|
|
||||||
Цена: `in a,(0xFE)` + проверка бита + дедуп фронта ≈ 40-50 тактов; при
|
|
||||||
~50 выборках это 2 500 тактов = 0,6 % растра.
|
|
||||||
|
|
||||||
Плюсы: **точно, и точность не зависит ни от DI, ни от звука, ни от
|
|
||||||
клавиатуры** — снимает P2, P3, P4(а) разом. Минус: заводит инвариант
|
|
||||||
«между выборками не больше 3 мс», который легко нарушить будущей правкой.
|
|
||||||
Инвариант проверяем в MAME тем же детектором разрыва.
|
|
||||||
|
|
||||||
### D. CTC как источник кадра
|
|
||||||
`irq_ctc_install` уже есть (каналы 2+3, вектор 0x06, отдельный от 0xFF).
|
|
||||||
Запрос CTC **защёлкивается** (daisy chain — в `_irq.h` прямо записано,
|
|
||||||
что без `RETI` следующего прерывания не будет), поэтому под `di` он не
|
|
||||||
теряется, а откладывается. Пресет 112 × 160 даёт ровно период кадра
|
|
||||||
без дрейфа (обе частоты — от одного X_SP).
|
|
||||||
|
|
||||||
Блокер: `irq_ctc_install` вектрится напрямую и требует кода в W2 →
|
|
||||||
сейчас только tiny/big, а roomtest — **huge**. Нужна W2-копия
|
|
||||||
CTC-трамплина (в дизайн-доке помечена как follow-up). Плюс защёлка
|
|
||||||
хранит только ОДИН отложенный запрос — на полноэкранной перерисовке
|
|
||||||
(P3) всё равно недосчитает.
|
|
||||||
|
|
||||||
### Рекомендация
|
|
||||||
|
|
||||||
**B как первый шаг, C — как способ закрыть P2**, и оба под одним
|
|
||||||
интерфейсом: приложение зовёт свой `pop_wait_logical_frame(n)`, а чем
|
|
||||||
внутри считаются кадры — деталь реализации. Тогда B→C не трогает ни
|
|
||||||
главный цикл, ни режимы.
|
|
||||||
|
|
||||||
D не нужен, пока C справляется, и требует работы в libc (W2-копия
|
|
||||||
CTC-трамплина) ради того же результата.
|
|
||||||
|
|
||||||
|
|
||||||
## 7. Порядок работ с критериями приёмки
|
|
||||||
|
|
||||||
**Ш0. Инструмент.** Скрипт замера потерь кадровых прерываний (детектор
|
|
||||||
разрыва) — зафиксировать как повторяемую процедуру, он понадобится на
|
|
||||||
каждом шаге. Критерий: воспроизводит числа §4 на 11/15 и на смене комнаты.
|
|
||||||
|
|
||||||
**Ш1. Свой `wait` (вариант B), делитель ещё не включён.** Вынести
|
|
||||||
хвост главного цикла в `pop_wait_logical_frame(n)`, поведение при n=3
|
|
||||||
должно быть **бит-в-бит прежним** (три фронта после работы). Критерий:
|
|
||||||
такты по фазам и распределение периода не изменились, клавиатура
|
|
||||||
работает.
|
|
||||||
|
|
||||||
**Ш2. Счётчик кадров.** Слот кадровой цепочки + `k = tick - anchor`.
|
|
||||||
Критерий: в 11/15 период стал ровно 3 растра ВСЕГДА (сейчас 3/4/5), а
|
|
||||||
на 13/23 — 3 вместо нынешних 3/4/5; клавиатура не деградировала
|
|
||||||
(проверка Shift+стрелки по методике KBD-1, не «на глаз»).
|
|
||||||
|
|
||||||
**Ш3. Режимы.** `POP_SPEED_FASTEST/FAST/NORMAL` + условие боя
|
|
||||||
`Kid.sword == SWORD_2_DRAWN` в верху цикла, как у оригинала. Критерий:
|
|
||||||
NORMAL секундомером совпадает с живым SDLPoP на одинаковом отрезке
|
|
||||||
(методика из L1-SPEED — секундомер, не глазомер).
|
|
||||||
|
|
||||||
**Ш4. Закрыть P2 (вариант C).** Выборка луча в `pop_blit_b` и в синей
|
|
||||||
фазе. Критерий: детектор разрыва не ловит ни одного расхождения между
|
|
||||||
программным счётчиком и лучом за 10 000 кадров, включая смену комнаты.
|
|
||||||
|
|
||||||
**Ш5. Звук.** Только после Ш4: открыть CBL и перемерить P4 —
|
|
||||||
`cbl_underruns()` и потери кадровых тиков.
|
|
||||||
|
|
||||||
## 8. Что проверить артефактом до начала
|
|
||||||
|
|
||||||
1. Сколько именно фронтов съедает вход в комнату — от этого зависит,
|
|
||||||
нужен ли отдельный «resync» на тяжёлых переходах или хватит того,
|
|
||||||
что якорь переставляется каждый кадр.
|
|
||||||
2. Ветка `frozen` (`roomtest.c:320`) — какой темп ей нужен под делителем.
|
|
||||||
3. Проверить, что при n=4/5 бит 5 всё ещё единственный источник фронта
|
|
||||||
(то есть `_cbl_port_ref` держится всё это время).
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 9. Предлагаемый вариант подробно: программный счётчик кадров по лучу
|
|
||||||
|
|
||||||
Дополнение от 2026-08-19 по запросу: как именно получается **точное**
|
|
||||||
число пройденных кадровых интервалов. Акселератор рассматриваем только
|
|
||||||
в нынешнем виде — с DI/EI (режим «акселератор с EI» из новой прошивки
|
|
||||||
из рассмотрения снят).
|
|
||||||
|
|
||||||
## 9.1. Сигнал и его геометрия
|
|
||||||
|
|
||||||
Единственный источник — **бит 5 порта `0xFE`**. Точная семантика по
|
|
||||||
исходнику MAME (`sprinter.cpp`, `kbd_fe_r`):
|
|
||||||
|
|
||||||
```c
|
|
||||||
data |= 0xe0;
|
|
||||||
data ^= 0x40;
|
|
||||||
if (cbl_mode()) {
|
|
||||||
data &= ~0xa0; /* гасит биты 5 и 7 */
|
|
||||||
data |= (vpos >= BORDER_TOP + SCREEN_YSIZE) << 5;
|
|
||||||
data |= ... & 0x80; /* бит 7 — CBL */
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
То есть **бит 5 = 1 ровно тогда, когда луч ниже картинки**
|
|
||||||
(`vpos >= 16 + 256 = 272`), и это ЧТЕНИЕ ПОЛОЖЕНИЯ ЛУЧА, а не событие:
|
|
||||||
ни прерывания, ни защёлки, ни очереди — его невозможно «потерять»,
|
|
||||||
можно только не посмотреть.
|
|
||||||
|
|
||||||
Важное следствие из той же строки: вне `cbl_mode` бит читается как 1
|
|
||||||
всегда (его выставляет `data |= 0xe0` и уже ничто не гасит). Поэтому
|
|
||||||
счётчик обязан работать только при взведённом `_cbl_port_ref()` — том
|
|
||||||
самом, который сейчас лениво взводит `gfx_wait_vsync`.
|
|
||||||
|
|
||||||
Геометрия кадра (320 строк × 896 пикселей при 14 МГц):
|
|
||||||
|
|
||||||
| | строк | мс | тактов CPU (21 МГц) |
|
|
||||||
|---|---:|---:|---:|
|
|
||||||
| бит 5 = 1 (нижний бланк) | 48 | 3,07 | **64 512** |
|
|
||||||
| бит 5 = 0 (картинка + верхний бордер) | 272 | 17,41 | 365 568 |
|
|
||||||
| кадр целиком | 320 | 20,48 | 430 080 |
|
|
||||||
|
|
||||||
Границей кадра берём **фронт 1→0** — это `vpos = 0`, ровно то же
|
|
||||||
событие, которого ждёт сегодняшний `gfx_wait_vsync`. Значит момент
|
|
||||||
свопа страниц не меняется: до начала картинки остаётся верхний бордер,
|
|
||||||
16 строк ≈ 1 мс запаса, как и сейчас.
|
|
||||||
|
|
||||||
## 9.2. Счётчик
|
|
||||||
|
|
||||||
```c
|
|
||||||
static uint8_t beam_prev; /* бит 5 на прошлой выборке */
|
|
||||||
static uint8_t frame_tick; /* счётчик кадров, разностная арифметика */
|
|
||||||
|
|
||||||
/* ~20 T-состояний тела + вызов; в тактах MAME ≈ 120 на выборку */
|
|
||||||
void pop_beam_sample(void)
|
|
||||||
{
|
|
||||||
uint8_t b = in_fe() & 0x20;
|
|
||||||
if (beam_prev && !b) frame_tick++; /* фронт 1→0 = начало кадра */
|
|
||||||
beam_prev = b;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Вся арифметика ожидания — разностная по модулю 256, wrap безопасен
|
|
||||||
(тот же приём, что в существующем `_gfx_fps_state`).
|
|
||||||
|
|
||||||
## 9.3. Почему счёт ТОЧНЫЙ (условие и запас)
|
|
||||||
|
|
||||||
Утверждение: **если между соседними выборками проходит меньше 64 512
|
|
||||||
тактов, то каждый фронт 1→0 будет засчитан ровно один раз.**
|
|
||||||
|
|
||||||
Доказательство прямое. Пусть максимальный зазор между выборками
|
|
||||||
Δ < 64 512. Окно «бит 5 = 1» длится 64 512 тактов, то есть длиннее Δ,
|
|
||||||
значит в него попадает хотя бы одна выборка → `beam_prev` обязательно
|
|
||||||
станет 1 внутри каждого бланка. Окно «бит 5 = 0» длится 365 568 — тем
|
|
||||||
более содержит выборку → сразу после бланка `beam_prev` перейдёт в 0 и
|
|
||||||
даст ровно один инкремент. Двойной счёт невозможен: инкремент
|
|
||||||
происходит только на переходе 1→0, а `beam_prev` тут же обновляется.
|
|
||||||
|
|
||||||
Условие ОДНО и оно про зазор, а не про нагрузку, не про DI, не про
|
|
||||||
прерывания. Отсюда все свойства варианта.
|
|
||||||
|
|
||||||
**Какой запас по факту.** Самый длинный неделимый кусок кода без
|
|
||||||
возможности выборки — одно DI-окно акселератора, то есть один вызов
|
|
||||||
`_bgi_blit_rows_raw`. Он по контракту режется вызывающим на чанки
|
|
||||||
**≤16 строк**; при ширине 32 это ≈ 6 200 тактов, при полной высоте
|
|
||||||
спрайта 63 строки самый дорогой замеренный блит целиком — 32 073.
|
|
||||||
Даже если мерить самым грубым образом (одна выборка на целый блит,
|
|
||||||
а не на чанк), зазор вдвое меньше окна бланка.
|
|
||||||
|
|
||||||
## 9.4. Где ставить выборки
|
|
||||||
|
|
||||||
Правило простое: **выборка обязана стоять так, чтобы ни один путь
|
|
||||||
исполнения не давал зазора длиннее 64 512 тактов.** По фазам:
|
|
||||||
|
|
||||||
- **Зелёная и циан** (436 494 и 382 770 тактов на 13/23) состоят из
|
|
||||||
блитов и хилов. Достаточно одной выборки на вызов наших обёрток
|
|
||||||
`pop_blit_b`, `pop_heal_off`, `pop_heal_fast` — но НЕ только их:
|
|
||||||
прямые вызовы `gfx_blit_*` / `gfx_heal*` разбросаны по шести файлам
|
|
||||||
(`pop_cdraw.c`, `pop_draw.c`, `pop_kdraw.c`, `pop_room.c`,
|
|
||||||
`pop_state.c`, `pop_tile.c`). Точный набор точек определяем НЕ
|
|
||||||
рассуждением, а замером (см. 9.7): ставим в обёртки, меряем худший
|
|
||||||
зазор, добавляем точки только там, где замер их требует.
|
|
||||||
- **Синяя** (149 106 тактов) — блитов нет вообще, это 2,3 окна бланка.
|
|
||||||
Нужны явные точки: после ввода, после физики, после `pop_process_trobs`,
|
|
||||||
после mob-тика. Ставятся на границах, которые и так размечены
|
|
||||||
зондами `pop_dbg_m*`.
|
|
||||||
|
|
||||||
Чего заведомо НЕ хватит: выборок только в главном цикле. Один
|
|
||||||
`pop_floor_bake` — 179 914 тактов, почти три окна бланка.
|
|
||||||
|
|
||||||
## 9.5. Ожидание — тот же примитив
|
|
||||||
|
|
||||||
Ожидание фронта и есть плотная выборка, поэтому оно сливается со
|
|
||||||
счётчиком, а опрос клавиатуры остаётся ровно таким же плотным, как
|
|
||||||
сегодня:
|
|
||||||
|
|
||||||
```c
|
|
||||||
static void wait_edge(void)
|
|
||||||
{
|
|
||||||
uint8_t t = frame_tick;
|
|
||||||
do {
|
|
||||||
pop_beam_sample();
|
|
||||||
kbd_raw_poll(); /* то, что сейчас висит idle-хуком */
|
|
||||||
} while (frame_tick == t);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`gfx_set_idle_hook` при этом больше не нужен — опрос зовётся прямо.
|
|
||||||
Обязателен аварийный выход по счётчику попыток (как в нынешнем
|
|
||||||
`gfx_wait_vsync`): на железе, где бит ведёт себя иначе, цикл не должен
|
|
||||||
виснуть насмерть.
|
|
||||||
|
|
||||||
## 9.6. Пейсинг целиком
|
|
||||||
|
|
||||||
```c
|
|
||||||
uint8_t k = (uint8_t)(frame_tick - anchor); /* фронтов съел рендер */
|
|
||||||
if (k >= n) {
|
|
||||||
wait_edge(); /* опоздали — выравниваемся на ближайший */
|
|
||||||
} else {
|
|
||||||
do { wait_edge(); } while ((uint8_t)(frame_tick - anchor) < n);
|
|
||||||
}
|
|
||||||
anchor = frame_tick; /* якорь по ФАКТУ, фаза не копится */
|
|
||||||
flip_page();
|
|
||||||
```
|
|
||||||
|
|
||||||
`anchor = frame_tick`, а не `anchor += n` — сознательно: догонять
|
|
||||||
пропущенное время нельзя, иначе после тяжёлого кадра игра рванёт
|
|
||||||
вперёд. Это же правило заложено в исходном дизайне делителя
|
|
||||||
(«выравнивание на ближайший фронт, без накопления фазовой ошибки»).
|
|
||||||
|
|
||||||
Поведение по случаям:
|
|
||||||
|
|
||||||
| работа W (растров) | период | комментарий |
|
|
||||||
|---|---|---|
|
|
||||||
| W ≤ n | ровно n | цель задачи |
|
|
||||||
| n < W ≤ n+1 | ceil(W) | подтормаживает ровно настолько, насколько не успели |
|
|
||||||
| вход в комнату, W ≫ n | ceil(W) + 1 | ветка «опоздали»: один фронт, без растягивания |
|
|
||||||
| загрузка уровня, файловые операции | ceil(W) + 1…n | выборок нет вовсе → k занижен; худшее — n лишних растров ОДИН раз |
|
|
||||||
|
|
||||||
Последняя строка — единственный случай, где счёт неточен, и он
|
|
||||||
безобиден: во время `ESTEX`-вызова выбирать нечего, а ошибка живёт один
|
|
||||||
кадр, потому что якорь переставляется по факту.
|
|
||||||
|
|
||||||
## 9.7. Как это доказывается, а не декларируется
|
|
||||||
|
|
||||||
Инструмент уже построен и проверен на нынешнем коде (§4): брейкпоинт на
|
|
||||||
следующей инструкции пишет `temp3 = totalcycles`, второй с условием
|
|
||||||
`(totalcycles - temp3) > 0x9D800` останавливает машину.
|
|
||||||
|
|
||||||
Для приёмки он ставится **на инструкцию инкремента `frame_tick`**.
|
|
||||||
Если хоть один фронт пропущен, зазор между инкрементами станет два
|
|
||||||
растра и детектор остановит машину. Критерий: **10 000 кадров без
|
|
||||||
единого срабатывания**, включая смену комнаты и смерть Кида.
|
|
||||||
|
|
||||||
Дополнительно, в отладочной сборке — перекрёстная проверка со счётчиком
|
|
||||||
кадровых прерываний (слот цепочки, один INC): прерывания теряются, луч
|
|
||||||
не должен, значит `frame_tick` обязан идти НЕ МЕДЛЕННЕЕ `irq_tick`.
|
|
||||||
Расхождение в другую сторону = пропущенная выборка.
|
|
||||||
|
|
||||||
Напоминание о методике: **счёт попаданий брейкпоинтом на этом драйвере
|
|
||||||
недостоверен** (WAIT-линия, инструкция пересчитывается) — только
|
|
||||||
детектор разрыва. И литералы в отладчике MAME шестнадцатеричные.
|
|
||||||
|
|
||||||
## 9.8. Цена
|
|
||||||
|
|
||||||
| статья | тактов |
|
|
||||||
|---|---:|
|
|
||||||
| одна выборка (с вызовом) | ≈ 120 |
|
|
||||||
| ~60 выборок за логический кадр | ≈ 7 200 |
|
|
||||||
| доля от бюджета при n=3 (1 290 240) | **0,6 %** |
|
|
||||||
|
|
||||||
В горячих местах (`pop_blit_b`) выборку можно заинлайнить и снять цену
|
|
||||||
вызова.
|
|
||||||
|
|
||||||
## 9.9. Чем это лучше счётчика прерываний
|
|
||||||
|
|
||||||
| | счётчик кадровых IRQ | счётчик по лучу |
|
|
||||||
|---|---|---|
|
|
||||||
| теряет тик под `di` акселератора | да, фазозависимо 0…3 % | нет — читается положение луча |
|
|
||||||
| теряет тик, если IRQ съела клавиатурная/CBL-ветка трамплина | да (приватный RETI) | нет |
|
|
||||||
| ломается от добавления звука | да (ещё один источник, `irqack` гасит все входы мержера) | нет |
|
|
||||||
| поведение на полной перерисовке | 3 тика подряд мимо | считает все |
|
|
||||||
| условие корректности | никакого — не в нашей власти | зазор выборок < 64 512 тактов, проверяется артефактом |
|
|
||||||
| риск залипнуть в плохой фазе и ровно потерять 25 % скорости | есть | нет |
|
|
||||||
|
|
||||||
## 9.10. Что осталось проверить зондами до кодирования
|
|
||||||
|
|
||||||
1. **Худший зазор между выборками** при размещении «только в обёртках»
|
|
||||||
— сколько точек реально нужно добавить. Это же число решает, нужна
|
|
||||||
ли выборка в синей фазе в четырёх местах или в двух.
|
|
||||||
2. **Владение `cbl_mode`**: сейчас бит 5 доступен потому, что
|
|
||||||
`gfx_wait_vsync` взвёл `_cbl_port_ref()` при первом вызове. Свой
|
|
||||||
ожидатель обязан взвести его сам — значит примитив логичнее держать
|
|
||||||
в libbgi (там доступен `_cbl_port_ref`), а не в приложении.
|
|
||||||
3. **Ветка `frozen`** (`roomtest.c:320`) — какой темп ей нужен.
|
|
||||||
4. Совпадает ли момент возврата `wait_edge()` с нынешним возвратом
|
|
||||||
`gfx_wait_vsync()` с точностью до микросекунд (иначе поедет момент
|
|
||||||
свопа и появятся разрывы картинки).
|
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 10. РЕЗУЛЬТАТ (2026-08-19, реализовано и проверено в MAME)
|
|
||||||
|
|
||||||
Реализовано в приложении (`roomtest/pop_pace.c/.h`), в libbgi пока НИЧЕГО не
|
|
||||||
переносили — по решению пользователя: сначала обкатать у себя.
|
|
||||||
|
|
||||||
## 10.1. Что сделано
|
|
||||||
|
|
||||||
- `pop_beam_sample()` — выборка бита 5 порта `0xFE`, 10 инструкций,
|
|
||||||
быстрый путь 46 T + вызов. Модуль НЕ банковый, поэтому из банков
|
|
||||||
зовётся прямым `call` (проверено: банки так зовут `_pop_cd_hit_slot`).
|
|
||||||
- `pop_wait_edge()` — ожидание одного фронта; внутри тот же
|
|
||||||
`kbd_raw_poll()`, что раньше висел idle-хуком.
|
|
||||||
- `pop_pace_end(n)` — добрать до n фронтов от якоря; якорь ставится ПО
|
|
||||||
ФАКТУ. Главный цикл: `pop_wait_edge()` → строб вспышки →
|
|
||||||
`pop_pace_end(n)` → своп страниц.
|
|
||||||
- `pop_pace_arm()` — взводит `cbl_mode` через `gfx_wait_vsync()` и
|
|
||||||
ПРОВЕРЯЕТ, что фронты идут; если нет — `pace_ok = 0` и всё молча
|
|
||||||
откатывается на прежние `gfx_wait_vsync`.
|
|
||||||
- Режимы FASTEST/FAST/NORMAL, клавиша **P** по кругу, дефолт FASTEST.
|
|
||||||
Условие боя — `Kid.sword == SWORD_2_DRAWN`, буквально как у оригинала.
|
|
||||||
|
|
||||||
## 10.2. Где стоят выборки и как они выбраны
|
|
||||||
|
|
||||||
Точки ставились **не на глаз, а по замеру**: детектор зазора между
|
|
||||||
выборками (порог 64 512) останавливает машину, адрес возврата со стека
|
|
||||||
называет виновника. Пять итераций «замерил → закрыл дыру → перемерил»:
|
|
||||||
|
|
||||||
| итерация | найденная дыра | тактов |
|
|
||||||
|---|---|---:|
|
|
||||||
| 1 | `kid_tick` + `pop_phys_tick` без выборок | 68 340 |
|
|
||||||
| 1 | весь циан, когда оба персонажа «тихие» | 100 044 |
|
|
||||||
| 2 | отрисовка персонажей идёт мимо `pop_blit_b` | 82 242 |
|
|
||||||
| 2 | полоса HP: `pop_kid_img_blit` в цикле | 86 586 |
|
|
||||||
| 3 | между двумя `pop_blit_b` — работа `pop_bg` | 75 000 |
|
|
||||||
| 4 | сам блит и сам heal (выборка была только НА ВХОДЕ) | 71 900 |
|
|
||||||
| 5 | `pop_loose_tick` | 73 990 |
|
|
||||||
|
|
||||||
Итог: выборки в `pop_blit_b` (вход и перед каждым `gfx_w0_unmap`),
|
|
||||||
`pop_heal_fast` (вход и выход), после каждого `gfx_set_bank(SPRITE)` в
|
|
||||||
`pop_cdraw/pop_kdraw/pop_room`, в 16 потайловых функциях `pop_bg`, после
|
|
||||||
зондов `pop_dbg_p1..p8` в физике и в 21 точке главного цикла.
|
|
||||||
|
|
||||||
## 10.3. Замеры
|
|
||||||
|
|
||||||
Метод точности счётчика — **атомарный снимок одной командой отладчика**:
|
|
||||||
`printf "%d %d", totalcycles, b@<адрес pop_frame_tick>`. Раздельные
|
|
||||||
`lmem` и `print totalcycles` НЕ ГОДЯТСЯ: между двумя обращениями к мосту
|
|
||||||
проходят десятки кадров, и «недосчёт» получается на ровном месте (на этом
|
|
||||||
я сначала и обжёгся).
|
|
||||||
|
|
||||||
| проверка | результат |
|
|
||||||
|---|---|
|
|
||||||
| счётчик, 11/15 покой, 5 окон | недосчёт **0** (270 растровых кадров) |
|
|
||||||
| счётчик, 11/15 тяжёлая позиция Кида | недосчёт **0** |
|
|
||||||
| счётчик, 13/23 | недосчёт **0** |
|
|
||||||
| период кадра, 11/15 | **ровно 3 растра**: ни длиннее 3,1, ни короче 2,9 на 302 логических кадрах |
|
|
||||||
| период кадра, 13/23 | **ровно 3 растра** на 308 логических (эталон был 3/4/5) |
|
|
||||||
| режим NORMAL | ровно 4 растра |
|
|
||||||
| NORMAL + `Kid.sword = 2` | ровно 5 растров |
|
|
||||||
| клавиша P | 0 → 1 → 2 → 0 |
|
|
||||||
| клавиатура | Кид отвечает на удержание и отпускание |
|
|
||||||
|
|
||||||
**Цена выборок** — A/B прямо в памяти (заглушить `pop_beam_sample`
|
|
||||||
байтом `C9` и снять `pace_ok`, чтобы игра не зависла в ожидании фронта):
|
|
||||||
работа за кадр 543 860 с выборками против 539 832 без — **≈4 000 тактов,
|
|
||||||
0,9 %**. На фоне бюджета, который вырос втрое, это ничто.
|
|
||||||
|
|
||||||
## 10.4. Грабли, стоившие времени
|
|
||||||
|
|
||||||
1. **Литералы в отладчике MAME шестнадцатеричные.** Порог «700000» на
|
|
||||||
деле проверял 0x700000 = 17 растров и не срабатывал никогда.
|
|
||||||
2. **Счёт попаданий брейкпоинтом на этом драйвере недостоверен** (WAIT-
|
|
||||||
линия, инструкция пересчитывается): наблюдались «попаданий больше, чем
|
|
||||||
растровых кадров». Достоверны только сравнения ВРЕМЁН.
|
|
||||||
3. **Раздельные чтения через мост не атомарны** (см. 10.3).
|
|
||||||
4. **Мёртвый Кид перезапускает уровень** раз в `RESPAWN_DELAY` тиков, а
|
|
||||||
рестарт уровня — это 3-5 растров без единой выборки. Полдня я гонялся
|
|
||||||
за «дырой в статике», которой не было: Кид успел убежать в соседнюю
|
|
||||||
комнату и погибнуть, пока я мерил. **Проверяй, что на экране, прежде
|
|
||||||
чем объяснять числа.**
|
|
||||||
5. **`make LEVEL=13` без `make clean` не пересобирает** — флаги в
|
|
||||||
зависимостях не участвуют, на диск уезжает старый уровень.
|
|
||||||
|
|
||||||
## 10.5. Что осталось
|
|
||||||
|
|
||||||
- Перенос примитива в libbgi — по решению пользователя ПОСЛЕ обкатки.
|
|
||||||
Там же уместнее взводить `_cbl_port_ref` напрямую, без обходного
|
|
||||||
`gfx_wait_vsync()` в `pop_pace_arm`.
|
|
||||||
- Загрузка уровня/комнаты остаётся без выборок (ESTEX-вызовы) — счётчик
|
|
||||||
там недосчитывает. Это безобидно: якорь переставляется по факту, и
|
|
||||||
ошибка живёт один кадр. Отдельного «resync» не потребовалось.
|
|
||||||
- Проверить на реальном железе, что `pace_ok` взводится (в MAME — да).
|
|
||||||
@@ -1,279 +0,0 @@
|
|||||||
# Интерактивные объекты (кнопки/гейты/пики) + HP/смерть — ПОДРОБНЫЙ план
|
|
||||||
|
|
||||||
> **Статус: РЕАЛИЗОВАНО (2026-08-01).** Все фазы плана (P0 персистентный
|
|
||||||
> per-room `room_modif`, S пики, B кнопки+ворота) сделаны и играются:
|
|
||||||
> `roomtest/pop_trob.c` (trob-диспетчер, `LINKLOC`/`LINKMAP`, ворота, дверь
|
|
||||||
> уровня, факелы, зелья), `pop_map.c` (HP, смерть на пиках, урон падения),
|
|
||||||
> `pop_redraw.c` (пометки перерисовки вместо прямых блитов). Ограничения
|
|
||||||
> из §0 закрыты: тайлы персистентны, HP/смерть есть, loose обобщён в trob;
|
|
||||||
> L3-вверх (climb-up в комнату сверху) тоже сделан (`pop_leave_dir = 3`).
|
|
||||||
> Из §5 остаётся открытым только **переход на следующий уровень через дверь
|
|
||||||
> уровня** — он вынесен в `levels_plan.md`.
|
|
||||||
>
|
|
||||||
> **Документ оставлен как СПРАВОЧНИК**, а не как план: §1 (раскладка
|
|
||||||
> объектов уровня 1 по комнатам, декод связей кнопка→цель) и §2 (точные
|
|
||||||
> ссылки на механику SDLPoP) продолжают экономить время при отладке.
|
|
||||||
> Текущие задачи — `../roomtest/TASKS_OPEN.md`.
|
|
||||||
|
|
||||||
Составлен 2026-07-20. Документ самодостаточный: рассчитан на старт
|
|
||||||
«с чистого листа» (пустой контекст). Всё сверено с
|
|
||||||
`applications/PoP/SDLPoP/src/` и данными `res2001.bin`.
|
|
||||||
|
|
||||||
Правило проекта (см. `applications/PoP/CLAUDE.md`): **SDLPoP — источник истины**,
|
|
||||||
перед кодингом читать соответствующий код seg*.c, не гадать.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 0. КОНТЕКСТ: текущее состояние `applications/PoP/roomtest` (что уже готово)
|
|
||||||
|
|
||||||
roomtest — живой прототип порта PoP: комната 1 уровня 1 живой композицией
|
|
||||||
тайлов + Kid (анимация/управление/коллизия/падение/зацеп/переходы). Собрать:
|
|
||||||
`cd applications/PoP/roomtest && make`. Тест в MAME: см. memory
|
|
||||||
`mame_mcp_bridge`/`mame_hdd_test_disk` (канонический цикл: `make` → пересобрать
|
|
||||||
`mame/v306/IMG/test_hdd.chd` через `toolchain/make_hdd.sh` со всеми ассетами →
|
|
||||||
рестарт `run_bridge.sh` → `resume` → ~13с бут → `type_string("d:{ENTER}roomtest.exe{ENTER}")`).
|
|
||||||
Отладка: клавиши `1`=freeze / `2`=resume в roomtest; MCP-мост `mame-z80`
|
|
||||||
(read_logical_memory, set_breakpoint, disassemble); адреса символов —
|
|
||||||
`.sprinter-cc-roomtest/roomtest.map` (сдвигаются при пересборке!).
|
|
||||||
|
|
||||||
### Модули (все в `applications/PoP/roomtest/`)
|
|
||||||
- `roomtest.c` — главный цикл (дабл-буфер 2 стр.), `enter_room(room)`,
|
|
||||||
обработчики переходов. File-static рабочие массивы (W2):
|
|
||||||
`room_fg[30]`, `room_bg[30]`, `lcol_fg/lcol_bg[3]`, `rcol_fg/rcol_bg[3]`,
|
|
||||||
`below_fg[10]`, `cur_room`.
|
|
||||||
- `pop_level.c/.h` — **уровень из файла** (Фаза L1):
|
|
||||||
- `pop_level_load("res2001.bin")` — читает сырой blueprnt в EMM-страницу
|
|
||||||
(данные с offset `0x100`, ISR-стаб как атлас).
|
|
||||||
- `pop_room_load(room, fg,bg, lcol_fg,lcol_bg, rcol_fg,rcol_bg, below_fg)` —
|
|
||||||
извлекает комнату (fg маскирован `&0x1F`, bg raw) + срезы соседей:
|
|
||||||
leftcol=col9 левого соседа, rightcol=col0 правого, belowrow=row0 нижнего.
|
|
||||||
- `pop_room_link(room, side)` — связь (side 0=L,1=R,2=U,3=D; 0=нет).
|
|
||||||
- `pop_level_start_room/pos/dir()`.
|
|
||||||
- `pop_bg.c/.h` — отрисовка тайлов (порт seg008 draw_tile), fore-окклюзия над
|
|
||||||
Kid (`pop_fore_over_char`, порт set_char_collision+redraw_at_char/char2),
|
|
||||||
**loose-полы** (shake/bake/mob). `draw_tile` — статическая, знает
|
|
||||||
`draw_gate_back` (грань гейта из левой комнаты).
|
|
||||||
- `pop_kid.c/.h` — анимация Kid (интерпретатор seqtbl `play_seq`, порт seg006),
|
|
||||||
`Kid` struct (frame,x,y,dir,curr_col,curr_row,action,fall_x,fall_y,repeat,
|
|
||||||
curr_seq); `knock`-флаг; `kid_cur_dx/flags`, `kid_fp_*` (футпринт).
|
|
||||||
- `pop_ctrl.c/.h` — ввод (порт seg005 control) через `<kbd_raw.h>`.
|
|
||||||
- `pop_map.c/.h` — коллизия/физика (порт seg005/006). Ключевое:
|
|
||||||
- `pop_map_set(fg)` — карта текущей комнаты.
|
|
||||||
- `pop_map_set_edges(l,r,u,d, lcol_fg, rcol_fg)` — связи + кромки швов для
|
|
||||||
коллизии (`get_tile(col=-1)`=lcol, `get_tile(col=10)`=rcol; порт
|
|
||||||
find_room_of_tile).
|
|
||||||
- `pop_phys_tick()` — кадр физики (fall/land/wall/knock/leave).
|
|
||||||
- Переходы: `pop_fell_out` (вниз, y>=211), `pop_leave_dir` (1=left,2=right,
|
|
||||||
x∓140).
|
|
||||||
- **loose-состояние**: `pop_loose_modif[30]` (публично, читает pop_bg),
|
|
||||||
`loose_bake[30]`, `loose_rest[30]` (static); `pop_loose_tick()`,
|
|
||||||
`pop_loose_reset()` (сброс при смене комнаты).
|
|
||||||
|
|
||||||
### Что сделано по фазам
|
|
||||||
- **L1** — данные уровня из файла (room1_data.h удалён). Коммит `21f978d`.
|
|
||||||
- **L2** — переход в комнату снизу (провал/спуск), фиксы окклюзии. Коммит `7f3e32d`.
|
|
||||||
- **L3** — переходы вбок (право+лево) через швы. **Не закоммичено** на момент
|
|
||||||
написания (вместе с этим планом). **L3-вверх (climb-up в комнату сверху) —
|
|
||||||
НЕ сделано.**
|
|
||||||
- **Loose-полы** (тряска knock / падение mob+окклюзия) — коммит `8b30dc2`.
|
|
||||||
|
|
||||||
### Известные ОГРАНИЧЕНИЯ (важно для этого плана)
|
|
||||||
1. **Нет персистентности тайлов**: `enter_room`→`pop_room_load` каждый раз
|
|
||||||
перезагружает ИСХОДНЫЕ тайлы из level-страницы. Изменения (упавший loose,
|
|
||||||
открытый гейт) при повторном входе ТЕРЯЮТСЯ. Для кнопок/гейтов это
|
|
||||||
блокер (см. P0).
|
|
||||||
2. **Нет HP/смерти** Кида (нужно для пик).
|
|
||||||
3. Loose-механика — частный случай trob (нужно обобщить).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. ДАННЫЕ УРОВНЯ (формат, offsets, объекты)
|
|
||||||
|
|
||||||
Сырой `res2001.bin` (2305 Б) = blueprnt DAT 1.0 (Table 6 в
|
|
||||||
`POP-DAT-FormatSpecifications.txt`). Читается в EMM-страницу с offset `0x100`.
|
|
||||||
Тайл-код = байт `& 0x1F`; верхние биты (модификатор BLUETYPE) сейчас отброшены.
|
|
||||||
|
|
||||||
| Блок | Offset | Размер |
|
|
||||||
|------|--------|--------|
|
|
||||||
| foretable (fg) | 0 | 720 (24 комн × 30) |
|
|
||||||
| backtable (bg=modifier) | 720 | 720 |
|
|
||||||
| **LINKLOC** (doorlink1) | **1440** | 256 |
|
|
||||||
| **LINKMAP** (doorlink2) | **1696** | 256 |
|
|
||||||
| links (roomlinks) | 1952 | 96 (24×{L,R,U,D}) |
|
|
||||||
| start_position | 2112 | 3 (room,pos,dir) |
|
|
||||||
|
|
||||||
Тайл-коды: `0x00`empty `0x01`floor `0x02`**SPIKE** `0x03`pillar `0x04`**GATE**
|
|
||||||
`0x06`**DROP-кнопка(closer)** `0x0B`loose `0x0F`**RAISE-кнопка(opener)**
|
|
||||||
`0x10`lvldoor-L `0x11`lvldoor-R `0x13`torch `0x14`wall.
|
|
||||||
|
|
||||||
### Объекты уровня 1 (по комнатам)
|
|
||||||
```
|
|
||||||
room 5: DROP(0,2)m11 RAISE(0,4)m9 GATE(0,5)m2 RAISE(0,6)m8 GATE(0,9)m1
|
|
||||||
room 6: RAISE(0,2)m7 SPIKE(2,3) SPIKE(2,4) <-- тестовая
|
|
||||||
room 7: RAISE(0,2)m6 GATE(0,9)m2
|
|
||||||
room 8: RAISE(0,6)m5 GATE(0,9)m2 RAISE(1,7)m4
|
|
||||||
room 9: RAISE(0,0)m3 (+ lvldoor(1,3)/(1,4) — выход на level2, отложено)
|
|
||||||
room12: RAISE(0,3)m2 GATE(0,9)m2 SPIKE(2,4)
|
|
||||||
room10/13/14/16/19/24: только SPIKE
|
|
||||||
room20: DROP(1,4)m1 RAISE(1,7)m0
|
|
||||||
```
|
|
||||||
(m = modifier тайла = ИНДЕКС в LINKLOC/LINKMAP.)
|
|
||||||
|
|
||||||
### Room6 (тестовая) — раскладка
|
|
||||||
```
|
|
||||||
fg row0: 13 01 0F 00 03 01 01 13 01 03 (0,2)=RAISE-кнопка, (0,0)/(0,7)=torch
|
|
||||||
fg row1: 14 14 14 00 14 14 14 14 14 14 (1,3)=empty
|
|
||||||
fg row2: 14 14 14 02 02 14 14 14 14 14 (2,3)(2,4)=SPIKE
|
|
||||||
links: L=8 R=2 U=5 D=0
|
|
||||||
```
|
|
||||||
- **Шахта пик**: col3 (row0=empty, row1=empty, row2=spike) + col4 (spike).
|
|
||||||
- **Гейт, видимый у ЛЕВОЙ кромки room6, — это гейт room8 (0,9)**, отрисованный
|
|
||||||
в col0 room6 через левый шов (L=8, leftcol=col9 room8). В room6 гейта НЕТ.
|
|
||||||
|
|
||||||
### Связь кнопка→цель (декод LINKLOC/LINKMAP), проверено:
|
|
||||||
- `get_doorlink_tile(i) = d1[i] & 0x1F`
|
|
||||||
- `get_doorlink_next(i) = !(d1[i] & 0x80)` (0 = конец цепочки)
|
|
||||||
- `get_doorlink_room(i) = ((d1[i]&0x60)>>5) + ((d2[i]&0xE0)>>3)`
|
|
||||||
- `get_doorlink_timer(i) = d2[i] & 0x1F`
|
|
||||||
- где `d1`=LINKLOC@1440, `d2`=LINKMAP@1696. Цепочка: idx++ пока next.
|
|
||||||
|
|
||||||
**Кнопка room6 (0,2) mod=7 → цель: room8 tile(0,9) = ГЕЙТ.** Подтверждено
|
|
||||||
в SDLPoP-скринах: нажатие кнопки поднимает решётку у левой кромки room6.
|
|
||||||
Связь КРОСС-КОМНАТНАЯ (кнопка в room6, гейт в room8) и задаётся таблицей,
|
|
||||||
а НЕ позицией. Кнопка может открывать НЕСКОЛЬКО гейтов в разных комнатах.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. МЕХАНИКА SDLPoP (точные ссылки)
|
|
||||||
|
|
||||||
### 2.1 Trob-система (анимируемые тайлы)
|
|
||||||
- `add_trob(room,tilepos,type)` seg007:0A5A — в список анимируемых.
|
|
||||||
- Каждый кадр `redraw_needed_tiles`/`process_trobs` продвигает; диспетч по
|
|
||||||
типу тайла → `animate_button/animate_door/animate_spike/animate_loose`
|
|
||||||
(seg007:0033+ таблица `animate_*`).
|
|
||||||
- Состояние тайла хранится в `curr_room_modif[tilepos]` (per-room modifier).
|
|
||||||
- У нас есть частный случай для loose (`pop_loose_tick`+`pop_loose_modif[30]`).
|
|
||||||
|
|
||||||
### 2.2 Пики (spike)
|
|
||||||
- **Триггер выдвижения** — `check_spike_below()` seg006:1199 (зовётся в
|
|
||||||
физике Кида каждый кадр): для каждой колонки футпринта Кида
|
|
||||||
(`get_tile_div_mod_m7(char_x_left)`..`char_x_right`) идёт ВНИЗ от
|
|
||||||
`Char.curr_row` через НЕ-floor тайлы; если встретил `tiles_2_spike` →
|
|
||||||
`start_anim_spike(room,tilepos)`. → Кид у края (0,2) правым краём задевает
|
|
||||||
col3 → скан вниз col3 (empty/empty/spike) → пики вылезают.
|
|
||||||
- `start_anim_spike` seg007:596: если `modif<=0`: `modif==0` → add_trob(type1)
|
|
||||||
+ звук; `modif<0` (кроме 0xFF disabled) → `modif=0x8F`.
|
|
||||||
- `animate_spike` seg007:317: автомат по modif — выдвиг `++modif` (1..4; на 5 →
|
|
||||||
`0x8F`; на 9 → 0, trob кончился); убирание `& 0x80` → `--modif` (на 0 → `=6`).
|
|
||||||
`0xFF` = disabled (не двигать).
|
|
||||||
- `is_spike_harmful` seg007:1178: modif `0/-1`→0 (безопасно); `<0`→1;
|
|
||||||
`1..4`→2; `>=5`→0.
|
|
||||||
- **Смерть**: `check_spiked` seg006:0968 — если тайл под Кидом = spike И harmful
|
|
||||||
И кадр бега (7..14) / старта прыжка (34..39) с harmful>=2, ИЛИ кадр приземления
|
|
||||||
(43/26) с harmful!=0 → `spiked()`. Падение на пики — отдельный путь (см.
|
|
||||||
`is_dead` seg006:1907, frame_177_spiked). Осторожный ШАГ по невыдвинутым — ок.
|
|
||||||
|
|
||||||
### 2.3 Кнопки
|
|
||||||
- `trigger_button(playsound, button_type, modifier)` seg007:0C53: modifier =
|
|
||||||
индекс LINKLOC. `link_timer = get_doorlink_timer(mod)`; если `!=0x1F`
|
|
||||||
(не заклинено): `set_doorlink_timer(mod,5)`; если был `<2` → `add_trob`
|
|
||||||
(кнопка нажимается) + звук; затем `do_trigger_list(mod, button_type)`.
|
|
||||||
- `do_trigger_list` seg007:09E5: идёт по цепочке LINKLOC от idx, для каждой
|
|
||||||
цели `trigger_1(target_type,room,tilepos,button_type)` → если >=0
|
|
||||||
`add_trob(room,tilepos,result)`.
|
|
||||||
- `animate_button` seg007:0D3A: `timer=get_doorlink_timer(mod)-1`;
|
|
||||||
`set_doorlink_timer(mod,timer)`; `timer<2` → кнопка отжимается.
|
|
||||||
- Когда Кид ВСТАЁТ на кнопку: `check_press`-путь seg006 (opener → trigger_button,
|
|
||||||
closer → тоже; если Кид мёртв — `died_on_button`). RAISE=`tiles_15_opener`
|
|
||||||
(0x0F), DROP=`tiles_6_closer` (0x06).
|
|
||||||
|
|
||||||
### 2.4 Гейты
|
|
||||||
- `trigger_1` seg007:0999 → для `tiles_4_gate` → `trigger_gate`.
|
|
||||||
- `trigger_gate(room,tilepos,button_type)` seg007:092C: modif = высота открытия.
|
|
||||||
opener: `0xFF`→игнор; `>=188`(открыт)→держать `238`; иначе `modif=(modif+3)&0xFC`,
|
|
||||||
return 1 (открывать). closer/иначе: `modif!=0` → return 3 (закрыть быстро).
|
|
||||||
- `animate_door` seg007:0522: анимация открытия/закрытия; `door_delta[]={-1,4,4}`,
|
|
||||||
`gate_close_speeds[]={0,0,0,20,40,60,80,100,120}`. Гейт медленно закрывается
|
|
||||||
после истечения таймера кнопки.
|
|
||||||
- Отрисовка: кадры гейта; у нас `draw_gate_back` в pop_bg (грань из левой комн.).
|
|
||||||
- **Проходимость**: Кид блокируется недостаточно открытым гейтом (коллизия
|
|
||||||
как стена, порог по высоте открытия); проходит при `modif` открытом.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Поправки к описанию пользователя (что важно)
|
|
||||||
|
|
||||||
1. Гейт НЕ в room6 (0,0) — он в **room8 (0,9)**, виден через левый шов; связь
|
|
||||||
кросс-комнатная (по таблице LINKLOC, не по позиции).
|
|
||||||
2. Кнопка может открывать несколько гейтов в разных комнатах.
|
|
||||||
3. Пики выдвигаются по `check_spike_below` (Кид над колонкой с пиками),
|
|
||||||
имеют состояния (не всегда смертельны), убираются со временем.
|
|
||||||
4. **Кросс-комнатное состояние тайлов ДОЛЖНО ПЕРСИСТИТЬ** — блокер (см. P0).
|
|
||||||
5. HP/смерть — новая подсистема.
|
|
||||||
6. Кнопка сама анимируется (нажата/отжата).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. ПЛАН РЕАЛИЗАЦИИ (фазы)
|
|
||||||
|
|
||||||
### P0 — Персистентное per-room modifier-состояние + trob-каркас (ПРЕРЕКВИЗИТ)
|
|
||||||
**Проблема:** нажатие кнопки в room6 меняет modif гейта room8 (не текущей
|
|
||||||
комнаты); при входе в room8 нужно отрисовать гейт в текущем состоянии. Плюс
|
|
||||||
это чинит «re-entry восстанавливает тайлы» (loose/гейты).
|
|
||||||
|
|
||||||
Дизайн (предложение — уточнить в реализации):
|
|
||||||
- Массив `room_modif[24][30]` (или lazy per-visited-room) — modifier каждого
|
|
||||||
тайла каждой комнаты. Инициализируется из bg уровня при первой загрузке
|
|
||||||
комнаты; далее ЖИВЁТ (не перезагружается). ~720 Б — влезает в W2 (или в
|
|
||||||
EMM-страницу уровня рядом с данными: остаётся >10КБ).
|
|
||||||
- `enter_room` берёт modif из `room_modif[room]`, а fg — из level-страницы
|
|
||||||
(fg почти не меняется; исключения — loose→empty, надо тоже персистить: либо
|
|
||||||
отдельный `room_fg_override`, либо флаг «loose упал»).
|
|
||||||
- Обобщить loose-trob: единый список trob (room,tilepos,type) + диспетчер
|
|
||||||
`animate_*` по коду тайла. Loose (`pop_loose_*`) — перевести на него.
|
|
||||||
- Кросс-комнатный trigger: `add_trob` в НЕ текущую комнату меняет
|
|
||||||
`room_modif[room][tilepos]`; анимация продвигается даже для невидимой комнаты
|
|
||||||
(в оригинале — да; можно упростить: для невидимой комнаты гейт сразу в
|
|
||||||
финальном состоянии, анимировать только при видимости — решить при реализации).
|
|
||||||
|
|
||||||
### Фаза S — Пики (самодостаточно; отладит HP/смерть)
|
|
||||||
Порядок:
|
|
||||||
1. `room_modif` для пик (из P0 или временно локально).
|
|
||||||
2. `check_spike_below` (порт seg006:1199) — в `pop_phys_tick`.
|
|
||||||
3. `start_anim_spike` + `animate_spike` (порт seg007) — состояние в modif.
|
|
||||||
4. `is_spike_harmful` + `check_spiked` (порт seg006:0968).
|
|
||||||
5. **HP/смерть**: ввести `hitp_curr` (старт напр. 3); `take_hp`; при пиках —
|
|
||||||
мгновенная смерть; seq смерти (`seq_22_crushed`/`frame_177_spiked`..185);
|
|
||||||
анимация смерти; респавн (kid_init на старте или чек-поинт).
|
|
||||||
6. Отрисовка: кадры выдвижения пик. В pop_bg есть `SPIKES_FRAM_RIGHT` — нужны
|
|
||||||
pop-out кадры (spikes_fram по modif) + fore над Кидом.
|
|
||||||
|
|
||||||
### Фаза B — Кнопки + гейты (нужен P0)
|
|
||||||
Порядок:
|
|
||||||
1. Доступ к LINKLOC/LINKMAP из pop_level (добавить геттеры doorlink1/2[i] с
|
|
||||||
маппингом W0 или скопировать таблицы в W2 при load — 512 Б).
|
|
||||||
2. `pop_map` детект «Кид встал на кнопку» (check_press-путь) → `trigger_button`.
|
|
||||||
3. `trigger_button` → таймер + `do_trigger_list` (обход цепочки) →
|
|
||||||
`trigger_gate` для целей → изменить `room_modif[целевой]`.
|
|
||||||
4. `animate_button` (кнопка отжимается) + `animate_door` (гейт откр/закр +
|
|
||||||
авто-закрытие).
|
|
||||||
5. Отрисовка гейта (кадры по modif) через ЛЕВЫЙ шов (гейт room8 в room6) +
|
|
||||||
при входе в room8. Расширить `draw_gate_back`/добавить `draw_gate`.
|
|
||||||
6. Коллизия: закрытый гейт = стена (порог по высоте открытия); открытый —
|
|
||||||
проход. Учесть кросс-комнатный гейт на шве (проход влево room6→room8).
|
|
||||||
|
|
||||||
**Порядок фаз:** P0 → S → B. S в основном независим (кроме HP/trob-каркаса),
|
|
||||||
но проще и отладит смерть/анимацию; B требует P0 (кросс-комнатное состояние).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Открытые вопросы / грабли
|
|
||||||
- Персистентность `fg` для loose (тайл→empty): решить в P0 (override-массив или
|
|
||||||
флаг), иначе упавший loose «вернётся».
|
|
||||||
- Анимация trob в НЕВИДИМОЙ комнате: упростить (финальное состояние сразу) или
|
|
||||||
портировать честно.
|
|
||||||
- Двоебуфер: любой транзиент (пики/гейт/кнопка) финализировать перерисовкой
|
|
||||||
«покоя» на ОБЕИХ страницах (урок из loose — см. memory `pop_loose_floors`).
|
|
||||||
- Дверь уровня (lvldoor room9) + переход на level 2 — ОТДЕЛЬНО, отложено.
|
|
||||||
- L3-**вверх** (climb-up в комнату сверху) — ещё не сделан; можно закрыть до
|
|
||||||
объектов или параллельно.
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
# План: модульные тесты движка roomtest под ucsim_z80
|
|
||||||
|
|
||||||
Обвязка общая — `testkit/` в корне репозитория (там же объяснение, почему
|
|
||||||
прогон именно под z80, а не хостовым gcc). Наборы лежат в
|
|
||||||
`../roomtest/tests-host/`.
|
|
||||||
|
|
||||||
Задача плана: **перестать чинить одно и то же дважды**. За два прогона
|
|
||||||
уровня 1 (2026-08-03) закрыто восемь корней, и часть из них — регрессии
|
|
||||||
соседней механики, внесённые предыдущим фиксом. Такие вещи ловятся тестом
|
|
||||||
за миллисекунды, а в MAME — часами ручного вождения Кида.
|
|
||||||
|
|
||||||
## Что уже есть
|
|
||||||
|
|
||||||
| набор | модуль | статус |
|
|
||||||
|-------|--------|--------|
|
|
||||||
| `t_geom` | `pop_geom.c` | 39 проверок, включая побитовую сверку asm-LCG с 32-битной формулой на 128 шагах |
|
|
||||||
|
|
||||||
`pop_geom.c` выбран первым, потому что не тянет ничего за собой. Дальше
|
|
||||||
начинаются швы.
|
|
||||||
|
|
||||||
## Фаза 1. Два шва (блокирует всё остальное)
|
|
||||||
|
|
||||||
### 1.1 Доступ к странице уровня
|
|
||||||
|
|
||||||
`pop_level.c` ходит по абсолютным адресам: `gfx_w0_map(lvl_page)`, затем
|
|
||||||
разыменование `(uint8_t *)(LVL_DATA_OFF + …)`. В тестовом бинаре это
|
|
||||||
обращение в никуда.
|
|
||||||
|
|
||||||
Нужен макрос `W0PTR(off)`:
|
|
||||||
|
|
||||||
- на таргете — `((uint8_t *)(off))`, то есть ровно как сейчас;
|
|
||||||
- в тестах — смещение в обычном массиве-подложке.
|
|
||||||
|
|
||||||
Правка механическая и компайл-таймовая, на размер продукта не влияет.
|
|
||||||
Заодно снимает магию абсолютных констант из тела функций.
|
|
||||||
|
|
||||||
Тестовая подложка должна уметь: загрузить синтетическую комнату (10×3
|
|
||||||
байта fg + mod) и целый синтетический уровень на 24 комнаты, чтобы
|
|
||||||
проверять межкомнатные вещи.
|
|
||||||
|
|
||||||
### 1.2 Журналирующий рендерер
|
|
||||||
|
|
||||||
Вместо `pop_bg.c`/`pop_cdraw.c` в тестовый бинарь линкуется модуль с теми
|
|
||||||
же прототипами, который **не рисует, а записывает вызовы**: какой тайл
|
|
||||||
помечен к перерисовке, каким кодом, с каким счётчиком страниц.
|
|
||||||
|
|
||||||
Это не обход проблемы, а самостоятельная ценность: `BUG-GATE-ANIM-1` был
|
|
||||||
ровно такой формы — ворота меняли состояние, но пометка на перерисовку не
|
|
||||||
ставилась. Проверяется утверждением, а не глазами.
|
|
||||||
|
|
||||||
Минимум, который надо перехватывать: `pop_set_redraw`,
|
|
||||||
`pop_set_redraw_above`, `pop_loose_mob_spawn`, `pop_gate_redraw`.
|
|
||||||
|
|
||||||
## Фаза 2. Регрессионные кейсы из `BUGS_CLOSED.md`
|
|
||||||
|
|
||||||
После швов `BUGS_CLOSED.md` превращается в готовую спецификацию: у каждой
|
|
||||||
записи есть симптом и ожидаемое поведение. Кандидаты, которые ловятся
|
|
||||||
логикой (без отрисовки и без железа):
|
|
||||||
|
|
||||||
| баг | что закрепить тестом |
|
|
||||||
|-----|----------------------|
|
|
||||||
| `BUG-LVLSTATE-1` | запись тайла переживает выход из комнаты |
|
|
||||||
| `BUG-RESPAWN-1` | рестарт уровня возвращает ВСЕ тайлы из эталонной копии |
|
|
||||||
| `BUG-RESPAWN-2` | рестарт возвращает таблицу стражей; убитый снова жив |
|
|
||||||
| `BUG-GATE-ANIM-1` | смена состояния ворот ставит пометку `POP_RD_GATE`; закрывающиеся — на обе страницы, открывающиеся — на одну |
|
|
||||||
| `BUG-COLL-1` | `check_collisions` сканирует ряд справа налево и выбирает НАИМЕНЬШУЮ занятую колонку |
|
|
||||||
| `BUG-STANDUP-1` | `bumped_floor` у трупа (`alive >= 0`) только выравнивает и не трогает последовательность |
|
|
||||||
| `BUG-DEATH-1` | `hitp_curr == 0` при живом Киде переводит его в «умирает» ровно один раз |
|
|
||||||
| `BUG-LOOSE-2` | кусок, начавший падать, долетает и кладёт щебень ПОСЛЕ смены комнаты |
|
|
||||||
| `BUG-CEIL-2` | loose-плита ряда 2 верхнего соседа живёт как «ряд −1» |
|
|
||||||
|
|
||||||
`BUG-LOOSE-2` стоит взять первым: он до сих пор помечен в `BUGS_OPEN.md`
|
|
||||||
как непроверенный именно потому, что гонку «уйти из комнаты раньше, чем
|
|
||||||
долетит плита» через мост MAME воспроизвести не удалось. На уровне логики
|
|
||||||
это несколько строк — заспавнить кусок, сменить комнату, тикать до
|
|
||||||
приземления, проверить щебень в данных уровня.
|
|
||||||
|
|
||||||
Не берутся (нужна картинка либо железо): `BUG-DOOR-CLIP`, `BUG-CEIL-1`,
|
|
||||||
`BUG-CEIL-3`, `BUG-OCCL-1`, `BUG-KBD-4`, `BUG-3`.
|
|
||||||
|
|
||||||
## Фаза 3. Сценарные тесты
|
|
||||||
|
|
||||||
Сейчас шаг кадра размазан по `main()` в `roomtest.c`. Вынести его в
|
|
||||||
`pop_frame_tick()` — тогда появляются тесты вида «поставить Кида в
|
|
||||||
известное состояние, скормить N тиков ввода, проверить итог»:
|
|
||||||
|
|
||||||
```
|
|
||||||
дано: комната 5, Кид на кнопке (0,6)
|
|
||||||
когда: 40 тиков без ввода
|
|
||||||
тогда: комната по-прежнему 5, Кид на полу ряда 2
|
|
||||||
```
|
|
||||||
|
|
||||||
Это тот самый BUG-STANDUP-1, который ловили потиковой трассой в MAME.
|
|
||||||
|
|
||||||
Ввод подаётся не через `kbd_raw_down()`, а через подменяемый источник —
|
|
||||||
это же даст возможность проигрывать записанные сценарии.
|
|
||||||
|
|
||||||
## Фаза 4. Дифф против SDLPoP
|
|
||||||
|
|
||||||
`SDLPoP/src/` лежит в дереве, собирается на хосте, и там **уже стоят
|
|
||||||
отладочные трассы** (`DBG kidobj tilepos=…` в seg008, `DBG make_loose_fall`
|
|
||||||
в seg007). Значит эталон можно заставить печатать потиковую трассу
|
|
||||||
автоматически.
|
|
||||||
|
|
||||||
Схема: общий формат скрипта ввода и общий формат трассы (тик, frame, x, y,
|
|
||||||
room, col, row, action, alive, hp). Гоняем обе реализации, диффим, первое
|
|
||||||
расхождение — номер тика и есть баг. Это ровно то, что делалось руками
|
|
||||||
через MAME, только бесплатно и повторяемо: `BUG-COLL-1` и `BUG-STANDUP-1`
|
|
||||||
такой дифф нашёл бы за секунды.
|
|
||||||
|
|
||||||
**Лицензия.** SDLPoP — GPLv3, правило подпроекта — читать и переписывать,
|
|
||||||
не линковать. Оракул обязан быть **отдельным исполняемым файлом**,
|
|
||||||
общающимся через файлы трасс, а не слинкованным с нашим кодом в один
|
|
||||||
бинарь.
|
|
||||||
|
|
||||||
Требование к детерминизму: сиды PRNG должны совпадать. У нас
|
|
||||||
`POP_PRANDOM_EXACT` даёт ту же последовательность, что в оригинале, и это
|
|
||||||
уже закреплено тестом `geom_lcg_matches_reference`.
|
|
||||||
|
|
||||||
## Чего эти тесты не поймают
|
|
||||||
|
|
||||||
Отрисовку, банки и W-окна, тайминги, клавиатуру — за этим остаётся MAME.
|
|
||||||
|
|
||||||
И отдельный класс: **баги порядка вызовов**. Свежий пример — окно
|
|
||||||
fore-клипа (`pop_fore_set_clip`) одно на всех, и его ставит каждый, кто
|
|
||||||
рисует персонажа; когда порядок «Кид/страж» стал переменным, окно осталось
|
|
||||||
стражьим, и Кид нарисовался поверх передних столбов. Это не «функция
|
|
||||||
вернула не то», unit-тест такое не видит. Ловится инвариантом,
|
|
||||||
вкомпилированным в safe-сборку: «в момент `pop_fore_over_char` окно клипа
|
|
||||||
принадлежит Киду». Отдельный инструмент, дополняющий тесты.
|
|
||||||
|
|
||||||
## Порядок работ
|
|
||||||
|
|
||||||
1. Шов `W0PTR` + подложка уровня.
|
|
||||||
2. Журналирующий рендерер.
|
|
||||||
3. `BUG-LOOSE-2` — закрыть висящий вопрос.
|
|
||||||
4. Остальные кейсы из таблицы фазы 2.
|
|
||||||
5. `pop_frame_tick()` + сценарные тесты.
|
|
||||||
6. Дифф против SDLPoP.
|
|
||||||
|
|
||||||
Правило приёмки: тест не считается написанным, пока не проверен мутацией —
|
|
||||||
сломать проверяемое место и убедиться, что набор краснеет.
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
# Идеи и вопросы «на подумать» (PoP)
|
|
||||||
|
|
||||||
Не план работ, а список того, что осознанно отложено: каждая запись —
|
|
||||||
гипотеза с причиной, по которой её стоит проверить, и с тем, что мешает
|
|
||||||
сделать это прямо сейчас.
|
|
||||||
|
|
||||||
## Зелье «переворот экрана» (upside-down)
|
|
||||||
|
|
||||||
**Вопрос пользователя (2026-08-01).** Тайлы фона у нас лежат строками, а
|
|
||||||
кадры Кида/стражей — КОЛОНКАМИ (`transpose_cols` в `pop_pack_kid.py`, ради
|
|
||||||
бесплатного горизонтального зеркала). Значит вертикальный переворот для
|
|
||||||
персонажей заметно сложнее, чем для фона. Верно; но прежде чем это чинить,
|
|
||||||
надо знать три факта.
|
|
||||||
|
|
||||||
**Факт 1 — когда оно вообще нужно.** Зелье переворота — тип 4
|
|
||||||
(`proc_get_object`, `seg006.c:1885` → `toggle_upside()`). Скан всех уровней
|
|
||||||
по данным (`res200N.bin`, тайл 10 = зелье, тип в backtable): тип 4
|
|
||||||
встречается **впервые на уровне 9** (две склянки), и больше нигде. Тип 3
|
|
||||||
(перо, медленное падение) — уровень 7. То есть **до уровня 9 механика не
|
|
||||||
нужна вообще**, и «на первом этапе просто не реализовывать» — не компромисс,
|
|
||||||
а точное соответствие данным уровней 1..8.
|
|
||||||
|
|
||||||
**Факт 2 — что именно делает оригинал.** НЕ переворачивает спрайты.
|
|
||||||
`flip_screen` (`seg009.c:1042`) → `flip_not_ega` (`seg009.c:1023`) меняет
|
|
||||||
местами СТРОКИ готового offscreen-буфера (top↔bottom, порядок пикселей
|
|
||||||
внутри строки не трогает — это вертикальное зеркало, не поворот на 180°).
|
|
||||||
Вызывается вокруг отрисовки кадра целиком (`seg003.c:296..301`): перевернул
|
|
||||||
буфер → дорисовал → перевернул обратно. Так что в оригинале это
|
|
||||||
post-process всего экрана, и вопрос «как перевернуть колоночный спрайт»
|
|
||||||
там просто не возникает.
|
|
||||||
|
|
||||||
**Факт 3 — почему нам этот приём не подходит как есть.** У нас нет шага
|
|
||||||
«готовый offscreen → экран»: рисуем прямо в видеостраницу, а heal берёт фон
|
|
||||||
из ОЗУ-копии этой же страницы. Переворот всей страницы построчно — это
|
|
||||||
320×192 Б копирования КАЖДЫЙ кадр, что мимо бюджета на порядок.
|
|
||||||
|
|
||||||
**Варианты, которые надо будет взвесить (не сейчас):**
|
|
||||||
1. **Предпечённые перевёрнутые атласы.** Второй набор кадров
|
|
||||||
Кида/стража, перевёрнутый по вертикали ещё в `pop_pack_kid.py` (там уже
|
|
||||||
есть транспонирование — добавляется одной строкой). Рантайм: выбор
|
|
||||||
набора + зеркальная арифметика Y. Память: ещё ~28 страниц EMM при
|
|
||||||
бюджете ~3.3 МБ — не проблема. Похоже, самый дешёвый по тактам путь.
|
|
||||||
2. **Фон рисовать с обратным Y** — для row-major тайлов строка остаётся
|
|
||||||
непрерывным accel-прогоном, меняется только адрес назначения; цена —
|
|
||||||
вызов на строку вместо вызова на тайл. Померить, прежде чем закладывать.
|
|
||||||
3. **Аппаратная помощь** — до проектирования проверить, есть ли у
|
|
||||||
акселератора направление копирования «вниз» (обратный инкремент адреса);
|
|
||||||
если есть, вариант 1 может и не понадобиться. Смотреть
|
|
||||||
`docs/new/06-accel.md` и `docs/reference/accel_r.txt`.
|
|
||||||
|
|
||||||
**Почему не сейчас.** Уровни 1..8 этого не требуют, а к уровню 9 у нас уже
|
|
||||||
будет ответ на вопрос «сколько стоит кадр» (задачи CLIP-1/T-2) — без него
|
|
||||||
выбирать между вариантами выше бессмысленно.
|
|
||||||
|
|
||||||
## Заменить генератор псевдослучайных чисел
|
|
||||||
|
|
||||||
Сейчас стоит LCG оригинала, шаг на ассемблере (~1 020 тактов), бит-в-бит
|
|
||||||
совместимый с SDLPoP. Есть более дешёвые Z80-генераторы (86–148 тактов),
|
|
||||||
но потолок выигрыша — 2 814 тактов за кадр, 0.65 %, и он растворяется в
|
|
||||||
обёртках вызова. Тексты процедур, разбор качества и порядок действий —
|
|
||||||
`prng_alternatives.md`. Первый шаг там не про генератор: слить приведение
|
|
||||||
к диапазону в ту же asm-процедуру, чтобы на вызов был один `call`, а не три.
|
|
||||||
|
|
||||||
## Отключать мышь на время игры
|
|
||||||
|
|
||||||
**Гипотеза.** Мышь на Sprinter — источник прерываний (обёртки RST 30h,
|
|
||||||
см. memory `mouse_api`). Игре она не нужна вообще: управление —
|
|
||||||
raw-клавиатура (`<kbd_raw.h>`), которую мы и так забираем у DSS целиком.
|
|
||||||
Значит каждое мышиное прерывание за кадр — украденные такты в бюджете,
|
|
||||||
который у нас и без того занят на 86 %.
|
|
||||||
|
|
||||||
**Откуда взялось (2026-07-30).** При замере бюджета по 100 кадрам три
|
|
||||||
кадра выбились до 552–647 К тактов (1.28–1.51 кадра) при типичных 371 К.
|
|
||||||
Причиной оказалось движение мыши на ХОСТЕ: при неподвижной мыши 225
|
|
||||||
кадров подряд прошли без единого превышения. То есть эффект реальный и
|
|
||||||
измеримый, просто в тесте он был наведён извне.
|
|
||||||
|
|
||||||
**Что проверить.**
|
|
||||||
1. Есть ли у драйвера мыши (RST 30h) функция «выключить/включить» —
|
|
||||||
разобрать список из 14 обёрток; если нет явной, посмотреть, что делает
|
|
||||||
«hide cursor» и снимает ли она обработчик.
|
|
||||||
2. Сколько тактов реально стоит одно мышиное прерывание на нашем железе
|
|
||||||
(замер: breakpoint на входе ISR + totalcycles, при движении мыши).
|
|
||||||
3. Не ломает ли отключение выход в DSS: состояние обязано
|
|
||||||
восстанавливаться при `exit`, включая аварийный (atexit).
|
|
||||||
|
|
||||||
**Почему не сейчас.** Выигрыш проявляется только когда игрок реально
|
|
||||||
двигает мышью, то есть в норме его нет; а риск оставить систему без мыши
|
|
||||||
после выхода — заметный. Делать после того, как закроем стражей и
|
|
||||||
займёмся бюджетом всерьёз (там же, где батчинг кроссбанковых вызовов и
|
|
||||||
возможный возврат `pop_bg` в резидент `--w3`).
|
|
||||||
@@ -1,532 +0,0 @@
|
|||||||
# Осознанные расхождения с SDLPoP
|
|
||||||
|
|
||||||
Правило подпроекта (`../CLAUDE.md`): расхождение нашей реализации с
|
|
||||||
`SDLPoP/src/` — по умолчанию **баг у нас**. Этот файл — список исключений:
|
|
||||||
мест, где мы сознательно сделали иначе, потому что платформа/ABI/бюджет
|
|
||||||
кадра требуют другого, а НАБЛЮДАЕМОЕ поведение обязано совпадать.
|
|
||||||
|
|
||||||
Формат записи: что делает оригинал → что делаем мы → почему → чем платим и
|
|
||||||
что проверять при регрессе. Если запись перестала быть верной (портировали
|
|
||||||
дословно, отказались от обхода) — удалять, а не оставлять «для истории»:
|
|
||||||
история в git.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## D-1. История флагов перекрытия у бокового шва: сдвиг вместо тега комнаты
|
|
||||||
|
|
||||||
**Файлы:** `roomtest/pop_map.c` (`pop_coll_shift`, `pop_coll_invalidate`,
|
|
||||||
`check_collisions`), `roomtest/roomtest.c` (`enter_room_side`).
|
|
||||||
**Связанный баг:** BUG-GATE-PASS-1 (`roomtest/BUGS_CLOSED.md`).
|
|
||||||
**Дата:** 2026-08-09.
|
|
||||||
|
|
||||||
### Как в оригинале
|
|
||||||
|
|
||||||
`check_collisions` (seg004:0004) вместе с `get_row_collision_data`
|
|
||||||
(seg004:0185) держит **10 слотов** флагов перекрытия и рядом —
|
|
||||||
**параллельный массив номера комнаты**:
|
|
||||||
|
|
||||||
```c
|
|
||||||
row_coll_flags_ptr[tile_col] = curr_flags; /* tile_col — колонка ВНУТРИ разрешённой комнаты (0..9) */
|
|
||||||
row_coll_room_ptr [tile_col] = curr_room; /* и номер этой комнаты */
|
|
||||||
...
|
|
||||||
for (short column = 9; column >= 0; --column) {
|
|
||||||
if (curr_row_coll_room[column] >= 0 &&
|
|
||||||
prev_coll_room[column] == curr_row_coll_room[column]) {
|
|
||||||
if ((prev_coll_flags[column] & 0x0F) == 0 &&
|
|
||||||
(curr_row_coll_flags[column] & 0x0F) != 0)
|
|
||||||
bump_col_left_of_wall = column;
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
Ключ слота — пара **(колонка в своей комнате, номер комнаты)**. Решётка
|
|
||||||
комнаты 8 и до перехода 8→6, и после лежит в слоте 9 с `room = 8`: история
|
|
||||||
переживает смену комнаты, переход флага 0→1 виден, `bumped()` срабатывает.
|
|
||||||
Комнату оригинал резолвит на лету через `find_room_of_tile` (seg006:005D),
|
|
||||||
никакого кэша всех комнат у него нет.
|
|
||||||
|
|
||||||
### Что делаем мы
|
|
||||||
|
|
||||||
Индекс — **колонка ОТРИСОВАННОЙ комнаты**, диапазон −2…11 (14 слотов,
|
|
||||||
`COLL_C0`/`COLL_N`/`COLL_IDX`), номер комнаты рядом не хранится. При смене
|
|
||||||
комнаты тот же физический тайл менял бы слот на ±10, поэтому раньше история
|
|
||||||
просто выбрасывалась (`pop_coll_invalidate` → `prev = 3` = «уже
|
|
||||||
перекрывал» → бампа нет). Именно это и был BUG-GATE-PASS-1.
|
|
||||||
|
|
||||||
Теперь при **боковом** переходе история не выбрасывается, а
|
|
||||||
**перенумеровывается**: `pop_coll_shift(∓10)` сдвигает `coll_curr`,
|
|
||||||
`coll_above`, `coll_below` на 10 слотов и заполняет освободившиеся
|
|
||||||
тройками. `enter_room_side` зовёт её сразу после `pop_map_set_edges`.
|
|
||||||
|
|
||||||
Корректность держится на том, что `check_leave` двигает `Char.x` ровно на
|
|
||||||
∓140 = 10 тайлов по 14 px, и координата грани (`pop_x_bump[col + …]`)
|
|
||||||
сдвигается на те же 140 вместе с габаритом Кида, — **сами флаги
|
|
||||||
инвариантны**, меняется только номер слота. Сдвигаются `curr/above/below`,
|
|
||||||
а не `prev`: `prev` на следующем кадре всё равно перезапишет
|
|
||||||
`move_coll_to_prev`, выбирая источник как раз из этих трёх.
|
|
||||||
|
|
||||||
Переходы **вверх/вниз** и все прочие входы в комнату (старт уровня,
|
|
||||||
респавн, чит-навигация) остаются на полной инвалидации: там колонки не
|
|
||||||
сдвигаются, но тайлы под ними принадлежат другой комнате — история
|
|
||||||
действительно недействительна.
|
|
||||||
|
|
||||||
### Почему не дословно (вариант A)
|
|
||||||
|
|
||||||
Дословный порт — 10 слотов + параллельный массив номера комнаты, индекс по
|
|
||||||
колонке разрешённой комнаты, бамп только при совпадении номеров; тогда
|
|
||||||
`pop_coll_invalidate` не нужен вовсе, история сама «не совпадает» там, где
|
|
||||||
колонка сменила комнату.
|
|
||||||
|
|
||||||
Не взяли по одной причине: **десяти слотов нам не хватит**. Оригинал
|
|
||||||
перебирает узкое окно вокруг Кида (от `col(char_x_left_coll) − 1` до
|
|
||||||
`col(char_x_right_coll) + 2`), поэтому коллизии слотов у него практически
|
|
||||||
не случаются. Мы держим четырнадцать колонок (−2…11) — при узком окне это
|
|
||||||
не мешает, а вот в десять слотов колонки −2/−1 и 8/9 сядут поверх 8/9.
|
|
||||||
|
|
||||||
**Окно перебора с 2026-08-09 у нас такое же, как в оригинале** (было: все
|
|
||||||
четырнадцать колонок каждый кадр). Признак годности слота при этом не
|
|
||||||
массив номеров комнат, как у оригинала, а ГРАНИЦЫ окна — четыре байта,
|
|
||||||
которые `move_coll_to_prev` переносит в `prev` вместе с флагами; сравнение
|
|
||||||
идёт по пересечению двух окон. Очистки массивов нет вовсе, то есть это
|
|
||||||
дешевле оригинала, а смысл тот же (у него слот вне окна помечен
|
|
||||||
`row_coll_room = −1` и в цикл бампа не попадает). `check_chomped_flags`
|
|
||||||
тоже ограничен окном — иначе протухшие слоты дали бы фантомный перемол.
|
|
||||||
|
|
||||||
### Чем платим
|
|
||||||
|
|
||||||
- Расхождение структур: если в будущем понадобится знать, из какой комнаты
|
|
||||||
пришёл тайл конкретного слота, этого у нас нет — придётся идти в вариант A.
|
|
||||||
- Границы окна надо переносить везде, где переносятся флаги: `pop_coll_shift`
|
|
||||||
двигает и их, `move_coll_to_prev` снимает их в `prev`. Забыть один из
|
|
||||||
переносов = молча потерять или, наоборот, разрешить лишний бамп.
|
|
||||||
- Сдвиг работает только для чисто горизонтальных переходов на ровно 10
|
|
||||||
колонок. Любая будущая диагональ/иная ширина комнаты его сломает молча.
|
|
||||||
- `coll_last_row`: `pop_coll_invalidate` прячет прошлый ряд, чтобы
|
|
||||||
`pop_coll_shift` мог отменить инвалидацию. Порядок вызовов в
|
|
||||||
`enter_room_side` (сначала `pop_map_set_edges`, потом `pop_coll_shift`)
|
|
||||||
стал значимым.
|
|
||||||
|
|
||||||
### Что проверять при регрессе
|
|
||||||
|
|
||||||
Это сердце коллизии, вокруг которого разбирался BUG-SEAM-PINGPONG. После
|
|
||||||
любой правки здесь — прогон швов:
|
|
||||||
|
|
||||||
1. Уровень 1, комнаты 6 ↔ 8, закрытая решётка, **обе** стороны.
|
|
||||||
2. Оба режима подхода: мелким шагом (упереться) и с разбега (не пройти
|
|
||||||
насквозь).
|
|
||||||
3. Проверить, что пинг-понг у шва не вернулся (экран не перескакивает
|
|
||||||
туда-сюда на кадре бампа о ворота).
|
|
||||||
4. `make -C roomtest/tests-host` — наборы `t_wall`/`t_char` ходят по этой же
|
|
||||||
геометрии.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## D-2. Кнопка в шве: перерисовываем, хотя оригинал не перерисовывает
|
|
||||||
|
|
||||||
### Что делает оригинал
|
|
||||||
|
|
||||||
Тайл-«трансформер» (кнопка, ворота, пика) перерисовывается только если он в
|
|
||||||
ОТРИСОВАННОЙ комнате: `redraw_11h` → `redraw_tile_height` →
|
|
||||||
`get_trob_pos_in_drawn_room` (seg007:0258), а та для `trob.room != drawn_room`
|
|
||||||
возвращает 30 — заведомо несуществующий tilepos, то есть «не рисовать».
|
|
||||||
Исключение сделано ровно одно — факелы (`animate_torch`, seg007:03CF, ветка
|
|
||||||
`trob.room == room_L && tilepos % 10 == 9`).
|
|
||||||
|
|
||||||
Кнопка соседа слева при этом ВЛИЯЕТ на картинку: `get_tile_to_draw`
|
|
||||||
(seg008:253) подменяет нажатый `tiles_15_opener` на `tiles_1_floor`, а
|
|
||||||
`load_leftroom` (seg008:360) кладёт результат в `leftroom_[row]`, откуда он
|
|
||||||
приходит в `draw_tile` как `tile_left`. У пола правая грань есть, у кнопки
|
|
||||||
нет — значит в оригинале нажатие кнопки, видимой через левый шов, меняет
|
|
||||||
картинку только при следующей ПОЛНОЙ отрисовке комнаты.
|
|
||||||
|
|
||||||
### Что делаем мы
|
|
||||||
|
|
||||||
Перерисовываем шов сразу: `seam_row_sig` (roomtest.c) подмешивает в сигнатуру
|
|
||||||
ряда бит «кнопка нажата» (`pop_doorlink2(mod) & 0x1F > 1`) для тайлов
|
|
||||||
`0x0F`/`0x06`, и change-driven редрой `pop_room_redraw_seam_left` срабатывает
|
|
||||||
на нём так же, как на openness ворот.
|
|
||||||
|
|
||||||
### Почему
|
|
||||||
|
|
||||||
Голый порт давал видимый залип (SEAM-BUTTON-STALE, roomtest/BUGS_CLOSED.md): кнопка
|
|
||||||
(1,9) комнаты 11 — она же (1,−1) комнаты 24 — оставалась нарисованной в том
|
|
||||||
состоянии, в каком была на входе в комнату, хотя связь срабатывала. Сигнатура
|
|
||||||
шва следила только за `room_modif`, а у кнопки `modif` — это ИНДЕКС LINKLOC,
|
|
||||||
константа уровня: нажатие живёт в `doorlinks2` и в сигнатуру не приходило
|
|
||||||
никогда. Добавить кнопку в сигнатуру — те же три сравнения на кадр, что уже
|
|
||||||
делались для ворот; воспроизводить артефакт оригинала смысла нет.
|
|
||||||
|
|
||||||
### Чем платим
|
|
||||||
|
|
||||||
- Резидент +200 Б (`_CODE` 24 739 → 24 939), куча W2 1795 → 1595 Б. Если
|
|
||||||
станет тесно — `seam_row_sig` переносится в банк 7 к
|
|
||||||
`pop_room_redraw_seam_left`, ценой одного трамплина на кадр.
|
|
||||||
- Сигнатура ряда стала разнотипной: для кнопки это булев бит, для остальных
|
|
||||||
тайлов — modif. Значения между собой не сравниваются (сравнивается только
|
|
||||||
ряд сам с собой), но при добавлении нового типа тайла в шов про это надо
|
|
||||||
помнить.
|
|
||||||
|
|
||||||
### Что проверять при регрессе
|
|
||||||
|
|
||||||
Уровень 5, кнопка нижних ворот комнаты 24 (она же (1,9) комнаты 11), оба
|
|
||||||
направления: нажать её из комнаты 11 и войти в 24; и наоборот — войти в 24
|
|
||||||
поверху и наступить на неё, стоя в шве. Картинка кнопки обязана совпадать
|
|
||||||
со статусом ворот в обоих случаях.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Перо (медленное падение) ловит ТОЛЬКО Кида
|
|
||||||
|
|
||||||
**Оригинал** (`fall_accel`, seg006:057C): `is_feather_fall` — глобальный флаг,
|
|
||||||
и медленное падение достаётся ЛЮБОМУ персонажу, который окажется в `Char`,
|
|
||||||
пока эффект жив. То есть страж, сошедший с уступа в те же секунды, парит
|
|
||||||
вместе с Кидом, хотя зелье пил не он. SDLPoP считает это багом и чинит
|
|
||||||
опцией `fix_feather_fall_affects_guards`.
|
|
||||||
|
|
||||||
**Мы** берём поведение С ФИКСОМ: `pop_feather` проверяется вместе с
|
|
||||||
`Char.charid == CHARID_0_KID` — и в `fall_accel` (`pop_map.c`), и в опкоде
|
|
||||||
`JMP_IF_FEATHER` интерпретатора seqtbl (`pop_kid.c`), чтобы физика и анимация
|
|
||||||
не разъехались.
|
|
||||||
|
|
||||||
**Чем платим.** Сцена, где страж падает при живом пере, будет выглядеть иначе,
|
|
||||||
чем в DOS-оригинале (у нас он падает нормально, там — парит). На уровне 7,
|
|
||||||
единственном с этим зельем, такой сцены нет: зелье в комнате 1, стражи — в
|
|
||||||
других комнатах.
|
|
||||||
|
|
||||||
**Что проверять при регрессе.** Уровень 7: выпить зелье в комнате 1, тут же
|
|
||||||
столкнуть стража в провал — он обязан падать БЫСТРО, а Кид рядом — медленно.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Синее зелье («−HP») не ставит свою вспышку
|
|
||||||
|
|
||||||
**Оригинал** (`proc_get_object`, seg006:1892): ветка `case 5` только глушит
|
|
||||||
звуки, играет `sound_13_kid_hurt` и ставит `hitp_delta`. Экран краснеет не
|
|
||||||
здесь, а общим механизмом «Кид ранен» (`flash_if_hurt`, seg003:0AFC).
|
|
||||||
|
|
||||||
**Мы** раньше ставили в этой ветке ещё и `pop_flash_*` (красную вспышку на 2
|
|
||||||
кадра) — то есть красили экран дважды: своей вспышкой и кадром урона.
|
|
||||||
Приведено к оригиналу: ветка правит только `hitp_delta`, краснеет `pop_kid_hurt`.
|
|
||||||
|
|
||||||
**Что проверять при регрессе.** Уровень 2, комната 13, зелье `(1,3)`: выпить —
|
|
||||||
HP убавляется на единицу, экран краснеет РОВНО один раз (без двойного строба).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Переворот (зелье инверсии) применяется НА ГРАНИЦЕ КАДРА, а не мгновенно
|
|
||||||
|
|
||||||
**Оригинал** (`toggle_upside`, seg000:15E9): `upside_down = ~upside_down` и
|
|
||||||
`need_redraw_because_flipped = 1` — флаг переключается прямо в момент глотка,
|
|
||||||
то есть в середине кадра. Оригиналу это ничего не стоит: он ВСЕГДА рисует в
|
|
||||||
offscreen неперевёрнутым, а зеркалит только при выводе на экран
|
|
||||||
(`flip_screen` вокруг `copy_screen_rect`, seg000:939/946). Внутренние
|
|
||||||
координаты у него от переворота не зависят вообще.
|
|
||||||
|
|
||||||
**Мы** offscreen-буфера не имеем (две видеостраницы + теневая ОЗУ-копия на
|
|
||||||
каждую), поэтому рисуем зеркально сразу — переворот «зашит» в координаты
|
|
||||||
каждого слоя. Из-за этого момент переключения важен: зелье выпивается из
|
|
||||||
`play_seq`, то есть в СЕРЕДИНЕ кадра, и остаток кадра рисовался бы уже
|
|
||||||
зеркально поверх ещё неперевёрнутого фона. Хуже всего пламя факела — оно
|
|
||||||
ЗАПЕКАЕТСЯ в ОЗУ-копию (`pop_torch_draw`, у него нет heal: каждый следующий
|
|
||||||
кадр непрозрачно накрывает предыдущий). Кадр пламени, положенный в
|
|
||||||
зеркальную позицию на старом фоне, оставался там навсегда — по комнате
|
|
||||||
рассыпались лишние языки огня.
|
|
||||||
|
|
||||||
Поэтому у нас два флага: `pop_upside_want` (пишут зелье, смерть Кида, чит U)
|
|
||||||
и `pop_upside` (читают все слои отрисовки). Переключение — ровно одно место,
|
|
||||||
начало кадра, вместе с перерисовкой: главный цикл делает
|
|
||||||
`pop_upside = pop_upside_want` и зовёт `pop_flip_screen`.
|
|
||||||
|
|
||||||
Сама перерисовка при этом СОВПАДАЕТ с оригиналом: там на
|
|
||||||
`need_redraw_because_flipped` вызывается `redraw_screen(0)` — полная
|
|
||||||
отрисовка, а не отражение уже нарисованного. У нас то же самое —
|
|
||||||
`pop_flip_screen` рисует комнату заново (и получает чистый фон по
|
|
||||||
построению), а вторую страницу дабл-буфера отдаёт копией акселератора.
|
|
||||||
|
|
||||||
**Что проверять при регрессе.** Уровень 9: выпить зелёное зелье — картинка
|
|
||||||
переворачивается ровно один раз, лишних языков пламени по комнате нет. Чит
|
|
||||||
U даёт тот же результат (он идёт тем же путём).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Окклюзия воротами: спрашиваем про рисуемого персонажа, а не жёстко про Кида
|
|
||||||
|
|
||||||
**Оригинал** (`draw_tile_fore`, seg008:0D15) первой строкой:
|
|
||||||
|
|
||||||
```c
|
|
||||||
if (tile_left == tiles_4_gate && Kid.curr_row == drawn_row &&
|
|
||||||
Kid.curr_col == drawn_col - 1 && Kid.room != room_R)
|
|
||||||
draw_gate_fore();
|
|
||||||
```
|
|
||||||
|
|
||||||
То есть бары ворот попадают в foretable — поверх всего нарисованного — когда
|
|
||||||
на тайле ворот стоит **именно Кид**. Это следствие устройства оригинала:
|
|
||||||
foretable ОДНА на весь проход тайлов, персонажи в неё уже добавлены, и
|
|
||||||
отдельного «переднего слоя на персонажа» там нет.
|
|
||||||
|
|
||||||
**Мы** ради скорости не рисуем foretable целиком, а возвращаем куски тайлов
|
|
||||||
поверх ТОЛЬКО в прямоугольнике персонажа (`pop_fore_over_char`, см. memory
|
|
||||||
`pop_fore_layer_cost`: полный проход стоил 78 % кадра). Проход идёт по
|
|
||||||
персонажу, значит и вопрос естественно задавать про него —
|
|
||||||
`pop_gate_over_char(ch)`, а не про глобального `Kid`.
|
|
||||||
|
|
||||||
**Чем платим.** Наш вариант — надмножество оригинального: страж (или тень),
|
|
||||||
стоящий в проёме ворот, у нас уходит ЗА решётку, а в оригинале остался бы
|
|
||||||
нарисованным поверх неё, пока на том же тайле нет Кида. Визуально это
|
|
||||||
правильнее, но формально расхождение. Обратной разницы нет: во всех случаях,
|
|
||||||
где оригинал рисует бары поверх, рисуем и мы.
|
|
||||||
|
|
||||||
**Что проверять при регрессе.** Уровень 10, комната 7, тайл (2,6): Кид,
|
|
||||||
стоящий в проёме ворот, виден ЗА прутьями. Решение покрыто хост-тестами
|
|
||||||
(`tests-host/t_char.c`, набор `char_gate_*`) — отрисовка в харнесс не
|
|
||||||
линкуется, поэтому проверяется предикат.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Тень рисуется спрайтами КИДА, а не XOR-силуэтом
|
|
||||||
|
|
||||||
**Файлы:** `roomtest/pop_cdraw.c` (выбор набора атласов по `charid`/`frame`).
|
|
||||||
**Дата:** 2026-08-18 (решение принималось раньше, записано здесь).
|
|
||||||
|
|
||||||
**Оригинал** рисует тень тем же кадром Кида, но ДВАЖДЫ — вторым проходом со
|
|
||||||
сдвигом на один пиксель и через XOR. Получается тёмный силуэт с контуром,
|
|
||||||
а не «второй Кид».
|
|
||||||
|
|
||||||
**Мы** рисуем тень обычными спрайтами Кида, обычным блиттером — она выглядит
|
|
||||||
как Кид.
|
|
||||||
|
|
||||||
**Почему.** Приём оригинала — read-modify-write по уже нарисованному, а
|
|
||||||
читать данные из ВИДЕО-ОЗУ (там, где спрайты) на Sprinter нельзя: читается
|
|
||||||
только ОЗУ-копия. Блочный XOR у акселератора есть и работает как раз по
|
|
||||||
ОЗУ-копии, но с нашей прозрачностью он несовместим: прозрачность сделана
|
|
||||||
подавлением записи 0xFF, а XOR прозрачные пиксели тоже смешает — под него
|
|
||||||
нужен набор с прозрачным 0x00 (замер: memory `accel_block_ops`,
|
|
||||||
`sprinter_vram_transparency`). То есть «сделать как в оригинале» всё равно
|
|
||||||
упирается в отдельный набор спрайтов.
|
|
||||||
|
|
||||||
**Чем платим.** Тень визуально неотличима от Кида (уровни 4/5/6/12). На
|
|
||||||
механику не влияет: слот, окна `Char`, ИИ и коллизия у тени свои и от
|
|
||||||
картинки не зависят.
|
|
||||||
|
|
||||||
**План.** Отдельный АТЛАС ТЕНИ (готовый силуэт), а не воспроизведение
|
|
||||||
XOR-прохода: один набор спрайтов вместо второго пути блита. До тех пор
|
|
||||||
расхождение сознательное — багом не заводить.
|
|
||||||
|
|
||||||
**Что проверять при регрессе.** Уровень 6 комната 1: тень стоит слева
|
|
||||||
через провал, поза совпадает с позой Кида-в-стойке. Родственная запись —
|
|
||||||
«Слияние с тенью» ниже.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Слияние с тенью: мигания Кида спрайтами тени нет
|
|
||||||
|
|
||||||
**Файлы:** `roomtest/guards.c` (`autocontrol_shadow_level12`),
|
|
||||||
`roomtest/pop_cdraw.c`.
|
|
||||||
**Дата:** 2026-08-13.
|
|
||||||
|
|
||||||
**Оригинал** (`draw_objtable_item`, seg008:20CA) во время вспышки слияния
|
|
||||||
(`united_with_shadow` считает 42 → 0) рисует КИДА как тень на чётных
|
|
||||||
значениях счётчика: тот же кадр уходит не обычным прозрачным блиттером, а
|
|
||||||
парой OR+XOR со сдвигом на пиксель. Получается мерцание «Кид/тень»
|
|
||||||
примерно полторы секунды.
|
|
||||||
|
|
||||||
**Мы** рисуем всё это время обычного Кида, а само событие обозначаем белой
|
|
||||||
вспышкой фона (`pop_flash_color = POP_FLASH_WHITE`, 18 кадров) — она в
|
|
||||||
оригинале тоже есть и ставится тем же кодом.
|
|
||||||
|
|
||||||
**Почему.** У нас Кид и соперник рисуются из РАЗНЫХ атласов своими
|
|
||||||
палитрами (`pop_cdraw.c`), а «тень» — это персонаж слота Guard с палитрой
|
|
||||||
комнаты; блиттеров OR/XOR в libbgi нет вовсе, прозрачность сделана
|
|
||||||
0xFF-подавлением записи. Воспроизвести эффект — значит завести Киду второй
|
|
||||||
набор спрайтов и второй путь блита ради 42 кадров за всю игру.
|
|
||||||
|
|
||||||
**Чем платим.** Момент слияния читается только по вспышке и по тому, что
|
|
||||||
тень исчезла, — без «двоящегося» силуэта. На механику не влияет: счётчик
|
|
||||||
`pop_united_shadow` тикает и уходит в −1 независимо от отрисовки, а от него
|
|
||||||
зависят и повторный подъём тени, и появление плит в комнатах 2/13.
|
|
||||||
|
|
||||||
**Что проверять при регрессе.** Уровень 12: после слияния экран белеет,
|
|
||||||
соперник пропал, HP-потолок вырос на единицу, тень в комнате 15 больше не
|
|
||||||
появляется. Логика покрыта `tests-host/t_shadow.c`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Отложенный старт падающих плит (уровень 13): фаза 0 у нас «не анимируется»
|
|
||||||
|
|
||||||
**Файлы:** `roomtest/pop_map.c` (`pop_check_fall_flo`, `pop_loose_tick`),
|
|
||||||
`roomtest/pop_trob.c` (`animate_loose`).
|
|
||||||
**Дата:** 2026-08-13.
|
|
||||||
|
|
||||||
**Оригинал** (`check_fall_flo`, seg000:1317) раздаёт шести плитам ряда 2
|
|
||||||
верхней комнаты модификатор `−(prandom(0xFF) & 0x0F)`, то есть 0..−15, и
|
|
||||||
заводит на каждую trob. Фаза считает вверх, проходит ноль и дальше идёт
|
|
||||||
обычным отсчётом до провала — плита падает через `n + 11` кадров. Ноль там
|
|
||||||
безопасен: плиту держит в игре СПИСОК trob, а не значение модификатора.
|
|
||||||
|
|
||||||
**Мы** списка trob для loose текущей комнаты не держим — плита анимируется
|
|
||||||
ровно тогда, когда её фаза не ноль (`pop_loose_modif[pos] != 0`). Значит
|
|
||||||
счёт, дойдя до нуля, оборвался бы навсегда. Компенсируем двумя правками,
|
|
||||||
которые работают только в паре:
|
|
||||||
|
|
||||||
* тик перескакивает ноль (`if (m == 0) m = 1`);
|
|
||||||
* стартовое значение берётся на единицу «отрицательнее» (`−n−1`).
|
|
||||||
|
|
||||||
**Чем платим.** Ничем в наблюдаемом поведении: суммарная задержка остаётся
|
|
||||||
`n + 11` кадров, проверено арифметикой на обоих концах диапазона (n = 0 и
|
|
||||||
n = 15). Платим связностью — две правки в разных функциях, и убрать любую
|
|
||||||
одну нельзя.
|
|
||||||
|
|
||||||
**Что проверять при регрессе.** Уровень 13, вход в комнату 23 (она же
|
|
||||||
стартовая): плиты сверху сыплются ВРАЗНОБОЙ, а не разом и не «никогда».
|
|
||||||
Логика покрыта `tests-host/t_jaffar.c`
|
|
||||||
(`jaffar_negative_phase_counts_through_to_fall` и парный контроль на другом
|
|
||||||
уровне).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Чит навигации по комнатам не запускает бесшовный переход уровня
|
|
||||||
|
|
||||||
**Файлы:** `roomtest/roomtest_cold.c` (`pop_dbg_roomnav`), `roomtest/roomtest.c`,
|
|
||||||
`roomtest/pop_state.c` (`pop_nav_hold`).
|
|
||||||
**Дата:** 2026-08-13.
|
|
||||||
|
|
||||||
**Оригинал** (`play_level_2`, seg000:0900) проверяет `Kid.room == 23` КАЖДЫЙ
|
|
||||||
кадр: уровень 12 кончается самим фактом присутствия Кида в комнате 23, двери
|
|
||||||
у него нет. Никакого «как он туда попал» там нет и быть не может —
|
|
||||||
телепорта между комнатами в игре 1989 года не существует.
|
|
||||||
|
|
||||||
**Мы** держим этот триггер, пока Кид попал в комнату ЧИТОМ навигации
|
|
||||||
(`+`/`−`), и отпускаем на первой же смене комнаты обычным ходом.
|
|
||||||
|
|
||||||
**Почему.** Чит перебирает комнаты ПО НОМЕРУ (1..24 с обёрткой), то есть
|
|
||||||
любой обход уровня 12 неизбежно наступает на 23-ю — и уровень молча
|
|
||||||
становится 13-м. Поймано на первом же прогоне 2026-08-13: проверяющий час
|
|
||||||
смотрел «комнату 20 уровня 12», которая на самом деле была комнатой 20
|
|
||||||
уровня 13, и сравнивал её с картой не того уровня. Комнату 23 уровня 12
|
|
||||||
читом не посмотреть в принципе. Это ровно та же болезнь чит-телепорта, что
|
|
||||||
BUG-CHEAT-FIGHT-1 (выход из боя), и лечится там же.
|
|
||||||
|
|
||||||
**Чем платим.** Ничем в игре: в обычном прохождении Кид входит в комнату 23
|
|
||||||
ногами, флаг снят, переход срабатывает как в оригинале. Расхождение видно
|
|
||||||
ТОЛЬКО при включённых читах.
|
|
||||||
|
|
||||||
**Что проверять при регрессе.** Уровень 12: пройти в комнату 23 ногами —
|
|
||||||
уровень меняется на 13-й без заставки и без сброса HP. Обойти уровень
|
|
||||||
читом `+` через 23-ю — уровень НЕ меняется.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Чит «убить стража» (K) идёт через штатный путь смерти
|
|
||||||
|
|
||||||
**Файлы:** `roomtest/pop_guard.c` (`pop_guard_kill`).
|
|
||||||
**Дата:** 2026-08-13. Решение пользователя.
|
|
||||||
|
|
||||||
**Оригинал** (seg000:786) ставит `guardhp_delta = -guardhp_curr` И
|
|
||||||
`Guard.alive = 0`. А гейт события смерти в `play_guard` (seg006:1490)
|
|
||||||
требует `Char.alive < 0` — то есть у оригинала чит убивает стража В ОБХОД
|
|
||||||
`on_guard_killed`. На 13-м уровне это заметно: победа над Джафаром по читу
|
|
||||||
не ставит `leveldoor_open = 2`, и выход на 14-й не открывается.
|
|
||||||
|
|
||||||
**Мы** `Guard.alive` в чите не трогаем: применённая дельта обнуляет HP, и
|
|
||||||
`play_guard` сам переводит стража в «умирает», вызвав `on_guard_killed` —
|
|
||||||
брызги, вспышка, флаг выхода. То есть чит даёт ровно «как будто убил Кид».
|
|
||||||
|
|
||||||
**Почему.** Отладочный прогон 13-го уровня иначе требует каждый раз честно
|
|
||||||
выигрывать бой с Джафаром (skill 9, 6 HP) — это дорого по времени, а
|
|
||||||
проверять надо совсем другое.
|
|
||||||
|
|
||||||
**Чем платим.** Ничем в игре: читы включаются флагом `pop_cheats`, в
|
|
||||||
релизной сборке они выключены. Расхождение наблюдаемо только с читами.
|
|
||||||
|
|
||||||
**Что проверять при регрессе.** Уровень 13: `K` на Джафаре → белая вспышка,
|
|
||||||
уход ВЛЕВО открывает дверь уровня. Честная победа в бою даёт то же самое.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Страж, вытесненный за правый край комнаты и там убитый, не виден нигде
|
|
||||||
|
|
||||||
**Не расхождение, а особенность оригинала.** Записано, чтобы вопрос не
|
|
||||||
возникал повторно (спросил пользователь 2026-08-19: бой шёл в комнате 15,
|
|
||||||
Кид вытеснил стража вправо — из-за края торчал только меч, — убил его, и
|
|
||||||
труп не появился ни в комнате 15, ни в соседней справа).
|
|
||||||
|
|
||||||
**Почему так.** Три механизма складываются:
|
|
||||||
|
|
||||||
1. **комнату страж не менял.** Его физика работает только в полосе
|
|
||||||
`x ∈ [44, 211)` (`seg000:1254`, у нас то же условие в
|
|
||||||
`pop_guard_phys_tick`), поэтому своим ходом за край он не уходит —
|
|
||||||
Кид вытолкнул его туда толчком, а `Guard.room` остался прежним;
|
|
||||||
2. **мёртвый за Кидом не идёт.** Единственный способ сменить комнату —
|
|
||||||
`follow_guard` при переходе Кида, и первое же условие там
|
|
||||||
(`seg002:0346`) — `Guard.alive < 0 && Guard.sword == sword_2_drawn`,
|
|
||||||
то есть ЖИВОЙ и с вынутым мечом. Мёртвый уходит веткой `leave_guard`,
|
|
||||||
которая сохраняет его в **`Guard.room`** — в старую комнату. У нас
|
|
||||||
ровно это же условие, `pop_guard_cold.c` (`pop_guard_follow`);
|
|
||||||
3. **из соседней комнаты страж не рисуется.** Оригинал при
|
|
||||||
`Guard.room != drawn_room` просто ГАСИТ слот (`seg000:422`:
|
|
||||||
`Guard.direction = dir_56_none`). Механизм «видно из-за шва»
|
|
||||||
(`xpos_in_drawn_room`) работает для коллизий и для Кида, но стража из
|
|
||||||
чужой комнаты на экран не выводит.
|
|
||||||
|
|
||||||
Итог: труп приписан комнате, где страж стоял, а его `guards_x` — за
|
|
||||||
правым краем. При возврате в ту комнату он честно восстанавливается там
|
|
||||||
же, то есть за пределами видимого поля; в соседней комнате его нет,
|
|
||||||
потому что в её данных стража и не было.
|
|
||||||
|
|
||||||
**Живой страж в этой ситуации ведёт себя иначе** — при уходе Кида вправо
|
|
||||||
он идёт следом, если стоит достаточно близко к краю (`Guard.x >= 165`).
|
|
||||||
Это портировано и работает.
|
|
||||||
|
|
||||||
**Чего я НЕ проверял:** живьём в SDLPoP этот сценарий не воспроизводил —
|
|
||||||
вывод сделан чтением трёх мест кода. Если понадобится подтверждение,
|
|
||||||
сценарий короткий: любой бой у правого края комнаты, вытеснить стража за
|
|
||||||
край и добить.
|
|
||||||
|
|
||||||
## ГСЧ разведён по доменам (у оригинала он ОДИН)
|
|
||||||
|
|
||||||
**Оригинал.** `random_seed` один на всё: кладка стены, анимация тайлов,
|
|
||||||
броски боя, модификаторы падающих плит — всё тянет из одной
|
|
||||||
последовательности (`seg009` PRNG, 32-битный LCG). Поэтому в оригинале
|
|
||||||
бой воспроизводим вместе со всем остальным: тот же сид — тот же бой.
|
|
||||||
|
|
||||||
**У нас.** Сидов несколько: `pop_t_seed` (кладка, `pop_tile.h`),
|
|
||||||
`pop_fight_seed` (броски боя, `pop_guard.h`), отдельные у trob и loose.
|
|
||||||
Сам генератор тот же (`pop_prandom`), таблицы вероятностей —
|
|
||||||
побайтно те же, что в `data.h`.
|
|
||||||
|
|
||||||
**Чем платим.** Конкретный бой у нас и в SDLPoP разойдётся: порядок
|
|
||||||
бросков другой, значит блоки/удары лягут иначе. Статистически поведение
|
|
||||||
то же (те же вероятности, тот же генератор), но «сверить бой кадр в кадр
|
|
||||||
с SDLPoP» нельзя, и QuickSave обязан сохранять ВСЕ сиды, а не один.
|
|
||||||
|
|
||||||
**Что проверять при регрессе.** Если страж кажется сильнее/слабее
|
|
||||||
оригинала — сначала проверить не таблицы (они сверены), а **режим
|
|
||||||
скорости**: `fight_speed` у оригинала 100 мс, а в нашем FASTEST бой идёт
|
|
||||||
61,4 мс, то есть в реальном времени на 63 % быстрее, и на глаз это ровно
|
|
||||||
«страж давит сильнее». Режим NORMAL (дефолт) даёт 102,4 мс — см.
|
|
||||||
`frame_pacing_plan.md`.
|
|
||||||
|
|
||||||
## Тень: кайма силуэта не подкрашивается фоном
|
|
||||||
|
|
||||||
**Оригинал.** Спрайт Тени не хранится — он кладётся ДВАЖДЫ: обычным
|
|
||||||
прозрачным блитом в x и «блиттером XOR» в x+1 (`draw_objtable_item`,
|
|
||||||
seg008.c:1600). XOR идёт по 24-битному RGB того, что УЖЕ на экране
|
|
||||||
(`blit_xor`, seg009.c:3190), поэтому там, где спрайт прозрачен в x, но
|
|
||||||
непрозрачен в x−1, цвет получается как `фон XOR цвет спрайта`.
|
|
||||||
|
|
||||||
**У нас.** Пакетный блит наложения на себя не умеет, поэтому результат
|
|
||||||
запечён в отдельный атлас (`toolchain/pop_pack_shadow.py`,
|
|
||||||
`docs/shadow_atlas_plan.md`). Запекать пришлось для КОНКРЕТНОГО фона, и
|
|
||||||
выбран чёрный: на нём `фон XOR цвет == цвет`, то есть запечка точна.
|
|
||||||
|
|
||||||
**Чем платим.** Ровно одним: **кайма в один пиксель по ЛЕВЫМ кромкам
|
|
||||||
силуэта** на НЕчёрном фоне. У оригинала она принимает оттенок фона, у нас
|
|
||||||
всегда «свой» цвет. Внутренность силуэта и правые кромки совпадают точно —
|
|
||||||
там первый проход уже закрасил пиксель, и от фона результат не зависит.
|
|
||||||
|
|
||||||
**Почему это приемлемо.** Тень бывает на четырёх уровнях, и почти всегда
|
|
||||||
на чёрном: у зеркала (ур. 4), в проёме (5), над пропастью (6), в бою (12).
|
|
||||||
|
|
||||||
**Что проверять при регрессе.** Если Тень окажется на светлом фоне и
|
|
||||||
кайма станет резать глаз — вариантов два: запечь второй набор под светлый
|
|
||||||
фон (ещё 32 страницы EMM) или считать эту кайму прозрачной (силуэт станет
|
|
||||||
на пиксель уже). Оба хуже нынешнего; трогать только по факту жалобы.
|
|
||||||
@@ -1,428 +0,0 @@
|
|||||||
# L9-INVERT — план реализации зелья переворота (уровень 9)
|
|
||||||
|
|
||||||
Рабочий план, по которому задача делается с чистого контекста. Доска —
|
|
||||||
[`../roomtest/TASKS_OPEN.md#l9-invert`](../roomtest/TASKS_OPEN.md); правила
|
|
||||||
подпроекта — `../CLAUDE.md` (SDLPoP = источник истины, диагноз железа —
|
|
||||||
только артефактом).
|
|
||||||
|
|
||||||
## 0. Что и зачем
|
|
||||||
|
|
||||||
Зелья типа 4 на уровне 9 (комната 7 тайл `(1,7)`, комната 10 тайл `(0,4)`)
|
|
||||||
переворачивают картинку вверх ногами. В оригинале это `toggle_upside()`
|
|
||||||
(seg000:15E9): `upside_down = ~upside_down`, `need_redraw_because_flipped = 1`.
|
|
||||||
Больше зелье не делает НИЧЕГО — ни вспышки, ни урона (seg006:1885, ветка
|
|
||||||
`case 4`). Снимается: смертью Кида (seg000:1224, при `alive >= 0`) и стартом
|
|
||||||
уровня (seg003:38/188). Управление НЕ инвертируется. Второе зелье
|
|
||||||
переворачивает обратно.
|
|
||||||
|
|
||||||
Оригинал переворачивает готовый offscreen на выводе (`flip_screen` перед
|
|
||||||
копированием прямоугольников на экран, seg000:939/946). Нам этот путь
|
|
||||||
закрыт: страниц ровно две (`gfx_set_visible_page` → ESTEX $54 SELPAGE, бит 0),
|
|
||||||
рабочей третьей нет. Поэтому:
|
|
||||||
|
|
||||||
- **уже нарисованное** переворачиваем ОДИН РАЗ построчной копией
|
|
||||||
акселератора (решение пользователя 2026-08-12);
|
|
||||||
- **всё, что рисуется дальше**, рисуем зеркально: фон (row-major) — новыми
|
|
||||||
vflip-блитами, персонажи (column-major) — заранее подготовленными
|
|
||||||
зеркальными кадрами.
|
|
||||||
|
|
||||||
Полоса HP и лейбл комнаты живут в борту (`y = 194 + POP_YOFF`, поле —
|
|
||||||
`POP_PLAYFIELD_H = 192`) и не переворачиваются, как и в оригинале.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Часть I — libbgi
|
|
||||||
|
|
||||||
## I.0 РАЗВЕДКА ЖЕЛЕЗА — ЗАКРЫТА 2026-08-12
|
|
||||||
|
|
||||||
**Оба факта подтверждены, ядро и обёртка написаны и проверены в MAME**
|
|
||||||
(`tests/pageflip`, 5/5 PASS — прямая копия, vflip, целость рамки, широкая
|
|
||||||
копия 320 = два прохода, неприкосновенность источника).
|
|
||||||
|
|
||||||
- **Р1 подтверждён**: Port_Y можно менять между read- и write-триггером,
|
|
||||||
если ПЕРЕД вторым OUT стоит STOP (`LD B,B`) — он разоружает accel, и fetch
|
|
||||||
immediate-операнда OUT уже безопасен. Приём не новый: ровно так работает
|
|
||||||
`_bgi_scroll_cols_raw` (ядро `gfx_scroll_v`), то есть он был в проде
|
|
||||||
задолго до этой задачи — указал пользователь. Значит и обходной путь
|
|
||||||
через ОЗУ-буфер не нужен;
|
|
||||||
- **Р2 подтверждён**: обе страницы адресуемы одновременно, копия между ними
|
|
||||||
идёт по разнице баз (`_gfx_addr_shadow_base` / `_gfx_addr_base`), строку
|
|
||||||
выбирает Port_Y.
|
|
||||||
|
|
||||||
Сделано: `libbgi/bgi256/_bgi_flip_rows_raw.c` (ядро) +
|
|
||||||
`libbgi/common/gfx_copy_page.c` (обёртка, режимы `GFX_COPY_DIRECT` /
|
|
||||||
`GFX_COPY_VFLIP`) + `tests/pageflip`. Пункты I.1 и I.3 ниже — ЗАКРЫТЫ этим
|
|
||||||
же коммитом; описание оставлено как контракт.
|
|
||||||
|
|
||||||
### Исходный текст разведки (для истории)
|
|
||||||
|
|
||||||
Два факта, на которых стоит вся схема, сейчас НЕ подтверждены артефактом.
|
|
||||||
Пока их нет, остальные пункты не начинать (memory `defer_unexplained_quirks`).
|
|
||||||
|
|
||||||
**Р1. Смена Port_Y между burst-чтением и burst-записью.** Схема требует:
|
|
||||||
армировать копию, `LD A,(HL)` (burst-чтение строки src), сменить Y,
|
|
||||||
`LD (DE),A` (burst-запись в другую строку). Между триггерами НЕЛЬЗЯ
|
|
||||||
выполнять инструкции, читающие ОПЕРАНД из памяти — fetch операнда
|
|
||||||
перезабивает буфер акселератора кодом (memory `accel_operand_fetch_retrigger`).
|
|
||||||
Значит:
|
|
||||||
|
|
||||||
- смена порта — только `out (c),a` (ED 79, регистровая форма); `out (#89),a`
|
|
||||||
(D3 89) читает immediate байт и УБЬЁТ буфер;
|
|
||||||
- новое значение Y — только из регистра (`ld a,d`), не `ld a,#n` и не из
|
|
||||||
памяти;
|
|
||||||
- `C` = 0x89 и оба значения Y должны лежать в регистрах ДО триггера чтения.
|
|
||||||
|
|
||||||
Проверка: `tests/accflip` по образцу `tests/accop` — заполнить строку
|
|
||||||
источника известным паттерном, скопировать в другую строку со сменой Y,
|
|
||||||
прочитать VRAM побайтно и сравнить. Отрицательный результат — стоп-сигнал:
|
|
||||||
переворот придётся делать через промежуточный ОЗУ-буфер (строка 320 Б), и
|
|
||||||
бюджет вырастет примерно вдвое.
|
|
||||||
|
|
||||||
**Р2. Адресация двух страниц в одном окне W3.** `gfx.h` утверждает, что
|
|
||||||
страница 1 «начинается на 320 байт дальше (0xC140)», но это комментарий, а не
|
|
||||||
замер. Нужно снять дампом: как адрес строки складывается из Port_Y и
|
|
||||||
смещения в окне, и видны ли обе страницы одновременно. От ответа зависит,
|
|
||||||
делается ли копия A→B одним проходом или через смену видеобанка.
|
|
||||||
|
|
||||||
Итог разведки записать в `libbgi/docs` (или `docs/new/06-accel.md`
|
|
||||||
дополнением) и в memory — это знание переиспользуется во всех будущих
|
|
||||||
экранных эффектах.
|
|
||||||
|
|
||||||
## I.1 Ядро копии с реверсом Y
|
|
||||||
|
|
||||||
`libbgi/bgi256/_bgi_copy_rows_raw.c` умеет только ИНКРЕМЕНТ Port_Y на строку
|
|
||||||
(`y0` + шаг вперёд; страйды патчатся SMC при входе). Нужен вариант, где
|
|
||||||
одна сторона идёт вниз, другая вверх.
|
|
||||||
|
|
||||||
- предпочтительно: новый leaf `_bgi_flip_rows_raw.c` (правило «1 функция =
|
|
||||||
1 модуль»), а не флаг в существующем — горячий путь блиттера трогать не
|
|
||||||
надо, и DCE не потащит лишнего в программы без переворота;
|
|
||||||
- вход тот же (`__sdcccall(1)`: src→HL, dst→DE, дальше стек), плюс
|
|
||||||
`y0_src` / `y0_dst` и признак направления;
|
|
||||||
- DI-окно — одна строка, как в остальных ядрах (арминг в каждой скобке:
|
|
||||||
в окне EI между чанками CBL-ISR армирует акселератор своим размером,
|
|
||||||
см. `docs/accel-fill-budget.md`).
|
|
||||||
|
|
||||||
## I.2 Публичные vflip-блиты для row-major — СДЕЛАНО 2026-08-12
|
|
||||||
|
|
||||||
Ассемблера не потребовалось вовсе: у row-major картинки вертикальное
|
|
||||||
зеркало — это порядок строк, а он задаётся ЗНАКОМ src-страйда, который
|
|
||||||
`_bgi_blit_rows_raw` и так принимает знаковым (`int sstride`) и патчит SMC
|
|
||||||
в adc-цепочку. Обёртки просто дают адрес ПОСЛЕДНЕЙ строки и `-stride`,
|
|
||||||
цена кадра та же, что у обычного блита.
|
|
||||||
|
|
||||||
Написаны `gfx_blit_noclip_vflip`, `gfx_blit_part_noclip_vflip`,
|
|
||||||
`gfx_blit_part_vflip`; `tests/pageflip` расширен до 8 проверок, все PASS.
|
|
||||||
|
|
||||||
Грабли, которые поймал тест: у клипающего варианта первая строка источника
|
|
||||||
обязана считаться от высоты ДО клипа (`h0`), иначе обрезка сверху экрана
|
|
||||||
сдвигает картинку — сначала формула брала уже укороченную `h`.
|
|
||||||
|
|
||||||
### Контракт (исходное описание)
|
|
||||||
|
|
||||||
Все row-major блиты приложения идут ровно через три функции (`pop_tile.c:281`,
|
|
||||||
`:283`, `:286`, `:325`, `:327`):
|
|
||||||
|
|
||||||
| есть | нужен vflip-двойник |
|
|
||||||
|---|---|
|
|
||||||
| `gfx_blit_noclip` | `gfx_blit_noclip_vflip` |
|
|
||||||
| `gfx_blit_part_noclip` | `gfx_blit_part_noclip_vflip` |
|
|
||||||
| `gfx_blit_part` (с клипом) | `gfx_blit_part_vflip` |
|
|
||||||
|
|
||||||
Семантика: `(x,y)` — левый ВЕРХНИЙ угол результата на экране (как у обычных
|
|
||||||
блитов), строки источника выводятся снизу вверх. Это позволяет вызывающему
|
|
||||||
не менять арифметику позиции, а только пересчитать `y` под поле.
|
|
||||||
|
|
||||||
`gfx_blit_cols*` (column-major) vflip-двойников НЕ получают: реверс идёт
|
|
||||||
внутри колонки, а accel копирует блок только вперёд. Для персонажей —
|
|
||||||
часть II.4.
|
|
||||||
|
|
||||||
`gfx_heal*` не трогаем: heal симметричен, он восстанавливает прямоугольник из
|
|
||||||
ОЗУ-копии по экранным координатам, а координаты вызывающий уже даёт
|
|
||||||
перевёрнутые.
|
|
||||||
|
|
||||||
Тест: `tests/blitvflip` — эталонный спрайт, побайтная сверка VRAM.
|
|
||||||
|
|
||||||
## I.3 Копия прямоугольника экран→экран
|
|
||||||
|
|
||||||
```c
|
|
||||||
#define GFX_COPY_DIRECT 0
|
|
||||||
#define GFX_COPY_VFLIP 1
|
|
||||||
void gfx_copy_rect(uint8_t src_page, int sx, int sy,
|
|
||||||
uint8_t dst_page, int dx, int dy,
|
|
||||||
uint16_t w, uint16_t h, uint8_t mode);
|
|
||||||
```
|
|
||||||
|
|
||||||
- `w > 256` режется на burst'ы вызывающим ядром (320 = 160+160 либо 256+64 —
|
|
||||||
выбрать по замеру, разницы в тактах на строку почти нет);
|
|
||||||
- **горизонтального флипа не будет**: accel копирует блок только вперёд,
|
|
||||||
побайтовый реверс дал бы 320 burst'ов на строку вместо двух. Если он
|
|
||||||
когда-нибудь понадобится — только через ОЗУ-буфер с CPU-реверсом;
|
|
||||||
- клип не нужен (вызывающий гарантирует границы), но проверка «прямоугольник
|
|
||||||
в пределах экрана» в safe-варианте библиотеки — обязательна.
|
|
||||||
|
|
||||||
Тест: `tests/pagecopy` — прямой режим и VFLIP, побайтная сверка.
|
|
||||||
|
|
||||||
## I.4 Чего в списке пользователя не хватало
|
|
||||||
|
|
||||||
1. **Размерный регресс**: после каждого шага `make size-check`; новые модули
|
|
||||||
не должны утянуть за собой рост в программах, которые их не зовут (проверка
|
|
||||||
на `examples/` и `tests/`).
|
|
||||||
2. **Обе сборки библиотеки** — fast и safe (`sprinter-cc --safe`): гарды
|
|
||||||
параметров в safe, критичные — в обеих.
|
|
||||||
3. **Документация**: `docs/sprite-api-design.md` (раздел про блиттер),
|
|
||||||
`docs/TODO.md`, справочник API; факты разведки I.0 — в `docs/new/06-accel.md`.
|
|
||||||
4. **IM2/CBL**: длинные серии коротких DI-окон не должны ломать кадровые
|
|
||||||
прерывания и клавиатуру — проверить `tests/kbdpoll`-сценарием после
|
|
||||||
внедрения (клавиатура вычерпывается idle-хуком, а переворот идёт вне
|
|
||||||
ожидания кадра).
|
|
||||||
5. **Порядок «страница ↔ ОЗУ-копия»**: копия должна идти банком
|
|
||||||
`GFX_BANK_NORMAL`, иначе перевёрнутый фон не попадёт в ОЗУ-копию и heal
|
|
||||||
начнёт восстанавливать старую картинку. Это ключевой инвариант всей
|
|
||||||
схемы — вынести отдельным утверждением в тест.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Часть II — приложение (roomtest)
|
|
||||||
|
|
||||||
## II.1 Состояние и момент переключения — СДЕЛАНО 2026-08-12
|
|
||||||
|
|
||||||
Проверено в MAME (уровень 9, комната 7): нажатие чита переворачивает всю
|
|
||||||
комнату целиком за один кадр, повторное — возвращает. Персонажи и
|
|
||||||
анимируемые тайлы (факелы) пока рисуются НЕ зеркально — это шаги II.3/II.4.
|
|
||||||
|
|
||||||
Что появилось:
|
|
||||||
|
|
||||||
- `pop_upside` + `pop_upside_dirty` (порт `upside_down` и
|
|
||||||
`need_redraw_because_flipped`) в `pop_map.c`;
|
|
||||||
- ветка `case 4` в `pop_proc_get_object`: только тоггл, без вспышки и урона
|
|
||||||
(как seg006:1885);
|
|
||||||
- сброс в `pop_start_level` (seg003:38/188) и по смерти Кида (seg000:1224);
|
|
||||||
- `pop_flip_screen()` в банке 8: `gfx_copy_page(VFLIP)` в скрытую страницу →
|
|
||||||
показать её → `gfx_copy_page(DIRECT)` во вторую → инвалидация слотов
|
|
||||||
персонажей, метки «фон трогали» и полосы HP;
|
|
||||||
- **чит-клавиша U** — это ПОРТ, а не костыль: в оригинале переворот тоже
|
|
||||||
висит на чит-клавише (seg000:0793). Без неё эффект проверяется только
|
|
||||||
честным проходом уровня 9 до комнаты 7 — чит-телепорт до зелья не
|
|
||||||
дотягивается (оно на `(1,7)`, а навигация ставит Кида на нижний ряд).
|
|
||||||
|
|
||||||
Цена: резидент +602 Б (новые функции libbgi линкуются в него), куча
|
|
||||||
1707 → 1050 Б. Это аргумент за [MEM-COLD2](../roomtest/TASKS_OPEN.md#mem-cold2)
|
|
||||||
до начала II.4.
|
|
||||||
|
|
||||||
### Контракт (исходное описание шага)
|
|
||||||
|
|
||||||
- `pop_upside` (uint8_t) — рядом с `pop_feather` в `pop_map.c`, экспорт в
|
|
||||||
`pop_map.h`;
|
|
||||||
- тоггл — в `pop_proc_get_object`, ветка `potion_type == 4` (сейчас там TODO,
|
|
||||||
как было у пера). Ничего кроме тоггла: ни вспышки, ни звука — так в
|
|
||||||
оригинале;
|
|
||||||
- сброс: `pop_start_level` (уже переехал в `roomtest_cold.c`) и смерть Кида —
|
|
||||||
по образцу seg000:1224 (в `kid_phys`/там, где ловится `alive >= 0`);
|
|
||||||
- переключение (функция `pop_flip_screen()`, банк 8 — холодный код):
|
|
||||||
1. `gfx_copy_rect(front, …, back, …, GFX_COPY_VFLIP)` — скрытая страница
|
|
||||||
получает перевёрнутый чистый фон (accel читает ОЗУ-копию, персонажей в
|
|
||||||
ней нет);
|
|
||||||
2. флип страниц, чтобы игрок сразу увидел результат;
|
|
||||||
3. `gfx_copy_rect(new_front, …, new_back, …, GFX_COPY_DIRECT)` — вторая
|
|
||||||
страница дабл-буфера получает то же;
|
|
||||||
4. инвалидация: `pop_cd[].valid = 0` для обоих слотов, `pop_mirror_heal`
|
|
||||||
-состояние, `pop_hp_invalidate()`, метки «фон трогали» (`pop_cd_touch`)
|
|
||||||
на обе страницы, `seam_sig` — чтобы шов перерисовался.
|
|
||||||
|
|
||||||
## II.2 Геометрия — единая точка пересчёта
|
|
||||||
|
|
||||||
```c
|
|
||||||
/* pop_bg.h рядом с POP_YOFF/POP_PLAYFIELD_H */
|
|
||||||
#define POP_FLIP_Y(y, h) (2 * POP_YOFF + POP_PLAYFIELD_H - (y) - (h))
|
|
||||||
```
|
|
||||||
|
|
||||||
Применяется ТОЛЬКО к тому, что внутри поля. Правило: любая функция,
|
|
||||||
получающая экранный `y`, либо сама зеркалит его по флагу, либо принимает уже
|
|
||||||
зеркальный — смешивать нельзя, иначе получим двойной переворот (это самый
|
|
||||||
вероятный класс багов здесь). Решение: зеркалит ВЫЗЫВАЮЩИЙ, у примитивов
|
|
||||||
семантика не меняется.
|
|
||||||
|
|
||||||
## II.3 Фон — СДЕЛАНО 2026-08-12
|
|
||||||
|
|
||||||
Флаг проведён в ЕДИНСТВЕННУЮ точку — `pop_blit_b`/`blit_b_clip`
|
|
||||||
(`pop_tile.c`): позиция пересчитывается макросом `FLIP_TOP` (он один на весь
|
|
||||||
слой — дублировать нельзя, иначе двойной переворот), а блит выбирает
|
|
||||||
vflip-двойник. Тем самым зеркалятся и полная отрисовка комнаты, и точечные
|
|
||||||
редрои, и анимации trob, и fore-слой, и кладка — все они ходят через эту
|
|
||||||
функцию.
|
|
||||||
|
|
||||||
Тонкость клипованного пути: при зеркале обрезка СВЕРХУ экрана съедает
|
|
||||||
НИЖНИЕ строки источника, поэтому кусок пересчитывается как
|
|
||||||
`sy' = h - sy - dh`.
|
|
||||||
|
|
||||||
Проверено в MAME (уровень 9, чит U): комната перевёрнута целиком — пол
|
|
||||||
вверху, дверь уровня и кладка вверх ногами. Персонажи пока обычные (II.4).
|
|
||||||
|
|
||||||
Цена: три функции libbgi ушли в резидент, куча 1574 → 450 Б, поэтому тем же
|
|
||||||
заходом сделан **MEM-COLD2 п.1** — `enter_room_side`/`enter_room` уехали в
|
|
||||||
банк 8 (куча вернулась к **1231 Б**, банк занят на 14.6%). Рабочие массивы
|
|
||||||
комнаты остались в `_DATA` и видны обеим половинам; `enter_room` обязана быть
|
|
||||||
`__banked` — её зовёт главный цикл.
|
|
||||||
|
|
||||||
### Контракт (исходное описание)
|
|
||||||
|
|
||||||
Одна точка: `pop_tile.c` (блит куска атласа) выбирает vflip-двойник по
|
|
||||||
`pop_upside` и пересчитывает `y`. Тогда автоматически попадают:
|
|
||||||
|
|
||||||
- полная отрисовка комнаты (`pop_room.c`);
|
|
||||||
- точечные редрои (`pop_redraw.c`: ворота, пики, кнопки, дверь уровня,
|
|
||||||
дрожащие плиты);
|
|
||||||
- анимации `pop_trob` (факелы, пузырьки зелий);
|
|
||||||
- fore-слой и оверлеи кромки, кладка стены (`pop_bg.c`);
|
|
||||||
- падающие куски loose (`pop_room.c`, `pop_loose_mob_*`).
|
|
||||||
|
|
||||||
Проверить отдельно: `wall_pattern` (кладка рисуется своей геометрией) и
|
|
||||||
оверлеи кромки — у них позиция считается от ряда, а не от `y` спрайта.
|
|
||||||
|
|
||||||
## II.4 Персонажи (column-major)
|
|
||||||
|
|
||||||
Нужны зеркальные кадры: 34 атласа, 228 563 Б (kid0..27 + sword, guard g0..g4).
|
|
||||||
|
|
||||||
- реверс колонки — через стек (`pop`/`push`), ~10 тактов номинала на байт;
|
|
||||||
- источник маппится в W0, приёмник — новая EMM-страница; на спрайт: прочитать
|
|
||||||
колонки в буфер W2 (максимальный кадр — 40×16 ≈ 640 Б), развернуть, писать
|
|
||||||
в приёмник (перемаппинг W0 на спрайт — один OUT);
|
|
||||||
- `pop_cdraw.c` выбирает набор атласов (оригинал/зеркальный) по `pop_upside`;
|
|
||||||
- **кэш ключуется по СТРАНИЦЕ атласа**, а не по кадру: страница = 8 кадров,
|
|
||||||
3-10 КБ, ~0.2-0.4 растрового кадра на переворот.
|
|
||||||
|
|
||||||
**Стратегия наполнения — ЛЕНИВЫЙ ПОСТРАНИЧНЫЙ** (решение пользователя
|
|
||||||
2026-08-12): страница переворачивается при первом обращении к ней в
|
|
||||||
перевёрнутом режиме. Реально в ходу 5-10 страниц из 34, и если игрок зелье не
|
|
||||||
трогал, не тратится ни такта, ни страницы.
|
|
||||||
|
|
||||||
**Время жизни кэша — ДО КОНЦА УРОВНЯ**: освобождаем страницы при смене
|
|
||||||
уровня, а не при снятии эффекта. На уровне 9 переворот случается минимум
|
|
||||||
дважды (второе зелье возвращает всё назад), плюс его снимает смерть Кида —
|
|
||||||
готовить пачку заново каждый раз было бы обидно.
|
|
||||||
|
|
||||||
После [UI-SPRITES](../roomtest/TASKS_OPEN.md#ui-sprites) страницы `kid27` и
|
|
||||||
`g0` станут чисто «полевыми» (деления HP уедут в константы резидента), и
|
|
||||||
исключений в кэше не останется — эту задачу логично сделать ДО II.4.
|
|
||||||
|
|
||||||
## II.5 Клип — ЧАСТЬ СДЕЛАНА 2026-08-12 (поле), остаётся clip_char
|
|
||||||
|
|
||||||
**Клип ПОЛЯ сделан.** Симптом (нашёл пользователь): после телепорта в
|
|
||||||
перевёрнутом виде ниже поля — мусор. Причина: спрайты, которые в обычном
|
|
||||||
виде торчат ВЫШЕ поля (полоса кладки у потолка, её режет `pop_t_clip_top`),
|
|
||||||
после отражения торчат НИЖЕ и лезут в борт, где живёт полоса HP.
|
|
||||||
|
|
||||||
Фикс в `blit_b_clip`: при перевороте режем по ОБЕИМ границам поля всегда, а
|
|
||||||
не по `pop_t_clip_top` — зеркальный спрайт может вылезти и там, где в
|
|
||||||
обычном виде клип не ставили. Плюс в `pop_blit_b` быстрый путь (без клипа)
|
|
||||||
теперь пропускает в клипующий всё, что выходит за поле.
|
|
||||||
|
|
||||||
Проверено в MAME: телепорт по комнатам в перевёрнутом виде — борта чистые,
|
|
||||||
комнаты рисуются зеркально (факелы, чомперы, пол). То есть **переходы
|
|
||||||
между комнатами в перевёрнутом виде работают**.
|
|
||||||
|
|
||||||
Осталось из этого пункта: `clip_char` (верх поля ↔ низ) и клип падающей
|
|
||||||
плиты — они про ПЕРСОНАЖЕЙ и падающие объекты, то есть идут вместе с II.4.
|
|
||||||
|
|
||||||
### Контракт (исходное описание)
|
|
||||||
|
|
||||||
- `clip_char` (`pop_map.c:2287`, таблица `y_clip[5] = {-60,3,66,129,192}`) —
|
|
||||||
режет верх спрайта по линии ряда; при перевороте линия становится нижней.
|
|
||||||
Симметрия сама не сойдётся: нужен зеркальный расчёт `y_clip` и обмен
|
|
||||||
«сверху/снизу» местами;
|
|
||||||
- то же для зеркала уровня 4 (`pop_map.c:2732`) — на уровне 9 зеркал нет, но
|
|
||||||
код общий, поэтому ветку надо хотя бы не сломать;
|
|
||||||
- `pop_clip_sprite` (`pop_room.c:1069`) — клип падающей плиты;
|
|
||||||
- `pop_room_clip_borders` — борта симметричны (28 сверху и снизу), но
|
|
||||||
проверить, что чистится именно поле.
|
|
||||||
|
|
||||||
## II.6 Проверка
|
|
||||||
|
|
||||||
- **хост-тест** (`tests-host`): арифметика `POP_FLIP_Y` и зеркальный `y_clip`
|
|
||||||
— сцена «Кид у верхней кромки» в обычном и перевёрнутом режиме даёт
|
|
||||||
симметричные значения клипа. Логику отрисовки хост-тесты не видят, поэтому
|
|
||||||
здесь проверяем только счёт;
|
|
||||||
- **MAME**: уровень 9, комната 7 — выпить зелье `(1,7)`, проверить: картинка
|
|
||||||
перевернулась целиком, Кид ходит и цепляется корректно, факелы анимируются
|
|
||||||
перевёрнуто, ворота/пики перерисовываются перевёрнуто, полоса HP осталась
|
|
||||||
внизу и не зеркальная; второе зелье (комната 10) возвращает как было;
|
|
||||||
смерть Кида снимает эффект;
|
|
||||||
- **регресс**: обычные уровни (1-8) не должны измениться ни на пиксель —
|
|
||||||
прогнать пару комнат и сверить скриншоты с прежними.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Часть III — ENTER-ROOM-FAST — СДЕЛАНО 2026-08-12
|
|
||||||
|
|
||||||
Комната теперь рисуется ОДИН раз — в скрытую страницу, — тут же
|
|
||||||
показывается флипом, а вторая страница получает её `gfx_copy_page(DIRECT)`.
|
|
||||||
Процесс рисования тайлов больше не виден: игрок получает готовый кадр.
|
|
||||||
Проверено в MAME: старт уровня и чит-переход между комнатами, два
|
|
||||||
последовательных снимка идентичны (обе страницы синхронны, мерцания нет).
|
|
||||||
|
|
||||||
Побочно понадобилось: видимую страницу главный цикл больше не ведёт сам, а
|
|
||||||
перечитывает у железа в начале кадра (`front = gfx_get_visible_page()`) —
|
|
||||||
её меняют и вход в комнату, и переворот, причём оба из банка, где локальные
|
|
||||||
переменные `main` недоступны.
|
|
||||||
|
|
||||||
# Часть III (исходное описание)
|
|
||||||
|
|
||||||
Идея пользователя, самостоятельная ценность (сейчас при входе в комнату видно,
|
|
||||||
как рисуются тайлы): рисовать комнату ОДИН раз в скрытую страницу, показать её
|
|
||||||
флипом, а во вторую страницу залить `gfx_copy_rect(…, GFX_COPY_DIRECT)`.
|
|
||||||
|
|
||||||
- сейчас `enter_room_side` рисует комнату дважды (цикл `for (pg = 0; pg < 2)`)
|
|
||||||
— вторая отрисовка (~полмиллиона тактов) меняется на копию (~115-200 К);
|
|
||||||
- процесс рисования перестаёт быть виден: игрок видит только готовый кадр;
|
|
||||||
- делается тем же `gfx_copy_rect`, что и I.3, то есть ничего дополнительно
|
|
||||||
писать не надо.
|
|
||||||
|
|
||||||
Проверять отдельно: состояние ОЗУ-копии второй страницы после копии
|
|
||||||
(инвариант из I.4 п.5), метки «фон трогали», и что heal на второй странице
|
|
||||||
работает.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Порядок работ
|
|
||||||
|
|
||||||
1. I.0 разведка (`tests/accflip`) → факты в docs + memory.
|
|
||||||
2. I.1 ядро + I.3 `gfx_copy_rect` (+ `tests/pagecopy`).
|
|
||||||
3. II.1 состояние и `pop_flip_screen` — уже можно посмотреть в MAME:
|
|
||||||
картинка обязана перевернуться целиком, дальше она «поедет» по мере
|
|
||||||
перерисовок (это ожидаемо на этом шаге).
|
|
||||||
4. Часть III (ENTER-ROOM-FAST) — дешёвый выигрыш на том же кирпиче,
|
|
||||||
заодно обкатывает копию в бою.
|
|
||||||
5. I.2 vflip-блиты (+ `tests/blitvflip`) → II.3 фон. После этого сцена
|
|
||||||
должна выглядеть правильно во всём, кроме персонажей.
|
|
||||||
6. UI-SPRITES (деления HP в резидент) → II.4 зеркальные кадры персонажей.
|
|
||||||
7. II.5 клип → II.6 проверка и регресс.
|
|
||||||
|
|
||||||
Критерий готовности каждого шага — артефакт (побайтный тест в MAME либо
|
|
||||||
скриншот сцены), а не «код написан».
|
|
||||||
|
|
||||||
# Риски
|
|
||||||
|
|
||||||
- **Р1 не подтвердится** (нельзя менять Y между триггерами) — переворот
|
|
||||||
пойдёт через ОЗУ-буфер строки, бюджет вырастет вдвое (всё ещё ≈0.5
|
|
||||||
логического кадра), схема в целом выживает;
|
|
||||||
- **двойной переворот координат** — самый вероятный баг: часть кода зеркалит
|
|
||||||
`y` сама, часть получает уже зеркальный. Лечится правилом из II.2 и тем,
|
|
||||||
что зеркалит только вызывающий;
|
|
||||||
- **кэш зеркальных кадров и память**: 34 страницы EMM в худшем случае; по
|
|
||||||
`sprinter_emm_budget` на старте свободно 215 — влезает, но проверить
|
|
||||||
фактический остаток на уровне 9 (там уже загружены атласы фона, Кида,
|
|
||||||
стража и уровень);
|
|
||||||
- **fore-слой поверх персонажа**: он рисуется по футпринту спрайта, а
|
|
||||||
футпринт после переворота другой — проверить именно на сцене с колоннами.
|
|
||||||
|
|
||||||
# Решения пользователя (2026-08-12)
|
|
||||||
|
|
||||||
1. **Кэш зеркальных кадров — ленивый постраничный**, наполняется по первому
|
|
||||||
обращению (см. II.4).
|
|
||||||
2. **Живёт до конца уровня**, освобождается при смене уровня.
|
|
||||||
3. **ENTER-ROOM-FAST делается сразу**, шагом 4 — на готовом `gfx_copy_rect`,
|
|
||||||
до того как на копию завяжется переворот.
|
|
||||||
|
|
||||||
Открытых вопросов не осталось: план исполняется как есть, новые развилки —
|
|
||||||
только если разведка I.0 даст отрицательный результат по Р1.
|
|
||||||
@@ -1,532 +0,0 @@
|
|||||||
# roomtest — план v2: размер кода и раскладка по окнам/банкам/страницам
|
|
||||||
|
|
||||||
> **Замер 2026-08-08, после MEM-BANK2 (актуальная сборка, ALLOCS=3000).**
|
|
||||||
> `_CODE` 22 556 Б, данные 4 375, куча 4 301. Банки: 1 (`guards.c`) 2 289,
|
|
||||||
> 2 (`pop_bg.c`, ГОРЯЧАЯ половина фона) 5 872, 3 (`pop_map.c`) 8 483,
|
|
||||||
> 4 (`pop_cdraw.c`) 5 664, 5 (`pop_ctrl.c`) 2 040, 6 (`pop_trob.c`) 2 889,
|
|
||||||
> 7 (`pop_room.c`, ХОЛОДНАЯ половина фона) 6 035 — все из 16 384.
|
|
||||||
>
|
|
||||||
> Слой фона разрезан надвое по ЧАСТОТЕ ВЫЗОВА (`TASKS_CLOSED.md#mem-bank2`):
|
|
||||||
> общие листья — в резидент `pop_tile.c` (W1 замаплен всегда, таблицы видны
|
|
||||||
> обеим половинам), горячий fore-проход остался в банке 2, холодная отрисовка
|
|
||||||
> комнаты и точечные перерисовки уехали в банк 7. Стык — три тонкие
|
|
||||||
> `__banked`-обёртки, чтобы трамплин платил только холодный путь.
|
|
||||||
> **Банк 2: 90.4 % -> 35.8 %.** Резидент вырос на 2 КБ (листья) — куча
|
|
||||||
> 6 333 -> 4 301 Б, это плата за то, что таблицы должны быть видны из двух
|
|
||||||
> банков.
|
|
||||||
>
|
|
||||||
> Число банков дублируется: `--bank N=` в Makefile И `const uint8_t n_banks`
|
|
||||||
> в `roomtest.c`. Расхождение — не ошибка сборки, а зависание до первого
|
|
||||||
> кадра.
|
|
||||||
>
|
|
||||||
> **Замер 2026-08-01 (историческая отметка).** `_CODE` 25 119 Б, `_DATA` 3 709,
|
|
||||||
> куча ~2.4 КБ. Банки: 1 (`guards.c`) 1 896, 2 (`pop_bg.c`) 13 792,
|
|
||||||
> 3 (`pop_map.c`) 6 331, 4 (отрисовка стража) 2 236 — все из 16 384.
|
|
||||||
> **Резидента `--w3` больше нет**: отрисовка уехала в банк 2, и это сняло
|
|
||||||
> главное ограничение резидента (из банка его было не достать) — банк→банк
|
|
||||||
> работает, трамплин сохраняет страницу окна на стеке. Отрисовка стража
|
|
||||||
> вынесена из банка 2 в собственный банк 4, потому что банк 2 подошёл к
|
|
||||||
> потолку (16 021 из 16 384) — коммит `2f3e854`.
|
|
||||||
>
|
|
||||||
> **Свободного места в банке 2 теперь 10.5 КБ**, в банке 7 — 10.3 КБ; туда
|
|
||||||
> просятся чомперы, зеркало и второй тайлсет (palace). Прежде чем начинать
|
|
||||||
> `levels_plan.md` §3 — всё равно посчитать, куда это ляжет. Следующий
|
|
||||||
> свободный номер банка — 8, гранулярность — файл.
|
|
||||||
>
|
|
||||||
> Из плана ниже **не сделаны шаги 5 (данные: `room_modif`, `dl1/dl2`,
|
|
||||||
> `_kbdraw_down`; потенциал ~1.5 КБ) и 7 (дедуп `draw_tile`, отложен по
|
|
||||||
> решению пользователя)**.
|
|
||||||
|
|
||||||
Статус: **план для отдельной сессии**, составлен 2026-07-29 по свежему замеру.
|
|
||||||
Заменял `size_optimization_plan.md` (v1, 2026-07-21) — тот удалён 2026-08-01
|
|
||||||
как полностью перекрытый этим документом. Документ самодостаточный
|
|
||||||
— рассчитан на старт с пустого контекста.
|
|
||||||
|
|
||||||
Повод: перед стражами и боёвкой (новый код ~5–8 КБ) надо понять, куда он
|
|
||||||
поместится, и заранее развести код так, чтобы банкованные модули не упёрлись в
|
|
||||||
ограничения окна W3.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## СТАТУС ВЫПОЛНЕНИЯ (обновлено 2026-07-29, коммит ecf5ecf)
|
|
||||||
|
|
||||||
| Шаг | Статус | Факт |
|
|
||||||
|-----|--------|------|
|
|
||||||
| 1. `kid_data.h` → EMM-страница + `load_frame`/`cur_frame` | **сделан** | `_CODE` −3 247 Б; попутно найден и обойдён баг кодогенерации SDCC (см. ниже) |
|
|
||||||
| 2. `pop_geom.c` (дедуп геометрии + PRNG) | **сделан** | −41 Б `_CODE`, −11 Б W3; ценность — не байты, а bank-safe слой |
|
|
||||||
| 3. `pop_map` без вызовов графики | **сделан** (фазы 1a/1b) | через пометки перерисовки, см. ниже |
|
|
||||||
| 4. Разгрузка/перебалансировка W3 | **сделан** | резидент = `pop_bg` + `pop_gdraw` (отрисовка стража, 2026-07-29); W3 14 376 → 12 819 (свободно 3 565 Б) |
|
|
||||||
| 5. Данные (`room_modif`, `dl1/dl2`, `_kbdraw_down`) | не начат | потенциал ~1.5 КБ |
|
|
||||||
| 6. Контракт банка стражей + пробник | **пробник сделан** | `tests/w3bankgfx` — модель подтверждена в MAME, см. ниже |
|
|
||||||
| 7. Дедуп семейства `draw_tile` | отложен по решению пользователя | «мороки много, выгода не так велика» |
|
|
||||||
|
|
||||||
**Замер сейчас против замера §1:** `_CODE` 24 881 → 24 421, куча W2 2 076 → 2 592 Б,
|
|
||||||
W3-резидент 14 376 → 11 632 (свободно 2 008 → 4 752 Б). Сумма кода упала
|
|
||||||
на ~3.2 КБ (данные Kid уехали в EMM), остальное — перераспределение.
|
|
||||||
|
|
||||||
**Замер 2026-07-29 (после стража).** Появление стража съело кучу до 805 Б;
|
|
||||||
разгрузка — вынос ОТРИСОВКИ стража в резидент (`pop_gdraw.c`, `--w3`), логика
|
|
||||||
и состояние остались в W1/W2, чтобы банк `guards.c` их видел (R2). Итог:
|
|
||||||
`_CODE` 26 149 → 25 703, куча **805 → 1 245 Б**, W3-резидент 11 656 → 12 819
|
|
||||||
(свободно 4 728 → 3 565 Б), банк 1 — 236 / 16 384 Б. Граница «что резидент»
|
|
||||||
теперь формулируется одним правилом: **резидент = только то, что рисует и
|
|
||||||
зовётся исключительно из главного цикла**; всё, что может понадобиться банку,
|
|
||||||
остаётся в W1/W2.
|
|
||||||
|
|
||||||
### Что сделано вместо §5.3 (вынос loose в W3)
|
|
||||||
|
|
||||||
Вместо переноса кода между окнами выбран (по обсуждению с пользователем)
|
|
||||||
**порт архитектуры оригинала**: логика ставит пометку, отрисовка идёт
|
|
||||||
отдельным проходом — `set_redraw_*` (seg007) + `redraw_needed` (seg008:0178).
|
|
||||||
Появился `pop_redraw.c/.h`; `pop_trob` и `pop_map` больше не рисуют тайлы.
|
|
||||||
Наши самодельные счётчики (`spike_rest`, `button_rest`, `ldoor_rest`,
|
|
||||||
`loose_bake`, `loose_rest`, `ceil_rest`, `ceil_bake`, `land_bake`) удалены —
|
|
||||||
их роль (вторая страница дабл-буфера) взял счётчик страниц в пометке.
|
|
||||||
|
|
||||||
**Два исключения остались** (обе — функции ТОЛЬКО главного цикла, звать из
|
|
||||||
банка нельзя):
|
|
||||||
- `pop_process_trobs` — пламя факела и пузырёк зелья (покадровый оверлей);
|
|
||||||
- `pop_loose_tick` — падающий кусок (mob): spawn/tick/pos. В оригинале это
|
|
||||||
отдельная подсистема (`mobs` + `draw_moving`), разделение на логику и
|
|
||||||
отрисовку — задел следующей фазы.
|
|
||||||
|
|
||||||
### Пробник банка (2026-07-29): модель ПОДТВЕРЖДЕНА
|
|
||||||
|
|
||||||
`tests/w3bankgfx` (huge + `--w3 res.c` + `--bank 1=bank1.c`, графика 256):
|
|
||||||
|
|
||||||
- банк рисует примитивом libbgi НАПРЯМУЮ — работает; страница W3 внутри
|
|
||||||
банка до блита, после блита и после возврата из вызванной им W1/W2-функции
|
|
||||||
одна и та же (0xF0), резидент — 0xF3. То есть `_bgi_begin`/`_bgi_end`
|
|
||||||
корректно возвращают ИМЕННО банковую страницу (правило R4);
|
|
||||||
- вызов W1/W2-функции из банка работает, и она тоже может рисовать;
|
|
||||||
- резидент W3 жив и вызывается после возврата из банка (R3).
|
|
||||||
|
|
||||||
**Дополнительно выяснено (важно для стражей):** писучие статики
|
|
||||||
`__banked`-модуля линкуются В СТРАНИЦУ БАНКА (0x1C000+) — снаружи их не
|
|
||||||
прочитать, из W1/W2 по 0xC000 видна резидентная страница. Значит всё
|
|
||||||
состояние банкованного кода (позиции стражей, таймеры боя) обязано жить в
|
|
||||||
W1/W2 как обычные глобалы, а банк — только код.
|
|
||||||
|
|
||||||
Ещё одна мина, найденная там же: инлайновый `in a,(#0xE2)` посреди тела
|
|
||||||
функции затирает A, куда SDCC уже положил параметр (у нас из-за этого цвет
|
|
||||||
заливки стал номером страницы, и «резидент не рисовал»). Читать порт
|
|
||||||
отдельной `__naked`-функцией.
|
|
||||||
|
|
||||||
### Найденная по дороге ловушка компилятора
|
|
||||||
|
|
||||||
`(const T *)КОНСТАНТА + var*K` SDCC 4.5 может собрать неверно: умножение
|
|
||||||
делает в 16 битах, а потом берёт только младший байт (`ld c,l` / `inc b`).
|
|
||||||
Кадры Kid с индексом ≥ 52 читались из чужой строки таблицы, у бега/шага
|
|
||||||
пропадал `FRAME_NEEDS_FLOOR` и персонаж проваливался сквозь пол. Лечение —
|
|
||||||
считать адрес в `uint16_t` и кастовать один раз. Тот же паттерн в
|
|
||||||
`pop_level.c` компилируется ПРАВИЛЬНО, т.е. полагаться на «у соседа
|
|
||||||
работает» нельзя. Подробности: memory `sdcc_z80_const_ptr_index_bug`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 0. Что уже сделано из v1 (не повторять)
|
|
||||||
|
|
||||||
- `--opt-code-size` и `--max-allocs 100000` **уже включены по умолчанию** в
|
|
||||||
`bin/sprinter-cc` (v1 §2.1 закрыт, выигрыш получен).
|
|
||||||
- Лишние блиты переднего слоя убраны (v1 §7 п.0): `fore_tile` больше не рисует
|
|
||||||
`bottom_id`, `_CODE` −388 Б.
|
|
||||||
- `gfx_blit_noclip` в libbgi (v1 §8 шаг 1): фоновые блиты в 2.9× дешевле.
|
|
||||||
- `--w3` как резидент окна 3 реализован и обкатан (memory `w3_resident_code`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. ЗАМЕР (сборка 2026-07-29, коммит 1214785)
|
|
||||||
|
|
||||||
Команда: `--memory small --gfx 256 --w3 pop_trob.c pop_map.c --w3 pop_bg.c`.
|
|
||||||
|
|
||||||
### 1.1 Окна
|
|
||||||
|
|
||||||
| Область | Занято | Свободно | Примечание |
|
|
||||||
|---|---|---|---|
|
|
||||||
| W1+W2 `_CODE` | 24 881 Б | — | 0x4100…0xA231 |
|
|
||||||
| W1+W2 `_HOME`+`_GSINIT`+`_DATA`+`_BSS` | ~4 240 Б | — | до 0xB2E4 |
|
|
||||||
| **W1+W2 куча** | 0 (никто не malloc'ит) | **2 076 Б** | 0xB2E4…0xBB00 |
|
|
||||||
| W1+W2 стек | — | 1 279 Б | 0xBB00…0xBFFE |
|
|
||||||
| **W3 резидент** | 14 376 Б | **2 008 Б** | 0xC000…0xF828 |
|
|
||||||
| EMM-страницы | 37 атласов + 1 уровень | ~215 страниц свободно | `sprinter_emm_budget` |
|
|
||||||
|
|
||||||
**Итого запаса до стены: ≈ 4 КБ** (2 КБ в W1/W2 + 2 КБ в W3). Стражи туда
|
|
||||||
не влезут.
|
|
||||||
|
|
||||||
### 1.2 Код по модулям (точно, из `.rel`)
|
|
||||||
|
|
||||||
```
|
|
||||||
W1/W2 (_CODE 24 881): W3 резидент (_W3CODE 14 376):
|
|
||||||
pop_kid 6 963 pop_bg 11 643
|
|
||||||
pop_map 6 157 pop_trob 2 733
|
|
||||||
roomtest 2 583
|
|
||||||
pop_level 1 425
|
|
||||||
pop_ctrl 1 145
|
|
||||||
crt0 333
|
|
||||||
libc+libbgi ~6 275
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.3 Крупнейшие функции/данные (из `.lst`)
|
|
||||||
|
|
||||||
```
|
|
||||||
pop_bg : draw_tile 3080, other_overlay_tile 1146, wall_pattern 944,
|
|
||||||
mob_render 720, mob_tick_one 661, overlay_mid_tile 498,
|
|
||||||
fore_only_tile 409, climb_overlay_tile 391, tile_table 371
|
|
||||||
pop_map : check_loose_fall_on_kid 674, check_bumped 567, jump_up_or_grab 413,
|
|
||||||
get_tile 266, do_knock 243, check_press 238, check_leave 212
|
|
||||||
pop_kid : kid_seqtbl 2310 + kid_frames 1205 + kid_seq_off 230 = 3745 Б ДАННЫХ
|
|
||||||
(в _CODE!), собственно кода ~3.2 КБ
|
|
||||||
roomtest : enter_room+main ~2.1 КБ
|
|
||||||
pop_level: room_bg_ptr 1084 (+ 515 Б таблиц LINKLOC/LINKMAP в _DATA)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.4 `_DATA` (3 710 Б)
|
|
||||||
|
|
||||||
```
|
|
||||||
pop_trob 963 (room_modif[24][30] = 720 + trobs + rest-массивы)
|
|
||||||
pop_level 515 (копии LINKLOC/LINKMAP уровня)
|
|
||||||
pop_map 163, roomtest 141, pop_kid 130, pop_bg 115, pop_ctrl 13
|
|
||||||
libc: _irq_state 818, _kbdraw_state 515, _gfx_pal_buf 256, прочее ~200
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.5 Находки замера (мелкие, но чинить)
|
|
||||||
|
|
||||||
1. **`--w3` берёт ОДИН файл на флаг.** В `Makefile` написано
|
|
||||||
`--w3 pop_trob.c pop_map.c --w3 pop_bg.c`, и это значит «W3 = pop_trob и
|
|
||||||
pop_bg», а `pop_map.c` компилируется как обычный исходник в W1/W2. Судя по
|
|
||||||
`.sprinter-cc-roomtest/w3_pop_map.rel` (устаревший артефакт), когда-то
|
|
||||||
pop_map был в W3. **Решить осознанно** (см. §4) и записать явно:
|
|
||||||
`--w3 pop_trob.c --w3 pop_bg.c`.
|
|
||||||
2. `libc` тянет `_irq_state` 818 Б + `_kbdraw_state` 515 Б в `_DATA`.
|
|
||||||
`__irq_vec_buf` (513 Б) — таблица векторов IM2; `__kbdraw_down` (512 Б) —
|
|
||||||
битмап клавиш на 512 скан-кодов. Оба можно ужать (см. §5.4), это ~0.7 КБ
|
|
||||||
в самом дефицитном окне.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. ПРАВИЛА ПЛАТФОРМЫ, ОТ КОТОРЫХ ПЛЯШЕТ РАСКЛАДКА
|
|
||||||
|
|
||||||
Это главное, что изменилось по сравнению с v1: модель банкинга уточнена по
|
|
||||||
`bin/sprinter-cc` (справка `--w3`/`--bank`) и по коду libbgi.
|
|
||||||
|
|
||||||
**(R1) Резидент W3 (`--w3`) и банки W3 (`--bank`) делят одно окно.**
|
|
||||||
Резидент лежит на своей странице 0xC000…0xFFFF; трамплин на время вызова
|
|
||||||
`__banked` подменяет страницу W3 на банковую и возвращает резидентную назад.
|
|
||||||
|
|
||||||
**(R2) Из банка резидент W3 НЕДОСТИЖИМ — и транзитивно тоже.**
|
|
||||||
Пока исполняется банк, резидентной страницы в адресном пространстве нет.
|
|
||||||
Значит нельзя не только `bank → pop_bg()`, но и `bank → pop_map() → pop_bg()`.
|
|
||||||
**Это ключевое ограничение при выборе, что делать банком.**
|
|
||||||
|
|
||||||
**(R3) Резидент → банк работает** (через трамплин в W1), резидент → W1/W2 —
|
|
||||||
тоже.
|
|
||||||
|
|
||||||
**(R4) Графические примитивы libbgi звать можно откуда угодно.**
|
|
||||||
`_bgi_begin` читает текущую страницу W3 из порта 0xE2, а `_bgi_end` её
|
|
||||||
возвращает — то есть скобка корректна и из банка, и из резидента. Нельзя
|
|
||||||
только одно: **звать `_bgi_begin`/`_bgi_end` ИЗ кода, который сам лежит в W3**
|
|
||||||
(после подмены страницы исчезнет исполняемый код — проверено, белый экран).
|
|
||||||
Поэтому `pop_bg` (резидент W3) обязан пользоваться готовыми примитивами
|
|
||||||
(`gfx_blit*`, `bar`, …), а батчинг скобки на весь `draw_tile` (v1 §8 шаг 1)
|
|
||||||
для него **невозможен** без переноса самого `draw_tile` в W1/W2.
|
|
||||||
|
|
||||||
**(R5) `--w3` кладёт в W3 код И rodata модуля** (`--codeseg/--constseg
|
|
||||||
W3CODE`), а писучие статики оставляет в `_DATA` (W2). То есть `const`-таблицы
|
|
||||||
переносятся в W3 бесплатно вместе с модулем (так уже лежит `tile_table` 371 Б).
|
|
||||||
|
|
||||||
**(R6) Данные в EMM-странице читаются, только пока страница в окне.**
|
|
||||||
`gfx_w0_map(page)` / `gfx_w0_unmap()` — окно W0 (0x0000…0x3FFF), первые 0x100
|
|
||||||
занимает ISR-стаб. Так уже работает `pop_level`. Цена — пара `OUT` на
|
|
||||||
маппинг, поэтому годится для «пачками», а не для чтения по байту в горячем
|
|
||||||
цикле.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. ЧТО ДЕЛАТЬ НЕЛЬЗЯ (анти-паттерны, чтобы не потерять время)
|
|
||||||
|
|
||||||
- **Нельзя банковать `pop_bg`.** Он вызывается из pop_map, pop_trob, roomtest,
|
|
||||||
pop_kid — то есть из главного цикла на каждом кадре; плюс он сам держит
|
|
||||||
`tile_table` и всю отрисовку. Банк дал бы трамплин на каждый блит.
|
|
||||||
- **Нельзя банковать `pop_map`, пока `pop_map` зовёт `pop_bg`** (R2). Сейчас
|
|
||||||
зовёт: `pop_loose_tick` и компания (~30 вызовов графики).
|
|
||||||
- **Нельзя тащить `kid_frames` в EMM «в лоб»**: он читается несколько раз за
|
|
||||||
кадр из коллизии (`kid_cur_dx`/`kid_cur_flags` → `dx_weight`,
|
|
||||||
`char_x_forward_edge`, …). Нужен кэш кадра (см. §5.1) — иначе маппинг
|
|
||||||
страницы окажется в горячем пути.
|
|
||||||
- **Нельзя «причёсывать» семейство `draw_tile` ради экономии, не имея
|
|
||||||
пиксельного теста.** Мы неделю выравнивали слои по SDLPoP; любой рефактор
|
|
||||||
этой зоны проверять диффом страниц (заморозка кадра клавишей `1` + сравнение
|
|
||||||
VRAM обеих страниц, приём из memory `mame_mcp_bridge`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. ЦЕЛЕВАЯ РАСКЛАДКА
|
|
||||||
|
|
||||||
Принцип: **W3-резидент = «толстая графика, которую зовёт только главный цикл»;
|
|
||||||
W1/W2 = ядро, которое должно быть достижимо ОТОВСЮДУ (включая банки); банки =
|
|
||||||
новая холодная логика (стражи, боёвка, будущие уровни)**.
|
|
||||||
|
|
||||||
```
|
|
||||||
W1/W2 (всегда отображено) W3 резидент (стр. 0xC000) Банки W3
|
|
||||||
────────────────────────── ───────────────────────── ─────────
|
|
||||||
libc + libbgi pop_bg (отрисовка тайлов) guards.c
|
|
||||||
pop_kid (интерпретатор+рисование) pop_trob (анимации тайлов) fight.c
|
|
||||||
pop_map (коллизия/физика/предметы) pop_loose.c (loose+потолок) debug/roomnav
|
|
||||||
pop_geom (общая геометрия/тайлы) enter_room-часть roomtest?
|
|
||||||
pop_ctrl (ввод/диспетчер)
|
|
||||||
roomtest (главный цикл)
|
|
||||||
```
|
|
||||||
|
|
||||||
Почему так:
|
|
||||||
|
|
||||||
- **`pop_map` остаётся в W1/W2** — его зовут и главный цикл, и (в будущем)
|
|
||||||
банк стражей; в W3 его класть нельзя именно из-за R2. Для этого из него надо
|
|
||||||
вынести графическую часть (loose/потолок) — она уезжает в W3 к `pop_bg`
|
|
||||||
(§5.3). После выноса `pop_map` становится **чистой логикой без единого
|
|
||||||
вызова графики** — тот самый bank-safe API.
|
|
||||||
- **`pop_kid` остаётся в W1/W2**: `play_seq`/`kid_set_seq`/`Kid` нужны и
|
|
||||||
стражам (у стражей ТА ЖЕ seqtbl), а `kid_draw` зовёт только libbgi (R4).
|
|
||||||
- **`pop_trob` остаётся резидентом**: его зовёт только главный цикл, и он сам
|
|
||||||
зовёт `pop_bg` — идеальный житель W3.
|
|
||||||
- **Банк стражей не зовёт ничего из W3.** Рисование стражей — либо через
|
|
||||||
libbgi напрямую (R4), либо (лучше) резидентный `guard_draw()` в W1/W2 рядом
|
|
||||||
с `kid_draw`, а банк только считает состояние. Тот же приём мы уже
|
|
||||||
используем для `pop_item_taken`/`pop_loose_fell`/`pop_ceil_fell`: банк
|
|
||||||
выставляет флаг — резидент рисует.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. ПЛАН РАБОТ
|
|
||||||
|
|
||||||
Порядок выбран так, чтобы каждый шаг был проверяем отдельно и давал место
|
|
||||||
следующему.
|
|
||||||
|
|
||||||
### Шаг 1. `kid_data.h` (3 745 Б) → EMM-страница + порт `load_frame` — **самый большой выигрыш**
|
|
||||||
|
|
||||||
Сейчас `kid_seqtbl` (2310) + `kid_frames` (1205) + `kid_seq_off` (230) лежат в
|
|
||||||
`_CODE` окна W1/W2 — это 15 % всего дефицитного пространства.
|
|
||||||
|
|
||||||
Как переносить:
|
|
||||||
|
|
||||||
1. `pop_extract_kid_data.py` дополнительно пишет `kid_data.bin` (те же три
|
|
||||||
таблицы подряд, фиксированные смещения).
|
|
||||||
2. Грузим её в отдельную EMM-страницу тем же способом, что уровень
|
|
||||||
(`pop_level_load` — готовый образец), хэндл держим в `pop_kid`.
|
|
||||||
3. **Порт `load_frame` (seg006) и глобала `cur_frame`** — в оригинале ровно
|
|
||||||
так и сделано: раз за тик кадр копируется в структуру, а весь остальной код
|
|
||||||
читает `cur_frame`, а не таблицу. У нас `kid_cur_dx()/kid_cur_flags()`
|
|
||||||
станут чтением из `cur_frame` (5 байт в `_DATA`).
|
|
||||||
4. `play_seq` оборачивается в один `gfx_w0_map(kid_data_page)` … `unmap` на
|
|
||||||
вызов (в тике, не в отрисовке — конфликта с атласом в W0 нет).
|
|
||||||
|
|
||||||
Выигрыш: **−3 745 Б из W1/W2**, цена — один маппинг страницы за тик и 5 байт
|
|
||||||
`_DATA`. Дополнительный бонус: `load_frame`/`cur_frame` — шаг К СХОДСТВУ с
|
|
||||||
оригиналом, а не отход от него.
|
|
||||||
|
|
||||||
Риск: сломать `play_seq` (сердце анимации). Проверка: прогон по комнатам с
|
|
||||||
эталонными позами (вис, подтягивание, прыжки, подъём меча).
|
|
||||||
|
|
||||||
### Шаг 2. Модуль `pop_geom.c` — дедуп + bank-safe фундамент
|
|
||||||
|
|
||||||
Сейчас продублировано между модулями:
|
|
||||||
|
|
||||||
| что | где | сколько |
|
|
||||||
|---|---|---|
|
|
||||||
| `y_to_row`/`y_to_row_mod4` | pop_bg + pop_map | 2 копии |
|
|
||||||
| `char_dx_forward` | pop_kid + pop_map | 2 копии |
|
|
||||||
| `x_bump[20]` | pop_kid (uint8) + pop_map (int16) | 20 + 40 Б, РАЗНЫЕ типы |
|
|
||||||
| `y_land[5]` | pop_kid + pop_map | 10 + 10 Б |
|
|
||||||
| `tile_is_floor` | pop_map (+ проверка кодов в roomtest) | 2 места |
|
|
||||||
| 32-битный LCG `prandom` | pop_bg (`prandom`) + pop_trob (`trob_prandom`) | 2 копии по ~60 Б + 2 сида |
|
|
||||||
|
|
||||||
Собрать в один W1/W2-модуль `pop_geom.c`: таблицы `x_bump/y_land/dir_front/
|
|
||||||
dir_behind`, `y_to_row`, `char_dx_forward`, `get_tile_div_mod(_m7)`,
|
|
||||||
`tile_is_floor`, `prandom`. Выигрыш прямой — сотни байт (оценка 250–400 Б),
|
|
||||||
но главное — **это и есть тот «чистый» API, который потом сможет звать банк**
|
|
||||||
(R2): вся геометрия оказывается в W1/W2 по определению.
|
|
||||||
|
|
||||||
Осторожно: `prandom` у pop_bg и pop_trob — РАЗНЫЕ последовательности с разными
|
|
||||||
сидами (стены vs фазы факелов). Объединять функцию можно, **сиды — нет**:
|
|
||||||
передавать сид указателем/по индексу, иначе поедет раскладка кладки.
|
|
||||||
|
|
||||||
### Шаг 3. Вынести loose/потолок из `pop_map` в W3
|
|
||||||
|
|
||||||
`pop_map` — единственный модуль W1/W2, который зовёт графику, и делает это
|
|
||||||
ровно в одном логическом блоке: `pop_loose_tick` + `check_press` + `do_knock` +
|
|
||||||
`fell_on_your_head` + `check_loose_fall_on_kid` + плита-потолок (~1.4–2 КБ).
|
|
||||||
|
|
||||||
Вынести их в `pop_loose.c`, собираемый `--w3` рядом с `pop_bg`/`pop_trob`.
|
|
||||||
Тогда:
|
|
||||||
|
|
||||||
- `pop_map` = чистая логика (bank-safe, R2 соблюдён);
|
|
||||||
- W1/W2 худеет ещё на ~1.5–2 КБ;
|
|
||||||
- W3 растёт на столько же — а место там появится после шага 4.
|
|
||||||
|
|
||||||
### Шаг 4. Перебалансировка резидента W3
|
|
||||||
|
|
||||||
После шага 3 в W3 будет тесно (14.4 + 2 ≈ 16.4 КБ > 16 КБ). Разгружаем:
|
|
||||||
|
|
||||||
1. **`wall_pattern` (944 Б) + `mob_render`/`mob_tick_one` (1381 Б)** — кандидаты
|
|
||||||
на переезд в W1/W2: их зовёт только `pop_bg`/`pop_loose`, но сами они уже
|
|
||||||
пользуются только libbgi (R4), значит из W1/W2 работают и остаются
|
|
||||||
достижимыми из банка.
|
|
||||||
2. `tile_table` и мелкие const-таблицы pop_bg (371 + ~300 Б) можно унести в
|
|
||||||
EMM-страницу **уровня** (там ~13.8 КБ свободно) — но только если чтение
|
|
||||||
происходит под уже замапленной страницей. Сейчас `draw_tile` читает
|
|
||||||
`tile_table` ВНЕ W0-контекста → потребуется явный маппинг на тайл. **Не
|
|
||||||
делать раньше замера**: 30 тайлов на входе в комнату × map/unmap — терпимо,
|
|
||||||
а вот в покадровых редроях (пики/loose/кнопка) — уже горячий путь.
|
|
||||||
3. Если и этого мало — `enter_room` (~2.1 КБ, зовётся только при смене комнаты)
|
|
||||||
переносится в резидент W3 или в БАНК (он вызывается из главного цикла =
|
|
||||||
резидента, значит банк допустим по R3).
|
|
||||||
|
|
||||||
### Шаг 5. Данные
|
|
||||||
|
|
||||||
1. **`room_modif[24][30]` = 720 Б** (pop_trob, `_DATA`). Нужен произвольный
|
|
||||||
доступ каждый кадр (анимации, ворота) — в EMM не годится. Но 24 комнаты ×
|
|
||||||
30 байт хранятся ЦЕЛИКОМ, хотя одновременно живут modif'ы только текущей
|
|
||||||
комнаты и соседей по швам. Вариант: хранить полный массив в EMM-странице
|
|
||||||
уровня, а в `_DATA` держать кэш на 2–3 комнаты (свою + левого/правого
|
|
||||||
соседа) с записью обратно при смене комнаты. Выигрыш ~600 Б, цена —
|
|
||||||
аккуратность на швах (кнопка в одной комнате открывает ворота в другой).
|
|
||||||
**Делать последним** — это самая «тонкая» правка по семантике.
|
|
||||||
2. **`dl1[256]`+`dl2[256]` = 512 Б** (pop_level, `_DATA` — копии LINKLOC/
|
|
||||||
LINKMAP уровня) — читаются при нажатии кнопки
|
|
||||||
и при отрисовке нажатой кнопки. Кандидат на чтение прямо из страницы
|
|
||||||
уровня (она и так маппится) — но проверить, что `pop_doorlink2` не зовётся
|
|
||||||
из отрисовки в тот момент, когда в W0 атлас. Выигрыш ~500 Б.
|
|
||||||
3. **`_kbdraw_down[512]` 512 Б** (libc): проверено — это **байт на скан-код**
|
|
||||||
(`libc/kbd/_kbdraw_state.c`), хотя комментарий называет его битовой картой.
|
|
||||||
Упаковка в биты даёт −448 Б, но добавляет сдвиг/маску в ISR-трамплин и в
|
|
||||||
`kbd_raw_down`. Трогать осторожно: raw-клавиатура уже дважды была
|
|
||||||
источником залипаний (memory `kbd_raw_fifo_drain`,
|
|
||||||
`kbd_overrun_wipe_modifiers`) — правку сопровождать прогоном docs/kbd-games.
|
|
||||||
4. **`__irq_vec_buf` 513 Б** (libc IM2): таблица векторов обязана быть
|
|
||||||
выровнена и полна — не трогать.
|
|
||||||
|
|
||||||
### Шаг 6. Контракт банка стражей (проектируется ДО написания кода)
|
|
||||||
|
|
||||||
Когда дойдём до стражей:
|
|
||||||
|
|
||||||
- `guards.c` собирается `--bank 1=guards.c`, режим `huge` (или `big` с
|
|
||||||
`BANKED=W1`, если W3 окажется тесен для трамплинов).
|
|
||||||
- **Банк зовёт только:** `pop_map` (чистая логика после шага 3), `pop_kid`
|
|
||||||
(`play_seq`, `kid_set_seq`, `cur_frame`), `pop_geom`, libc/libbgi.
|
|
||||||
- **Банк НЕ зовёт:** `pop_bg`, `pop_trob`, `pop_loose` (резидент W3) — ни
|
|
||||||
прямо, ни через промежуточные функции. Нужна отрисовка — выставляет флаг,
|
|
||||||
рисует резидент (идиома `pop_item_taken`).
|
|
||||||
- Первым делом — **пробник** (`tests/` или `--bank` на пустышке): банк зовёт
|
|
||||||
`pop_map`-функцию, та зовёт libbgi-примитив; убедиться в MAME, что скобка
|
|
||||||
W3 корректно возвращает банковую страницу (R4) — это проверка модели, а не
|
|
||||||
веры в неё.
|
|
||||||
|
|
||||||
### Шаг 7. Мелкий дедуп в `pop_bg` (после того, как появится тест страниц)
|
|
||||||
|
|
||||||
- Пять функций-редроев (`pop_spike_redraw`, `pop_loose_shake_draw`,
|
|
||||||
`pop_floor_bake`, `pop_button_redraw`, `pop_leveldoor_redraw`) отличаются
|
|
||||||
только прямоугольником heal, банком и набором тайлов — свести к одному
|
|
||||||
параметризованному хелперу (оценка −150…250 Б).
|
|
||||||
- `env_b/wall_b/fore_b/pot_b` — четыре одинаковых обёртки над `blit_b`
|
|
||||||
(оставить: экономия единицы байт, читаемость дороже).
|
|
||||||
- `overlay_mid_tile` / `fore_only_tile` / `climb_overlay_tile` / `draw_tile`
|
|
||||||
— общая структура «взять code/lcode, посчитать x/dmy/dby, разобрать слои».
|
|
||||||
Тут экономия потенциально сотни байт, но это **та самая зона риска из §3** —
|
|
||||||
только с пиксельным диффом до/после и по одному слою за раз.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Ожидаемый итог
|
|
||||||
|
|
||||||
| Шаг | W1/W2 | W3 | Риск |
|
|
||||||
|---|---|---|---|
|
|
||||||
| 1. kid_data → EMM + load_frame | **−3 745** | — | средний (сердце анимации) |
|
|
||||||
| 2. pop_geom (дедуп) | −250…400 | — | низкий |
|
|
||||||
| 3. loose → W3 | −1 500…2 000 | +1 500…2 000 | низкий (перенос как есть) |
|
|
||||||
| 4. разгрузка W3 (wall_pattern, mob) | +2 300 | −2 300 | низкий |
|
|
||||||
| 5. данные (room_modif, LINKLOC, kbd) | −1 000…1 600 | — | средний/высокий |
|
|
||||||
| 7. дедуп редроев pop_bg | — | −150…250 | средний |
|
|
||||||
|
|
||||||
Суммарно: **W1/W2 освобождается ~4.5–6 КБ**, W3 остаётся примерно в нынешнем
|
|
||||||
объёме, но становится «правильно заполненным» — в нём только то, что банк
|
|
||||||
никогда не позовёт. Плюс открывается путь к банкам: стражи и боёвка получают
|
|
||||||
до 16 КБ на банк, не трогая резидент.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Как мерить и проверять (обязательно к каждому шагу)
|
|
||||||
|
|
||||||
1. **До/после по `.rel`** — точные размеры на модуль:
|
|
||||||
`for f in .sprinter-cc-roomtest/*.rel; do grep '^A ' $f; done`
|
|
||||||
(области `_CODE`/`_W3CODE`/`_DATA`). Итоги окон печатает сам `sprinter-cc`.
|
|
||||||
2. **Функции** — из `.lst` (метки `_name:` и адреса), скрипт в истории этой
|
|
||||||
сессии; полезно ловить «функция распухла после рефактора».
|
|
||||||
3. **MAME**: любой перенос кода между окнами/страницами — это класс «молча
|
|
||||||
ломается» (`sprinter_memory_modes`). Минимум: комната 1 (loose), 12
|
|
||||||
(вис/подтягивание), 15 (меч), 9 (дверь уровня), 6 (кнопка/ворота).
|
|
||||||
4. **Пиксельный дифф** для правок отрисовки: заморозить кадр (`1`), сравнить
|
|
||||||
обе страницы дабл-буфера через `vram` (см. memory `mame_mcp_bridge`) и/или
|
|
||||||
сверить с эталонным рендером `render_room.py`.
|
|
||||||
5. **Скорость** — после шагов 1 и 4 замерить кадр маркерами в порт 0xFE
|
|
||||||
(приём из v1 §8), чтобы маппинг страницы за тик не съел бюджет.
|
|
||||||
|
|
||||||
## 8. Ссылки
|
|
||||||
|
|
||||||
- `bin/sprinter-cc` — справка по `--w3`, `--bank`, `--memory`, `--memory-manual`.
|
|
||||||
- `runtime/crt0_banked.s`, `runtime/bank.s` — трамплины и захват резидентной
|
|
||||||
страницы W3.
|
|
||||||
- `libbgi/common/_bgi_begin.c` / `_bgi_end.c` — механика скобки W3 (R4).
|
|
||||||
- memory: `w3_resident_code`, `pop_banking_architecture`, `sdcc_banking`,
|
|
||||||
`bank_local_data_pattern`, `sprinter_memory_modes`, `memory_modes_implemented`,
|
|
||||||
`sprinter_emm_budget`, `mame_mcp_bridge`, `avoid_32bit_arith_z80`,
|
|
||||||
`libc_one_function_per_module`.
|
|
||||||
- `applications/PoP/roomtest/TASKS_OPEN.md` — что из этого берётся в работу сейчас.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Скорость отрисовки: замеры и запас
|
|
||||||
|
|
||||||
Перенесено из удалённого `size_optimization_plan.md` §8 (замер 2026-07-27) —
|
|
||||||
единственная его часть, которая не была перекрыта этим документом.
|
|
||||||
Профилирование в MAME: маркеры в порт 0xFE + `wpiset … totalcycles` (приём из
|
|
||||||
memory `mame_mcp_bridge`); в самом `roomtest.c` для этого уже стоят полосы
|
|
||||||
бордюра `PROF()`. Кадр Sprinter = **430 080 тактов**.
|
|
||||||
|
|
||||||
**Стоимость блита почти НЕ зависит от размера** — платим за проход по цепочке
|
|
||||||
`gfx_blit → gfx_blit_part → _gfx_blit_full` (16-битная арифметика, клип,
|
|
||||||
пересчёт src, нарезка полос >256), а не за пиксели:
|
|
||||||
|
|
||||||
| путь (спрайт 32×3) | тактов |
|
|
||||||
|---|---|
|
|
||||||
| `gfx_blit` (общее ядро, с клипом) | 13 288 |
|
|
||||||
| линейное ядро без клипа | 4 617 |
|
|
||||||
|
|
||||||
Отсюда `draw_tile(0,0)` тайла шва (9 блитов) стоил **183 690 тактов = 43 %
|
|
||||||
кадра**; сам `bar` — только 13 308.
|
|
||||||
|
|
||||||
**Сделано:** `gfx_blit_noclip()` в libbgi, фоновые блиты `pop_bg` уходят на
|
|
||||||
него, когда спрайт целиком на экране (~2.9×, подтверждено в MAME). Позже
|
|
||||||
тем же приёмом закрыты спрайты персонажей (`gfx_blit_cols_part_noclip`).
|
|
||||||
**Не закрыт heal** — задача CLIP-1 в `../roomtest/TASKS_CLOSED.md`.
|
|
||||||
|
|
||||||
**ВАЖНО:** W3-скобку (`_bgi_begin`/`_bgi_end`) ставит САМА libbgi — вызывать
|
|
||||||
её из модуля, собранного с `--w3`, нельзя: после `_bgi_begin` окно W3 занято
|
|
||||||
видеобанком и код вызывающего исчезает из адресного пространства (проверено:
|
|
||||||
белый экран).
|
|
||||||
|
|
||||||
**Запас, когда перестанет хватать бюджета кадра:**
|
|
||||||
|
|
||||||
1. **Батчинг W3-скобки** — одна `_bgi_begin`/`_bgi_end` на весь `draw_tile`
|
|
||||||
вместо скобки на блит; нужен публичный batch-API в libbgi.
|
|
||||||
**Осторожно, и это стало важнее, чем было:** между begin/end стоит `DI`,
|
|
||||||
длинная серия задержит кадровое прерывание — а по разбору KBD-1
|
|
||||||
(`../roomtest/TASKS_CLOSED.md`) длинные DI-окна и есть причина потери байт
|
|
||||||
клавиатуры. Батчинг эту проблему УХУДШИТ, если делать его вслепую.
|
|
||||||
2. **Решётка ворот одним спрайтом** — `draw_gate_back` рисует бары по одному
|
|
||||||
(до 7 блитов). Сгенерировать в атласе «столб решётки» и выводить одним
|
|
||||||
`gfx_blit_part` с обрезкой по фазе `gate_bot_y & 7`: 7 блитов → 1.
|
|
||||||
3. **Не перерисовывать статичные части шва** — грань ворот, пол и кромка при
|
|
||||||
анимации решётки не меняются (см. OPT-1 в `../roomtest/BUGS_CLOSED.md` —
|
|
||||||
решено не делать, стоимость транзиентная).
|
|
||||||
4. **T-1 / T-2** (`../roomtest/BUGS_OPEN.md`) — перерисовка пик по причине и
|
|
||||||
idle-skip Кида: самый большой оставшийся резерв, потому что убирает работу
|
|
||||||
целиком, а не удешевляет её.
|
|
||||||
@@ -1,462 +0,0 @@
|
|||||||
# Что осталось на уровнях 12, 13, 14, 15 и 0
|
|
||||||
|
|
||||||
Статус: разбор по `../SDLPoP/src/`, 2026-08-13. Продолжает
|
|
||||||
[`levels_plan.md`](levels_plan.md) (машинерия уровней и тайлсеты — уже
|
|
||||||
сделаны). Здесь только СПЕЦСОБЫТИЯ, которых у нас ещё нет.
|
|
||||||
|
|
||||||
Правило проекта: источник истины — SDLPoP; все ссылки ниже даны на функцию и
|
|
||||||
строку, чтобы порт начинался с чтения, а не с гипотезы.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 0. Что из этой области УЖЕ есть
|
|
||||||
|
|
||||||
Проверено грепом по `roomtest/`:
|
|
||||||
|
|
||||||
| механика | где у нас | статус |
|
|
||||||
|---|---|---|
|
|
||||||
| слот соперника, общий на всех Char | `pop_guard.c`, `pop_cdraw.c` | готово |
|
|
||||||
| `check_shadow` (спецвход тени) | `guards.c:310` | готово для уровней 4/5/6 |
|
|
||||||
| `do_init_shad` + таблицы `init_shad_5/6` | `guards.c:284` | готово |
|
|
||||||
| ИИ тени 4/5/6 | `guards.c:599/658/680` | готово |
|
|
||||||
| диспетчер `autocontrol_shadow` | `guards.c:711` | ветки 12 НЕТ |
|
|
||||||
| боёвка (удар/блок/парирование/HP соперника) | `guards.c`, `pop_ctrl.c` | готово |
|
|
||||||
| `flash_color` / `flash_time` | `pop_map.c`, `roomtest.c` | готово |
|
|
||||||
| `add_life` | `pop_map.c` | готово |
|
|
||||||
| таблицы уровней (`guard_type`, `guard_hp`, `entry_pose`, `level_type`) | `pop_level_cold.c:41..52` | готово, включая 12=SHADOW, 13=VIZIER |
|
|
||||||
| loose-полы, `make_loose_fall`, mob | `pop_map.c`, `pop_room.c` | готово (без спецкейсов ур. 13) |
|
|
||||||
|
|
||||||
То есть каркас есть весь; ниже — недостающие спецсобытия.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Уровень 12 — тень: встреча, бой, слияние
|
|
||||||
|
|
||||||
### 1.1 Подъём тени в комнате 15 (`check_shadow`, seg002:0070)
|
|
||||||
|
|
||||||
```c
|
|
||||||
if (current_level == 12) {
|
|
||||||
if (!united_with_shadow && drawn_room == 15) {
|
|
||||||
Char.room = drawn_room;
|
|
||||||
if (get_tile(15, 1, 0) == tiles_22_sword) return; // меч ещё лежит
|
|
||||||
shadow_initialized = 0;
|
|
||||||
do_init_shad(init_shad_12, 7 /* fall */);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Отличия от наших веток 5/6: тень поднимается **в падении** (seq 7), условием
|
|
||||||
служит содержимое тайла (меч уже подобран) и флаг `united_with_shadow`.
|
|
||||||
Таблица `init_shad_12 = {0x0F, 0x51, 0xE8, 0, 0, 0, 0, 0}` — то есть
|
|
||||||
x=81, y=232, вправо, колонка 0, ряд 0.
|
|
||||||
|
|
||||||
**Что добавить:** ветку в `pop_check_shadow` + константу `init_shad_12` +
|
|
||||||
глобалы `united_with_shadow`, `shadow_initialized`.
|
|
||||||
|
|
||||||
### 1.2 ИИ тени (`autocontrol_shadow_level12`, seg002:1184)
|
|
||||||
|
|
||||||
Самая содержательная функция уровня. Три режима:
|
|
||||||
|
|
||||||
1. **Первый кадр в комнате 15**: пока Кид не подошёл (`Opp.x < 150`) —
|
|
||||||
`shadow_initialized = 1`; иначе тень ещё раз падает (`do_init_shad`).
|
|
||||||
2. **Кид с мечом** (`Char.sword >= sword_2_drawn`) → тень дерётся обычным
|
|
||||||
`autocontrol_guard_active` (у нас есть, `guards.c:535`). Особый случай:
|
|
||||||
если тень уже ранена (`offguard != 0 && guard_refrac != 0`) — она убирает
|
|
||||||
меч (`move_4_down`).
|
|
||||||
3. **Кид убрал меч** → тень тоже убирает и идёт навстречу; на дистанции
|
|
||||||
`< 10` — **СЛИЯНИЕ**:
|
|
||||||
|
|
||||||
```c
|
|
||||||
flash_color = color_15_brightwhite; flash_time = 18;
|
|
||||||
add_life(); // +1 к максимуму HP
|
|
||||||
united_with_shadow = 42; // время вспышки Кид-тень
|
|
||||||
Char.charid = charid_0_kid; savekid(); // Кид ПЕРЕЕЗЖАЕТ на место тени
|
|
||||||
clear_char(); // тень со сцены
|
|
||||||
```
|
|
||||||
|
|
||||||
Плюс «если Кид бежит к тени — тень бежит к Киду» (кадры бега 3..14 и
|
|
||||||
шага 127..132).
|
|
||||||
|
|
||||||
**Что добавить:** `autocontrol_shadow_level12` в `guards.c` + ветку в
|
|
||||||
диспетчере `autocontrol_shadow` (там уже три ветки, будет четвёртая).
|
|
||||||
|
|
||||||
### 1.3 Общий урон (`do_delta_hp`, seg000:1518)
|
|
||||||
|
|
||||||
```c
|
|
||||||
if (Opp.charid == charid_1_shadow && current_level == 12 && guardhp_delta != 0)
|
|
||||||
hitp_delta = guardhp_delta; // ранил тень — ранил себя
|
|
||||||
```
|
|
||||||
|
|
||||||
Три строки, но без них бой с тенью теряет смысл. У нас `do_delta_hp`
|
|
||||||
портирован — добавить условие.
|
|
||||||
|
|
||||||
### 1.4 Таймер вспышки (`do_timers`, seg003:503)
|
|
||||||
|
|
||||||
```c
|
|
||||||
if (united_with_shadow > 0) {
|
|
||||||
--united_with_shadow;
|
|
||||||
if (united_with_shadow == 0) { --united_with_shadow; /* → -1 */ ... }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`united_with_shadow` живёт как счётчик, потом как «уже слились» (−1).
|
|
||||||
На него смотрят `check_shadow` и `check_can_guard_see_kid`.
|
|
||||||
|
|
||||||
### 1.5 Луч видимости (`check_can_guard_see_kid`, seg003:702)
|
|
||||||
|
|
||||||
```c
|
|
||||||
if ((Guard.charid != charid_1_shadow || current_level == 12) && ...
|
|
||||||
```
|
|
||||||
|
|
||||||
У нас (`guards.c:103`) условие про тень **нужно сверить**: на уровне 12 тень
|
|
||||||
ОБЯЗАНА быть видимой (иначе Кид не достанет меч и бой не начнётся).
|
|
||||||
|
|
||||||
### 1.6 Меч исчезает (`sword_disappears`, seg002:0536)
|
|
||||||
|
|
||||||
```c
|
|
||||||
if (current_level == 12 && Char.room == 18) {
|
|
||||||
get_tile(15, 1, 0);
|
|
||||||
curr_room_tiles[curr_tilepos] = tiles_1_floor;
|
|
||||||
curr_room_modif[curr_tilepos] = 0;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Срабатывает при уходе Кида ВПРАВО из комнаты 18 (`leave_room`, ветка 1).
|
|
||||||
У нас есть `pop_level_set_tile` — порт на пять строк.
|
|
||||||
|
|
||||||
### 1.7 Переход 12 → 13: НЕТ двери уровня, есть «бесшовный выход»
|
|
||||||
|
|
||||||
Проверено по данным (`res2012.bin` / `res2013.bin`) — портала действительно
|
|
||||||
нет, уровень кончается **фактом попадания в комнату**:
|
|
||||||
|
|
||||||
```c
|
|
||||||
// play_level_2, seg000:900
|
|
||||||
} else if (custom->tbl_seamless_exit[current_level] >= 0) {
|
|
||||||
if (Kid.room == /*23*/ custom->tbl_seamless_exit[current_level]) {
|
|
||||||
++next_level;
|
|
||||||
stop_sounds();
|
|
||||||
seamless = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`tbl_seamless_exit[12] = 23` (пара «уровень, комната» читается из оригинального
|
|
||||||
`PRINCE.EXE`, options.c:724). Дальше геометрия складывается так:
|
|
||||||
|
|
||||||
| | комната | ряд 1 | что происходит |
|
|
||||||
|---|---|---|---|
|
|
||||||
| ур. 12 | 13 | `floor bigpil empty empty wall wall …` | Кид бежит ВЛЕВО с колонки 0 |
|
|
||||||
| ур. 12 | 23 | `empty ×6, floor floor floor floor` | попал сюда → **уровень сменился** |
|
|
||||||
| ур. 13 | 23 | `floor bigpil floor ×6 bigpil floor` | старт: ряд 1, кол 9, `seq_84_run` |
|
|
||||||
|
|
||||||
Связи: `комната 13.left = 23`, и в комнату 23 больше ниоткуда не войти.
|
|
||||||
Номер комнаты у обоих уровней **один и тот же (23)**, стартовая позиция
|
|
||||||
уровня 13 — `BP_START = 23, поз 19 (ряд 1, кол 9), dir 0`, а
|
|
||||||
`tbl_entry_pose[13] = 2` даёт «вбегающий» вход (`seq_84_run`, seg003:172).
|
|
||||||
То есть Кид вбегает в комнату слева-направо… нет, `Char.direction =
|
|
||||||
~level.start_dir` — влево, тем же ходом, каким выбежал из уровня 12. Швов не
|
|
||||||
видно.
|
|
||||||
|
|
||||||
Что делает флаг `seamless` (всего два места, оба косметические):
|
|
||||||
|
|
||||||
* `start_level`, seg003:158 — **НЕ сбрасывает HP**: Кид уносит на уровень 13
|
|
||||||
то здоровье, с которым добежал;
|
|
||||||
* `show_level`, seg008:1861/1870 — не показывает заставку «LEVEL 13» и тут же
|
|
||||||
гасит флаг.
|
|
||||||
|
|
||||||
Двери уровня при этом на карте есть, но не при делах: у уровня 12 она одна
|
|
||||||
(комната 3, tilepos 23) — это ВХОД (стартовая комната 12-го — 3), а у уровня
|
|
||||||
13 их две (комната 3 tilepos 13 и комната 5 tilepos 24) — это выходы,
|
|
||||||
открываемые кнопкой из `Jaffar_exit`.
|
|
||||||
|
|
||||||
**Что это значит для нас.** Наш переход уровня сейчас идёт только через
|
|
||||||
`SEQ_END_LEVEL` в двери. Для 12-го нужен второй триггер — проверка в главном
|
|
||||||
цикле «`pop_current_level == 12 && cur_room == 23` → `pop_next_level++`» плюс
|
|
||||||
флаг `seamless`, который пропустит сброс HP в `pop_start_level`. Обе правки
|
|
||||||
маленькие и локальные.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Уровень 13 — Джафар
|
|
||||||
|
|
||||||
### 2.1 Кто такой Джафар
|
|
||||||
|
|
||||||
`tbl_guard_type[13] = 3` → `VIZIER.DAT` (у нас в `pop_level_cold.c` уже 3).
|
|
||||||
`tbl_guard_hp[13] = 6`. Отдельного ИИ у него нет:
|
|
||||||
|
|
||||||
```c
|
|
||||||
void autocontrol_Jaffar() { autocontrol_guard(); } // seg002:0697
|
|
||||||
```
|
|
||||||
|
|
||||||
То есть **бой с Джафаром — обычный бой стражи**, отличаются только спрайты,
|
|
||||||
HP и три спецсобытия ниже. Это хорошая новость: боёвка у нас есть.
|
|
||||||
|
|
||||||
### 2.2 Встреча (`meet_Jaffar`, seg002:0544)
|
|
||||||
|
|
||||||
```c
|
|
||||||
if (current_level == 13 && leveldoor_open == 0 && Char.room == 3) {
|
|
||||||
play_sound(sound_29_meet_Jaffar);
|
|
||||||
guard_notice_timer = 28; // Джафар ждёт 28/12 ≈ 2.33 с
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Срабатывает при уходе Кида ВПРАВО (`leave_room`, ветка 1).
|
|
||||||
Пара к нему — в `autocontrol_guard_inactive` (seg002:0734), она у нас уже
|
|
||||||
портирована (`guards.c:401`), но **без условия по уровню**:
|
|
||||||
|
|
||||||
```c
|
|
||||||
if (can_guard_see_kid) {
|
|
||||||
if (current_level != 13 || guard_notice_timer == 0) move_down_forw();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Что добавить:** глобал `guard_notice_timer` + его тик в `do_timers`
|
|
||||||
(seg003:509) + оба условия.
|
|
||||||
|
|
||||||
### 2.3 Победа (`on_guard_killed`, seg006:1929)
|
|
||||||
|
|
||||||
```c
|
|
||||||
} else if (current_level == 13) {
|
|
||||||
flash_color = color_15_brightwhite; flash_time = 18;
|
|
||||||
is_show_time = 1;
|
|
||||||
leveldoor_open = 2; // ← ключ к выходу
|
|
||||||
play_sound(sound_43_victory_Jaffar);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
и парная `Jaffar_exit` (seg002:0517), срабатывающая при уходе Кида ВЛЕВО:
|
|
||||||
|
|
||||||
```c
|
|
||||||
if (leveldoor_open == 2) { get_tile(24, 0, 0); trigger_button(0, 0, -1); }
|
|
||||||
```
|
|
||||||
|
|
||||||
То есть смерть Джафара не открывает дверь сама — она ставит флаг, а дверь
|
|
||||||
открывается кнопкой, «нажатой» при уходе влево. `trigger_button` у нас есть.
|
|
||||||
|
|
||||||
### 2.4 Падающие плиты (`check_fall_flo`, seg000:1319)
|
|
||||||
|
|
||||||
```c
|
|
||||||
if (current_level == 13 && (drawn_room == 23 || drawn_room == 16)) {
|
|
||||||
curr_room = room_A; // комната СВЕРХУ
|
|
||||||
for (curr_tilepos = 22; curr_tilepos <= 27; ++curr_tilepos)
|
|
||||||
make_loose_fall(-(prandom(0xFF) & 0x0F)); // ОТРИЦАТЕЛЬНЫЙ модификатор
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Вот это и есть «плиты появляются»: при входе в комнаты 23/16 шесть плит ряда 2
|
|
||||||
комнаты СВЕРХУ получают отрицательную фазу — то есть отложенный старт, и
|
|
||||||
сыплются на Кида вразнобой. Зовётся из `check_the_end` при смене комнаты.
|
|
||||||
|
|
||||||
**Три спецкейса уровня 13 в loose-механике**, без них это не работает:
|
|
||||||
|
|
||||||
| место | что | зачем |
|
|
||||||
|---|---|---|
|
|
||||||
| `animate_loose`, seg007:823 | при `modif & 0x80` НЕ останавливать тряску | иначе отрицательная фаза не досчитает до нуля и плита не упадёт |
|
|
||||||
| `loose_make_shake`, seg007:949 | на уровне 13 сотрясение НЕ трясёт плиты | иначе отложенные плиты сбрасываются в 0x80 |
|
|
||||||
| `fell_on_your_head`, seg007:1218 | плита бьёт и в БЕГЕ (кадры 5..14) | на прочих уровнях бегущего не задевает |
|
|
||||||
|
|
||||||
У нас первый пункт критичен: `pop_loose_tick` трактует бит 7 как «тряска» и
|
|
||||||
на `>= 0x84` гасит фазу — отрицательный старт умрёт, не начавшись. Заодно
|
|
||||||
это ровно та же ветка, что мы правили сегодня в
|
|
||||||
[`LOOSE-ROOM-CHANGE`](../roomtest/BUGS_OPEN.md#loose-room-change), так что
|
|
||||||
код на виду.
|
|
||||||
|
|
||||||
### 2.5 Поза входа
|
|
||||||
|
|
||||||
`tbl_entry_pose[13] = 2` — у нас в таблице уже есть; проверить, что режим 2
|
|
||||||
(`seg003:172`) реализован.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Уровень 14 — принцесса и конец игры
|
|
||||||
|
|
||||||
Боя нет вовсе (`tbl_guard_type[14] = -1`). Всё сводится к одному событию:
|
|
||||||
|
|
||||||
```c
|
|
||||||
// check_the_end, seg000:1299
|
|
||||||
if (current_level == 14 && drawn_room == 5) end_sequence();
|
|
||||||
```
|
|
||||||
|
|
||||||
`end_sequence` (seg001:573) → `end_sequence_anim` (seg001:332): катсцена
|
|
||||||
«Кид добежал до принцессы» — обнимаются, появляется мышь, затухание, Hall of
|
|
||||||
Fame. Персонажи там играются ТЕМ ЖЕ интерпретатором `seqtbl`
|
|
||||||
(`seq_108_princess_turn_and_hug`, `seq_101_mouse_stands_up`), то есть движок
|
|
||||||
у нас уже подходит — нужны спрайты принцессы (`PRINCESS.DAT`) и раскадровка.
|
|
||||||
|
|
||||||
**Оценка:** это не игровая механика, а ролик. Логично делать вместе с интро
|
|
||||||
и межуровневыми вставками — отдельным банком, как и договаривались
|
|
||||||
(см. memory `pop_banking_architecture`). На проходимость игры не влияет:
|
|
||||||
достаточно довести Кида до комнаты 5 и показать заглушку.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Уровень 15 — «уровень зелий» (защита от копирования)
|
|
||||||
|
|
||||||
Не часть сюжета. Это экран проверки подлинности из оригинала: после
|
|
||||||
уровня `copyprot_level` игра подменяет номер на 15, показывает комнату с
|
|
||||||
**14 зельями, на которых нарисованы буквы**, и требует выпить нужное.
|
|
||||||
|
|
||||||
Механика (всё под `USE_COPYPROT` в SDLPoP):
|
|
||||||
|
|
||||||
| место | что делает |
|
|
||||||
|---|---|
|
|
||||||
| `play_level`, seg003:53 | `level_number == copyprot_level` → грузим 15 |
|
|
||||||
| `redraw_screen`, seg003:273 | поверх зелий рисуются БУКВЫ (`copyprot_letter`) |
|
|
||||||
| `load_alter_mod`, seg008:1204 | одно зелье в комнате делается «открытым» (тип 6) |
|
|
||||||
| `animate_potion`, seg007:259 | на уровне 15 своя ветка перерисовки |
|
|
||||||
| `up_pressed`/`do_pickup`, seg005:657 | выпитое зелье убирает букву из таблицы |
|
|
||||||
| `seq` эффект зелья, seg006:1896 | синие зелья на уровне 15 отнимают ПОЛОВИНУ HP |
|
|
||||||
| выход, seg000:700 | из 15 возвращаемся в `copyprot_level` |
|
|
||||||
|
|
||||||
**Рекомендация: не портировать.** Это анти-пиратский экран 1989 года,
|
|
||||||
требующий книжки-манускрипта; SDLPoP держит его выключенным по умолчанию
|
|
||||||
(`enable_copyprot`). Единственное, что стоит взять — **половинный урон
|
|
||||||
синего зелья**, если вдруг захочется полной совместимости; остальное только
|
|
||||||
съест банк. Если решим делать — это отдельная фича «уровень 15», а не часть
|
|
||||||
основного прохождения.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Уровень 0 — демо-уровень (аттракт)
|
|
||||||
|
|
||||||
`res2000.bin` у нас распакован. Это тот самый ролик, который крутится на
|
|
||||||
титульном экране: Кид сам бежит, дерётся со стражем и убегает.
|
|
||||||
|
|
||||||
Как устроено:
|
|
||||||
|
|
||||||
| место | что |
|
|
||||||
|---|---|
|
|
||||||
| `do_demo`, seg006:1409 | на уровне 0 вместо чтения клавиатуры зовётся `do_demo()` + `control()` |
|
|
||||||
| `autocontrol_kid`, seg002:702 | Кид управляется тем же `autocontrol_guard` |
|
|
||||||
| `do_auto_moves`, seg002:1089 | проигрыватель ЗАПИСИ ходов: таблица `(time, move)` |
|
|
||||||
| `on_guard_killed`, seg006:1928 | на уровне 0 после убийства стража Кид убегает (`checkpoint = 1`, сброс демо) |
|
|
||||||
| `demo_index` / `demo_time` | позиция в записи; у нас уже объявлены в `guards.c:267` |
|
|
||||||
|
|
||||||
**Хорошая новость:** движок автодвижений (`do_auto_moves`) у нас уже есть —
|
|
||||||
он нужен был тени на уровнях 4/5/6, и `demo_time`/`demo_index` объявлены там
|
|
||||||
же. То есть демо-уровень — это в основном таблица ходов + ветка «Кидом
|
|
||||||
управляет ИИ» в `pop_ctrl`.
|
|
||||||
|
|
||||||
**Когда делать:** вместе с интро/титульным экраном, не раньше. На
|
|
||||||
прохождение не влияет.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Порядок работ
|
|
||||||
|
|
||||||
Порядок задан пользователем 2026-08-13: **строго по номерам уровней**, а не
|
|
||||||
по дешевизне кода — уровень 13 не имеет смысла раньше, чем на него можно
|
|
||||||
попасть.
|
|
||||||
|
|
||||||
1. ✅ **Уровень 12** — тень: `init_shad_12`, `autocontrol_shadow_level12`,
|
|
||||||
`united_with_shadow`, общий урон, «убил тень — убил себя»,
|
|
||||||
`sword_disappears`, появление плит. Сделано 2026-08-13, хост-набор
|
|
||||||
`roomtest/tests-host/t_shadow.c` (45 проверок); живой проверки в MAME
|
|
||||||
ещё не было — доска [`L12-SHADOW`](../roomtest/TASKS_OPEN.md#l12-shadow).
|
|
||||||
2. ✅ **Переход 12 → 13** (§1.7) — room-триггер вместо двери и флаг
|
|
||||||
`pop_seamless` (не сбрасывать HP). Сделано там же.
|
|
||||||
3. ✅ **Уровень 13** — Джафар: `guard_notice_timer`,
|
|
||||||
`on_guard_killed`/`Jaffar_exit`, три спецкейса loose (§2.4) и
|
|
||||||
`check_fall_flo`. Плюс СПРАЙТЫ визиря (VIZIER.DAT) — без них он
|
|
||||||
рисовался обычным стражем. Сделано 2026-08-13, хост-набор
|
|
||||||
`roomtest/tests-host/t_jaffar.c` (44 проверки); живой проверки в MAME ещё
|
|
||||||
не было — доска [`L13-JAFFAR`](../roomtest/TASKS_OPEN.md#l13-jaffar).
|
|
||||||
4. **Уровень 14** — довести до комнаты 5 и поставить заглушку вместо ролика;
|
|
||||||
сам ролик — в общую задачу «катсцены».
|
|
||||||
5. **Уровень 0 и 15** — отложить: аттракт и защита от копирования на
|
|
||||||
прохождение не влияют.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Сверка констант с ОРИГИНАЛЬНЫМИ данными (сделана 2026-08-13)
|
|
||||||
|
|
||||||
Всё ниже снято скриптом прямо с `../SDLPoP/data/LEVELS/res2013.bin` и
|
|
||||||
`res2012.bin` — не из головы и не из констант SDLPoP.
|
|
||||||
|
|
||||||
### 7.1 Падающие плиты уровня 13 — константы сходятся ТОЧНО
|
|
||||||
|
|
||||||
Комнаты, у которых в ряду 2 вообще есть loose:
|
|
||||||
|
|
||||||
| комната | loose в колонках | комната снизу |
|
|
||||||
|---|---|---|
|
|
||||||
| 17 | 2, 3, 4, 5, 6, 7 | **23** |
|
|
||||||
| 1 | 2, 3, 5, 6, 7 | **16** |
|
|
||||||
| 14 | 2 | 24 |
|
|
||||||
|
|
||||||
Связи: `комната 23: up = 17`, `комната 16: up = 1`. То есть
|
|
||||||
`loose_tiles_room_1 = 23` и `room_2 = 16` — это ровно те две комнаты, над
|
|
||||||
которыми лежит ПОЛНАЯ гряда плит, а `first_tile = 22, last_tile = 27` — ровно
|
|
||||||
ряд 2, колонки 2..7:
|
|
||||||
|
|
||||||
```
|
|
||||||
комната 23 → сверху 17: 22:LOOSE 23:LOOSE 24:LOOSE 25:LOOSE 26:LOOSE 27:LOOSE [6/6]
|
|
||||||
комната 16 → сверху 1: 22:LOOSE 23:LOOSE 24:empty 25:LOOSE 26:LOOSE 27:LOOSE [5/6]
|
|
||||||
```
|
|
||||||
|
|
||||||
Два вывода:
|
|
||||||
|
|
||||||
* диапазон 22..27 — **надмножество**: в комнате 1 тайл 24 пустой, и
|
|
||||||
`make_loose_fall` его молча пропустит (проверяет тип тайла). Копировать
|
|
||||||
константы можно как есть;
|
|
||||||
* пара «комната 14 → 24» НЕ входит в спецсобытие намеренно — одна плита, это
|
|
||||||
обычный loose.
|
|
||||||
|
|
||||||
**Важное следствие, которого не было в плане:** стартовая комната уровня 13 —
|
|
||||||
**23** (`BP_START = 23`, поз 19, dir 0), а `check_fall_flo` зовётся из
|
|
||||||
`draw_level_first` → `check_the_end` (seg003:217). Значит плиты начинают
|
|
||||||
сыпаться СРАЗУ при входе на уровень, это его первый кадр, а не событие
|
|
||||||
где-то в середине.
|
|
||||||
|
|
||||||
### 7.2 Джафар — один страж со skill 9
|
|
||||||
|
|
||||||
В `res2013.bin` заполнен ровно один слот стража:
|
|
||||||
|
|
||||||
| комната | тайл | ряд, кол | направление | skill |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| 1 | 7 | 0, 7 | 255 (влево) | **9** |
|
|
||||||
|
|
||||||
И это сходится с `meet_Jaffar`: событие срабатывает, когда Кид уходит ВПРАВО
|
|
||||||
из комнаты 3, а `комната 3: right = 1` — то есть ровно туда, где стоит
|
|
||||||
Джафар. Skill 9 у нас поддержан: `NUM_GUARD_SKILLS = 12`, все шесть таблиц
|
|
||||||
вероятностей (`guards.c:411..415`) имеют индекс 9. HP = `tbl_guard_hp[13] = 6`
|
|
||||||
(в `pop_level_cold.c` уже стоит).
|
|
||||||
|
|
||||||
`Jaffar_exit` тоже проверен: **комната 24, tilepos 0 = `opener`** (кнопка),
|
|
||||||
модификатор 0 — то есть `trigger_button(0,0,-1)` жмёт реальную кнопку, а не
|
|
||||||
пустой тайл.
|
|
||||||
|
|
||||||
### 7.3 Уровень 12 — меч на месте
|
|
||||||
|
|
||||||
`комната 15, tilepos 1 = SWORD` — условие подъёма тени
|
|
||||||
(`get_tile(15,1,0) == tiles_22_sword`) на наших данных выполняется.
|
|
||||||
`комната 18: right = 19` — `sword_disappears` срабатывает при уходе вправо.
|
|
||||||
|
|
||||||
Комната 15 целиком (по ней видно всю сцену встречи с тенью):
|
|
||||||
|
|
||||||
```
|
|
||||||
ряд 0: floor SWORD floor torch torch LOOSE LOOSE floor empty empty
|
|
||||||
ряд 1: wall wall wall empty empty empty empty wall floor floor
|
|
||||||
ряд 2: wall opener pillar empty empty empty empty wall pillar empty
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.4 `pop_loose_tick` действительно убьёт отрицательную фазу
|
|
||||||
|
|
||||||
Подтверждено чтением кода: `m = ++pop_loose_modif[pos]; if (m & 0x80) { if (m
|
|
||||||
>= 0x84) → сброс в 0 }`. Для стартового `0xF0..0xFF` первый же тик даёт
|
|
||||||
`m >= 0x84` → фаза обнуляется, плита не падает. **Это и есть та правка №1 из
|
|
||||||
таблицы §2.4**, и она обязательна.
|
|
||||||
|
|
||||||
### 7.5 `init_shad_12` и байтовое переполнение `y`
|
|
||||||
|
|
||||||
`init_shad_12 = {0x0F, 0x51, 0xE8, 0, 0, 0, 0, 0}` → frame 15, x 81,
|
|
||||||
**y 232**, вправо, колонка 0, ряд 0, action 0 + `seq 7 (fall)`. Поле `y` в
|
|
||||||
`char_type` — `byte` (беззнаковое), то есть 232 лежит НИЖЕ поля (192), и
|
|
||||||
падение уводит его дальше с переполнением байта: тень «падает» из-под экрана и
|
|
||||||
появляется сверху. У нас `pop_char_t.y` тоже `uint8_t` — поведение
|
|
||||||
переносится без правок; при портировании просто не «чинить» это как ошибку.
|
|
||||||
|
|
||||||
Отдельно про ряд: `curr_row` берётся ИЗ ТАБЛИЦЫ и равен **0**, хотя
|
|
||||||
`pop_y_to_row(232)` дал бы −1. `do_init_shad` копирует семь полей как есть и
|
|
||||||
ряд не пересчитывает, так что «согласовать» их — значит разойтись с
|
|
||||||
оригиналом. Закреплено тестом `shadow12_rises_when_sword_gone`.
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
# План: от одного уровня к нескольким (загрузка, переходы, тайлсеты)
|
|
||||||
|
|
||||||
Статус: план, 2026-08-01. Продолжает `PORT_PLAN.md` §7 (Фаза 1: «переходы
|
|
||||||
между экранами» → теперь между УРОВНЯМИ). Текущая точка: `roomtest` играет
|
|
||||||
уровень 1 целиком в одной комнате-за-комнатой модели, но уровень нельзя
|
|
||||||
ни выбрать, ни закончить.
|
|
||||||
|
|
||||||
Источник истины — `../SDLPoP/src/` (правило `../CLAUDE.md`). Ключевые
|
|
||||||
места: `seg000.c: load_lev_spr/play_level_2/init_game`, `seg005.c:
|
|
||||||
up_pressed/go_up_leveldoor`, `seg006.c: play_seq → SEQ_END_LEVEL`,
|
|
||||||
`seg002.c` (спецсобытия уровней), `data.h:835..850` (потабличные различия
|
|
||||||
уровней).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 0. Что уже готово (не проектировать заново)
|
|
||||||
|
|
||||||
- **Формат и загрузчик уровня.** `pop_level.c/.h` читает сырой
|
|
||||||
`res200N.bin` (2305 Б) в отдельную EMM-страницу; путь — параметр
|
|
||||||
`pop_level_load(const char *)`. Мультиуровневость здесь стоит одной
|
|
||||||
функции формирования имени.
|
|
||||||
- **Стартовая позиция уровня** уже разобрана: `pop_level_start_room()`,
|
|
||||||
`pop_level_start_pos()`, `pop_level_start_dir()` — реализованы и пока
|
|
||||||
НЕ вызываются (см. `../roomtest/TASKS_CLOSED.md` L1-START).
|
|
||||||
- **Страж по данным уровня**: `pop_level_guard()` (порт `enter_guard`),
|
|
||||||
сохранение состояния между комнатами (`pop_guard_state_save`).
|
|
||||||
- **Палитра разложена по слотам ровно как в оригинале** (`pop_pack_kid.py`
|
|
||||||
`build_palette`): env 0x50, wall 0x60, pot 0x40, kid 0x70, меч 0x80,
|
|
||||||
страж 0x90. Это тот же раскрой, что `set_pal_arr(0x50/0x60)` в
|
|
||||||
`seg000.c:1140..1148`, — значит смена тайлсета не требует переиндексации
|
|
||||||
спрайтов Кида (см. §3).
|
|
||||||
- **Все 16 файлов уровней распакованы**: `../SDLPoP/data/LEVELS/res2000..
|
|
||||||
res2015.bin` (0 — демо-уровень).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Что реально различается между уровнями (замер по данным, не по памяти)
|
|
||||||
|
|
||||||
Таблицы из `../SDLPoP/src/data.h:840..847` + инвентарь тайлов, снятый прямо
|
|
||||||
с `res200N.bin` (маска `fg & 0x1F`):
|
|
||||||
|
|
||||||
| Ур. | Тайлсет | Страж | Новое против предыдущих |
|
|
||||||
|-----|---------|-------|--------------------------|
|
|
||||||
| 1 | dungeon | обычный | — (текущая база) |
|
|
||||||
| **2** | **dungeon** | **обычный** | **ничего нового: тот же набор объектов минус меч** |
|
|
||||||
| 3 | dungeon | СКЕЛЕТ | чомперы |
|
|
||||||
| 4 | palace | обычный | **тайлсет palace**, зеркало (спецсобытие `mirror_level`) — **СДЕЛАНО** |
|
|
||||||
| 5 | palace | обычный | новых ТАЙЛОВ нет; спецсобытие **тень крадёт зелье** (комната 24) — [L5-SHADOW](../roomtest/TASKS_OPEN.md#l5-shadow) |
|
|
||||||
| 6 | palace | ТОЛСТЫЙ | падение на входе (спецсобытие) |
|
|
||||||
| 7 | dungeon | обычный | — |
|
|
||||||
| 8, 9 | dungeon | обычный | — |
|
|
||||||
| 10, 11 | palace | обычный | — |
|
|
||||||
| 12 | dungeon | ТЕНЬ | seamless-выход (комната 23), исчезающий меч |
|
|
||||||
| 13 | dungeon | ВИЗИРЬ | мышь, особый выход |
|
|
||||||
| 14 | palace | нет | — |
|
|
||||||
| 15 | dungeon | нет | финал |
|
|
||||||
|
|
||||||
Прямое следствие для порядка работ: **уровень 2 не требует ни одного нового
|
|
||||||
ассета и ни одной новой механики** — он проверяет ровно машинерию перехода.
|
|
||||||
Это и есть первый шаг.
|
|
||||||
|
|
||||||
Прочие потабличные различия, которые придётся завести массивами по 16:
|
|
||||||
`tbl_level_type` (тайлсет), `tbl_guard_type` (−1 = стражей нет),
|
|
||||||
`tbl_guard_hp`, `tbl_level_color` (вариантные палитры, 1.3), `tbl_entry_pose`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Шаг 1 — машинерия перехода (цель: уровни 1 → 2 → 3) — **СДЕЛАН 2026-08-04**
|
|
||||||
|
|
||||||
> **Итог.** Всё в этом разделе портировано и проверено в MAME: уровень 1 →
|
|
||||||
> Shift+L → уровень 2 (комната 5, дверь захлопывается за спиной, большие
|
|
||||||
> колонны рисуются) → уровень 3. Разбор что именно сделано и что по
|
|
||||||
> уровню 2 осталось — `../roomtest/TASKS_CLOSED.md`, запись **L2**.
|
|
||||||
>
|
|
||||||
> Сверх плана пришлось доделать две вещи, без которых уровень 2 не играется:
|
|
||||||
> **`find_start_level_door`** (стартовый тайл уровня 2 — это правая половина
|
|
||||||
> двери уровня) и **большую склянку** `add_life` (тип зелья 2, комната 20).
|
|
||||||
|
|
||||||
Порядок именно такой; каждый пункт проверяем в MAME отдельно.
|
|
||||||
|
|
||||||
**2.1 Выход с уровня.** Портировать `up_pressed()` ветку двери
|
|
||||||
(`seg005.c:410..423`) + `go_up_leveldoor()` (`seg005.c:497`): дверь рядом
|
|
||||||
(при/за/перед персонажем) И `drawn_room != level.start_room` И створка
|
|
||||||
открыта полностью (`curr_room_modif >= 42` — вариант `fix_exit_door`) →
|
|
||||||
`Char.x = x_bump[...] + 10`, направление влево, `seq_70_go_up_on_level_door`.
|
|
||||||
Затем оживить опкод `0xF1 END_LEVEL` в `play_seq` (`../roomtest/pop_kid.c:418`
|
|
||||||
— сейчас пустой `break`): `++pop_next_level`, как `seg006.c:662`.
|
|
||||||
|
|
||||||
**2.2 Цикл уровня.** В `main()` после тика: `if (pop_next_level !=
|
|
||||||
pop_current_level) → load_level(pop_next_level)`. Порядок сноса/подъёма
|
|
||||||
состояния (порт `load_lev_spr` + `play_level_2`):
|
|
||||||
`pop_level_free` → `pop_level_load("LEVELS\\res200%d.bin")` →
|
|
||||||
`pop_trob_reset` → `pop_guard_reset` → сброс tile-override'ов
|
|
||||||
(`ovr_*` в `roomtest.c`) → `enter_room(pop_level_start_room())` →
|
|
||||||
`kid_init(поза/позиция/направление из данных уровня)`.
|
|
||||||
**HP через уровень переносится** (в оригинале `hitp_beg_lev`), не сбрасывать
|
|
||||||
в максимум — сверить с `seg000.c` `init_game`/`play_level_2`.
|
|
||||||
|
|
||||||
**2.3 Стражи по уровню.** Завести `tbl_guard_type[16]`/`tbl_guard_hp[16]`;
|
|
||||||
`-1` = стражей на уровне нет (уровни 14, 15) — `pop_guard_enter` обязан это
|
|
||||||
понимать, иначе на 14-м полезут стражи из мусора. Для шага 1 (уровни 2, 3)
|
|
||||||
достаточно обычного стража, но проверку `-1` заложить сразу.
|
|
||||||
|
|
||||||
**2.4 Чит «следующий уровень» (Shift+L).** Реализуется ровно тем же
|
|
||||||
`pop_next_level` — и без него отладка уровней превращается в прохождение
|
|
||||||
игры руками. Делать в этом же шаге, не позже (см. §4).
|
|
||||||
|
|
||||||
**Приёмка шага 1:** дверь уровня 1 → уровень 2 играется целиком → его дверь
|
|
||||||
→ уровень 3 стартует (чомперы могут быть ещё не портированы — тогда
|
|
||||||
фиксируем как известное ограничение, а не «баг»).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Шаг 2 — второй тайлсет (palace, уровни 4+) — **СДЕЛАН**
|
|
||||||
|
|
||||||
> **Закрыт (ревизия 2026-08-11 по коду).** В дереве есть всё, что
|
|
||||||
> проектировалось ниже: `pop_pack_bg.py` печёт ДВА набора атласов
|
|
||||||
> (`pop_*` из VDUNGEON, `pal_*` из VPALACE, каскад с обменом приоритетов),
|
|
||||||
> `pop_bg_load` принимает тип набора и ставит флаг `pop_palace`, по которому
|
|
||||||
> `pop_room.c` выбирает дворцовую ветку (`wall_pattern` дворца — сплошная
|
|
||||||
> заливка + пять mono-полос, `PALACE_WALL_MONO_IDS`), решётчатые тайлы
|
|
||||||
> 25-29 есть и в `tile_table` (`pop_tile.c`), и в коллизии — `tile_is_floor`
|
|
||||||
> совпадает с `seg006:0628` тайл в тайл. Уровень 4 проходится smoke-тестом.
|
|
||||||
>
|
|
||||||
> Текст ниже оставлен как справочник по раскрою палитры и по тому, почему
|
|
||||||
> смена тайлсета — это перезапись 32 записей, а не переиндексация спрайтов.
|
|
||||||
|
|
||||||
**Ассеты.** `toolchain/pop_pack_bg.py` уже читает PNG каскадом
|
|
||||||
VDUNGEON→VPALACE (та же логика, что в игре), но печёт ОДИН набор атласов
|
|
||||||
(`pop_env0..4.atl`, `pop_wall.atl`, `pop_fore.atl` ≈ 75 КБ). Нужен второй
|
|
||||||
набор из VPALACE (`pal_env*.atl` / `pal_wall.atl`), плюс `torch_debris` —
|
|
||||||
тайл, который встречается только на palace-уровнях. По EMM это ещё ~6
|
|
||||||
страниц при бюджете ~3.3 МБ — не проблема.
|
|
||||||
|
|
||||||
**Палитра — главный технический вопрос, и он уже решён раскроем.**
|
|
||||||
Тайлсет живёт в слотах `0x50..0x5F` (env) и `0x60..0x6F` (wall); Кид, меч,
|
|
||||||
страж, склянки — в других слотах. Значит смена тайлсета = перезапись 32
|
|
||||||
записей палитры (`gfx_pal_set` на обе страницы, как `flash_bg` в
|
|
||||||
`roomtest.c`), а НЕ перезагрузка `kid.pal` и не переиндексация спрайтов.
|
|
||||||
Сделать `pal_dungeon.bin` / `pal_palace.bin` (по 32 записи) и грузить при
|
|
||||||
смене типа уровня. Проверить артефактом: скриншот palace-комнаты против
|
|
||||||
рендера `render_room.py` для того же уровня.
|
|
||||||
|
|
||||||
**Вариантные цвета уровней** (`tbl_level_color`, `level_var_palettes` — это
|
|
||||||
уже 1.3, в 1.0 их нет): по той же механике, тот же диапазон слотов. Решение
|
|
||||||
на будущее — сначала базовые два тайлсета, потом при желании цвета.
|
|
||||||
|
|
||||||
**Выбор набора в коде.** `pop_bg_load()` сейчас грузит фиксированные имена;
|
|
||||||
превратить в `pop_bg_load(type)` с двумя таблицами имён + выгрузка старых
|
|
||||||
атласов при смене типа (`atlas_free`). Переключение — только на границе
|
|
||||||
уровня, не в кадре.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Читы SDLPoP: что взять на следующем этапе
|
|
||||||
|
|
||||||
Из `../SDLPoP/README.md` (раздел Cheats). У нас уже есть: **K** — убить
|
|
||||||
стража, **I** — бессмертие (наш, в оригинале нет), **S** — выдать меч (наш),
|
|
||||||
**+/−** — обход комнат (`ROOMNAV`, наш).
|
|
||||||
|
|
||||||
**Брать сразу вместе с переходами уровней** (без них отладка дороже самой
|
|
||||||
работы):
|
|
||||||
|
|
||||||
| Чит | Что даёт | Цена |
|
|
||||||
|-----|----------|------|
|
|
||||||
| **Shift+L — следующий уровень** | единственный вменяемый способ тестировать уровни 2..15 | тривиально: `++pop_next_level` из §2.2 |
|
|
||||||
| **R — воскресить Кида** | у нас респавн по ↑ + таймаут; порт `resurrect` ближе к оригиналу и не мешает управлению | низкая |
|
|
||||||
| **Shift+S / Shift+T — +1 HP / +максимум** | отладка боёвки без «ровно трёх попыток»; честная замена нашему читу бессмертия | низкая, HP-машинерия уже есть |
|
|
||||||
| **[ и ] — сдвинуть Кида на пиксель** (debug-чит SDLPoP) | прямо бьёт в наш класс багов «окклюзия/шов на один пиксель» — воспроизведение позы без ловли момента | тривиально |
|
|
||||||
|
|
||||||
**Брать во вторую очередь:**
|
|
||||||
|
|
||||||
| Чит | Почему позже |
|
|
||||||
|-----|--------------|
|
|
||||||
| **H / J / U / N + Ctrl+B — смотреть соседние комнаты** | требует честной модели `drawn_room ≠ Kid.room` (наш S3-straddle, каркас есть: `update_kid_render_dx`). Зато потом заменяет самодельный `ROOMNAV` и попутно закрывает straddle-задачу |
|
|
||||||
| **Shift+W — медленное падение (feather)** | ветка `JMP_IF_FEATHER` (опкод `0xF7`) в `play_seq` уже есть, но не проверена ничем — чит станет её единственным тестом |
|
|
||||||
| **C / Shift+C — номера комнат** | у нас номер рисуется палочками именно потому, что текст тянет 2 КБ знакогенератора в W2 (`roomtest.c`). Ждёт своего шрифта |
|
|
||||||
|
|
||||||
**Не брать:** `Shift+I` (переворот экрана), `Shift+B` (blind mode) —
|
|
||||||
развлекательные, к отладке порта отношения не имеют. `−/+` (время) — нужен
|
|
||||||
таймер уровня, которого у нас нет (Фаза 6).
|
|
||||||
|
|
||||||
**Отдельно, дорого, но очень ценно — `F6`/`F9` (quicksave/quickload точного
|
|
||||||
состояния).** Это сериализация `Char` + `room_modif` всех комнат + trob'ов +
|
|
||||||
состояния стражей. Даёт то, чего нам сейчас сильно не хватает:
|
|
||||||
воспроизводимый регресс в MAME («вот кадр, где баг») вместо ручного подхода
|
|
||||||
к позиции. Кандидат сразу после того, как заработают уровни.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Риски и что проверить артефактом до кодинга
|
|
||||||
|
|
||||||
1. **Размер кода.** Замер сборки 2026-08-01: `_CODE` 25 119 Б,
|
|
||||||
куча ~2.4 КБ, банк 2 (`pop_bg`) 13 792 / 16 384, банк 3 (`pop_map`)
|
|
||||||
6 331, банк 1 (`guards`) 1 896, банк 4 (`pop_gdraw`) 2 236. Чомперы,
|
|
||||||
зеркало, скелет и второй тайлсет пойдут в банк 2 — там осталось 2.6 КБ.
|
|
||||||
**Прежде чем начинать §3, посчитать, куда лягут новые тайлы**, иначе
|
|
||||||
повторится история «банк 2 упёрся в потолок» (коммит 2f3e854). Свободные
|
|
||||||
номера банков есть (5+), гранулярность — файл.
|
|
||||||
2. **Спецсобытия уровней** (`seg002.c`: `level3_set_chkp`, `sword_disappears`,
|
|
||||||
`Jaffar_exit`, зеркало, мышь) — их НЕ надо портировать заранее. Для
|
|
||||||
уровней 2 и 3 нужен только чекпойнт уровня 3. Остальное — по мере
|
|
||||||
подхода к уровню.
|
|
||||||
3. **Чомперы** (уровень 3 и почти все дальше) — отдельная механика
|
|
||||||
(`animate_chomper` + коллизия + смерть); шаблон работы тот же, что у
|
|
||||||
пик/ворот, см. `gates_spikes_plan.md`.
|
|
||||||
4. **`tbl_guard_type = -1`** на уровнях 14/15: без проверки страж
|
|
||||||
«появится» из неинициализированных данных.
|
|
||||||
5. **Уровень 0 (демо)** существует в данных, но в скоуп не входит.
|
|
||||||
@@ -1,275 +0,0 @@
|
|||||||
# Слои отрисовки: как устроен оригинал и чего стоит порт
|
|
||||||
|
|
||||||
Разбор 2026-08-13, по `../SDLPoP/src/seg008.c`. Повод — семь дефектов
|
|
||||||
падающих плит на уровне 13, из которых три оказались не багами кода, а
|
|
||||||
следствием того, что у нас нет слоя, в котором объекты и куски тайлов
|
|
||||||
сортируются между собой. Решение по этому документу ещё не принято.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Как это работает в оригинале
|
|
||||||
|
|
||||||
### 1.1 Три таблицы, а не «слои»
|
|
||||||
|
|
||||||
`draw_tables` (seg008:1373) рисует ровно в таком порядке:
|
|
||||||
|
|
||||||
```
|
|
||||||
restore_peels();
|
|
||||||
draw_wipes(0);
|
|
||||||
draw_table(0); // BACKTABLE
|
|
||||||
draw_table(3); // MIDTABLE
|
|
||||||
draw_wipes(1);
|
|
||||||
draw_table(1); // FORETABLE
|
|
||||||
```
|
|
||||||
|
|
||||||
Это грубое разделение на три уровня глубины. Куда попадёт кусок тайла,
|
|
||||||
решает переменная `ptr_add_table`, которую вызывающий переставляет перед
|
|
||||||
`draw_tile*`: по умолчанию `add_backtable`, в оверлее кромки —
|
|
||||||
`add_midtable` (`draw_other_overlay`, seg008:1499), а `add_foretable`
|
|
||||||
вызывается явно и точечно.
|
|
||||||
|
|
||||||
### 1.2 Объекты живут НЕ в таблицах, а в objtable — и привязаны к ТАЙЛУ
|
|
||||||
|
|
||||||
Персонажи, падающие куски, мечи, брызги попадают в `objtable`, и у каждой
|
|
||||||
записи есть поле `tilepos` — тайл, которому объект принадлежит.
|
|
||||||
|
|
||||||
Ключевое: объекты рисуются **не отдельным проходом после фона**, а ВНУТРИ
|
|
||||||
обхода тайлов. В `redraw_needed_tile` (seg008:207) стоит:
|
|
||||||
|
|
||||||
```c
|
|
||||||
if (tile_object_redraw[tilepos]) {
|
|
||||||
if (tile_object_redraw[tilepos] == 0xFF)
|
|
||||||
draw_objtable_items_at_tile(tilepos - 1);
|
|
||||||
draw_objtable_items_at_tile(tilepos);
|
|
||||||
tile_object_redraw[tilepos] = 0;
|
|
||||||
}
|
|
||||||
if (redraw_frames_fore[tilepos]) draw_tile_fore();
|
|
||||||
```
|
|
||||||
|
|
||||||
То есть на каждом тайле: сначала его фоновые куски, потом объекты ЭТОГО
|
|
||||||
тайла, потом его передние куски.
|
|
||||||
|
|
||||||
### 1.3 Порядок глубины складывается из ТРЁХ независимых механизмов
|
|
||||||
|
|
||||||
| механизм | что даёт |
|
|
||||||
|---|---|
|
|
||||||
| порядок обхода тайлов: ряды **2, 1, 0**, колонки 0..9 (seg008:129) | тайл, обойдённый позже, рисуется поверх |
|
|
||||||
| сортировка объектов ВНУТРИ одного тайла (`sort_curr_objs`, seg008:1553) | кто из объектов одного тайла поверх кого |
|
|
||||||
| три таблицы back/mid/fore | грубая глубина для кусков тайлов |
|
|
||||||
|
|
||||||
Сортировка внутри тайла (`compare_curr_objs`, seg008:1572) — пузырьком, и
|
|
||||||
правил в ней три:
|
|
||||||
|
|
||||||
```
|
|
||||||
объект типа 1 (ТЕНЬ) — всегда первым;
|
|
||||||
оба объекта — падающие плиты (0x80): y1 < y2 → по УБЫВАНИЮ y;
|
|
||||||
любая другая пара: y1 > y2 → по ВОЗРАСТАНИЮ y.
|
|
||||||
```
|
|
||||||
|
|
||||||
Обратный порядок для пары плит — не описка: две плиты из `loose_fall` летят
|
|
||||||
в 6 пикселях друг от друга, и верхняя обязана лечь поверх нижней.
|
|
||||||
|
|
||||||
### 1.4 Что из этого следует
|
|
||||||
|
|
||||||
**«Сортируемый midtable» — неточное имя.** Глобальной сортировки среднего
|
|
||||||
слоя в оригинале нет. Есть привязка объекта к тайлу и сортировка внутри
|
|
||||||
тайла; всё остальное решает порядок обхода. Это принципиально дешевле
|
|
||||||
общей сортировки: объектов на один тайл обычно 1-2.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Что делаем мы
|
|
||||||
|
|
||||||
Наш кадр — жёсткая последовательность проходов, без привязки объектов к
|
|
||||||
тайлам:
|
|
||||||
|
|
||||||
```
|
|
||||||
фон: pop_loose_tick (физика кусков) -> pop_process_trobs -> pop_redraw_needed
|
|
||||||
-> редрой шва
|
|
||||||
объекты: pop_loose_mob_draw (куски ПОД Kid, отсортированы по y между собой)
|
|
||||||
pop_char_draw(OPP/KID) (порядок задаёт guard_over_kid)
|
|
||||||
pop_loose_mob_draw_over (куски ПОВЕРХ Kid)
|
|
||||||
перед: pop_fore_over_char — ТОЛЬКО в окне вокруг персонажа
|
|
||||||
```
|
|
||||||
|
|
||||||
Отличия, из которых растут все три оставшихся дефекта:
|
|
||||||
|
|
||||||
1. **Объект не знает своего тайла.** Глубина «кусок против Кида» считается
|
|
||||||
отдельной формулой (`pop_room.c`, поле `defer`), а «кусок против КУСКА
|
|
||||||
ТАЙЛА» не считается вовсе — куски тайлов рисуются раньше всех объектов.
|
|
||||||
2. **Передний слой считается только вокруг персонажа.** Это наша
|
|
||||||
оптимизация (memory `pop_fore_layer_cost`: полный проход стоил 78 %
|
|
||||||
кадра). Падающая плита в чужом углу комнаты передних частей тайлов
|
|
||||||
поверх себя не получает — отсюда «плита перед колонной».
|
|
||||||
3. **Оверлей кромки идёт после персонажей** и потому безусловно поверх
|
|
||||||
всех, тогда как у оригинала он в midtable и сортируется.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Что затрагивает порт
|
|
||||||
|
|
||||||
| участок | объём правки |
|
|
||||||
|---|---|
|
|
||||||
| `pop_room.c` — куски | привязать к тайлу, убрать `defer`, убрать собственную сортировку |
|
|
||||||
| `pop_cdraw.c` — персонажи | то же: объект вместо слота, привязка к тайлу |
|
|
||||||
| `pop_bg.c` — `overlay_mid_tile`, `fore_only_tile`, `ceil_over_kid_tile` | вызов из обхода тайлов, а не из отдельного прохода |
|
|
||||||
| `pop_redraw.c` — пометки | добавить «на этом тайле есть объект» (порт `tile_object_redraw`) |
|
|
||||||
| `roomtest.c` — главный цикл | вместо трёх проходов один: обход тайлов с объектами внутри |
|
|
||||||
| окно fore-клипа (`pop_t_fclip_*`) | смысл меняется: клип по тайлу, а не по персонажу |
|
|
||||||
|
|
||||||
Плюс новая структура objtable и её сортировка — но маленькая, на тайл.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Плюсы
|
|
||||||
|
|
||||||
* **Уходят разом** MOB-CLIP-RIGHT, MID-OVERLAY-LAYER и «плита перед
|
|
||||||
колонной»: все три — следствие отсутствия привязки к тайлу, а не
|
|
||||||
самостоятельные баги.
|
|
||||||
* **Уходят подпорки.** Перерисовка соседнего тайла поверх куска,
|
|
||||||
`defer`, ручная сортировка кусков, отдельный проход `draw_over` —
|
|
||||||
всё это заменяется одним механизмом.
|
|
||||||
* **Совпадение с оригиналом по построению.** Дальше любой вопрос «что
|
|
||||||
поверх чего» решается чтением seg008, а не экспериментом в MAME.
|
|
||||||
* **Возможный выигрыш по кадру.** Сейчас fore-проход считает окно вокруг
|
|
||||||
персонажа и всё равно перебирает до девяти тайлов; при привязке к тайлу
|
|
||||||
передние части рисуются только там, где реально есть помеченный объект.
|
|
||||||
Но это НАДО ЗАМЕРИТЬ, а не обещать.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Минусы и риски
|
|
||||||
|
|
||||||
* **Риск регресса широкий.** Трогается порядок отрисовки ВСЕГО: Кид,
|
|
||||||
соперник, меч, брызги, зеркало, куски, оверлеи, полоса потолка.
|
|
||||||
Уровни 1-11 приняты и держатся на текущем порядке.
|
|
||||||
* **Наша оптимизация fore-окна может не пережить порт в прежнем виде.**
|
|
||||||
Она даёт 3.2x на самом дорогом проходе (memory `pop_fore_layer_cost`).
|
|
||||||
Если привязка к тайлу заставит рисовать передние части шире — можно
|
|
||||||
потерять больше, чем выиграть.
|
|
||||||
* **Дабл-буфер.** У оригинала один экран с dirty-rect, у нас две страницы
|
|
||||||
со своими копиями фона и heal. Пометка «на тайле есть объект» обязана
|
|
||||||
быть счётчиком страниц, как остальные наши пометки, — иначе объект
|
|
||||||
перерисуется на одной странице и не перерисуется на другой.
|
|
||||||
* **Банки.** Отрисовка размазана по трём банкам (`pop_bg` 2, `pop_cdraw` 4,
|
|
||||||
`pop_room` 7) плюс резидент. Единый обход тайлов с объектами внутри
|
|
||||||
означает, что цикл обхода зовёт код из всех трёх — надо проверить, что не
|
|
||||||
упрёмся в границы банков и трамплины.
|
|
||||||
* **Объём.** Это не правка, а этап: сопоставимо с тем, что делалось для
|
|
||||||
fore-слоя.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Развилки
|
|
||||||
|
|
||||||
**A. Полный порт** — objtable с привязкой к тайлу, сортировка внутри тайла,
|
|
||||||
объекты внутри обхода. Максимально близко к оригиналу, максимальный риск и
|
|
||||||
объём.
|
|
||||||
|
|
||||||
**B. Частичный: только привязать КУСКИ к тайлам.** Персонажей оставить как
|
|
||||||
есть. Закрывает MOB-CLIP-RIGHT и «плиту перед колонной», не трогает
|
|
||||||
проверенный порядок персонажей. Дешевле и безопаснее; MID-OVERLAY-LAYER
|
|
||||||
остаётся.
|
|
||||||
|
|
||||||
**C. Отложить** до этапа BG-ONCE и делать вместе — там всё равно
|
|
||||||
пересматриваются слои, и два пересмотра подряд дороже одного.
|
|
||||||
|
|
||||||
Рекомендация: **B или C**. Вариант A целиком оправдан только если мы
|
|
||||||
одновременно берёмся за BG-ONCE — тогда это один пересмотр слоёв вместо
|
|
||||||
двух, и замер кадра делается один раз.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. ЗАМЕРЫ (сделаны 2026-08-13, уровень 13)
|
|
||||||
|
|
||||||
Метод — маркеры-пустышки в РЕЗИДЕНТЕ вокруг измеряемого вызова плюс
|
|
||||||
брейкпоинты с `printf totalcycles` (memory `z80_profiling_method`). В
|
|
||||||
банковый код брейкпоинт ставить нельзя: 0xC000 — общее окно всех банков.
|
|
||||||
|
|
||||||
| что | такты | доля логического кадра |
|
|
||||||
|---|---|---|
|
|
||||||
| логический кадр целиком | **1 289 526** | 100 % |
|
|
||||||
| `pop_redraw_needed` | **978** | 0,08 % |
|
|
||||||
| **fore-проход, ОДИН персонаж** | **128 778** | **10 %** |
|
|
||||||
|
|
||||||
Плюс два счётчика за прогон ~229 логических кадров:
|
|
||||||
|
|
||||||
* fore-проход вызван **10 раз** — на 96 % кадров он не выполняется вовсе
|
|
||||||
(пропуск неизменившегося персонажа, `pop_char_skip_mask`). Средняя цена
|
|
||||||
по кадру выходит ~0,4 %, но КАК ТОЛЬКО персонаж движется — платим все 10 %
|
|
||||||
каждый кадр, и при двух персонажах это ~20 %;
|
|
||||||
* **максимум объектов на одном тайле = 2** (комната 16, пара из
|
|
||||||
`loose_fall`: плита сбивает плиту, дальше летят обе). В комнате 23, где
|
|
||||||
гряда падает в пустоту, максимум 1.
|
|
||||||
|
|
||||||
### 7.1 Что эти числа меняют в оценке
|
|
||||||
|
|
||||||
**Сортировка внутри тайла — бесплатна.** Два объекта, пузырёк на два
|
|
||||||
элемента. Возражение против варианта A, которое закладывалось в §5, снято.
|
|
||||||
|
|
||||||
**`pop_redraw_needed` можно не считать вовсе.** 978 тактов против 128 778 у
|
|
||||||
fore-прохода — соотношение 131 к 1.
|
|
||||||
|
|
||||||
**Единственный настоящий риск порта — окно клипа fore-прохода.** Если
|
|
||||||
привязка объектов к тайлам заставит рисовать передние части шире нынешнего
|
|
||||||
окна вокруг персонажа, мы потеряем 10 % кадра, и потеряем их НА ДВИЖЕНИИ,
|
|
||||||
когда бюджет и так самый напряжённый.
|
|
||||||
|
|
||||||
### 7.2 Насколько узко оригинал помечает передний слой — ВЫЯСНЕНО
|
|
||||||
|
|
||||||
Пометки `redraw_frames_fore[]` ставит ровно одна функция — `set_redraw_fore`
|
|
||||||
(seg007:0550), и зовут её из трёх мест. Ни в одном нет «пометить всё».
|
|
||||||
|
|
||||||
**Персонаж — `redraw_at_char` (seg003:0576).** Помечается ПРЯМОУГОЛЬНИК
|
|
||||||
футпринта:
|
|
||||||
|
|
||||||
```c
|
|
||||||
for (tile_row = x_top_row; tile_row <= char_bottom_row; ++tile_row)
|
|
||||||
for (tile_col = x_col_left; tile_col <= x_col_right; ++tile_col)
|
|
||||||
set_redraw_fore(get_tilepos(tile_col, tile_row), 1);
|
|
||||||
```
|
|
||||||
|
|
||||||
с двумя уточнениями: при вынутом мече прямоугольник расширяется на колонку в
|
|
||||||
сторону клинка, а для КИДА берётся объединение с футпринтом ПРОШЛОГО кадра
|
|
||||||
(`prev_char_*`) — чтобы освободившиеся тайлы тоже вернули свои передние
|
|
||||||
части.
|
|
||||||
|
|
||||||
**Падающий кусок — `draw_mob` (seg007:~1147).** Каждый кадр помечается
|
|
||||||
СОСЕД СПРАВА (`++tile_col`), и второй тайл, если кусок висит на границе
|
|
||||||
рядов:
|
|
||||||
|
|
||||||
```c
|
|
||||||
++tile_col;
|
|
||||||
tilepos = get_tilepos(tile_col, tile_row);
|
|
||||||
set_redraw2(tilepos, 1);
|
|
||||||
set_redraw_fore(tilepos, 1);
|
|
||||||
top_row = y_to_row_mod4(ypos - 18);
|
|
||||||
if (top_row != tile_row) { ... то же для top_row ... }
|
|
||||||
add_mob_to_objtable(ypos);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Анимация тайла — `draw_trob` (seg007:01E6):** один тайл.
|
|
||||||
|
|
||||||
**Вывод: пометка переднего слоя в оригинале НЕ ШИРЕ нашего окна.** Она
|
|
||||||
пообъектная — футпринт персонажа и 1-2 тайла на кусок. Значит полный порт
|
|
||||||
objtable **не отнимает** нашу оптимизацию fore-окна, а формализует её:
|
|
||||||
вместо «окно вокруг персонажа» будет «тайлы, помеченные объектами», что
|
|
||||||
как минимум не шире, а для одиночного куска заметно уже.
|
|
||||||
|
|
||||||
Риск, вокруг которого крутилась вся оценка, снят.
|
|
||||||
|
|
||||||
### 7.3 Побочный результат: готовый рецепт для MOB-CLIP-RIGHT
|
|
||||||
|
|
||||||
`draw_mob` даёт точный ответ на вопрос, как оригинал прячет правую часть
|
|
||||||
куска за соседним полом: он НЕ рисует сосед поверх куска (наша подпорка) и
|
|
||||||
НЕ полагается только на `clip.right`. Он помечает соседний тайл СРАЗУ
|
|
||||||
двумя пометками — `set_redraw2` (фон) и `set_redraw_fore` (передний слой).
|
|
||||||
Дальше порядок делает всё сам: фон соседа рисуется ДО куска, его передние
|
|
||||||
части — ПОСЛЕ.
|
|
||||||
|
|
||||||
Это же закрывает и «плиту перед колонной»: передние части соседнего тайла
|
|
||||||
(колонна) ложатся поверх куска, потому что тайл помечен.
|
|
||||||
|
|
||||||
**Рекомендация после разбора: вариант A (полный порт).** Оба возражения
|
|
||||||
против него сняты замерами и этим разбором — сортировка внутри тайла
|
|
||||||
бесплатна (максимум 2 объекта), окно переднего слоя не теряется.
|
|
||||||
@@ -1,335 +0,0 @@
|
|||||||
# Оптимизация отрисовки — что НЕ сделано (замеры на 2026-08-10)
|
|
||||||
|
|
||||||
Список отложенных идей с измеренной ценой. Всё измерено брейкпоинтами в
|
|
||||||
MAME (`z80_profiling_method`) на роомтесте, уровень 1 комната 1.
|
|
||||||
|
|
||||||
Прежде чем брать что-то отсюда — перечитать «Как мерить» ниже: половина
|
|
||||||
прошлых гипотез не подтвердилась, и подтвердились не те, что казались
|
|
||||||
очевидными.
|
|
||||||
|
|
||||||
## Как мерить (иначе цифры не сходятся)
|
|
||||||
|
|
||||||
- **Такт `totalcycles` ≠ номинальный T-такт Z80.** У ОЗУ Sprinter
|
|
||||||
wait-state'ы, замеренная стоимость ≈ **2,4× справочной** (`get_tile`: 574
|
|
||||||
против 1 422). Считать по таблице тактов нельзя. Подробности —
|
|
||||||
memory `sprinter_wait_states_2x`.
|
|
||||||
- **Растровый кадр = 430 000 тактов.** Главный цикл спейсится тремя
|
|
||||||
`gfx_wait_vsync`, поэтому работа сверх 430 000 стоит СРАЗУ целый лишний
|
|
||||||
кадр. Граница дискретная: 3 растровых кадра на логический или 4.
|
|
||||||
- **Адреса символов меняются после КАЖДОЙ пересборки** (`roomtest.map`,
|
|
||||||
`bank*_*.sym`). Маркер со старым адресом молча не срабатывает, и разбивка
|
|
||||||
выглядит правдоподобно, но врёт.
|
|
||||||
- **Сцена между сессиями не воспроизводится точно**: позиция Кида до
|
|
||||||
пикселя, состояние плиты (2,6), фаза факелов. Сравнивать «до/после» можно
|
|
||||||
только по ОДНОЙ функции с одинаковыми входами, а не по общей работе за
|
|
||||||
кадр.
|
|
||||||
- Кто делит: брейкпоинт на `__divsint`/`__divuint`/`__divuchar` с печатью
|
|
||||||
адреса возврата — `bpset <addr>,1,{printf "ret=%04X\n",w@(sp); g}`. Если
|
|
||||||
адрес возврата в банке (>= 0xC000), поставить тот же брейкпоинт с условием
|
|
||||||
`w@(sp)==<адрес>` и БЕЗ `g`: машина встанет с нужным банком в окне, и
|
|
||||||
`dasm` покажет вызывающего.
|
|
||||||
- **Брейкпоинт по адресу в банке ловит ВСЕ банки.** 0xC000..0xFFFF — общее
|
|
||||||
окно, и один и тот же адрес есть у семи модулей сразу. Либо ловить через
|
|
||||||
трамплин (`hl==<адрес>&&(de&0xff)==<банк>`), либо перепроверять, что
|
|
||||||
срабатывания идут из нужной фазы: иначе в интервал попадает чужой код и
|
|
||||||
цифры врут (так я намерил несуществующие 134 730 тактов в прологе
|
|
||||||
`pop_char_fore`).
|
|
||||||
- Трасса вызовов графики с параметрами: скрипт в истории сессии, ставит
|
|
||||||
маркеры фаз на трамплин `___sdcc_bcall_ehl` (условие `hl==<адрес>&&(de&0xff)==<банк>`)
|
|
||||||
и брейкпоинты на листья libbgi с печатью аргументов
|
|
||||||
(`__sdcccall(1)`: arg1 = HL, arg2 = DE, дальше стек с sp+2).
|
|
||||||
|
|
||||||
## Профиль на 2026-08-10
|
|
||||||
|
|
||||||
Сцена: комната 1, два факела, стража нет.
|
|
||||||
|
|
||||||
| сцена | работа за кадр | период |
|
|
||||||
|---|---|---|
|
|
||||||
| Кид в покое, факел не задет (пропуск работает) | 244 026 (57 %) | 3 кадра |
|
|
||||||
| Кид стоит на факеле (перерисовывается каждый кадр) | 366 240 (85 %) | 3 кадра |
|
|
||||||
| Кид в щебне (2,4), движется | ~357 000 (83 %) | 3 кадра |
|
|
||||||
| Кид (0,5) в движении | 421 254 (98 %) | **4 кадра** |
|
|
||||||
|
|
||||||
Одна перерисовка персонажа = **~137 000 тактов = 32 % растрового кадра**,
|
|
||||||
из них полезной работы (heal 20×19 + спрайт 12×41) — меньше трети.
|
|
||||||
|
|
||||||
## Профиль дворца (уровень 4) на 2026-08-10
|
|
||||||
|
|
||||||
Сцена: уровень 4, Кид НЕПОДВИЖНО стоит на (1,7) (комната с решёткой),
|
|
||||||
стража нет. Разбивка одного логического кадра брейкпоинтами на границах
|
|
||||||
фаз (адреса `PROF()` из `roomtest.lst` + базы `_CODE = 0x42AD`):
|
|
||||||
|
|
||||||
| фаза | тактов | доля работы |
|
|
||||||
|---|---:|---:|
|
|
||||||
| ввод + читы + heal | 59 340 | 11 % |
|
|
||||||
| логика (kid_tick, phys_tick, страж, боёвка) | 85 356 | 16 % |
|
|
||||||
| `pop_loose_tick` | 27 396 | 5 % |
|
|
||||||
| `pop_process_trobs` | 106 107 | 21 % |
|
|
||||||
| `pop_redraw_needed` | 447 | — |
|
|
||||||
| шов / смена уровня / вспышка | 5 448 | 1 % |
|
|
||||||
| `guard_over_kid` + `pop_char_skip_mask` | 4 788 | 1 % |
|
|
||||||
| `pop_char_draw(KID)` | 52 206 | 10 % |
|
|
||||||
| соперник + `loose_mob_draw_over` + `hp_draw` | 4 086 | 1 % |
|
|
||||||
| **`pop_char_fore(KID)`** | **171 693** | **33 %** |
|
|
||||||
| `pop_room_clip_borders` + прочее | 742 | — |
|
|
||||||
| **ИТОГО работа** | **517 609** | 120 % растрового кадра → период **4 кадра** |
|
|
||||||
|
|
||||||
По цветам бордюра: синий (`PROF(2)`) 144 696 (28 %), зелёный (`PROF(4)`)
|
|
||||||
139 398 (27 %), циан (`PROF(6)`) 233 515 (45 % работы = 54 % растрового
|
|
||||||
кадра, начинается на 74 % первого кадра и кончается на 123 %).
|
|
||||||
|
|
||||||
## СДЕЛАНО 2026-08-10: метка «фон трогали» стала маской ТАЙЛОВ
|
|
||||||
|
|
||||||
Было: один union-прямоугольник на страницу. Три факела трогают по пятну
|
|
||||||
16x18 в колонках 1, 6 и 8, а их объединение — полоса `x 40..280` на всю
|
|
||||||
комнату; Кид, стоящий где угодно между крайними факелами, в неё попадал и
|
|
||||||
перерисовывался каждый кадр со всем fore-проходом.
|
|
||||||
|
|
||||||
Стало: `uint16_t pop_cd_dmask[2][3]` — бит на колонку, слово на ряд, набор на
|
|
||||||
страницу. Колонка берётся сдвигом (`x >> 5`), ряд — цепочкой сравнений;
|
|
||||||
проверка в `cd_quiet` — три `AND` через резидентный `pop_cd_hit`.
|
|
||||||
|
|
||||||
Гранулярность тайла — это гранулярность ОРИГИНАЛА: пометки там тоже по
|
|
||||||
тайлам (`redraw_frames_anim[tilepos]`, `set_wipe`), персонажи привязаны к
|
|
||||||
тайлу через `tile_object_redraw[tilepos]`, а единственное подтайловое
|
|
||||||
уточнение (`wipe_heights`) — по высоте, не по ширине. Полутайл (16 px) не дал
|
|
||||||
бы ничего: пламя рисуется с отступом 8 px и шириной 16, то есть занимает
|
|
||||||
середину тайла и задевает обе половины.
|
|
||||||
|
|
||||||
Замер на той же сцене, где снимался профиль ниже (Кид неподвижно на (1,7)):
|
|
||||||
|
|
||||||
| | было | стало |
|
|
||||||
|---|---:|---:|
|
|
||||||
| циан (спрайты + fore) | 233 515 | **24 781** |
|
|
||||||
| работа за кадр | 517 609 | **306 553** |
|
|
||||||
| период | 4 растровых кадра | **3** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## СДЕЛАНО 2026-08-10: деление в луче видимости стража (Кид у шва)
|
|
||||||
|
|
||||||
`tile_at_kid` (`guards.c`) считала колонку честным `/` и `%`, тогда как везде
|
|
||||||
уже стоит резидентная таблица `POP_TILE_DIV` (это и есть `tile_div_tbl`
|
|
||||||
оригинала). У SDCC z80 это `__divsint` плюс `__modsint`, а тот внутри снова
|
|
||||||
зовёт `__divsint` — ~5 400 тактов на вызов.
|
|
||||||
|
|
||||||
Зовут её В ЦИКЛЕ по колонкам между стражем и Кидом
|
|
||||||
(`check_can_guard_see_kid`, seg003:761). Когда Кид стоит У ШВА, его
|
|
||||||
`curr_col = −1`, луч тянется через всю комнату, и за кадр набегало ВОСЕМЬ пар
|
|
||||||
делений — около 43 000 тактов, 10 % растрового кадра, в фазе ЛОГИКИ.
|
|
||||||
|
|
||||||
Замер: брейкпоинт на `__divsint` с печатью адреса возврата дал `ret=C033`
|
|
||||||
восемь раз за кадр; остановка на нём и дизассемблирование с правильным банком
|
|
||||||
показали `HL−65`, `ld de,#14`, `call __divsint` по смещению 0x24 банка 1 —
|
|
||||||
`tile_at_kid`. После фикса пар `C033` не остаётся ни одной.
|
|
||||||
|
|
||||||
**Заодно снята ложная тревога.** В прошлом замере я записал, что на шве
|
|
||||||
пролог `pop_char_fore` разбухает до 134 730 тактов. Это была ошибка зонда:
|
|
||||||
брейкпоинт на `fore_tile` стоял по адресу, который совпадает с кодом ДРУГИХ
|
|
||||||
банков, и в интервал попадали чужие срабатывания. Чистый замер (Кид в
|
|
||||||
колонке 0, спрайт свисает за левый край, окно `x −8..5`): пролог **16 950** —
|
|
||||||
ровно как в середине комнаты, весь fore-проход 41 803, `pop_room_clip_borders`
|
|
||||||
14 364. Урок в «Как мерить» выше: адрес в банке нужно либо проверять на
|
|
||||||
уникальность, либо ловить через трамплин с условием на банк.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 0. Дворцовая кладка в fore-проходе — **СДЕЛАНО 2026-08-10**
|
|
||||||
|
|
||||||
Все три шага выполнены; замер после — в конце пункта. Ниже сохранён исходный
|
|
||||||
разбор: он объясняет, почему предфильтра `fore_tile` мало и откуда взялись
|
|
||||||
габариты кусков.
|
|
||||||
|
|
||||||
**Было: 139 863 такта за кадр (32 % растрового кадра), полезных пикселей —
|
|
||||||
ровно ноль.**
|
|
||||||
|
|
||||||
Замер (уровень 4, Кид на (1,7)). Окно fore-клипа в этот момент —
|
|
||||||
`x 229..241, y 106..147` (прочитано из `pop_t_fclip_*` брейкпоинтом).
|
|
||||||
`pop_fore_over_char` обходит 6 тайлов:
|
|
||||||
|
|
||||||
| тайл | тактов |
|
|
||||||
|---|---:|
|
|
||||||
| (0,6) (0,7) (1,6) (1,7) | 2 800 – 4 600 каждый |
|
|
||||||
| **(2,6) — стена** | **74 310** |
|
|
||||||
| **(2,7) — стена** | **65 553** |
|
|
||||||
|
|
||||||
Ряд 2 этой комнаты — стена, и он лежит ПОД ногами Кида, то есть попадает в
|
|
||||||
его fore-окно всегда. Внутри одного тайла стены `wall_pattern_palace`
|
|
||||||
делает 6 × `wpp_fill` (≈ 3 100 каждый) + 5 × `pop_wall_b` (≈ 9 760 каждый)
|
|
||||||
≈ 60 000 тактов.
|
|
||||||
|
|
||||||
Ни один кусок в окно не попадает:
|
|
||||||
- верхняя заливка стоит на `dmy - 59 = 157`, окно кончается на `y = 147`;
|
|
||||||
все остальные куски ещё ниже;
|
|
||||||
- тайл (2,6) занимает `x 192..223`, окно начинается с `x = 229` — он
|
|
||||||
промахивается и по горизонтали тоже.
|
|
||||||
|
|
||||||
Тайл всё равно проходит, потому что предфильтр в `fore_tile` (pop_bg.c,
|
|
||||||
`x0 < fclip_x1 && x0 + 40 > fclip_x0 && y0 - 8 < fclip_y1 && y0 + 70 >
|
|
||||||
fclip_y0`) намеренно грубый — габарит 40×78 на тайл. Дальше `wpp_fill`
|
|
||||||
честно режет по окну и выходит с пустым прямоугольником, но 3 100 тактов на
|
|
||||||
арифметику клипа уже потрачены, а `pop_wall_b` о существовании окна не знает
|
|
||||||
вовсе: он идёт в `atlas_image` + `gfx_w0_map`, читает `w`/`h` и только там
|
|
||||||
обнаруживает, что рисовать нечего.
|
|
||||||
|
|
||||||
Что делать (по возрастанию объёма):
|
|
||||||
1. **Ранний выход из `wall_pattern_palace`**: самый верхний пиксель узора —
|
|
||||||
`dmy - 59`, самый нижний — `dby + высота нижнего декаля`. Один
|
|
||||||
`if (pop_t_fclip_on && (fclip_y1 <= dmy - 59 || fclip_y0 > dby + h))
|
|
||||||
return;` плюс такая же проверка по `x` убивает оба тайла целиком почти
|
|
||||||
даром.
|
|
||||||
2. Прогнать `pop_wall_b` в этом узоре через ту же проверку окна, что уже
|
|
||||||
есть у `wpp_fill` (нужны размеры кусков — см. п. 2 ниже, «размеры из
|
|
||||||
каталога атласа»).
|
|
||||||
3. Сузить сам предфильтр `fore_tile` до реального габарита узора вместо
|
|
||||||
40×78 — тогда лечится не только дворец.
|
|
||||||
|
|
||||||
Порядок в подземелье тот же, но дешевле: `wall_pattern` в подземелье делает
|
|
||||||
до 3 блитов и ни одной заливки (~29 000 на тайл против ~60 000). Это же
|
|
||||||
объясняет, почему после перехода на дворцовый тайлсет период вырос.
|
|
||||||
|
|
||||||
Сверено с SDLPoP (`seg008.c:1943 wall_pattern`, ветка
|
|
||||||
`!is_dungeon && GRAPHICS_VGA`): состав узора у нас дословный — 5
|
|
||||||
`add_wipetable` + 4 декаля + нижняя заливка + нижний декаль. Расходимся не
|
|
||||||
составом, а тем, что оригинал складывает всё в `foretable` и рисует одним
|
|
||||||
`draw_table()`, у которого «посетить тайл» стоит копейки (см. п. 6).
|
|
||||||
|
|
||||||
### Что сделано и сколько дало
|
|
||||||
|
|
||||||
1. **Ранний выход** из `wall_pattern_palace` по окну fore-клипа (узор целиком
|
|
||||||
в `x [xh*8, xh*8+32)`, `y [dmy−59, dby]`).
|
|
||||||
2. **Отсев каждого декаля** (`wp_blit`) по реальному габариту вместо
|
|
||||||
заведомо большего 64×64 в `pop_blit_b`. Размеры сняты из каталогов
|
|
||||||
атласов: `pal_wall.atl` — группы 3..5 = 8×7, 6..8 и 9..11 = 32×12,
|
|
||||||
12..14 = 30×5, 15..17 = 32×3.
|
|
||||||
3. **То же для ПОДЗЕМЕЛЬЯ**: ранний выход `wall_pattern` (габарит там выше —
|
|
||||||
левая марка уходит на `dby+POP_YOFF−67`) плюс `wp_blit` на RNDBLOCK
|
|
||||||
(32×21), обоих разделителях (9×21) и обеих марках (`pop_wall.atl`:
|
|
||||||
16/17 = 7×10, 14/15 = 14×5).
|
|
||||||
|
|
||||||
Замер: уровень 4, комната 18, Кид сдвигается читом `]` по пикселю (skip
|
|
||||||
выключен, идёт полный путь), в fore-окне ШЕСТЬ тайлов, ТРИ из них —
|
|
||||||
дворцовая стена.
|
|
||||||
|
|
||||||
| участок | тактов |
|
|
||||||
|---|---:|
|
|
||||||
| пролог `pop_char_fore` до первого `fore_tile` | 17 208 |
|
|
||||||
| обычный тайл | 4 000 – 4 600 |
|
|
||||||
| **тайл стены (было 60 000 – 74 000)** | **~12 700** |
|
|
||||||
| хвост + чистка бортов | 6 055 |
|
|
||||||
| **fore-проход целиком (было 171 693)** | **70 681** |
|
|
||||||
|
|
||||||
Кадр целиком в этой сцене: работа **419 839**, период **3** растровых кадра
|
|
||||||
(было 517 609 и 4).
|
|
||||||
|
|
||||||
Остаток в проходе — пролог 17 208, это уже пункт 1 ниже (футпринт из физики).
|
|
||||||
Отдельная находка: когда Кид стоит НА ШВЕ (окно `x −8..5`), пролог разбухает
|
|
||||||
до **134 730** — 86 % прохода; причина не разобрана, см. пункт 1.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Футпринт персонажа — брать из физики, а не считать заново
|
|
||||||
|
|
||||||
**Цена: 11 574 такта на каждый fore-проход** (от входа в `char_footprint` до
|
|
||||||
первого `fore_tile`).
|
|
||||||
|
|
||||||
`redraw_at_char` (seg003:0430) берёт ГОТОВЫЕ `char_col_left/right`,
|
|
||||||
`char_top_row`, `char_bottom_row` — их в этом же кадре посчитала физика
|
|
||||||
(`set_char_collision`, seg006:0723). У нас `char_footprint` (pop_bg.c)
|
|
||||||
считает их заново внутри fore-прохода.
|
|
||||||
|
|
||||||
Мешает то, что физика (банк 3) держит их в статиках, а слой фона — банк 2.
|
|
||||||
Надо опубликовать их так же, как уже опубликованы `pop_cd[who].fpx/fpy/fpw/fph`.
|
|
||||||
|
|
||||||
**Заодно:** оригинал расширяет футпринт ТОЛЬКО на одну колонку при вынутом
|
|
||||||
мече и объединяет с футпринтом ПРОШЛОГО кадра (`prev_char_col_left/right`).
|
|
||||||
Мы вместо этого расширяем окном fore-клипа и посещаем 6 тайлов там, где
|
|
||||||
оригинал посетил бы 4. Разница видна в замере: fore-проход стоит 58 764
|
|
||||||
там, где реально рисует, и **112 758 там, где не рисует ничего** — вся
|
|
||||||
разница в числе посещённых тайлов.
|
|
||||||
|
|
||||||
Осторожно: окно клипа заводилось под клинок и брызги (они уходят
|
|
||||||
вперёд-вверх за габарит кадра). Менять — с прогоном боя и падений.
|
|
||||||
|
|
||||||
## 2. Размеры ленты — из каталога атласа, а не через окно 0
|
|
||||||
|
|
||||||
**Цена: ~750 тактов на `atlas_image` + часть из 6 396 на «чтение w/h и
|
|
||||||
арифметика клипа», на КАЖДЫЙ блит фона.**
|
|
||||||
|
|
||||||
Сейчас `pop_blit_b`, чтобы узнать размер куска, зовёт `atlas_image` (тот
|
|
||||||
мапит страницу в W3, читает запись каталога, возвращает W3 назад), потом
|
|
||||||
`gfx_w0_map` и читает `w`/`h` из шапки ленты.
|
|
||||||
|
|
||||||
А размеры **уже лежат в каталоге**: запись 8 байт — `offset u16, fw u8,
|
|
||||||
fh u8, nx u8, ny u8, резерв u16`, и у всех фоновых лент `nx = ny = 1`, то
|
|
||||||
есть `fw`/`fh` в точности равны `w`/`h` из шапки (проверено по
|
|
||||||
`pop_env0.atl`). `atlas_image` их читает и выбрасывает.
|
|
||||||
|
|
||||||
Вариант A (0 байт памяти): `atlas_image_wh()` рядом с `atlas_image` —
|
|
||||||
вернуть заодно размер.
|
|
||||||
Вариант B (без маппинга вовсе): снять каталоги при загрузке в резидентную
|
|
||||||
таблицу. Объём: фон (env0-4 + wall + fore + pot) = **313 лент**, по 2 байта
|
|
||||||
= **626 Б**; всё вместе с Кидом и стражем = 600 лент = 1200 Б. Свободной
|
|
||||||
кучи на 2026-08-10 — 2873 Б.
|
|
||||||
|
|
||||||
Ожидаемый выигрыш скромный: ~2 000–3 000 из ~16 000 накладных на блит.
|
|
||||||
|
|
||||||
## 3. Один `gfx_w0_map`/`unmap` на группу блитов
|
|
||||||
|
|
||||||
**Цена: ~5 500 тактов на блит** (unmap плюс возвраты по цепочке
|
|
||||||
`pop_pot_b` → `pop_blit_b` → трамплин).
|
|
||||||
|
|
||||||
Куски одного прохода часто лежат на одной странице атласа, а мапим и
|
|
||||||
размапливаем на каждый. Мешает то, что `pop_blit_b` — общий лист для всех
|
|
||||||
вызывающих; нужна форма «открыть страницу, N блитов, закрыть».
|
|
||||||
|
|
||||||
## 4. Единый проход по тайлам вместо трёх
|
|
||||||
|
|
||||||
У оригинала за кадр ОДИН обход тайлов — `redraw_needed_tiles` (seg008):
|
|
||||||
контекст тайла (`curr_tile`, `curr_modifier`, `draw_xh`, `draw_main_y`)
|
|
||||||
ставится по разу на тайл в `load_curr_and_left_tile`, а `redraw_needed`
|
|
||||||
смотрит **семь** независимых счётчиков (`wipe_frames`, `redraw_frames_full`,
|
|
||||||
`redraw_frames_anim`, `redraw_frames2`, `redraw_frames_floor_overlay`,
|
|
||||||
`redraw_frames_fore`, `tile_object_redraw`) и делает только помеченное.
|
|
||||||
|
|
||||||
У нас **три** обхода: `pop_redraw_needed`, `pop_process_trobs` и
|
|
||||||
`pop_fore_over_char`. Плюс один `kind` на тайл вместо семи счётчиков — две
|
|
||||||
разные причины перерисовки одного тайла конфликтуют.
|
|
||||||
|
|
||||||
Это большой рефакторинг всего слоя фона; браться только если понадобится
|
|
||||||
ещё заметный запас.
|
|
||||||
|
|
||||||
## 5. objtable: персонажи, привязанные к тайлу
|
|
||||||
|
|
||||||
Оригинал кладёт персонажей в `objtable` и рисует их в
|
|
||||||
`draw_objtable_items_at_tile(tilepos)` во время обхода тайлов — порядок
|
|
||||||
окклюзии получается сам. У нас отдельный fore-проход НА КАЖДОГО персонажа.
|
|
||||||
Со вторым персонажем (страж) цена удваивается.
|
|
||||||
|
|
||||||
## 6. Отложенные таблицы back/mid/fore
|
|
||||||
|
|
||||||
`add_backtable`/`add_midtable`/`add_foretable` только КЛАДУТ запись в массив,
|
|
||||||
рисование — один `draw_table()` в конце. Поэтому «посетить тайл» у
|
|
||||||
оригинала стоит копейки. У нас блит идёт сразу из обхода.
|
|
||||||
|
|
||||||
## 7. Мелочи с известной ценой
|
|
||||||
|
|
||||||
| что | цена | где |
|
|
||||||
|---|---|---|
|
|
||||||
| `pop_clip_char_top` — трамплин банк 4 → банк 3 ради одной проверки тайла над головой | 8 892 | `pop_cdraw.c` / `pop_map.c` |
|
|
||||||
| `pop_loose_tick` при полном отсутствии падающих плит в комнате | 27 438 | `pop_map.c` |
|
|
||||||
| `obj_x * 8 / 7` — единственное оставшееся `__divsint` в горячем пути | ~2 400 | `pop_char_draw` |
|
|
||||||
| `cd_sig_make` + возврат из `pop_char_draw` | 7 944 | `pop_cdraw.c` |
|
|
||||||
| `pop_loadkid` + расчёт координат кадра | 7 410 | `pop_cdraw.c` |
|
|
||||||
|
|
||||||
## Что уже проверено и НЕ сработало
|
|
||||||
|
|
||||||
- **Маска «у тайла есть передний слой» (`FORE_ANY`) + контекст тайла один
|
|
||||||
раз.** Сделано (коммит `a9f4521`), эффект **нулевой**: в футпринте Кида
|
|
||||||
тайлы почти всегда С передним слоем, а снятое второе чтение кода съедено
|
|
||||||
проверкой маски. Оставлено как сближение с оригиналом.
|
|
||||||
- **«Быстрый путь для окна коллизии целиком внутри комнаты».** Не
|
|
||||||
срабатывал почти никогда: Кид в колонке 0 даёт окно с −1. Заменён на
|
|
||||||
разбиение окна на непрерывные пробеги.
|
|
||||||
- **Флаг «фон трогали» вместо позиционной метки** — нулевой выигрыш,
|
|
||||||
факелы гасили пропуск для всех сразу (см. `pop_cdraw.h`).
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
# ЦИАН фаза (персонажи + передний слой) — анализ и оптимизация
|
|
||||||
|
|
||||||
Рабочий документ: живёт между сессиями. Внизу **журнал правок** — каждая
|
|
||||||
запись с замером до/после. Сцена, рецепт воспроизведения и зонды —
|
|
||||||
[`perf_l13_room23.md`](perf_l13_room23.md); там же ответ про 8-битные
|
|
||||||
габариты. Парная фаза — [`perf_green_phase.md`](perf_green_phase.md).
|
|
||||||
|
|
||||||
Границы фазы в `roomtest.c`: от `PROF(6)` (строка 620) до `PROF(0)`
|
|
||||||
(строка 677). Содержимое: `pop_check_mirror`, `pop_loose_mob_draw`,
|
|
||||||
соперник, `pop_char_draw(KID)`, `pop_loose_mob_draw_over`, `pop_fore_needed`,
|
|
||||||
`pop_hp_draw`, `pop_char_fore(KID)`, `pop_cd_clear`,
|
|
||||||
`pop_room_clip_borders`.
|
|
||||||
|
|
||||||
**Бюджет: ≤ 400 000 тактов (растровый кадр = 430 000).**
|
|
||||||
|
|
||||||
| состояние | было (2026-08-17, `c312e4a`) | стало (`af189a1`) |
|
|
||||||
|---|---:|---:|
|
|
||||||
| покой, Кид пропущен | 20 550 | 20 550 |
|
|
||||||
| Кид перерисовывается, кусков нет | 193 000 | 192 000 |
|
|
||||||
| **пик каскада (6 кусков + Кид)** | **631 800** | **270 510 ✔** |
|
|
||||||
|
|
||||||
**ЦЕЛЬ ФАЗЫ ВЫПОЛНЕНА** (270 510 при бюджете 400 000, запас 32 %).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Что решило дело
|
|
||||||
|
|
||||||
### C1. Пометки «фон трогали» в `mob_render` подавлены — −55 000
|
|
||||||
|
|
||||||
`pop_loose_mob_tick` помечает **весь коридор** куска одним вызовом, а три
|
|
||||||
блита внутри `mob_render` метили подмножества того же прямоугольника по
|
|
||||||
**4 502 такта** каждый. Механизм — `pop_cd_mute()`/`pop_cd_unmute()`
|
|
||||||
(в `pop_tile.c`; отдельное значение того же флага `pop_cd_batch`, чтобы у
|
|
||||||
`pop_cd_touch` на общем пути осталась ОДНА проверка).
|
|
||||||
|
|
||||||
Добавлена пометка в `mob_spawn_copy`: кусок, рождённый ВНУТРИ тика
|
|
||||||
(`loose_fall` сбил плиту), получает слот с начала таблицы, то есть уже
|
|
||||||
пройденный циклом, — своей пометки в этом кадре он бы не получил, а нарисован
|
|
||||||
был бы. Без этого пропущенная пометка = стёртый и не перерисованный
|
|
||||||
персонаж.
|
|
||||||
|
|
||||||
### C4. Кусок клипуется САМ, вместо чистки бортов после — −138 000
|
|
||||||
|
|
||||||
Самая крупная и самая неожиданная статья. В `mob_render` стоял
|
|
||||||
`pop_clip_sprite`, то есть кусок рисовался в борт целиком и взводил
|
|
||||||
`border_dirty`; `pop_room_clip_borders` потом стирал ДВЕ полосы во всю ширину
|
|
||||||
экрана (320×28 и 320×28) — **150 978 тактов в КАЖДОМ кадре**, пока хоть один
|
|
||||||
кусок торчит выше поля. А гряда 13-го уровня рождается ровно у потолка
|
|
||||||
(`y = 2`), то есть почти весь каскад. Стало 1 722.
|
|
||||||
|
|
||||||
Теперь окно клипа (`pop_t_win_set(0, POP_YOFF, 320, POP_PLAYFIELD_H)`)
|
|
||||||
ставится ТОЛЬКО когда кусок реально задевает борт: внутри поля блиты идут
|
|
||||||
быстрым путём.
|
|
||||||
|
|
||||||
### Композит куска: три блита → один — −163 000
|
|
||||||
|
|
||||||
Части `env 70 / 74 / 72` складываются в ОДИН getimage-блоб при загрузке
|
|
||||||
тайлсета (`mob_spr_build` в `pop_room.c`). Мотив прямо из
|
|
||||||
[[blit_cost_model]]: у блита ~8 800 такта постоянных накладных против ~5 000
|
|
||||||
на пиксели, а шесть кусков в воздухе давали 18 вызовов = **258 708 такта**,
|
|
||||||
больше половины фазы.
|
|
||||||
|
|
||||||
Тонкости, которые пришлось соблюсти:
|
|
||||||
|
|
||||||
- части **перекрываются** (74 и 70 обе идут от `mob_x`), поэтому композит
|
|
||||||
собирается попиксельно с пропуском `0xFF` — ровно как три прозрачных блита
|
|
||||||
друг поверх друга;
|
|
||||||
- габариты частей **читаются**, а не берутся константами: у тайлсетов правая
|
|
||||||
часть разная (26 px в подземелье, 25 во дворце);
|
|
||||||
- блоб лежит в обычной памяти (W2), поэтому блит идёт мимо `atlas_image` и
|
|
||||||
`gfx_w0_map/unmap` — ещё ~1 350 такта на вызов. Новый резидентный лист
|
|
||||||
`pop_mem_b` (`pop_tile.c`);
|
|
||||||
- страйд блоба = его ширина; сначала считается точный габарит, потом
|
|
||||||
копирование. Промежуточная версия объявляла блоб шириной 63 при
|
|
||||||
фактических 58 и переносила пять прозрачных колонок на каждом кадре;
|
|
||||||
- собирается на КАЖДУЮ смену тайлсета; резервный путь на три блита остался
|
|
||||||
(`mob_spr_ok`).
|
|
||||||
|
|
||||||
### Общие правки, попавшие и в эту фазу
|
|
||||||
|
|
||||||
- `blit_b_clip`: байтовый габарит + file-scope вместо локалей (кадр 22 → 12 Б,
|
|
||||||
обращений `(ix)` 211 → 51). Подробности — в
|
|
||||||
[`perf_green_phase.md`](perf_green_phase.md) §G3.
|
|
||||||
- `pop_blit_b`: аргументы в file-scope (76 → 11 обращений `(ix)`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Раскладка на 2026-08-17 (до правок) — для истории
|
|
||||||
|
|
||||||
Подфазы (три готовых `PROF(6)`: 0x4BCE / 0x4C26 / 0x4C93), пик:
|
|
||||||
|
|
||||||
| участок | покой | пик |
|
|
||||||
|---|---:|---:|
|
|
||||||
| `pop_check_mirror` + `pop_loose_mob_draw` + соперник | 23 250 | 154 512 |
|
|
||||||
| `pop_char_draw(KID)` + `pop_loose_mob_draw_over` + `pop_fore_needed` + HP | 3 726 | 445 284 |
|
|
||||||
| `pop_char_fore(KID)` + `pop_cd_clear` + **чистка бортов** | 1 722 | 150 978 |
|
|
||||||
|
|
||||||
Разбор одного `pop_blit_b` (157 замеров быстрого пути, зонды b1..b5):
|
|
||||||
|
|
||||||
| участок | такты | доля |
|
|
||||||
|---|---:|---:|
|
|
||||||
| `atlas_image` + `gfx_w0_map` + чтение габарита | 1 086 | 7 % |
|
|
||||||
| ядро блита (libbgi, `gfx_blit_noclip`) | 10 422 | 64 % |
|
|
||||||
| `pop_cd_touch` — пометка «фон тронут» | 4 502 | 28 % |
|
|
||||||
| `gfx_w0_unmap` + возврат | 264 | 2 % |
|
|
||||||
| ИТОГО | 16 273 | |
|
|
||||||
|
|
||||||
Клипованный путь тогда же: ядро `blit_b_clip` 14 088, итого 16 409.
|
|
||||||
После правок: клипованный блит 11 848, быстрый ~13 900 (у него больше
|
|
||||||
пикселей).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Что осталось в запасе (если понадобится ещё)
|
|
||||||
|
|
||||||
Фаза в бюджете, поэтому это задел, а не план.
|
|
||||||
|
|
||||||
### C5. Один `gfx_w0_map`/`unmap` на группу блитов
|
|
||||||
|
|
||||||
1 086 + 264 на вызов. Для композита куска уже не нужно (он в обычной
|
|
||||||
памяти), но остаётся для тайлов фона: куски одного тайла часто лежат в одной
|
|
||||||
странице атласа. Мешает то, что `pop_blit_b` — общий лист для всех
|
|
||||||
вызывающих; нужна форма «открыть страницу, N блитов, закрыть».
|
|
||||||
|
|
||||||
### C6. Размеры ленты — из каталога атласа
|
|
||||||
|
|
||||||
`fw`/`fh` уже лежат в записи каталога (8 байт: `offset u16, fw u8, fh u8,
|
|
||||||
nx u8, ny u8, резерв u16`), и у всех фоновых лент `nx = ny = 1`, то есть они
|
|
||||||
равны `w`/`h` из шапки. `atlas_image` их читает и выбрасывает.
|
|
||||||
|
|
||||||
### C7. objtable вместо отдельного fore-прохода на персонажа
|
|
||||||
|
|
||||||
Позиции 5 и 6 старого `perf_backlog.md` — большой рефакторинг. Оригинал
|
|
||||||
кладёт персонажей и куски в `objtable` и рисует их при обходе тайлов
|
|
||||||
(`draw_objtable_items_at_tile`), порядок окклюзии получается сам; у нас
|
|
||||||
отдельный fore-проход НА КАЖДОГО персонажа.
|
|
||||||
|
|
||||||
### Снять временную оснастку
|
|
||||||
|
|
||||||
Шесть вызовов `pop_dbg_b1..b6` внутри `pop_blit_b` — ~400 такта на блит
|
|
||||||
(`call` + `ret` × 6), плюс `pop_dbg_kind`/`m16` в `pop_redraw_needed`.
|
|
||||||
Снимать ПОСЛЕ того, как оптимизация закончена: без них не мерить.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Что НЕ делать
|
|
||||||
|
|
||||||
- **Не ставить W3-скобку из кода с `--w3`** — белый экран
|
|
||||||
(memory `gfx_blit_noclip_fast`).
|
|
||||||
- **Не ускорять передачу пикселей** — предел железа
|
|
||||||
(memory `blit_cost_model`).
|
|
||||||
- **Не возвращать клип куска по `clip.right = 40`** оригинала
|
|
||||||
(`add_mob_to_objtable`, seg007:1161): единицы этого поля не выяснены,
|
|
||||||
буквальные 40 экранных пикселей срезают правый задний угол плиты
|
|
||||||
(прогон 2026-08-13).
|
|
||||||
- **Не сужать коридор heal «по палаццовому следу»** — габариты частей у
|
|
||||||
тайлсетов разные; брать высоту СОБРАННОГО композита (так и сделано).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Журнал правок
|
|
||||||
|
|
||||||
| дата | что сделано | циан: покой / Кид / пик | коммит |
|
|
||||||
|---|---|---|---|
|
|
||||||
| 2026-08-17 | базовый замер | 20 550 / 193 000 / **631 800** | `c312e4a` |
|
|
||||||
| 2026-08-17 | C1 подавление пометок в `mob_render` + пометка в `mob_spawn_copy` | — / — / **577 050** | `a3c473d` |
|
|
||||||
| 2026-08-17 | C4 кусок клипуется сам вместо чистки бортов; `blit_b_clip` байты+file-scope | — / — / **438 546** | `a3c473d` |
|
|
||||||
| 2026-08-17 | `pop_blit_b` аргументы в file-scope | — / — / **435 180** | `b2da0b8` |
|
|
||||||
| 2026-08-17 | **композит куска: один блит вместо трёх** | — / — / **273 078** | `b2da0b8` |
|
|
||||||
| 2026-08-17 | точный габарит композита (было 63 при 58) | 20 550 / 192 000 / **270 510 ✔** | `af189a1` |
|
|
||||||
| 2026-08-17 | замер после фиксов уровня 1 | — / — / **379 482** | `40f0d46` |
|
|
||||||
| 2026-08-17 | **регресс после фиксов уровня 2** — без изменений | — / — / **379 488 ✔** | `ec1f384` |
|
|
||||||
@@ -1,327 +0,0 @@
|
|||||||
# ЗЕЛЁНАЯ фаза (слой фона) — анализ и оптимизация
|
|
||||||
|
|
||||||
Рабочий документ: живёт между сессиями. Внизу **журнал правок** — каждая
|
|
||||||
запись с замером до/после. Сцена, рецепт воспроизведения и зонды —
|
|
||||||
[`perf_l13_room23.md`](perf_l13_room23.md); там же ответ про 8-битные
|
|
||||||
габариты. Парная фаза — [`perf_cyan_phase.md`](perf_cyan_phase.md) (её цель
|
|
||||||
достигнута).
|
|
||||||
|
|
||||||
Границы фазы в `roomtest.c`: от `PROF(4)` (строка 450) до `PROF(6)`
|
|
||||||
(строка 620). Содержимое: `pop_loose_tick`, `pop_process_trobs`,
|
|
||||||
`pop_redraw_needed`, шов, смена уровня, сигналы провалов, вспышка.
|
|
||||||
|
|
||||||
**Бюджет: ≤ 400 000 тактов (растровый кадр = 430 000).**
|
|
||||||
|
|
||||||
| состояние | было (`c312e4a`) | стало (`af189a1`) |
|
|
||||||
|---|---:|---:|
|
|
||||||
| покой в комнате 23 | 35 760 | 35 760 |
|
|
||||||
| дрожат 6 плит-потолков | 366 000 | 335 400 |
|
|
||||||
| **пик каскада** | **805 000** | **546 900** |
|
|
||||||
|
|
||||||
**ЦЕЛЬ ФАЗЫ НЕ ДОСТИГНУТА: 546 900 против 400 000 (1,37×).** Что осталось
|
|
||||||
сделать и почему это именно раскол `draw_tile` — §3.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Раскладка ПОСЛЕ правок (замер `af189a1`)
|
|
||||||
|
|
||||||
Пик — кадры 24-28 (посадки плит), больше не кадры провалов.
|
|
||||||
|
|
||||||
| участок | покой | дрожь | пик |
|
|
||||||
|---|---:|---:|---:|
|
|
||||||
| `pop_loose_tick` | 36 234 | 36 234 | **156 762** |
|
|
||||||
| `pop_process_trobs` | 1 050 | 1 050 | 1 050 |
|
|
||||||
| **`pop_redraw_needed`** | 924 | 288 800 | **380 568** |
|
|
||||||
| хвост (шов, смена уровня, сигналы, вспышка) | 9 336 | 9 336 | 10 776 |
|
|
||||||
|
|
||||||
`pop_loose_tick` изнутри на пике: два цикла по тайлам 9 852,
|
|
||||||
**`pop_loose_mob_tick` 154 074** (heal шести летящих кусков), остальное мелочь.
|
|
||||||
|
|
||||||
### Цена одной перерисовки: было → стало
|
|
||||||
|
|
||||||
| вид | было | стало | чем |
|
|
||||||
|---|---:|---:|---|
|
|
||||||
| `RDA_CEIL` — дрожащая плита-потолок | 48 785 | **43 536** | G2 + G3 |
|
|
||||||
| `RDA_CEIL_GONE` — запечь колодец | 251 335 | **138 318** | **G1** (клип полосы) + G2 + G3 |
|
|
||||||
| `RD_FLOOR` — щебень на месте посадки | 198 805 | **179 914** | G2 + G3 + снятая двойная пометка |
|
|
||||||
|
|
||||||
Штук за кадр: `RDA_CEIL` до 6, `RDA_CEIL_GONE` до 2, `RD_FLOOR` до 2.
|
|
||||||
|
|
||||||
### Детальный профиль `RD_FLOOR` (179 914) — главная оставшаяся статья
|
|
||||||
|
|
||||||
Снят зондами по каждому блиту (`pop_dbg_b1/b5`) и по рамкам
|
|
||||||
(`pop_bar_black`, `pop_cd_batch_begin/end`):
|
|
||||||
|
|
||||||
| участок | такты |
|
|
||||||
|---|---:|
|
|
||||||
| вход `pop_floor_bake` + `gfx_set_bank` | 2 382 |
|
|
||||||
| `pop_bar_black` 60×39 (включая `pop_cd_touch` 4 502) | ~15 500 |
|
|
||||||
| **контекст `draw_tile` #1** (5 чтений тайлов, `63*row`, индексация таблицы) | **13 584** |
|
|
||||||
| 4 блита тайла #1 | 50 718 |
|
|
||||||
| **диспетчер между блитами #1** (все `if (code == …)`) | **11 388** |
|
|
||||||
| **контекст `draw_tile` #2** | **13 584** |
|
|
||||||
| 4 блита тайла #2 | ~54 000 |
|
|
||||||
| **диспетчер между блитами #2** | **11 388** |
|
|
||||||
| хвост | 6 474 |
|
|
||||||
|
|
||||||
Итого: **105 500 — сами блиты (реальные пиксели), 74 400 — накладные**, из
|
|
||||||
которых 27 168 контекст двух `draw_tile` и 22 776 их диспетчер.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Что сделано (с чем сравнивать)
|
|
||||||
|
|
||||||
### G1. Окно клипа для точечной перерисовки — −90 000
|
|
||||||
|
|
||||||
`pop_t_win_set(x, ytop, w, h)` / `pop_t_win_clear()` в `pop_tile.c`: ставит
|
|
||||||
уже существующее окно `pop_t_fclip_*` на прямоугольник, который перерисовка
|
|
||||||
восстанавливает. Работает в обе стороны — и предфильтр `pop_blit_b`
|
|
||||||
отсеивает куски мимо окна ДО `atlas_image`/`gfx_w0_map`, и `blit_b_clip`
|
|
||||||
режет остальные по нему.
|
|
||||||
|
|
||||||
Где сработало: `pop_ceil_bake_empty` — куски ряда 0 высотой 63 px рисовались
|
|
||||||
целиком, хотя восстановить надо девять строк полосы. **Блит 21 447 → 7 619**,
|
|
||||||
вся перерисовка 251 335 → 138 318.
|
|
||||||
|
|
||||||
Где НЕ сработало — см. §4, отрицательные результаты.
|
|
||||||
|
|
||||||
Побочно: пока окно стоит, `pop_blit_b` не ставит пометку «фон трогали»
|
|
||||||
(признак fore-прохода), поэтому вызывающий обязан пометить прямоугольник сам.
|
|
||||||
В `pop_ceil_shake_draw` добавлен явный `pop_cd_touch` на область heal'а; в
|
|
||||||
`pop_ceil_bake_empty` и `pop_floor_bake` метит `pop_bar_black`, а лишний
|
|
||||||
второй вызов на ту же область снят.
|
|
||||||
|
|
||||||
### G2. Контекст тайла — file-scope, а не локали `draw_tile` — −55 000
|
|
||||||
|
|
||||||
Порт `load_curr_and_left_tile` (seg008:0339): у оригинала это
|
|
||||||
`curr_tile`/`curr_modifier`/`draw_xh`/`draw_main_y`/`draw_bottom_y` —
|
|
||||||
переменные модуля, а не локали.
|
|
||||||
|
|
||||||
Причина в кодогене: в `draw_tile` **57 вызовов**, и каждое живое через вызов
|
|
||||||
значение SDCC спиливал в стековый кадр — 26 байт кадра и **513 обращений
|
|
||||||
`-N(ix)`** (при ~46 замеренных тактах на обращение это ~23 600, что и
|
|
||||||
намерено). Стало **33 обращения**, кадра нет, банк 7 −703 Б.
|
|
||||||
|
|
||||||
### G3. `blit_b_clip` — байтовый габарит + file-scope
|
|
||||||
|
|
||||||
Два шага, и важен порядок наблюдений:
|
|
||||||
|
|
||||||
1. **Байтового габарита ОДНОГО НЕ ХВАТИЛО.** `sx/sy/dw/dh` → `uint8_t`
|
|
||||||
(корректно: кадры атласов ≤ 56×63) дало 211 → 173 обращения, а
|
|
||||||
22-байтовый кадр остался: значений, живых через шесть вызовов ядер libbgi,
|
|
||||||
всё равно больше, чем регистров у Z80.
|
|
||||||
2. **Решило вынесение из локалей** (`bc_*`): 51 обращение, кадр 22 → 12 Б.
|
|
||||||
|
|
||||||
Клипованный блит 14 088 → 11 848. Заодно `blit_b_oversize` больше не ходит
|
|
||||||
через `blit_b_clip` (там теперь байтовый габарит) — рисует напрямую
|
|
||||||
`gfx_blit_part`; это путь под полноэкранные подложки интро/финала.
|
|
||||||
|
|
||||||
### G4. Мелочи
|
|
||||||
|
|
||||||
- `pop_blit_b`: аргументы в file-scope (третий и дальше SDCC передаёт стеком,
|
|
||||||
каждое чтение шло через `-N(ix)`) — 76 → 11 обращений.
|
|
||||||
- `pop_loose_mob_tick`: пометки всех кусков ОДНИМ пакетом
|
|
||||||
(`pop_cd_batch_begin/end`) — было по 4 502 такта на кусок.
|
|
||||||
176 772 → 168 600.
|
|
||||||
- Коридор heal куска — по фактической высоте СОБРАННОГО композита (было 24
|
|
||||||
строки константой, стало 20). 168 600 → 156 762.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Что осталось: раскол `draw_tile` (позиция G5)
|
|
||||||
|
|
||||||
**Оставшийся разрыв: −147 000.** Он весь в двух местах.
|
|
||||||
|
|
||||||
### G5. Расколоть `draw_tile` на узкие части, как в оригинале
|
|
||||||
|
|
||||||
**Ожидание: −50 000 … −60 000.**
|
|
||||||
|
|
||||||
У оригинала `draw_tile` (seg008:01C7) — это девять независимых вызовов:
|
|
||||||
`draw_tile_floorright`, `draw_tile_anim_topright`, `draw_tile_right`,
|
|
||||||
`draw_tile_anim_right`, `draw_tile_bottom`, `draw_loose`, `draw_tile_base`,
|
|
||||||
`draw_tile_anim`, `draw_tile_fore`. Для ряда −1 он зовёт шесть из них
|
|
||||||
(`draw_tile_aboveroom`, seg008:01F2), для полосы у потолка — те же шесть плюс
|
|
||||||
`draw_tile_wipe(3)` (`redraw_needed_above`, seg008:02C1).
|
|
||||||
|
|
||||||
У нас всё это — ветки `if (row >= 0)` ВНУТРИ одной функции, то есть контекст
|
|
||||||
(13 584) и диспетчер (11 388) оплачиваются целиком всегда. Расколов, каждая
|
|
||||||
точечная перерисовка сможет звать только нужные части:
|
|
||||||
|
|
||||||
- `pop_floor_bake`: вместо второго полного `draw_tile(row, col+1)` — только
|
|
||||||
его правую грань и базу;
|
|
||||||
- `pop_ceil_shake_draw` / `pop_ceil_bake_empty`: дословный
|
|
||||||
`draw_tile_aboveroom`;
|
|
||||||
- `pop_loose_shake_draw`, `pop_spike_redraw`, `pop_gate_redraw` — то же.
|
|
||||||
|
|
||||||
Риск средний: у `draw_tile` собрано много инвариантов (BUG-LOOSE-3,
|
|
||||||
BUG-LATTICE-DOORTOP, BUG-SEAM-WEDGE-1), проверять придётся прогонами всех
|
|
||||||
уровней. Поэтому делать отдельным заходом, а не хвостом другой правки.
|
|
||||||
|
|
||||||
### G6. Меньше блитов в `RD_FLOOR`
|
|
||||||
|
|
||||||
**Ожидание: неизвестно, надо мерить.** 105 500 из 179 914 — это 7,6 блита,
|
|
||||||
и они рисуют настоящие пиксели. Сократить можно только сократив то, что
|
|
||||||
восстанавливается: бар сейчас 60×39 от `yb+26`, а плита занимает по вертикали
|
|
||||||
меньше (её куски: левая грань `POP_LOOSE_FRAM_LEFT` 32×13 на `dmy = yb+62`,
|
|
||||||
низ `POP_LOOSE_FRAM_BOTTOM` 32×3 на `dby = yb+65`, правая грань в соседе
|
|
||||||
26×16 на `dby−1`). То есть плита живёт в `yb+47 .. yb+65`, а бар начинается с
|
|
||||||
`yb+26` — **21 лишняя строка сверху**.
|
|
||||||
|
|
||||||
Проверять осторожно: бар заодно стирает и то, что рисует ДРУГИЕ куски тайла
|
|
||||||
(орнаментная лента `stripe_id` на `dmy−27 = yb+35` попадает как раз в
|
|
||||||
«лишнюю» часть). Сузишь бар — надо убедиться, что ничего не осталось.
|
|
||||||
|
|
||||||
### G7. heal летящих кусков — 154 074 (28 % фазы)
|
|
||||||
|
|
||||||
Шесть кусков × ~25 700: сам heal 64×20 (по модели ~19 700) + пакетная
|
|
||||||
пометка + накладные `mob_tick_one` (16-байтовый кадр, 99 обращений `(ix)`).
|
|
||||||
**Сам heal у предела железа** — это 1 280 пикселей на кусок, оптимизировать
|
|
||||||
нечего, кроме площади. Площадь уже подрезана до габарита композита.
|
|
||||||
|
|
||||||
Остаётся `mob_tick_one` (~4 500 на кусок = 27 000 на кадр) — то же лечение
|
|
||||||
file-scope, что у `draw_tile`.
|
|
||||||
|
|
||||||
### G8. Пометка соседа — узкой полосой, а не полным тайлом (идея пользователя)
|
|
||||||
|
|
||||||
**Ожидание: заметное, но не мерено. Взять ПОСЛЕ обхода всех уровней**
|
|
||||||
(решение пользователя 2026-08-17: пока идёт отлов багов слоёв, каждая правка
|
|
||||||
добавляет переменных в картину).
|
|
||||||
|
|
||||||
Когда плита (1,8) падает, помечаются ДВА тайла:
|
|
||||||
|
|
||||||
| пометка | что делает |
|
|
||||||
|---|---|
|
|
||||||
| `(1,8)` → `RD_LOOSE_GONE` | бар 40 на своём x, бар 32 на соседе, `draw_tile(1,8)` + `draw_tile(1,9)` |
|
|
||||||
| `(1,9)` → `RD_FLOOR` | бар **60** на x соседа, `draw_tile(1,9)` ЕЩЁ РАЗ |
|
|
||||||
|
|
||||||
То есть сосед перезапекается ЦЕЛИКОМ и ПОВТОРНО, хотя потревожили у него
|
|
||||||
только левые 28 пикселей — там, куда свисает правая грань упавшего тайла.
|
|
||||||
`draw_tile(1,9)` при этом вызывается дважды на одну пометку.
|
|
||||||
|
|
||||||
Что такое эти числа (чтобы не сузить лишнего):
|
|
||||||
|
|
||||||
- **60 = 32 свой тайл + 28 СОБСТВЕННЫЙ свес.** Правая грань пола (кадр 42,
|
|
||||||
26 px) рисуется в клетке соседа с `x+32`, занимая `x+32..x+57`. Для запечки
|
|
||||||
САМОГО тайла 60 уже минимальны — сужать их нельзя;
|
|
||||||
- сузить можно только тот случай, когда тайл помечен ПОТОМУ ЧТО ИЗМЕНИЛСЯ ЕГО
|
|
||||||
ЛЕВЫЙ СОСЕД: тогда нужна полоса 28 px у левого края, а не весь тайл.
|
|
||||||
|
|
||||||
Почему выигрыш не символический: `pop_floor_bake` стоит **179 914** тактов,
|
|
||||||
из них 105 500 — сами блиты. Узкая полоса срезала бы и площадь бара
|
|
||||||
(60×39 → 28×39), и часть блитов — окно клипа там теперь стоит обязательным
|
|
||||||
(см. §4), так что отсев достаётся даром.
|
|
||||||
|
|
||||||
**Условия, из-за которых это не «просто уменьшить число»:**
|
|
||||||
|
|
||||||
1. `pop_floor_bake` — ОБЩАЯ функция: её же зовут кнопка (`pop_button_redraw`),
|
|
||||||
зеркало, подобранный предмет и щебень на месте посадки. Там меняется сам
|
|
||||||
тайл и 60 нужны целиком. Значит нужен отдельный вход (напр.
|
|
||||||
`pop_floor_bake_edge(row, col)`) или параметр-прямоугольник — именно под
|
|
||||||
пометку «изменился мой левый сосед».
|
|
||||||
2. Прежде чем выкидывать вторую пометку целиком, сверить ВЕРТИКАЛЬНЫЕ
|
|
||||||
диапазоны: `pop_loose_bake_empty` кроет `63*row+46 .. +65` (20 строк), а
|
|
||||||
`pop_floor_bake` — `yb+26 .. yb+64` (39 строк). То есть сосед покрыт
|
|
||||||
ВТОРЫМ баром не полностью, и просто снять пометку нельзя.
|
|
||||||
3. Ширина полосы = свес ЛЕВОГО тайла, а он зависит от типа тайла (у loose это
|
|
||||||
8 px по комментарию в `pop_loose_bake_empty`, у пола 26). Брать по
|
|
||||||
максимуму (28) — безопасно.
|
|
||||||
|
|
||||||
### G9. Снять временную оснастку
|
|
||||||
|
|
||||||
Шесть `pop_dbg_b1..b6` внутри `pop_blit_b` — ~400 такта на блит; при 15
|
|
||||||
блитах зелёной это 6 000 на кадр. Плюс `pop_dbg_kind`/`m16` (2 вызова на
|
|
||||||
перерисовку) и `pop_dbg_m5..m15`. Снимать ПОСЛЕ окончания оптимизации: без
|
|
||||||
них не мерить.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Копия второй страницы — и почему она ТРЕБУЕТ окна клипа
|
|
||||||
|
|
||||||
Точечные запечки ставятся с `pages = 2`, срабатывают два кадра подряд (по разу
|
|
||||||
на страницу дабл-буфера) и оба раза считают одно и то же. После первого раза
|
|
||||||
нужный прямоугольник уже лежит в ОЗУ-копии первой страницы, и его можно
|
|
||||||
скопировать: `gfx_copy_page` берёт источником ОЗУ-копию НЕактивной страницы
|
|
||||||
(то есть ЧИСТЫЙ фон — спрайты рисуются банком без тени и в копию не попадают),
|
|
||||||
а приёмник обновляет и в видео-ОЗУ, и в ОЗУ-копии. Идея пользователя: тот же
|
|
||||||
приём, что при перевороте экрана (зелёное зелье), только без зеркала.
|
|
||||||
|
|
||||||
| | полная запечка | копия |
|
|
||||||
|---|---:|---:|
|
|
||||||
| щебень / кнопка (60×39) | 179 914 | ~35 000 |
|
|
||||||
| колодец полосы потолка (64×9) | 138 318 | ~17 500 |
|
|
||||||
|
|
||||||
**ДВА УСЛОВИЯ КОРРЕКТНОСТИ.** Оба нарушались и оба дали видимые баги.
|
|
||||||
|
|
||||||
1. **Запечка обязана быть ОГРАНИЧЕНА копируемым прямоугольником.**
|
|
||||||
`draw_tile` рисует тайлы ЦЕЛИКОМ, то есть пишет ШИРЕ бара; копия переносит
|
|
||||||
ровно бар, и всё, что легло вне него, на второй странице остаётся прежним —
|
|
||||||
страницы расходятся, это видно как МЕРЦАНИЕ через кадр. У полосы потолка
|
|
||||||
окно стояло с самого начала (G1), у `pop_floor_bake` — нет, и он мерцал
|
|
||||||
торцами полов, плит и кнопок (найдено пользователем 2026-08-17: уровень 1,
|
|
||||||
комната 6, Кид на кнопке (0,2)). Поэтому в `pop_floor_bake` окно теперь
|
|
||||||
стоит КАК УСЛОВИЕ КОРРЕКТНОСТИ, хотя по скорости само по себе убыточно
|
|
||||||
(см. §5) — снимать его нельзя.
|
|
||||||
2. **Копия годится только если содержимое тайла между двумя кадрами не
|
|
||||||
изменилось.** У анимированного тайла (кнопка с идущим таймером связи)
|
|
||||||
пометка обновляется КАЖДЫЙ кадр и картинка каждый раз другая. Поэтому
|
|
||||||
`pop_set_redraw`/`pop_set_redraw_above` гасят слот копии при ПЕРЕпометке
|
|
||||||
(`pop_bake_slot_reset*`).
|
|
||||||
|
|
||||||
Плюс слот `bake_pg`/`bake_pg_above` помнит, НА КАКОЙ странице сделана первая
|
|
||||||
запечка: копируем только если первая была на ДРУГОЙ странице и дабл-буфер
|
|
||||||
включён. Это покрывает переплетение двух запечек в одном кадре, однобуфер
|
|
||||||
(чит SPACE) и смену комнаты (`pop_bake_forget`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Отрицательные результаты — НЕ повторять
|
|
||||||
|
|
||||||
### Окно клипа в `pop_floor_bake` — по СКОРОСТИ проверено ТРИ раза, каждый раз хуже
|
|
||||||
|
|
||||||
**Но оно всё равно стоит на месте: без него ломается копия второй страницы
|
|
||||||
(§4).** Ниже — только про скорость самого окна.
|
|
||||||
|
|
||||||
| попытка | было | стало |
|
|
||||||
|---|---:|---:|
|
|
||||||
| до G3 | 187 266 | 198 279 |
|
|
||||||
| после G3 | 182 124 | 188 460 |
|
|
||||||
| после `pop_blit_b` file-scope, с детальным зондом | 179 914 | 188 417 |
|
|
||||||
|
|
||||||
Третья попытка объяснила причину: клипованный путь стоит **+1 500 такта на
|
|
||||||
КАЖДОМ** из 7,6 блитов (+11 400), а режет он только редкие высокие куски — в
|
|
||||||
трассе такие нашлись (34 878 → 25 872 и 28 818 → 24 090, всего −13 700), но в
|
|
||||||
среднем по 12 перерисовкам их нет. Запись стоит в коде.
|
|
||||||
|
|
||||||
### Прочее (проверено раньше)
|
|
||||||
|
|
||||||
- **Не откладывать запекание на другой кадр** — запечка пишет ОЗУ-копию, из
|
|
||||||
которой восстанавливает heal; отложенная даёт призрак плиты на месте дыры.
|
|
||||||
- **Не батчить смежные колонки в `pop_ceil_shake_draw`** — плиты стартуют со
|
|
||||||
случайными задержками, в кадре дрожат разрозненные колонки, пробег почти
|
|
||||||
всегда длиной в одну.
|
|
||||||
- **Не ускорять передачу пикселей** — предел железа (3+3 такта на байт,
|
|
||||||
memory `blit_cost_model`). В пиковом кадре «железный» минимум всех блитов
|
|
||||||
≈150 000 из 916 458.
|
|
||||||
- **`LOOSE-SHAKE-RUNS`** (пометки по сменам кадра): наивный вариант выигрыша
|
|
||||||
НЕ даёт — пять смен × две страницы = те же десять перерисовок. Работает
|
|
||||||
только версия «пары и тройки», ~20 % и только на дрожащих плитах; оценка
|
|
||||||
2026-08-13, не перемерена. Подробности —
|
|
||||||
`roomtest/TASKS_OPEN.md#loose-shake-runs`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Журнал правок
|
|
||||||
|
|
||||||
| дата | что сделано | зелёная: покой / дрожь / пик | коммит |
|
|
||||||
|---|---|---|---|
|
|
||||||
| 2026-08-17 | базовый замер | 35 760 / 366 000 / **805 000** | `c312e4a` |
|
|
||||||
| 2026-08-17 | G2 контекст тайла в file-scope | — / — / **747 954** | `a3c473d` |
|
|
||||||
| 2026-08-17 | G1 окно клипа в `pop_ceil_bake_empty` | — / — / **663 250** | `a3c473d` |
|
|
||||||
| 2026-08-17 | G3 `blit_b_clip` байты + file-scope | — / 335 400 / **575 730** | `a3c473d` |
|
|
||||||
| 2026-08-17 | `pop_blit_b` file-scope; пакетная пометка кусков | — / — / **557 706** | `b2da0b8` |
|
|
||||||
| 2026-08-17 | коридор heal по высоте композита | 35 760 / 335 400 / **546 900** | `9a50ab2` |
|
|
||||||
| 2026-08-17 | **копия второй страницы вместо второй запечки** | — / — / **423 558** | `5ef721e` |
|
|
||||||
| 2026-08-17 | `mob_tick_one` в file-scope; снята оснастка из горячих путей | 35 760 / 326 130 / **414 456** | `18ee60e` |
|
|
||||||
| 2026-08-17 | фикс мерцания: окно клипа в `pop_floor_bake` как условие корректности копии | замер после фикса — ниже | `35b7cd5` |
|
|
||||||
| 2026-08-17 | замер после фиксов уровня 1 (мерцание торцов, потолочный fore, сосед под плитой, блеск меча) | 35 760 / — / **417 630** | `40f0d46` |
|
|
||||||
| 2026-08-17 | **регресс после фиксов уровня 2** (чёрные бары, чит бессмертия) — в пределах шума | — / — / **419 526** | `ec1f384` |
|
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
# Сцена и замер: факел + чомпер + страж, уровень 11 комната 15
|
|
||||||
|
|
||||||
Вторая целевая сцена для оптимизации (первая — [`perf_l13_room23.md`](perf_l13_room23.md),
|
|
||||||
каскад плит). Здесь узкое место другое: не разовый пик на каскаде, а
|
|
||||||
**постоянная** цена статичной комнаты, в которой одновременно живут два
|
|
||||||
факела, чомпер и страж.
|
|
||||||
|
|
||||||
Такты — `totalcycles` MAME (не такты Z80, ≈2,4× номинала, memory
|
|
||||||
`sprinter_wait_states_2x`). **Растровый кадр = 430 000.** Хвост кадра — три
|
|
||||||
`gfx_wait_vsync`, поэтому логический кадр занимает 3 растра, пока работа
|
|
||||||
укладывается в один; при работе 1..2 растра период становится 4.
|
|
||||||
|
|
||||||
## 1. Сцена
|
|
||||||
|
|
||||||
Уровень 11, комната 15. Проверено чтением состояния машины:
|
|
||||||
`pop_current_level` = 0x0B, `cur_room` = 15.
|
|
||||||
|
|
||||||
| кто | где | кадр |
|
|
||||||
|---|---|---|
|
|
||||||
| Кид | (0,2), x=98 | 15 (стойка с мечом) |
|
|
||||||
| чомпер | (0,3) | застывший (trob мёртв) |
|
|
||||||
| факел | (0,2) — пламя рисуется в ячейке (0,3) | анимируется каждый кадр |
|
|
||||||
| факел | (0,7) — пламя в ячейке (0,8) | анимируется каждый кадр |
|
|
||||||
| страж | (0,8), x=170 | 171 (боевая стойка) |
|
|
||||||
|
|
||||||
Ни Кид, ни страж не двигаются: сцена статична, разброс замера — сотни тактов
|
|
||||||
на 800 000.
|
|
||||||
|
|
||||||
## 2. Замер (`09f32ce`, база модуля `roomtest.c` = 0x42AD)
|
|
||||||
|
|
||||||
867 кадров, зонды A/C/D/E; детализация — тремя отдельными прогонами
|
|
||||||
(m1, m5..m7, M/F). **Период кадра: 4 растра во всех 866 интервалах.**
|
|
||||||
|
|
||||||
| участок | зонды | медиана | доля работы |
|
|
||||||
|---|---|---:|---:|
|
|
||||||
| **синяя: ввод + heal** | A→m1 | 141 048 | 17,6 % |
|
|
||||||
| **синяя: логика** | m1→C | 152 190 | 19,0 % |
|
|
||||||
| синяя, всего | A→C | **293 238** | 36,6 % |
|
|
||||||
| зелёная: `pop_loose_tick` | C→m5 | 28 872 | 3,6 % |
|
|
||||||
| **зелёная: `pop_process_trobs`** | m5→m6 | 92 448 | 11,5 % |
|
|
||||||
| **зелёная: `pop_redraw_needed`** | m6→m7 | 190 260 | 23,7 % |
|
|
||||||
| зелёная: шов/ворота соседа | m7→D | 9 336 | 1,2 % |
|
|
||||||
| зелёная, всего | C→D | **320 916** | 40,0 % |
|
|
||||||
| циан: `check_mirror` + `loose_mob_draw` | D→M | 37 458 | 4,7 % |
|
|
||||||
| **циан: Кид + страж + fore + HP** | M→F | 148 566 | 18,5 % |
|
|
||||||
| циан: `char_fore(KID)` + борта | F→E | 1 740 | 0,2 % |
|
|
||||||
| циан, всего | D→E | **187 758** | 23,4 % |
|
|
||||||
| **работа** | A→E | **801 768** | 1,86 растра |
|
|
||||||
|
|
||||||
Циан здесь **не** выделяется: 187 758 — это 0,44 растрового кадра, а разброс
|
|
||||||
за 867 кадров всего 174 такта (187 674..187 848). Впечатление «циан ~150 %
|
|
||||||
кадра» на глаз не подтвердилось — при периоде 4 растра полосы бордюра
|
|
||||||
размазаны по кадрам и на глаз не читаются.
|
|
||||||
|
|
||||||
## 3. Главная находка: чомпер справа от факела — 190 260 тактов/кадр
|
|
||||||
|
|
||||||
`pop_dbg_rdmax_tot` = **1**: за кадр перерисовывается РОВНО ОДИН тайл, и
|
|
||||||
стоит он все 190 260 тактов зелёной фазы.
|
|
||||||
|
|
||||||
> **ПОПРАВКА 2026-08-19 (после реализации P1).** Механизм ниже описан
|
|
||||||
> верно, но ГЛАВНЫМ источником 190 260 тактов он НЕ был. Зонд
|
|
||||||
> `pop_dbg_kind` показал, что все 312 перерисовок в прогоне — вид
|
|
||||||
> `POP_RD_CHOMP` (полная), и ни одной от факела: собственная пометка
|
|
||||||
> чомпера просто перебивала пометку соседа. Настоящая причина — в §6.
|
|
||||||
> Урок ровно тот, что уже записан в `defer_unexplained_quirks`: механизм,
|
|
||||||
> который правдоподобно объясняет цифру, ещё не доказан цифрой.
|
|
||||||
|
|
||||||
Цепочка:
|
|
||||||
|
|
||||||
1. `TORCH_ANIM_DIV = 1` — факел меняет кадр пламени КАЖДЫЙ логический кадр;
|
|
||||||
2. пламя запекается в фон (`pop_torch_draw`, `GFX_BANK_NORMAL`), а канвас
|
|
||||||
пламени 16×18 лежит **в ячейке правого соседа** (seg008:560) — то есть
|
|
||||||
поверх чомпера;
|
|
||||||
3. поэтому `pop_process_trobs` метит соседа: `if (trob_rcode[i] == TILE_CHOMP)
|
|
||||||
pop_set_redraw(tp + 1, POP_RD_CHOMP, 1)` (порт `set_redraw_anim_right`);
|
|
||||||
4. `pop_chomp_redraw` отвечает на пометку **heal 32×64 + полный `draw_tile`**.
|
|
||||||
|
|
||||||
Расхождение с оригиналом именно в шаге 4. `set_redraw_anim_right` метит
|
|
||||||
слой **anim**, и оригинал возвращает только его (`draw_tile_anim_topright` →
|
|
||||||
`draw_tile_anim_right` → `draw_tile_anim`) — одну графику чомпера поверх
|
|
||||||
огня. Мы вместо этого стираем и пересобираем тайл целиком, со всеми слоями
|
|
||||||
(`draw_tile_right`, `base`, `bottom`, `loose`), которые пламя вообще не
|
|
||||||
трогало.
|
|
||||||
|
|
||||||
Цена по модели блита (`blit_cost_model`, 8791 + 198·h + 5,96·w·h):
|
|
||||||
heal 32×64 ≈ 33 700, значит на один `draw_tile` уходит ≈ 156 000 — сходится
|
|
||||||
с известным замером «полная запечка щебня 179 914».
|
|
||||||
|
|
||||||
Чомпер при этом **застывший**: своей анимации у него нет, поза не меняется,
|
|
||||||
возвращать нужно ровно ту же графику поверх свежего пламени.
|
|
||||||
|
|
||||||
## 4. Что это даёт и куда смотреть дальше
|
|
||||||
|
|
||||||
Ранжирование по цене (доля от 801 768):
|
|
||||||
|
|
||||||
| # | участок | такты | что делать |
|
|
||||||
|---|---|---:|---|
|
|
||||||
| 1 | `redraw_needed`: чомпер под факелом | 190 260 | вернуть только слой anim, как в оригинале — без heal и без остальных слоёв |
|
|
||||||
| 2 | синяя: логика двух Char | 152 190 | графики нет вообще; разобрать `pop_check_can_guard_see_kid` и два `play_seq` |
|
|
||||||
| 3 | циан: Кид + страж | 148 566 | оба будятся каждый кадр — метки фона от чомпера/факела накрывают обоих |
|
|
||||||
| 4 | синяя: heal двух Char | 141 048 | следствие того же: skip не срабатывает ни разу |
|
|
||||||
| 5 | `process_trobs`: два факела | 92 448 | ≈46 000 на факел при блите пламени 16×18 ≈ 14 000 — разобрать накладные |
|
|
||||||
| 6 | `loose_tick` | 28 872 | в комнате нет ни одной loose-плиты |
|
|
||||||
|
|
||||||
Пункты 3 и 4 — одна тема: пока фон трогают каждый кадр, `pop_char_skip_mask`
|
|
||||||
не может пропустить ни Кида, ни стража. Пламя метит узко (16×18), а вот
|
|
||||||
`pop_chomp_redraw` метит весь тайл со свесом — то есть пункт 1 чинит и часть
|
|
||||||
пунктов 3/4.
|
|
||||||
|
|
||||||
Связанные задачи: `HEAL-WIDTH` и G8 в [`perf_green_phase.md`](perf_green_phase.md)
|
|
||||||
— та же болезнь (полный тайл там, где хватает полосы), но на другом
|
|
||||||
материале.
|
|
||||||
|
|
||||||
## 6. Настоящая причина 190 260 тактов: перерисовка неизменной позы
|
|
||||||
|
|
||||||
Найдено при реализации P1, сверкой с `animate_chomper` (seg007:0448).
|
|
||||||
Функция оригинала заканчивается так:
|
|
||||||
|
|
||||||
```c
|
|
||||||
if ((curr_modifier & 0x7F) < 6) {
|
|
||||||
redraw_at_trob();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
То есть чомпер перерисовывается **только пока фаза меньше 6** — пять кадров
|
|
||||||
из пятнадцати (`POP_CHOMPER_SPEED = 15`). Это не оптимизация оригинала, а
|
|
||||||
следствие таблицы поз: `chomper_fram1 = {3,2,0,1,4,3,3}`, и начиная с фазы 5
|
|
||||||
и до конца круга поза одна и та же — 3. Рисовать её десять кадров подряд
|
|
||||||
значит рисовать ровно ту же картинку.
|
|
||||||
|
|
||||||
Мы же метили тайл БЕЗУСЛОВНО, каждый кадр, пока trob жив — то есть платили
|
|
||||||
полный `draw_tile` плюс heal 32×64 за неизменную картинку в двух третях
|
|
||||||
кадров. А trob у чомпера живёт, пока Кид в том же РЯДУ (`animate_chomper`
|
|
||||||
снимает его только при фазе ≥ 6 и ушедшем Киде) — в 11/15 Кид стоит в (0,2),
|
|
||||||
чомпер в (0,3), ряд один.
|
|
||||||
|
|
||||||
**Что сделано:**
|
|
||||||
|
|
||||||
1. пометка только при фазе < 6, и на фазе 5 — на ОБЕ страницы дабл-буфера
|
|
||||||
(она последняя рисуемая, её поза обязана лечь на обе; вторую страницу
|
|
||||||
пометка догоняет в кадре фазы 6, где поза та же самая);
|
|
||||||
2. пометка от факела (`set_redraw_anim_right`) переведена на новый вид
|
|
||||||
`POP_RD_CHOMP_ANIM` → `pop_chomp_anim_draw`: три блита графики чомпера
|
|
||||||
поверх свежего пламени, без heal и без остальных слоёв — порт ветки
|
|
||||||
`redraw_frames_anim` (seg008:0211);
|
|
||||||
3. приоритет полной перерисовки над anim в `pop_set_redraw` — у оригинала
|
|
||||||
это два независимых счётчика, и `full` побеждает.
|
|
||||||
|
|
||||||
**Результат** (замер, 552 кадра): полная перерисовка теперь в **40 %**
|
|
||||||
кадров, лёгкий возврат челюстей — в 60 %. Зелёная фаза: 291 888 в дорогом
|
|
||||||
кадре против 183 414 в дешёвом.
|
|
||||||
|
|
||||||
| | работа | зелёная |
|
|
||||||
|---|---:|---:|
|
|
||||||
| до P1 | 768 684 | 294 510 |
|
|
||||||
| после P1, медиана | **657 882** | **183 420** |
|
|
||||||
| после P1, дорогой кадр (40 %) | 765 936 | 291 888 |
|
|
||||||
|
|
||||||
**−110 802 на медиане** при ожидании −160 000. Разница в том, что 40 %
|
|
||||||
кадров по-прежнему платят полную цену: там поза реально меняется, и это уже
|
|
||||||
не лишняя работа, а честная. Дальше её можно резать только раскладом
|
|
||||||
`draw_tile` на части (P7) или сужением heal (P8).
|
|
||||||
|
|
||||||
## 5. Журнал правок по этой сцене
|
|
||||||
|
|
||||||
| дата | правка | работа | синяя | зелёная | циан |
|
|
||||||
|---|---|---:|---:|---:|---:|
|
|
||||||
| 2026-08-19 | базовый замер (`09f32ce`) | 801 768 | 293 238 | 320 916 | 187 758 |
|
|
||||||
| 2026-08-19 | **P5**: гейты холостого хода в `pop_loose_tick` | **767 928** | 285 864 | 294 384 | 187 764 |
|
|
||||||
| | | −33 840 | −7 374 | −26 532 | +6 |
|
|
||||||
| 2026-08-19 | зонды для замера P2/P6 (временные) | 768 684 | 286 518 | 294 510 | 187 761 |
|
|
||||||
| 2026-08-19 | **P1**: чомпер — перерисовка только при фазе < 6 (медиана) | **657 882** | 286 503 | 183 420 | 187 761 |
|
|
||||||
| | | −110 802 | −15 | −111 090 | 0 |
|
|
||||||
|
|
||||||
Оснастка P2/P6 стоит 756 тактов на кадр — замеры до и после сопоставимы.
|
|
||||||
|
|
||||||
Циан не изменился (+6 тактов — шум), и это ожидаемо: `loose_tick` целиком
|
|
||||||
лежит в зелёной. Синяя просела на 7 374 без прямой причины в правке —
|
|
||||||
скорее всего перераскладка кода банка 3 компилятором; проверять отдельно
|
|
||||||
не стали, знак верный.
|
|
||||||
|
|
||||||
## 7. ТЯЖЁЛАЯ позиция: Кид на шаг правее [замер 2026-08-19]
|
|
||||||
|
|
||||||
Поставлена пользователем: один осторожный шаг вправо (x = 106 вместо 99,
|
|
||||||
колонка та же). Спрайт Кида начинает пересекаться с тайлом (0,3), где
|
|
||||||
одновременно чомпер и пламя факела — и `skip_mask` перестаёт его
|
|
||||||
пропускать.
|
|
||||||
|
|
||||||
| фаза | лёгкая | **тяжёлая** | разница |
|
|
||||||
|---|---:|---:|---:|
|
|
||||||
| синяя | 259 500 | 257 520 | −1 980 |
|
|
||||||
| зелёная (медиана) | 181 068 | 180 870 | −198 |
|
|
||||||
| **циан** | 187 761 | **319 842** | **+132 081** |
|
|
||||||
| **работа (медиана)** | 628 542 | **758 358** | **+129 816** |
|
|
||||||
| работа (максимум) | 744 384 | **876 612** | |
|
|
||||||
|
|
||||||
**Период кадра: 4 растра в 335 кадрах, 5 растров в 123 (27 %).** Это уже
|
|
||||||
не «стабильно медленно», а рывки: каждый четвёртый кадр длиннее соседних.
|
|
||||||
|
|
||||||
Разбор циана показывает, куда ушли 132 тысячи:
|
|
||||||
|
|
||||||
| участок | лёгкая | тяжёлая |
|
|
||||||
|---|---:|---:|
|
|
||||||
| `check_mirror` | 3 198 | 3 198 |
|
|
||||||
| `mob_draw` + `guard_over_kid` + `skip_mask` | 34 374 | 18 672 |
|
|
||||||
| **`pop_char_draw(KID)`** | **204** | **54 738** |
|
|
||||||
| соперник: `char_draw` + `char_fore` | 145 896 | 149 748 |
|
|
||||||
| **`fore_needed` + `char_fore(KID)` + борта** | **4 356** | **93 486** |
|
|
||||||
|
|
||||||
То есть Кид из «пропущен за 204 такта» превращается в полноценного
|
|
||||||
персонажа за ~144 000 — ровно столько же, сколько стоит страж.
|
|
||||||
|
|
||||||
**Главный вывод замера: самая дорогая единичная статья кадра — это
|
|
||||||
fore-проход персонажа.** 62 778 у стража и ~89 000 у Кида, вместе около
|
|
||||||
**152 000, то есть 20 % работы кадра**. У Кида он дороже потому, что в его
|
|
||||||
футпринте лежит чомпер, а у чомпера есть собственный передний слой
|
|
||||||
(`POP_CHOMP_FRAM_FOR`), который перерисовывается поверх персонажа каждый
|
|
||||||
кадр.
|
|
||||||
|
|
||||||
## 8. После P15 (точная метка «фон трогали»)
|
|
||||||
|
|
||||||
| фаза | лёгкая до | лёгкая после | тяжёлая до | тяжёлая после |
|
|
||||||
|---|---:|---:|---:|---:|
|
|
||||||
| синяя | 259 050 | **223 902** | 257 520 | **245 808** |
|
|
||||||
| зелёная | 181 494 | 181 494 | 180 870 | 181 761 |
|
|
||||||
| циан | 194 262 | **59 406** | 319 842 | **192 090** |
|
|
||||||
| **работа** | 628 542 | **464 796** | 758 358 | **617 487** |
|
|
||||||
|
|
||||||
В лёгкой позиции не рисуется НИ ОДИН персонаж (циан 59 406 — это уже только
|
|
||||||
`check_mirror`, проверки и передний слой по пометкам). В тяжёлой рисуется
|
|
||||||
один Кид: он действительно стоит под пламенем, а страж — нет.
|
|
||||||
|
|
||||||
**Пятирастровые кадры в тяжёлой позиции исчезли** (было 27 %), период стал
|
|
||||||
ровно 4.
|
|
||||||
|
|
||||||
**Полная очередь оптимизаций с оценками — [`perf_registry.md`](perf_registry.md).**
|
|
||||||
Там же разложена цена одного блита фона по этапам (замер 2026-08-19, 1603
|
|
||||||
блита) и модель зелёной фазы этой сцены.
|
|
||||||
@@ -1,271 +0,0 @@
|
|||||||
# Сцена и метод замера: каскад плит, уровень 13 комната 23
|
|
||||||
|
|
||||||
Общий документ для двух фазовых: [`perf_green_phase.md`](perf_green_phase.md)
|
|
||||||
(слой фона) и [`perf_cyan_phase.md`](perf_cyan_phase.md) (персонажи + передний
|
|
||||||
слой). Здесь — как воспроизвести сцену, чем мерить, сводка по кадрам и
|
|
||||||
разбор габаритов спрайтов (он общий для обеих фаз).
|
|
||||||
|
|
||||||
Сцена: старт уровня 13. Комната 23 стартовая, ряд 2 комнаты СВЕРХУ (17) —
|
|
||||||
шесть loose-плит в колонках 2..7 (`res2013.bin`: коды `11` в позициях 22..27),
|
|
||||||
`check_fall_flo` раздаёт им отложенный старт `0xF0..0xFF`, и они сыплются
|
|
||||||
вразнобой. Кид стоит у правого края и не двигается.
|
|
||||||
|
|
||||||
Все числа — такты `totalcycles` MAME (системный клок ~21,5 МГц, **НЕ** такты
|
|
||||||
Z80: у ОЗУ Sprinter wait-state'ы, ≈2,4× номинала — memory
|
|
||||||
`sprinter_wait_states_2x`). **Растровый кадр = 430 000.** Логический кадр
|
|
||||||
спейсится тремя `gfx_wait_vsync`, поэтому работа сверх 430 000 стоит сразу
|
|
||||||
целый лишний растровый кадр.
|
|
||||||
|
|
||||||
Сборка: `make LEVEL=13` на `c312e4a`, `_CODE = 0x4100`, база модуля
|
|
||||||
`roomtest.c` = **0x42AD**. **Адреса зондов меняются после КАЖДОЙ
|
|
||||||
пересборки** — брать заново из `.sprinter-cc-roomtest/roomtest.map` и
|
|
||||||
`roomtest.lst`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Как воспроизвести сцену
|
|
||||||
|
|
||||||
**Только перезапуском программы.** Проверено и отвергнуто:
|
|
||||||
|
|
||||||
- **выход из комнаты и возврат** (чит `+`/`-`) — не работает: провалившаяся
|
|
||||||
плита-потолок уходит в страницу уровня насовсем (`pop_level_set_tile` в
|
|
||||||
`roomtest.c` по сигналу `pop_ceil_fell`, плюс `animate_loose` в
|
|
||||||
`pop_trob.c`), и при повторном входе `check_fall_flo` не находит ни одной
|
|
||||||
`TILE_LOOSE`;
|
|
||||||
- **рестарт уровня** (`pop_kid_dead = 1` + чит навигации) — не работает по
|
|
||||||
другой причине: `pop_start_level()` сам заходит в стартовую комнату 23,
|
|
||||||
взводит гряду, и она доваливается ЗАОЧНО (через `trob` комнаты 17), пока
|
|
||||||
телепорт уносит Кида в комнату 24;
|
|
||||||
- **поставить сцену руками** (записать `pop_ceil_modif[2..7]` и копию ряда
|
|
||||||
сверху `pop_t_above[2..7]` отладчиком) — записи ложатся, но пока машина
|
|
||||||
БЕЖИТ, их успевает обнулить тот же доваливающийся `trob`.
|
|
||||||
|
|
||||||
Рабочий рецепт (идея пользователя, самый чистый): **`ESC` → зонды →
|
|
||||||
`roomtest`**. `ESC` выходит в DSS, запуск заново стартует уровень 13 с нуля,
|
|
||||||
Кид сразу в комнате 23, каскад начинается через ~5 логических кадров после
|
|
||||||
отрисовки комнаты. Зонды обязаны стоять **ДО** набора `roomtest` — за время
|
|
||||||
набора (9 клавиш ≈ 1,8 с) и загрузки атласов каскад успевает пройти целиком.
|
|
||||||
|
|
||||||
## 2. Канал вывода замеров
|
|
||||||
|
|
||||||
`printf` из действия брейкпоинта в `error.log` **не** попадает. Читается
|
|
||||||
verb'ом **`clog N`** плагина `mamebridge` — а его нет в MCP-обёртке
|
|
||||||
(`mame_mcp.py` знает только `cmd`). Годится прямой файловый IPC:
|
|
||||||
положить `/tmp/mame_mcp/req_<ЧИСЛО>.txt` с телом команды и прочитать
|
|
||||||
`resp_<ЧИСЛО>.txt`. **Имя обязано содержать ЧИСЛО** (`init.lua`:
|
|
||||||
`entry:match("^req_(%d+)%.txt$")`) — с буквенным id запрос молча не
|
|
||||||
обслуживается.
|
|
||||||
|
|
||||||
Скрипты сессии (в scratchpad, при необходимости пересоздать): `mrpc.py` —
|
|
||||||
клиент IPC; `run.sh` — цикл «`bpclear` → `ESC` → зонды → `roomtest` → `clog`»;
|
|
||||||
`parse*.py` — разбор трассы по кадрам.
|
|
||||||
|
|
||||||
Форма зонда: `bpset <addr>,1,{printf "<метка> %d",totalcycles; g}`.
|
|
||||||
Для `pop_dbg_kind` — `printf "K %d %d",a,totalcycles` (аргумент `uint8_t`
|
|
||||||
приходит в `A`, `__sdcccall(1)`).
|
|
||||||
|
|
||||||
## 3. Зонды
|
|
||||||
|
|
||||||
Адреса `out (_io_border), a` (полосы бордюра) из `roomtest.lst` плюс
|
|
||||||
однобайтовые пустышки `pop_dbg_*` из резидентного `pop_state.c`. Резидент
|
|
||||||
важен принципиально: у банковых функций один адрес 0xC000+ есть у восьми
|
|
||||||
модулей сразу, и брейкпоинт ловит все банки (так в прошлой сессии намерили
|
|
||||||
несуществующие 134 730 тактов).
|
|
||||||
|
|
||||||
| зонд | адрес | что |
|
|
||||||
|---|---|---|
|
|
||||||
| A | 0x43E8 | `PROF(2)` — начало кадра (ввод + heal) |
|
|
||||||
| — | 0x46AF | `PROF(2)` — начало логики |
|
|
||||||
| C | 0x47D1 | `PROF(4)` — начало слоя фона (**зелёная**) |
|
|
||||||
| D | 0x4BCE | `PROF(6)` — начало спрайтов (**циан**) |
|
|
||||||
| M | 0x4C26 | `PROF(6)` — кадр Кида |
|
|
||||||
| F | 0x4C93 | `PROF(6)` — fore поверх Кида |
|
|
||||||
| E | 0x4CB7 | `PROF(0)` — конец работы, ждём vsync |
|
|
||||||
| m5/m6/m7 | 0x4DC6 / C7 / C8 | границы внутри зелёной |
|
|
||||||
| m9..m12 | 0x4DCA..CD | внутренности `pop_loose_tick` |
|
|
||||||
| m13/m14/m15 | 0x4DCE / CF / D0 | `pop_ceil_shake_draw`: вход / heal / draw_tile |
|
|
||||||
| kind / m16 | 0x4DD1 / D2 | вид и цена одной перерисовки в `pop_redraw_needed` |
|
|
||||||
| b1..b5 | 0x4DD3..D7 | участки одного `pop_blit_b` |
|
|
||||||
|
|
||||||
Полезные адреса состояния (из `roomtest.map`): `pop_t_room` 0x95F2,
|
|
||||||
`pop_current_level` 0x9945, `pop_ceil_modif` 0x9C9E, `pop_t_above` 0x95EE
|
|
||||||
(указатель), `pop_kid_dead` 0x9C4E, `pop_dbg_rdmax` 0x95B1.
|
|
||||||
Запись в память через MCP — по адресу `0x10000 | addr` (логический вид Z80);
|
|
||||||
присваивание выражением дебаггера (`print b@... = 1`) **не работает**.
|
|
||||||
|
|
||||||
**Грабли:** проверять, что запущен РОВНО ОДИН MAME (`pgrep -f mame.arm | wc -l`).
|
|
||||||
Мост говорит с одним, замеры собираются с другого, и точки «не срабатывают».
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Сводка по кадрам
|
|
||||||
|
|
||||||
**Базовый замер (`c312e4a`, ДО оптимизации):**
|
|
||||||
|
|
||||||
| фаза каскада | работа | синяя | зелёная | циан | период (растр.) |
|
|
||||||
|---|---:|---:|---:|---:|---:|
|
|
||||||
| покой в комнате 23 | 190 860 | 134 550 | 35 760 | 20 550 | **3** |
|
|
||||||
| дрожат 6 плит | 537 400 | 142 700 | 366 000 | 28 700 | **4** |
|
|
||||||
| провалы + полёт, ПИК | **1 437 150** | 142 700 | **663 250** | **631 200** | **5–6** |
|
|
||||||
| максимум по секции | | 142 830 | **805 000** | **631 800** | |
|
|
||||||
|
|
||||||
**После оптимизации (`af189a1`, 2026-08-17):**
|
|
||||||
|
|
||||||
| максимум по секции | работа | синяя | зелёная | циан |
|
|
||||||
|---|---:|---:|---:|---:|
|
|
||||||
| было | 1 437 150 | 142 830 | 805 000 | 631 800 |
|
|
||||||
| стало | **916 458** | 142 830 | **546 900** | **270 510** |
|
|
||||||
| | −36 % | — | −32 % | −57 % |
|
|
||||||
|
|
||||||
**Регресс после обхода уровней 1-2 (`ec1f384`, 2026-08-17), 418 кадров:**
|
|
||||||
|
|
||||||
| максимум по секции | работа | синяя | зелёная | циан |
|
|
||||||
|---|---:|---:|---:|---:|
|
|
||||||
| после оптимизации (`af189a1`) | 916 458 | 142 830 | 546 900 | 270 510 |
|
|
||||||
| после фиксов ур. 1 (`40f0d46`) | 873 930 | 158 874 | 417 630 | 379 482 |
|
|
||||||
| **после фиксов ур. 2 (`ec1f384`)** | **878 550** | **158 880** | **419 526** | **379 488** |
|
|
||||||
|
|
||||||
Фиксы второго уровня (чёрные бары, чит бессмертия) на бюджет не повлияли:
|
|
||||||
разница с предыдущим замером +4 620 работы и +1 896 зелёной — шум прогона.
|
|
||||||
|
|
||||||
**Регресс после обхода уровней 3-7 (`3bcaf51`, 2026-08-18), 2435 кадров:**
|
|
||||||
|
|
||||||
| максимум по секции | работа | синяя | зелёная | циан |
|
|
||||||
|---|---:|---:|---:|---:|
|
|
||||||
| после фиксов ур. 2 (`ec1f384`) | 878 550 | 158 880 | 419 526 | 379 488 |
|
|
||||||
| **после фиксов ур. 3-7 (`3bcaf51`)** | **880 170** | **159 774** | **419 520** | **380 244** |
|
|
||||||
| разница | +1 620 | +894 | −6 | +756 |
|
|
||||||
| | +0,2 % | +0,6 % | 0,0 % | +0,2 % |
|
|
||||||
|
|
||||||
Все четыре секции — в пределах шума прогона (сравнить с +4 620 / +1 896
|
|
||||||
выше, которые уже признаны шумом). Зелёная совпала с точностью до 6 тактов.
|
|
||||||
|
|
||||||
Что за это время добавилось в горячий путь: `pop_spike_frame` и
|
|
||||||
`pop_chomp_pose` (SPIKE-BAKED) — один резидентный `call` на слой и ТОЛЬКО на
|
|
||||||
тайлах-ловушках, в этой комнате их нет; и снятие раннего выхода для трупа
|
|
||||||
(DIED-ON-BUTTON) — цепочка физики на мёртвом Киде, а он тут жив. Замер это
|
|
||||||
подтверждает: цена не сдвинулась.
|
|
||||||
|
|
||||||
Распределение периода тоже совпало с эталоном кадр в кадр: **3 растра в
|
|
||||||
2408 кадрах, 4 в 24, 5 в одном** — против «3 в 392, 4 в 24, 5 в одном»
|
|
||||||
у `ec1f384` (кадров в этом прогоне больше просто потому, что дольше стояли в
|
|
||||||
покое после каскада). То есть за бюджет вылезает ровно тот же кусок сцены и
|
|
||||||
ровно на столько же кадров.
|
|
||||||
|
|
||||||
**Регресс после обхода уровней 8-9 (`0dd2f6a`, 2026-08-18), 2701 кадр:**
|
|
||||||
|
|
||||||
| максимум по секции | работа | синяя | зелёная | циан |
|
|
||||||
|---|---:|---:|---:|---:|
|
|
||||||
| после фиксов ур. 3-7 (`3bcaf51`) | 880 170 | 159 774 | 419 520 | 380 244 |
|
|
||||||
| **после фиксов ур. 8-9** | **880 272** | **159 822** | **419 562** | **380 202** |
|
|
||||||
| разница | +102 | +48 | +42 | −42 |
|
|
||||||
|
|
||||||
Разброс ±100 тактов на 880 000 — это 0,01 %, то есть чистый шум прогона
|
|
||||||
(циан вообще ушёл в минус). Период снова совпал кадр в кадр: 4 растра в
|
|
||||||
24 кадрах, 5 в одном.
|
|
||||||
|
|
||||||
Что добавилось за это время и почему не подорожало: фиксы стража
|
|
||||||
(`c40ae3f`) правят только вход в комнату — кода в кадре не прибавилось; фикс
|
|
||||||
боя у шва (`a498255`) добавил два сравнения в `check_leave`, а в этой сцене
|
|
||||||
Кид неподвижен и до порогов не доходит. Отладочная трасса `DBG_KIDOBJ`
|
|
||||||
выключена дефайном и в сборку не попадает.
|
|
||||||
Скачок циан на фиксах ПЕРВОГО уровня (270 510 → 379 482) объяснён там же:
|
|
||||||
восстановлены потерянные половины слоёв (`set_redraw2`, ряд −1 foretable), то
|
|
||||||
есть это плата за корректность, а не регрессия.
|
|
||||||
|
|
||||||
Период кадра по прогону: **3 растра в 392 кадрах, 4 в 24, 5 в одном** — то
|
|
||||||
есть за бюджет вылезает только сам каскад.
|
|
||||||
|
|
||||||
Цель — каждая секция ≤ 400 000. **Синяя и циан в бюджете**; зелёная 419 526,
|
|
||||||
то есть 1,05× цели (и ниже растрового кадра 430 000), остаток разобран в
|
|
||||||
[`perf_green_phase.md`](perf_green_phase.md) §3 (нужен раскол `draw_tile` на
|
|
||||||
узкие части, как в оригинале) и в идее G8 (сузить инвалидацию соседнего тайла
|
|
||||||
до 28-пиксельной полосы).
|
|
||||||
|
|
||||||
Где что расходуется и как это чинить — в фазовых документах:
|
|
||||||
[зелёная](perf_green_phase.md), [циан](perf_cyan_phase.md).
|
|
||||||
|
|
||||||
### Общий вывод по пиковому кадру базового замера (1 437 150)
|
|
||||||
|
|
||||||
| | такты | доля |
|
|
||||||
|---|---:|---:|
|
|
||||||
| блиты (все 27–28 вызовов `pop_blit_b`) | 488 100 | 34 % |
|
|
||||||
| из них «железный» минимум пикселей (модель `198*h + 5,96*w*h`) | ~150 000 | 10 % |
|
|
||||||
| синяя (ввод + heal + логика) | 142 700 | 10 % |
|
|
||||||
| **наши накладные: `draw_tile`, IX-кадры, диспетчер, пометки** | **~1 150 000** | **~80 %** |
|
|
||||||
|
|
||||||
Узкое место — НЕ передача пикселей (она на пределе железа, 3+3 такта на байт,
|
|
||||||
memory `blit_cost_model`), а 16-битная арифметика в стековых кадрах.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Габариты спрайтов: можно ли всё перевести на `uint8_t`
|
|
||||||
|
|
||||||
Просканированы каталоги ВСЕХ `.atl` (109 файлов) и исходные PNG наборов
|
|
||||||
`TITLE`/`PV` — тех, что понадобятся для интро, финала и роликов между
|
|
||||||
уровнями.
|
|
||||||
|
|
||||||
**Игровой кадр — весь укладывается в байт:**
|
|
||||||
|
|
||||||
| набор | максимум |
|
|
||||||
|---|---|
|
|
||||||
| фон подземелья/дворца (`*_env*`, `*_wall`, `*_fore`, `pop_pot`) | **48 × 63** |
|
|
||||||
| Кид (`kid0..27`, `sword`) | **56 × 63** (kid3, idx 0) |
|
|
||||||
| страж / скелет / Джафар | **53 × 42** |
|
|
||||||
| спрайты комнаты принцессы (`PV.DAT`: персонажи, песочные часы, факел, звёзды) | **49 × 60** |
|
|
||||||
|
|
||||||
**Больше 255 — только полноэкранные подложки титров и сюжетных экранов.**
|
|
||||||
Их восемь, и все рисуются ОДИН раз при показе экрана:
|
|
||||||
|
|
||||||
| ресурс | размер | где (`data.h`, `full_image[]`) |
|
|
||||||
|---|---|---|
|
|
||||||
| `TITLE/res51` | 320 × 200 | `TITLE_MAIN`, xpos 0 ypos 0 |
|
|
||||||
| `TITLE/res41` | 320 × 200 | `STORY_FRAME`, xpos 0 ypos 0 |
|
|
||||||
| `PV/res951` | 320 × 200 | фон комнаты принцессы (`chtab_9_princessbed`) |
|
|
||||||
| `TITLE/res42..res45` | 272 / 267 / 264 / **256** × 134..142 | «presents», «Prince of Persia», «Mechner» |
|
|
||||||
| `TITLE/res54` | 272 × 65 | заголовок Hall of Fame |
|
|
||||||
|
|
||||||
Высота нигде не превышает 200 — в байт лезет. По ширине не лезут ровно эти
|
|
||||||
восемь, и ни одна из них не участвует в игровом кадре.
|
|
||||||
|
|
||||||
**Вывод: горячий путь можно переводить на 8-битные габариты целиком.**
|
|
||||||
Для подложек — решение пользователя (2026-08-17): работу с роликами вынести в
|
|
||||||
отдельный банк с версиями блита под большие спрайты либо звать libbgi напрямую
|
|
||||||
— клип и проверка выхода за экран им не нужны (рисуются в x = 0/24/48/96,
|
|
||||||
заведомо внутри 320×200). Ширина 320 всё равно потребует ДВУХ burst-скобок
|
|
||||||
акселератора на строку — как уже сделано в `pop_vflip`.
|
|
||||||
|
|
||||||
Существующая страховка уже есть и остаётся: `pop_blit_b` уводит кадр с
|
|
||||||
`img[1] | img[3] != 0` на общий путь `blit_b_oversize`.
|
|
||||||
|
|
||||||
**Регресс после дня оптимизации 11/15 (`d0ac4b1`, 2026-08-19), 2367 кадров:**
|
|
||||||
|
|
||||||
| максимум по секции | эталон `mob-order-B-done` | сейчас | разница |
|
|
||||||
|---|---:|---:|---:|
|
|
||||||
| работа | 913 848 | **911 862** | −1 986 |
|
|
||||||
| синяя | 159 810 | **149 106** | −10 704 |
|
|
||||||
| зелёная | 440 418 | **436 494** | −3 924 |
|
|
||||||
| циан | 393 000 | **382 770** | −10 230 |
|
|
||||||
|
|
||||||
Период: **3 растра в 2341 кадре, 4 в 23, 5 в 2** — как в эталоне.
|
|
||||||
|
|
||||||
Почему сумма минусов по фазам не равна минусу по работе: максимумы разных
|
|
||||||
фаз достигаются В РАЗНЫХ КАДРАХ (пик синей — не тот кадр, где пик зелёной),
|
|
||||||
а «работа» здесь — максимум СУММЫ, а не сумма максимумов.
|
|
||||||
|
|
||||||
Что из правок 11/15 сюда дошло: P16 и P2b дали синюю и циан (они про
|
|
||||||
проверки и луч видимости, а те работают в любой сцене), HEAL-WIDTH дал
|
|
||||||
зелёную (плита 64 → 58 на шести heal'ах кадра).
|
|
||||||
|
|
||||||
**Зелёная по-прежнему выше растрового кадра** (436 494 против 430 000).
|
|
||||||
Главный оставшийся кандидат именно для этой сцены — **P9 (G8)**: при
|
|
||||||
падении плиты помечаются ДВА тайла, и соседний перезапекается целиком и
|
|
||||||
повторно (`draw_tile` соседа дважды на одну пометку), хотя потревожены у
|
|
||||||
него только левые 28 пикселей. При шести падающих плитах это умножается
|
|
||||||
на шесть.
|
|
||||||
|
|
||||||
**ВАЖНО ДЛЯ ПРОЦЕССА.** Этот прогон вскрыл регрессию, которую не поймали
|
|
||||||
ни хост-тесты, ни сцена 11/15: гейт `loose_any` (позиция P5) не взводился
|
|
||||||
в `check_fall_flo`, и плиты уровня 13 дрожали, не падая. Сцену 13/23 надо
|
|
||||||
прогонять после КАЖДОЙ правки loose-механики, а не только когда меняешь её
|
|
||||||
сознательно.
|
|
||||||
@@ -1,871 +0,0 @@
|
|||||||
# Реестр оптимизаций: всё отложенное, в одном списке
|
|
||||||
|
|
||||||
Собрано 2026-08-19 из [`perf_green_phase.md`](perf_green_phase.md) (G1-G9),
|
|
||||||
[`perf_cyan_phase.md`](perf_cyan_phase.md) (C1-C7),
|
|
||||||
[`perf_backlog.md`](perf_backlog.md) (позиции 1-7),
|
|
||||||
[`../roomtest/TASKS_OPEN.md`](../roomtest/TASKS_OPEN.md) (HEAL-WIDTH) и из
|
|
||||||
свежего разбора сцены [`perf_l11_room15.md`](perf_l11_room15.md).
|
|
||||||
|
|
||||||
**База для процентов — работа кадра в 11/15: 801 768 тактов** (замер
|
|
||||||
`09f32ce`). Где эффект относится к другой сцене, это сказано явно.
|
|
||||||
|
|
||||||
Оценки помечены: **[замер]** — измерено; **[модель]** — посчитано по
|
|
||||||
измеренным составляющим; **[гипотеза]** — не мерено, нужен прогон.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Цена одного блита фона — разложена [замер 2026-08-19]
|
|
||||||
|
|
||||||
Метод: брейкпоинты на резидентных адресах внутри `pop_blit_b` (0x5C50),
|
|
||||||
`temp0` на входе, разница `totalcycles` на каждом вызове. 1603 блита.
|
|
||||||
|
|
||||||
| этап | такты | постоянство |
|
|
||||||
|---|---:|---|
|
|
||||||
| пролог + аргументы + грубый отсев | **810** | ровно, всегда |
|
|
||||||
| `atlas_image` | **672** | ровно, всегда |
|
|
||||||
| `gfx_w0_map` + чтение шапки ленты + арифметика клипа | **2 400** | ровно, всегда |
|
|
||||||
| ядро блита (пиксели) | 4 380 … 26 592 | по размеру кадра |
|
|
||||||
| `pop_cd_touch` | **2 069** (пакетный путь) / 4 115 (настоящая пометка) | почти ровно |
|
|
||||||
| `gfx_w0_unmap` + эпилог | **175** | ровно, всегда |
|
|
||||||
| **весь блит** | 10 458 … 32 718, медиана **16 674** | |
|
|
||||||
|
|
||||||
**Фиксированная накладная = 6 126 тактов на любой блит, хоть 8×8.**
|
|
||||||
У самого дешёвого блита (10 458) это **59 % цены**; у пламени факела 16×18
|
|
||||||
пиксели тянут ~1 700 из ~14 000, то есть **12 %**.
|
|
||||||
|
|
||||||
Это и есть ответ на вопрос «почему маленький блит стоит 14 000». Причины
|
|
||||||
ровно те, о которых спрашивал пользователь:
|
|
||||||
|
|
||||||
- **810 на пролог** — `call ___sdcc_enter_ix`, IX-фрейм и шесть чтений
|
|
||||||
`N(ix)`: третий и четвёртый аргументы (`int x`, `int ybottom`) идут
|
|
||||||
СТЕКОМ, каждое обращение 19 тактов Z80;
|
|
||||||
- **2 069 на `pop_cd_touch`** даже по пакетному пути, где вся работа — четыре
|
|
||||||
сравнения. Сигнатура `(int x, int y, int w, int h)` = 8 байт аргументов,
|
|
||||||
два из них через стек; `w`/`h` никогда не больше 64, `y` не больше 255,
|
|
||||||
то есть три из четырёх могли быть `uint8_t`;
|
|
||||||
- **2 400 на map + шапку** — `gfx_w0_map` (1 086) + `unmap` (264, платится в
|
|
||||||
конце) + ~1 000 на чтение четырёх байт заголовка и арифметику;
|
|
||||||
- **672 на `atlas_image`** — маппинг W3, чтение записи каталога, возврат W3;
|
|
||||||
размеры при этом читаются и выбрасываются (см. §3, C6).
|
|
||||||
|
|
||||||
**Блитов за кадр в 11/15: 8** [замер] — все в зелёной фазе (6 на `draw_tile`
|
|
||||||
чомпера, 2 на пламя факелов). Синяя и циан через `pop_blit_b` не ходят
|
|
||||||
вовсе: heal и персонажи идут своими путями. Значит фиксированные накладные
|
|
||||||
блита стоят сцене **8 × 6 126 = 49 000 тактов/кадр (6,1 % работы)**.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Модель зелёной фазы 11/15 [модель, сходится с 320 916 замера]
|
|
||||||
|
|
||||||
| статья | такты | доля фазы |
|
|
||||||
|---|---:|---:|
|
|
||||||
| 8 блитов фона (из них 6 126×8 = 49 000 накладных) | 133 400 | 41 % |
|
|
||||||
| диспетчер `draw_tile` (один тайл чомпера) | 56 200 | 18 % |
|
|
||||||
| цикл `pop_process_trobs` без блитов пламени | 59 100 | 18 % |
|
|
||||||
| `pop_loose_tick` (плит в комнате НЕТ) | 28 872 | 9 % |
|
|
||||||
| heal 32×64 в `pop_chomp_redraw` | 34 000 | 11 % |
|
|
||||||
| шов / ворота соседа | 9 336 | 3 % |
|
|
||||||
|
|
||||||
Больше половины фазы — не пиксели, а обвязка вокруг них.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Список приёмов, отсортированный по эффекту
|
|
||||||
|
|
||||||
### P1. Чомпер: перерисовка неизменной позы ✅ СДЕЛАНО 2026-08-19 — −110 802
|
|
||||||
|
|
||||||
**Диагноз, с которым позиция заводилась, оказался неполным.** Я приписал
|
|
||||||
190 260 тактов пометке от факела; зонд `pop_dbg_kind` показал, что все 312
|
|
||||||
перерисовок прогона — вид `POP_RD_CHOMP` (полная), а пометку соседа она
|
|
||||||
просто перебивала. Настоящая причина нашлась сверкой с `animate_chomper`
|
|
||||||
(seg007:0448): оригинал перерисовывает чомпер **только при фазе < 6**, пять
|
|
||||||
кадров из пятнадцати, потому что с фазы 5 поза не меняется
|
|
||||||
(`chomper_fram1 = {3,2,0,1,4,3,3}`). Мы метили тайл каждый кадр.
|
|
||||||
|
|
||||||
Сделано три вещи: условие фазы (с пометкой обеих страниц на фазе 5), новый
|
|
||||||
вид `POP_RD_CHOMP_ANIM` → `pop_chomp_anim_draw` (три блита поверх огня, порт
|
|
||||||
ветки `redraw_frames_anim`) и приоритет полной перерисовки над anim в
|
|
||||||
`pop_set_redraw`. Обе половины работают: замер даёт 40 % полных
|
|
||||||
перерисовок и 60 % лёгких.
|
|
||||||
|
|
||||||
Работа 768 684 → **657 882** (медиана), зелёная 294 510 → **183 420**.
|
|
||||||
В 40 % кадров цена осталась прежней — там поза действительно меняется, и это
|
|
||||||
уже честная работа; резать её дальше только через P7 (раскол `draw_tile`)
|
|
||||||
или P8 (ширина heal).
|
|
||||||
|
|
||||||
Полный разбор — [`perf_l11_room15.md`](perf_l11_room15.md) §6.
|
|
||||||
|
|
||||||
<details><summary>Исходная (неполная) постановка</summary>
|
|
||||||
|
|
||||||
Разбор в [`perf_l11_room15.md`](perf_l11_room15.md) §3. Сейчас пометка от
|
|
||||||
факела обрабатывается как `heal 32×64 + полный draw_tile` (190 260 тактов на
|
|
||||||
единственный перерисованный тайл кадра); оригинал в этом случае рисует
|
|
||||||
ТОЛЬКО `draw_tile_anim` — графику чомпера поверх свежего пламени.
|
|
||||||
|
|
||||||
Останется 1-2 блита челюстей ≈ 20 000-33 000. **Риск низкий**: это
|
|
||||||
сближение с оригиналом, а не отход от него. Побочно снимает широкую пометку
|
|
||||||
«фон трогали» вокруг тайла чомпера — см. P3.
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
### P2. Синяя фаза разложена [ЗАМЕР 2026-08-19] — гипотеза не подтвердилась
|
|
||||||
|
|
||||||
Замер зондами внутрь обеих половин синей (286 518 тактов):
|
|
||||||
|
|
||||||
| участок | такты | доля работы |
|
|
||||||
|---|---:|---:|
|
|
||||||
| ввод + читы | 14 154 | 1,8 % |
|
|
||||||
| три спецсобытия уровней (skel / mouse / killed_shadow) | **1 650** | 0,2 % |
|
|
||||||
| `pop_frame_timers` + **луч видимости стража** | **37 032** | 4,8 % |
|
|
||||||
| `pop_ctrl_tick` | 18 648 | 2,4 % |
|
|
||||||
| `skip_mask` + **heal двух Char** | **67 734** | 8,8 % |
|
|
||||||
| `mirror_heal` + `fore_heal` | 2 040 | 0,3 % |
|
|
||||||
| `kid_tick` (play_seq) | 10 098 | 1,3 % |
|
|
||||||
| **`pop_phys_tick`** (физика Кида) | **61 266** | 8,0 % |
|
|
||||||
| `pop_guard_tick` (логика стража) | 19 932 | 2,6 % |
|
|
||||||
| **`pop_guard_phys_tick`** (физика стража) | **43 980** | 5,7 % |
|
|
||||||
| боёвка (`sword_hurting` / `sword_hurt` / `delta_hp`) | 9 558 | 1,2 % |
|
|
||||||
| `guard_fallout` + уход из комнаты | 366 | — |
|
|
||||||
|
|
||||||
**Что оказалось не так, как ждали.** Я предполагал, что дорогие тут
|
|
||||||
банковые трамплины на спецсобытиях (по аналогии с `pop_clip_char_top`,
|
|
||||||
8 892 такта за трамплин ради одной проверки). Замер это отверг: три
|
|
||||||
спецсобытия уровней вместе стоят **1 650** — они гейтятся внутри и на
|
|
||||||
уровне 11 честно выходят сразу.
|
|
||||||
|
|
||||||
**Настоящие статьи — три:**
|
|
||||||
|
|
||||||
1. **Физика двух Char — 105 246** (61 266 + 43 980), при том что оба
|
|
||||||
персонажа СТОЯТ и кадр позы не меняется. Это 13,7 % работы кадра и
|
|
||||||
самая крупная статья синей. Нужен ещё один уровень разбора — внутрь
|
|
||||||
`pop_phys_tick` (позиция **P2a**, отдельным заходом).
|
|
||||||
2. **Луч видимости стража — до 37 032** (вместе с `pop_frame_timers`, но тот
|
|
||||||
заведомо копеечный: три счётчика). Считается КАЖДЫЙ кадр, хотя ни Кид,
|
|
||||||
ни страж не сдвинулись. Кандидат на гейт «пересчитывать только при
|
|
||||||
смене позиции или комнаты любого из двоих» — позиция **P2b**,
|
|
||||||
ожидание −30 000, риск низкий.
|
|
||||||
3. **heal двух Char — 67 734.** Отдельной правки не требует: он платится
|
|
||||||
ровно потому, что `skip_mask` никого не пропускает, и уйдёт вместе с
|
|
||||||
P1/P3.
|
|
||||||
|
|
||||||
Новое, найдено 2026-08-19.
|
|
||||||
|
|
||||||
### P15. Точность метки «фон трогали» ✅ СДЕЛАНО 2026-08-19 — −163 746 / −140 871
|
|
||||||
|
|
||||||
Постановка пользователя: не перерисовывать стража, пока он не двигается.
|
|
||||||
|
|
||||||
**Две правки, и вторая оказалась решающей:**
|
|
||||||
|
|
||||||
1. **метка**: вместо «маска колонок × три ряда по 63 px» — диапазон y на
|
|
||||||
каждую колонку (`ymin`/`ymax`, 40 байт на обе страницы). Прежняя
|
|
||||||
гранулярность склеивала пламя факела (y 33..50) с клинком стоящего
|
|
||||||
стража (y 59..65), между которыми девять пикселей зазора;
|
|
||||||
2. **проверка**: `cd_quiet` сверяет спрайт и накладной (клинок, брызги)
|
|
||||||
ДВУМЯ отдельными прямоугольниками вместо объединённого bbox.
|
|
||||||
Объединение включает пустой угол: спрайт стража в колонке 8, клинок
|
|
||||||
уходит в колонку 7 на y 59..65, пламя метит колонку 7 на y 33..50 —
|
|
||||||
и прямоугольник «спрайт + клинок» цеплял метку углом.
|
|
||||||
|
|
||||||
**Без второй правки первая дала почти ноль** (632 676 против 628 542 до
|
|
||||||
неё) — это стоит помнить: точность структуры бесполезна, пока запрос к ней
|
|
||||||
остаётся грубым.
|
|
||||||
|
|
||||||
| | лёгкая позиция | тяжёлая позиция |
|
|
||||||
|---|---:|---:|
|
|
||||||
| синяя | 259 050 → **223 902** | 257 520 → **245 808** |
|
|
||||||
| зелёная | 181 494 → 181 494 | 180 870 → 181 761 |
|
|
||||||
| циан | 194 262 → **59 406** | 319 842 → **192 090** |
|
|
||||||
| **работа** | 628 542 → **464 796** | 758 358 → **617 487** |
|
|
||||||
|
|
||||||
В тяжёлой позиции вдобавок исчезли пятирастровые кадры (было 27 %).
|
|
||||||
|
|
||||||
Проверено в MAME: статика чистая, динамика (пробежка, бой, переход в
|
|
||||||
соседнюю комнату) без хвостов и просвечивания; хост-тесты зелёные.
|
|
||||||
|
|
||||||
Побочно исправлены два собственных дефекта первой редакции: обе страницы
|
|
||||||
обновлялись по условию, проверяющему только страницу 0 (после
|
|
||||||
`pop_cd_clear(0)` метка второй переставала расти), и отсутствовала явная
|
|
||||||
инициализация — пустая колонка обозначается `ymin = 255`, а нули от crt0
|
|
||||||
читались бы как «затронута строка 0».
|
|
||||||
|
|
||||||
### P16. Цианные проверки ✅ СДЕЛАНО 2026-08-19 — −25 818
|
|
||||||
|
|
||||||
Раскладка остатка цианной фазы (59 406) показала, что 47 883 из них — три
|
|
||||||
вызова, а не отрисовка:
|
|
||||||
|
|
||||||
| вызов | было | стало |
|
|
||||||
|---|---:|---:|
|
|
||||||
| `pop_loose_mob_draw` | 978 | 978 (гейт `mobs_live` работает) |
|
|
||||||
| `guard_over_kid` | 14 424 | **0** |
|
|
||||||
| `pop_char_skip_mask` | 28 605 | ~21 000 |
|
|
||||||
|
|
||||||
Три правки:
|
|
||||||
|
|
||||||
1. **`cd_sig_same`** — сравнение снимка БЕЗ построения структуры.
|
|
||||||
`cd_sig_make` записывал тринадцать полей в стековый кадр (через
|
|
||||||
`-n(ix)`), и лишь потом шёл побайтовый цикл; теперь сравнение идёт прямо
|
|
||||||
с источником и выходит на первом расхождении. **−8 016**;
|
|
||||||
2. **`guard_over_kid` по условию** — вопрос «кто поверх кого» не имеет
|
|
||||||
смысла, когда не рисуется никто. Вызов перенесён ПОСЛЕ `skip_mask` и
|
|
||||||
идёт только при `skip != 3`. **−14 118**;
|
|
||||||
3. **`pop_cd_hit_slot`** — проверка «задет ли слот» брала пять аргументов,
|
|
||||||
три из них стеком (45 % тактов на IX). Теперь координаты берутся из
|
|
||||||
`pop_cd`, а сравнение вынесено в `hit_rect` с file-scope аргументами.
|
|
||||||
**−3 684**.
|
|
||||||
|
|
||||||
**Отрицательный результат внутри третьей правки** (не повторять): первая
|
|
||||||
версия была обёрткой, которая внутри всё равно звала `pop_cd_hit` с пятью
|
|
||||||
аргументами — стало ХУЖЕ (1799 тактов Z80 вместо 1318). Снимать аргументы
|
|
||||||
со стека надо у того, кто их читает, а не этажом выше.
|
|
||||||
|
|
||||||
### Отрицательные результаты 2026-08-19 — НЕ ПОВТОРЯТЬ
|
|
||||||
|
|
||||||
Три попытки подряд сделали ХУЖЕ. Общая ошибка в двух из них — я оценивал
|
|
||||||
правку по СУММЕ ТАКТОВ ИНСТРУКЦИЙ в листинге, а не по реально исполняемому
|
|
||||||
пути.
|
|
||||||
|
|
||||||
**1. `cd_sig_same` блоком вместо тринадцати сравнений.** Снимок был
|
|
||||||
переложен так, чтобы сравнивать непрерывные 10 байт начала `pop_char_t`
|
|
||||||
циклом `do { if (*a++ != *b++) return 0; } while (--i)`. По листингу
|
|
||||||
функция стала короче (1939 → 1290 тактов), а на машине **стало хуже:
|
|
||||||
438 978 → 450 426 (+11 448)**.
|
|
||||||
|
|
||||||
Причина: сумма по листингу считает каждую инструкцию ОДИН раз, а тело
|
|
||||||
цикла исполняется ДЕСЯТЬ раз. Тринадцать линейных сравнений выполняются по
|
|
||||||
разу каждое и выходят раньше на первом же расхождении. **Урок: короткий
|
|
||||||
листинг ≠ быстрый код; цикл надо разворачивать в уме.**
|
|
||||||
|
|
||||||
**2. `cd_touch_pb` — пометка «для блита» из file-scope.** `pop_cd_touch`
|
|
||||||
зовётся из `pop_blit_b` с четырьмя аргументами, хотя тот держит те же
|
|
||||||
значения в `pb_x`/`pb_top`/`pb_w`/`pb_h`. Специализированный вход без
|
|
||||||
аргументов дал **438 978 → 442 242 (+3 264)**.
|
|
||||||
|
|
||||||
Причина: в зелёной фазе блиты идут ПАКЕТНЫМ путём (`draw_tile` открывает
|
|
||||||
`pop_cd_batch`), а там нужны все четыре значения сразу — и в регистрах
|
|
||||||
(`x`, `y` приходят в HL/DE) они дешевле, чем чтение из статиков.
|
|
||||||
**Снятие аргументов со стека помогает не всегда: если значение и так живёт
|
|
||||||
в регистре, статик его туда ещё и загружать заставит.**
|
|
||||||
|
|
||||||
**3. Обёртка `pop_cd_hit_slot` поверх `pop_cd_hit`** (описана в P16):
|
|
||||||
внутри всё равно звала функцию с пятью аргументами и добавила свои — стало
|
|
||||||
1799 тактов вместо 1318. Помогло только когда сравнение переехало внутрь.
|
|
||||||
|
|
||||||
### P14. Fore-проход персонажа — от 4 122 до 117 570 [замеры 2026-08-19]
|
|
||||||
|
|
||||||
**Самая НЕСТАБИЛЬНАЯ статья кадра.** Замеры на одной и той же сцене:
|
|
||||||
|
|
||||||
| ситуация | fore-проход |
|
|
||||||
|---|---:|
|
|
||||||
| персонаж пропущен (`skip`) | 4 122 |
|
|
||||||
| стоящий страж | 62 778 |
|
|
||||||
| живой Кид у чомпера | ~89 000 |
|
|
||||||
| **труп Кида в челюстях** | **117 570** |
|
|
||||||
|
|
||||||
Растёт от двух вещей: ширины футпринта (широкий кадр смерти, вынутый меч
|
|
||||||
добавляет колонку) и числа тайлов с передним слоем внутри футпринта (здесь
|
|
||||||
чомпер со своими зубьями). Отсюда практический вывод: **в бою проход будет
|
|
||||||
ближе к сотне тысяч, чем к шестидесяти** — кадры выпадов и ударов широкие.
|
|
||||||
|
|
||||||
Замер трупа сделан по просьбе пользователя. Сама по себе эта ситуация не
|
|
||||||
игровая («когда Кид — труп, игры нет»), но именно она показала верхнюю
|
|
||||||
границу цены.
|
|
||||||
|
|
||||||
**РАЗБОР 2026-08-19: P14 сводится к P4.** Fore-проход Кида в тяжёлой
|
|
||||||
позиции (89 268) разложен зондами:
|
|
||||||
|
|
||||||
| участок | такты |
|
|
||||||
|---|---:|
|
|
||||||
| вход + `pop_fore_set_clip` + `char_footprint` | 10 872 |
|
|
||||||
| арифметика границ окна | 3 786 |
|
|
||||||
| шов ворот + overlay-цикл | 3 294 |
|
|
||||||
| **цикл `fore_tile` по тайлам** | **67 854** (76 %) |
|
|
||||||
| `pop_gate_over_char` + хвост | 3 462 |
|
|
||||||
|
|
||||||
А счётчик показал, что цикл обходит **всего 4 тайла**, и 3 из них реально
|
|
||||||
рисуют (`FORE_ANY != 0`). То есть 67 854 — это НЕ перебор лишних тайлов
|
|
||||||
(их четыре) и не проверки, а **цена самих блитов переднего слоя**: около
|
|
||||||
четырёх блитов по ~16 000, из которых 6 765 на каждом — фиксированная
|
|
||||||
накладная (см. §1).
|
|
||||||
|
|
||||||
**Отсюда вывод для плана:** отдельной «оптимизации fore-прохода» почти нет.
|
|
||||||
Срезать там можно ровно три вещи, и только первая крупная:
|
|
||||||
|
|
||||||
1. **цену блита (P4)** — 4 блита × 6 765 накладных = ~27 000 из 67 854;
|
|
||||||
2. `char_footprint` из физики (**P10**) — часть от 10 872;
|
|
||||||
3. слияние двух трамплинов в банк 2 — ~4 000.
|
|
||||||
|
|
||||||
Иначе говоря, **P4 ускоряет и зелёную фазу (8 блитов), и fore-проход
|
|
||||||
(4 блита), то есть работает и в статике, и в динамике** — в отличие от
|
|
||||||
P14, который я считал самостоятельной позицией.
|
|
||||||
|
|
||||||
`pop_char_fore` = два трамплина в банк 2 (`pop_fore_set_clip` +
|
|
||||||
`pop_fore_over_char`) плюс обход тайлов футпринта, в каждом `fore_tile`.
|
|
||||||
У Кида дороже, чем у стража, потому что в его футпринте лежит чомпер, а у
|
|
||||||
чомпера есть собственный передний слой (`POP_CHOMP_FRAM_FOR`), который
|
|
||||||
перерисовывается поверх персонажа каждый кадр.
|
|
||||||
|
|
||||||
Что можно пробовать, по возрастанию радикальности:
|
|
||||||
|
|
||||||
1. слить два трамплина в один вызов (мелочь, ~4 000);
|
|
||||||
2. **P10** — брать футпринт из физики, а не считать заново (−11 574 на
|
|
||||||
проход, то есть до −23 000 на двоих);
|
|
||||||
3. гейт по сигнатуре: пропускать проход, если не изменились ни кадр
|
|
||||||
персонажа, ни тайлы его футпринта. **Это расхождение с оригиналом** —
|
|
||||||
он рисует foretable безусловно;
|
|
||||||
4. **P13** — objtable и отложенные таблицы: у оригинала «посетить тайл»
|
|
||||||
стоит копейки именно потому, что таблицы только копят записи.
|
|
||||||
|
|
||||||
### P3. Персонажи будятся каждый кадр ✅ ЧАСТИЧНО СБЫЛОСЬ
|
|
||||||
|
|
||||||
**В лёгкой позиции — да:** после P1 `pop_char_draw(KID)` стоит 204 такта,
|
|
||||||
метка от чомпера до Кида больше не дотягивается.
|
|
||||||
|
|
||||||
**В тяжёлой позиции — нет:** стоит Киду шагнуть на 7 пикселей вправо, и его
|
|
||||||
спрайт пересекается с тайлом чомпера и пламени, `skip_mask` перестаёт
|
|
||||||
пропускать, и он снова стоит ~144 000 (54 738 draw + ~89 000 fore). То
|
|
||||||
есть выигрыш P3 держится только пока персонаж не подошёл к анимированному
|
|
||||||
тайлу — а в игре он к нему подходит постоянно.
|
|
||||||
|
|
||||||
Исходная оценка (−70 000) была:
|
|
||||||
|
|
||||||
heal 141 048 + отрисовка 148 566 = 290 000 тактов (36 % работы) уходят на то,
|
|
||||||
что `pop_char_skip_mask` не может пропустить ни Кида, ни стража: фон трогают
|
|
||||||
каждый кадр.
|
|
||||||
|
|
||||||
- **Кида** спасает P1: широкая пометка вокруг чомпера исчезнет;
|
|
||||||
- **стража спасти нельзя** — пламя правого факела (0,7) рисуется в ячейке
|
|
||||||
(0,8), где он и стоит. Там фон честно меняется, и оригинал персонажа тоже
|
|
||||||
перерисовывает.
|
|
||||||
|
|
||||||
### P17. 16 бит там, где хватает 8 ✅ 2026-08-19 (замечание пользователя)
|
|
||||||
|
|
||||||
**1. Границы экрана — беззнаковыми сравнениями.** Проверка «спрайт целиком
|
|
||||||
на экране» стояла как четыре ЗНАКОВЫХ 16-битных сравнения, а знаковое у
|
|
||||||
SDCC z80 разворачивается в `sbc` плюс `jp PO / xor 0x80 / jp P`.
|
|
||||||
Беззнаковая форма делает то же двумя: отрицательная координата становится
|
|
||||||
очень большой и проваливает условие так же, как `>= 0`. **−378.**
|
|
||||||
|
|
||||||
**2. Габариты спрайтов в байтах.** `w`/`h`, `ow`/`oh`, `fpw`/`fph`,
|
|
||||||
`cw`/`ch` в `pop_cdraw_t`, параметры `cd_overlay_add`/`cd_clip_add`, локали
|
|
||||||
в `pop_char_draw`/`cd_splash` и чтение габарита из шапки ленты были
|
|
||||||
`uint16_t`, хотя спрайты атласов не крупнее 64×64 (memory
|
|
||||||
`pop_sprite_size_limits`). **−276 в статике, −1 134 в циане динамики**,
|
|
||||||
плюс 24 байта `_DATA`.
|
|
||||||
|
|
||||||
### P6a. Кэш указателя модификаторов ✅ 2026-08-19 — −840 (ждали −20 000)
|
|
||||||
|
|
||||||
`pop_trob_modif` объявлен `__banked`, а звался на КАЖДЫЙ trob внутри цикла
|
|
||||||
`pop_process_trobs`, хотя комната у них в подавляющем большинстве кадров
|
|
||||||
одна. Указатель теперь кэшируется между итерациями.
|
|
||||||
|
|
||||||
Цикл trobs 78 726 → **74 964**, работа кадра 438 324 → **437 484**.
|
|
||||||
|
|
||||||
**Оценка в реестре была завышена в двадцать раз**, и стоит понять почему:
|
|
||||||
я перенёс её по аналогии с лучом видимости (P2b), где трамплин звался
|
|
||||||
ДЕВЯТЬ раз за кадр. Здесь trob'ов в комнате всего несколько, и кэш
|
|
||||||
экономит два-три вызова. **Урок: «тот же паттерн» не означает «тот же
|
|
||||||
порядок величины» — считать надо число вызовов, а не узнавать шаблон.**
|
|
||||||
|
|
||||||
### P6b. Кэш префетча кодов тайлов — НЕ ДЕЛАЛОСЬ
|
|
||||||
|
|
||||||
Префетч (`pop_level_access_begin/end` плюс чтение кода на каждый trob)
|
|
||||||
стоит **11 058** за кадр. Кэшировать мешает инвалидация: код тайла меняет
|
|
||||||
`pop_level_set_tile` (кнопка → пол, loose → empty), вход в комнату и
|
|
||||||
добавление trob'а — пропустить хоть один источник значит получить
|
|
||||||
застывшую анимацию. С учётом того, что P6a дал 840 вместо 20 000,
|
|
||||||
ожидаемый выигрыш тут тоже стоит считать скромным, а риск он несёт
|
|
||||||
несоразмерный.
|
|
||||||
|
|
||||||
### P10. Футпринт из физики — РАЗБОР 2026-08-19 (без реализации)
|
|
||||||
|
|
||||||
Идея из `perf_backlog.md` §1: `redraw_at_char` (seg003:0430) берёт ГОТОВЫЕ
|
|
||||||
`char_col_left/right`, `char_top_row`, `char_bottom_row`, посчитанные в том
|
|
||||||
же кадре физикой (`set_char_collision`, seg006:0723), а наш
|
|
||||||
`char_footprint` (pop_bg.c) считает их заново внутри fore-прохода.
|
|
||||||
|
|
||||||
**Разбор показал, что «просто передать» не получится: величины разные.**
|
|
||||||
|
|
||||||
| | `char_footprint` (банк 2, fore) | `set_char_collision` (банк 3, физика) |
|
|
||||||
|---|---|---|
|
|
||||||
| ширина | габарит КАДРА `w` из атласа, `wh = (w+1)/2` | то же `fpw`, но затем **`FRAME_THIN` сдвигает края на ±4** |
|
|
||||||
| меч | расширяет диапазон на колонку (`sword >= DRAWN`) | не расширяет |
|
|
||||||
| колонки | `cLraw` до клампа (нужен для шва), затем кламп 0..9 | `coll_xl`/`coll_xr` в пикселях, колонки считает уже `calc_coll_window` |
|
|
||||||
| ряды | `rT`/`rB` от ВЕРХА и НИЗА спрайта, с форсом `rT = rB-1` | `Char.curr_row` — опорный ряд, это другое |
|
|
||||||
|
|
||||||
То есть у оригинала обе задачи пользуются ОДНИМИ величинами, потому что он
|
|
||||||
считает их один раз в `set_char_collision`. У нас они исторически
|
|
||||||
разошлись: коллизии считают своё окно (с поправкой `FRAME_THIN`), fore —
|
|
||||||
своё (габарит кадра плюс колонка под меч).
|
|
||||||
|
|
||||||
**Значит P10 — это не «передать готовое», а сперва СВЕСТИ обе величины к
|
|
||||||
одной, как в оригинале.** Работа не механическая: `FRAME_THIN` влияет на
|
|
||||||
коллизии осознанно (узкие кадры не должны цеплять стену), а fore-проходу
|
|
||||||
нужен полный габарит, иначе передние грани в крайней колонке не
|
|
||||||
перерисуются.
|
|
||||||
|
|
||||||
**Чего не хватает для решения:** отдельного замера самого
|
|
||||||
`char_footprint`. Сейчас известно только «вход + `pop_fore_set_clip` +
|
|
||||||
`char_footprint` = 10 872», а оценка −11 574 в backlog взята из старого
|
|
||||||
замера другой сборки. Первым шагом нужен зонд между `set_clip` и
|
|
||||||
`char_footprint`.
|
|
||||||
|
|
||||||
**Оценка приоритета:** низкая. Даже если `char_footprint` окажется всеми
|
|
||||||
10 872, он платится только когда персонаж рисуется (в статике fore-прохода
|
|
||||||
нет), а сведение двух геометрий к одной — это риск для коллизий, то есть
|
|
||||||
для физики, которая сейчас работает правильно.
|
|
||||||
|
|
||||||
### P18. Метка «фон трогали» огрублена по X — ОТЛОЖЕНО (решение пользователя)
|
|
||||||
|
|
||||||
**Найдено 2026-08-19 пользователем:** Кид перерисовывается, хотя с пламенем
|
|
||||||
не пересекается; на пиксель левее — перестаёт.
|
|
||||||
|
|
||||||
Разбор по памяти машины. Кид `x = 156`, спрайт занимает **x 213..224**,
|
|
||||||
экранные y 43..83. Метка колонки 7 — y 33..50 (пламя правого факела).
|
|
||||||
Колонка считается как `x >> 5`, то есть по 32 пикселя, и спрайт достаёт до
|
|
||||||
224 — ровно первый пиксель колонки 7. По вертикали пересечение с меткой
|
|
||||||
настоящее (43..50), поэтому слот считается задетым.
|
|
||||||
|
|
||||||
А по горизонтали пересечения НЕТ: пламя лежит в колонке 7 на x 232..247,
|
|
||||||
между ним и Кидом восемь пикселей зазора. На пиксель левее спрайт
|
|
||||||
кончается на 223, `223 >> 5 = 6`, колонка 7 не задета — и перерисовка
|
|
||||||
пропадает.
|
|
||||||
|
|
||||||
То есть P15 исправил огрубление по Y и оставил его по X.
|
|
||||||
|
|
||||||
**Почему отложено (аргументы пользователя):**
|
|
||||||
|
|
||||||
- x лежит в 0..319 и в байт не влезает — нужен `uint16_t` на границу, то
|
|
||||||
есть 4 байта на колонку (80 байт на две страницы), и **16-битные
|
|
||||||
сравнения в горячем пути**. А они у SDCC z80 дороги ровно настолько,
|
|
||||||
что могут съесть весь выигрыш (см. отрицательные результаты выше);
|
|
||||||
- огрубить x вдвое (`x >> 1`, диапазон 0..159 влезает в байт) — это лишний
|
|
||||||
сдвиг и при записи, и при проверке, плюс точность падает до 2 пикселей.
|
|
||||||
|
|
||||||
**Непроверенная идея на будущее:** хранить границы НЕ в экранных x, а как
|
|
||||||
смещение ВНУТРИ колонки (0..31, пять бит). Тогда байта хватает и сравнение
|
|
||||||
8-битное, но запись усложняется: прямоугольник, пересекающий несколько
|
|
||||||
колонок, даёт частичные диапазоны у крайних и полные у средних.
|
|
||||||
|
|
||||||
**Когда браться:** если после других позиций бюджет всё ещё не сойдётся.
|
|
||||||
Выигрыш будет именно в пограничных положениях, а их в игре много —
|
|
||||||
персонаж почти всегда стоит рядом с чем-то анимированным.
|
|
||||||
|
|
||||||
### P4. Накладные блита — ОТКАЧЕНО
|
|
||||||
|
|
||||||
**Правка сделана и отменена по решению пользователя.** Критерий: если
|
|
||||||
выигрыш получен ценой сильно усложнённого кода — откатывать.
|
|
||||||
|
|
||||||
Что было: `atlas_image_w0` в libbgi читал каталог из уже подключённой в W0
|
|
||||||
страницы. **−408 на кадре** при ожидании −5 400.
|
|
||||||
|
|
||||||
Почему откачено: цена — вторая публичная функция в API libbgi с НЕЯВНЫМ
|
|
||||||
контрактом («страница обязана быть подключена до вызова»), которую легко
|
|
||||||
вызвать неправильно и молча получить мусор, плюс дублирование чтения
|
|
||||||
каталога. 408 тактов — 0,09 % кадра, меньше разброса между прогонами.
|
|
||||||
|
|
||||||
**Что осталось знанием:** сам `gfx_w0_map` стоит всего **324** такта, а 672
|
|
||||||
у `atlas_image` — это почти целиком вызов функции и арифметика `idx * 8`.
|
|
||||||
Значит непробованная часть P4 («один map на группу блитов») имеет потолок
|
|
||||||
~2 600 за кадр, а не 10 000, как считалось.
|
|
||||||
|
|
||||||
Ожидание было −5 400 (672 такта × 8 блитов зелёной фазы), и оно НЕ
|
|
||||||
оправдалось: цена блита 16 107 → 16 005, то есть −102. Причина в том, что
|
|
||||||
эти 672 — почти целиком вызов функции и арифметика `idx * 8`, а не само
|
|
||||||
переключение окна. Замер после правки показывает, что работа просто
|
|
||||||
переехала между статьями:
|
|
||||||
|
|
||||||
| этап | до | после |
|
|
||||||
|---|---:|---:|
|
|
||||||
| пролог + отсев | 810 | 762 |
|
|
||||||
| `gfx_w0_map` | (в составе 2 400) | **324** |
|
|
||||||
| каталог + шапка ленты + клип | | **2 694** |
|
|
||||||
| ядро | 8 508 | 8 508 |
|
|
||||||
| `cd_touch` + `unmap` + эпилог | 2 883 | 2 883 |
|
|
||||||
| **фиксированная накладная** | **6 765** | **6 663** |
|
|
||||||
|
|
||||||
Правка оставлена: не вредит, убирает лишнее переключение W3 и делает
|
|
||||||
контракт честнее (страница мапится один раз). Но как способ снять
|
|
||||||
накладные она не работает.
|
|
||||||
|
|
||||||
**Что осталось непробованным** (и во что я теперь верю меньше): один
|
|
||||||
`gfx_w0_map` на ГРУППУ блитов — судя по замеру, сам map стоит 324, так что
|
|
||||||
потолок этой правки ~2 600 за кадр, а не 10 000, как считалось.
|
|
||||||
|
|
||||||
<details><summary>Исходная постановка (модель −28 000)</summary>
|
|
||||||
|
|
||||||
| правка | на блит | источник |
|
|
||||||
|---|---:|---|
|
|
||||||
| `pop_cd_touch`: `uint8_t` вместо `int` для `y`/`w`/`h`, ранний выход пакетного пути | ~−1 300 | новое |
|
|
||||||
| один `gfx_w0_map`/`unmap` на ГРУППУ блитов | ~−1 350 | C5 / backlog §3 |
|
|
||||||
| размеры ленты из каталога, без `atlas_image` и чтения шапки | ~−670 | C6 / backlog §2 |
|
|
||||||
| `pop_blit_b`: аргументы в 8 бит, где хватает | ~−400 | новое |
|
|
||||||
|
|
||||||
Все четыре — низкий риск, механическая работа. Вместе снимают ~3 700 из
|
|
||||||
6 126 фиксированных.
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
### P5. `pop_loose_tick` при пустой комнате — 28 872 → 2 760 ✅ СДЕЛАНО 2026-08-19
|
|
||||||
|
|
||||||
**Получено −26 112 внутри функции, −33 840 на кадре** (замер до/после в
|
|
||||||
11/15). Оценка была −28 000.
|
|
||||||
|
|
||||||
Раскладка холостого хода (замер зондами m9..m12) и что с ней стало:
|
|
||||||
|
|
||||||
| участок | было | стало |
|
|
||||||
|---|---:|---:|
|
|
||||||
| два цикла по тайлам (30 + 10 позиций) | 9 852 | **132** |
|
|
||||||
| `pop_loose_mob_tick` (обход 14 слотов) | 12 090 | **996** |
|
|
||||||
| `check_loose_fall_on_kid` (трамплин + обход) | 5 868 | **546** |
|
|
||||||
| вход + хвост | 1 062 | 1 086 |
|
|
||||||
| **итого** | **28 872** | **2 760** |
|
|
||||||
|
|
||||||
Сделано двумя гейтами:
|
|
||||||
|
|
||||||
- `loose_any` (статик `pop_map.c`) — «идёт ли анимация плит». Ставится в
|
|
||||||
пяти местах записи ненулевой фазы, снимается САМИМ циклом по факту
|
|
||||||
прохода, где не осталось ни одной живой фазы;
|
|
||||||
- `pop_mob_busy` (резидент `pop_state.c`) — «занят ли хоть один слот
|
|
||||||
падающего куска» (`active` или дочистка `clean`). Ставит `mob_alloc`,
|
|
||||||
снимает обход по факту пустой таблицы. В резиденте, а не в `pop_room.c`,
|
|
||||||
потому что читает его `pop_map` из банка 3.
|
|
||||||
|
|
||||||
**Важно про границу:** гейт отвечает не на «есть ли в комнате плиты», а на
|
|
||||||
«идёт ли анимация». У лежащей плиты-потолка фаза 0, и крутить нечего —
|
|
||||||
вопрос пользователя 2026-08-19. Асимметрия намеренная: ложная единица
|
|
||||||
стоит одного холостого прохода, ложный ноль — застывшей навсегда плиты,
|
|
||||||
поэтому взвод стоит рядом с КАЖДОЙ записью, а снятие только по факту.
|
|
||||||
|
|
||||||
Покрытие: `phys_loose_floor_breaks` (взвод от шага и сотрясения) и новый
|
|
||||||
`phys_loose_gate_survives_room_change` — на пятое место взвода
|
|
||||||
(фаза восстановлена входом в комнату), которое не покрывал никто.
|
|
||||||
Мутационная проверка: со снятым взводом тест падает.
|
|
||||||
|
|
||||||
### P6. `pop_process_trobs` разложен [ЗАМЕР 2026-08-19] — 89 784
|
|
||||||
|
|
||||||
| участок | такты |
|
|
||||||
|---|---:|
|
|
||||||
| вход + префетч кодов тайлов (маппинг окна 0) | **11 058** |
|
|
||||||
| цикл: два `pop_torch_draw` | ~36 000 |
|
|
||||||
| цикл: обход самих trob'ов | ~43 000 |
|
|
||||||
|
|
||||||
Цена одного `pop_pot_b` (пламя факела) измерена отдельно, брейкпоинтами на
|
|
||||||
резидентных адресах: **17 346 тактов**, и это ЕДИНСТВЕННАЯ группа в
|
|
||||||
распределении — то есть `pop_pot_b` в кадре зовут только два факела. При
|
|
||||||
канвасе пламени 16×18 сами пиксели там 1 716, то есть **10 % цены**; всё
|
|
||||||
остальное — накладные (см. §1) плюс ~6 900 сверх `pop_blit_b` на самом
|
|
||||||
`pop_pot_b`.
|
|
||||||
|
|
||||||
Направления:
|
|
||||||
|
|
||||||
- **P6a**: `pop_trob_modif(room)` зовётся банковым вызовом на КАЖДЫЙ trob
|
|
||||||
внутри цикла, хотя комната у них одна и та же — вынести наружу;
|
|
||||||
- **P6b**: префетч 11 058 маппит окно 0 каждый кадр, а коды тайлов trob'ов
|
|
||||||
меняются редко — кэшировать с инвалидацией по смене тайла/комнаты;
|
|
||||||
- **P6c**: цена факела — это цена блита, то есть позиция P4.
|
|
||||||
|
|
||||||
Новое, найдено 2026-08-19.
|
|
||||||
|
|
||||||
### P7. G5. Раскол `draw_tile` на узкие части [оценка дока: −50 000 … −60 000]
|
|
||||||
|
|
||||||
Диспетчер + контекст оплачиваются целиком всегда; у оригинала это девять
|
|
||||||
независимых функций. В 11/15 это те самые 56 200 на один тайл — но если
|
|
||||||
сделан P1, `draw_tile` чомпера вообще не вызывается, и здесь эффект пропадёт.
|
|
||||||
Ценность приёма — в ДРУГИХ сценах (13/23, любая комната с плитами).
|
|
||||||
|
|
||||||
**Риск средний**: в `draw_tile` собрано много инвариантов (BUG-LOOSE-3,
|
|
||||||
BUG-LATTICE-DOORTOP, BUG-SEAM-WEDGE-1) — только отдельным заходом с прогоном
|
|
||||||
всех уровней.
|
|
||||||
|
|
||||||
### P8. HEAL-WIDTH — ширина heal'ов по фактическому следу [оценка: 5-6 % цены heal'ов]
|
|
||||||
|
|
||||||
ОБЯЗАТЕЛЬНАЯ по решению пользователя (2026-08-18). След плиты 58 px в
|
|
||||||
подземелье / 57 во дворце против используемых 60 и 64.
|
|
||||||
Постановка — `TASKS_OPEN.md`, якорь `heal-width`.
|
|
||||||
|
|
||||||
### P9. G8 — пометку СОСЕДА ставить узкой полосой (28 px), а не тайлом [гипотеза]
|
|
||||||
|
|
||||||
Парная к HEAL-WIDTH. Относится к сценам с падающими плитами (13/23), в 11/15
|
|
||||||
не играет. Разбор — `perf_green_phase.md` §G8, там же три условия, из-за
|
|
||||||
которых это не «просто уменьшить число».
|
|
||||||
|
|
||||||
### P10. Футпринт персонажа — из физики, а не считать заново [замер: −11 574 на fore-проход]
|
|
||||||
|
|
||||||
`backlog` §1. С двумя персонажами — ~23 000 за кадр. Мешает то, что физика
|
|
||||||
(банк 3) держит `char_col_left/right` в статиках, а слой фона — банк 2.
|
|
||||||
**Риск средний**: окно fore-клипа заводилось под клинок и брызги.
|
|
||||||
|
|
||||||
### P11. Мелочи с известной ценой [замер, `backlog` §7]
|
|
||||||
|
|
||||||
| что | цена | где |
|
|
||||||
|---|---:|---|
|
|
||||||
| `pop_clip_char_top` — трамплин банк 4 → банк 3 ради одной проверки | 8 892 | `pop_cdraw.c` |
|
|
||||||
| `cd_sig_make` + возврат из `pop_char_draw` | 7 944 | `pop_cdraw.c` |
|
|
||||||
| `pop_loadkid` + расчёт координат кадра | 7 410 | `pop_cdraw.c` |
|
|
||||||
| `obj_x * 8 / 7` — последнее `__divsint` в горячем пути | ~2 400 | `pop_char_draw` |
|
|
||||||
|
|
||||||
### P12. G9 — снять временную оснастку [замер: −6 000]
|
|
||||||
|
|
||||||
`pop_dbg_b1..b6` в `pop_blit_b` (~400 на блит), `pop_dbg_kind`/`m16`,
|
|
||||||
`pop_dbg_m5..m15`, счётчик `rd_cnt` в `pop_redraw_needed`.
|
|
||||||
**Только ПОСЛЕ окончания оптимизации** — без них не мерить.
|
|
||||||
|
|
||||||
### P13. Крупные рефакторинги — брать, только если понадобится ещё запас
|
|
||||||
|
|
||||||
**Чем P13 НЕ является (вопрос пользователя 2026-08-19).** Это не «рисовать
|
|
||||||
комнату заново каждый кадр в скрытый буфер». Такой вариант исключён
|
|
||||||
арифметикой: 30 тайлов по 5-6 спрайтов при цене блита 16 674 (и 179 914 за
|
|
||||||
полную запечку одного тайла) дают порядка **3 000 000 тактов — семь
|
|
||||||
растровых кадров**. Оригинал так тоже не делает: у него та же
|
|
||||||
инкрементальная схема с пометками (`redraw_frames_full` / `_anim` /
|
|
||||||
`_fore`), перерисовываются только помеченные тайлы.
|
|
||||||
|
|
||||||
Разница не в объёме отрисовки, а в цене ПОСЕЩЕНИЯ тайла: у нас
|
|
||||||
`fore_tile(r, c)` сразу блитит (со всеми 6 126 фиксированных накладных), а
|
|
||||||
у оригинала `add_backtable`/`add_midtable`/`add_foretable` только кладут
|
|
||||||
запись в массив, и рисует один `draw_table()` в конце. Плюс у него ОДИН
|
|
||||||
обход тайлов за кадр против наших трёх.
|
|
||||||
|
|
||||||
- **C7 / backlog §5-6: objtable + отложенные таблицы back/mid/fore.** У
|
|
||||||
оригинала «посетить тайл» стоит копейки, потому что таблицы только копят
|
|
||||||
записи, а рисует один `draw_table()` в конце. У нас блит идёт сразу из
|
|
||||||
обхода, и fore-проход отдельный НА КАЖДОГО персонажа.
|
|
||||||
- **backlog §4: единый проход по тайлам вместо трёх** (`pop_redraw_needed`,
|
|
||||||
`pop_process_trobs`, `pop_fore_over_char`) и семь счётчиков причин
|
|
||||||
перерисовки вместо одного `kind`.
|
|
||||||
- **G6: меньше блитов в `RD_FLOOR`**, **G7: `mob_tick_one` в file-scope**.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. ПЛАН РАБОТ — состояние между сессиями
|
|
||||||
|
|
||||||
Рабочий чеклист. Правило: одна позиция = один заход = один коммит с замером
|
|
||||||
до/после на сцене 11/15. Замер обязателен даже когда «очевидно» — из семи
|
|
||||||
закрытых позиций ТРИ дали не то, что ожидалось (P1 — вдвое меньше, P2a —
|
|
||||||
почти ничего, таблицы порогов — регресс).
|
|
||||||
|
|
||||||
### Закрыто
|
|
||||||
|
|
||||||
| # | что | факт |
|
|
||||||
|---|---|---|
|
|
||||||
| P15 | точность метки «фон трогали» + раздельная проверка клинка | **−163 746** лёгкая / **−140 871** тяжёлая |
|
|
||||||
| P16 | цианные проверки: снимок без структуры, `guard_over_kid` по условию, `hit_slot` без пяти аргументов | **−25 818** |
|
|
||||||
| P5 | `loose_tick`: гейты холостого хода | **−33 840** (ждали −28 000) |
|
|
||||||
| P1 | чомпер: перерисовка только при фазе < 6 | **−110 802** (ждали −160 000) |
|
|
||||||
| P2b | луч видимости: колонки + один банковый вызов | **−26 448** (ждали −30 000) |
|
|
||||||
| P2a | `coll_scan` в 8 бит + снят с IX | **−2 892** (крупной статьи в физике нет) |
|
|
||||||
| P3 | Кид перестал будиться каждый кадр | сбылось само после P1 — но только в ЛЁГКОЙ позиции |
|
|
||||||
| P2/P6 | замеры синей фазы и `process_trobs` | гипотеза «трамплины на спецсобытиях» отвергнута |
|
|
||||||
| — | замер цианной фазы | крупного лишнего в отрисовке персонажа нет |
|
|
||||||
|
|
||||||
### Осталось, по убыванию ожидаемого эффекта
|
|
||||||
|
|
||||||
| # | что | ожидание | риск | комментарий |
|
|
||||||
|---|---|---:|---|---|
|
|
||||||
|
|
||||||
| P4 | накладные блита — 6 663 на КАЖДЫЙ блит | частично сделано: **−408** | низкий | из четырёх правок сработала слабо; разбор ниже |
|
|
||||||
| P14 | fore-проход персонажа | сводится к P4 + P10 | — | разбор ниже: цикл обходит всего 4 тайла |
|
|
||||||
| P11 | мелочи с известной ценой | −26 000 | низкий | `clip_char_top` 8 658 подтверждён замером |
|
|
||||||
| P10 | футпринт персонажа из физики | ? (нужен замер) | **высокий** | разбор ниже: величины физики и fore РАЗНЫЕ |
|
|
||||||
| P6a/P6b | `trob_modif` из цикла, кэш префетча | −20 000 | низкий | тот же паттерн трамплина в цикле |
|
|
||||||
| P7 | раскол `draw_tile` (G5) | −50 000 в 13/23 | средний | в 11/15 не играет |
|
|
||||||
| ~~P8~~ | HEAL-WIDTH | ✅ сделано: плита 64→58, чомпер 64→61 | — | эффект ждёт прогона 13/23 |
|
|
||||||
| P9 | G8 — пометка соседа полосой | не оценено | средний | для сцен с плитами |
|
|
||||||
| P13 | objtable + отложенные таблицы, единый проход по тайлам | не оценено | очень высокий | большой рефакторинг слоя фона |
|
|
||||||
| P12 | снять оснастку | −6 000 | нулевой | **последней**: без неё не мерить |
|
|
||||||
|
|
||||||
### Текущее состояние бюджета
|
|
||||||
|
|
||||||
| | работа | синяя | зелёная | циан | период |
|
|
||||||
|---|---:|---:|---:|---:|---:|
|
|
||||||
| до оптимизации | 801 768 | 293 238 | 320 916 | 187 758 | 4 растра |
|
|
||||||
| после P5 | 767 928 | 285 864 | 294 384 | 187 764 | 4 |
|
|
||||||
| после P1 (медиана) | 657 882 | 286 503 | 183 420 | 187 761 | 4 |
|
|
||||||
| после P2a | 654 990 | 283 215 | 183 798 | 187 812 | 4 |
|
|
||||||
| **после P2b (лёгкая позиция)** | **628 542** | 259 500 | 181 068 | 187 761 | 4 |
|
|
||||||
| ТЯЖЁЛАЯ позиция (Кид на шаг правее) | 758 358 | 257 520 | 180 870 | 319 842 | **4 и 5** |
|
|
||||||
| **после P15, лёгкая** | **464 796** | 223 902 | 181 494 | **59 406** | 4 |
|
|
||||||
| после P15, тяжёлая | 617 487 | 245 808 | 181 761 | 192 090 | **4 везде** |
|
|
||||||
| после P16, лёгкая | 438 978 | 218 052 | 181 494 | 39 438 | 4 |
|
|
||||||
| ~~после P4~~ | ~~438 570~~ | | | | правка **ОТКАЧЕНА** |
|
|
||||||
| после P17, лёгкая | 438 324 | 217 590 | 181 098 | 39 192 | 4 |
|
|
||||||
| **после P6a, лёгкая** | **437 484** | 217 704 | 180 252 | 39 306 | 4 |
|
|
||||||
| **после P17, тяжёлая** | **603 684** | 241 956 | 181 464 | 180 270 | 4 |
|
|
||||||
|
|
||||||
Итог восьми позиций: **801 768 → 464 796 в лёгкой позиции (−42 %)** и
|
|
||||||
**758 358 → 617 487 в тяжёлой (−19 %)**. Отдельно важно: в тяжёлой позиции
|
|
||||||
исчезли пятирастровые кадры (было 27 %), период стал ровно 4 — рывки ушли.
|
|
||||||
|
|
||||||
### Достижима ли цель — арифметика на 2026-08-19
|
|
||||||
|
|
||||||
Цель: работа ≤ 430 000, тогда период станет 3 растра (хвост кадра — три
|
|
||||||
`gfx_wait_vsync`).
|
|
||||||
|
|
||||||
- в ЛЁГКОЙ позиции снять надо **7 484**;
|
|
||||||
- в ТЯЖЁЛОЙ — **173 684**.
|
|
||||||
|
|
||||||
**Лёгких путей больше не осталось.** За 2026-08-19 отвергнуто ЧЕТЫРЕ
|
|
||||||
правки подряд (три с регрессом, одна почти без эффекта), и все они целили
|
|
||||||
в накладные проверок и блита. Фиксированная часть блита 6 663 держится
|
|
||||||
ядром `gfx_w0_map`/`cd_touch`/чтения шапки, а не «лишними» вызовами.
|
|
||||||
|
|
||||||
Всё оставшееся в списке, кроме P13, даёт по оценкам **порядка 100 000** — и
|
|
||||||
это оптимистично. **Арифметика не сходится:** сцена с двумя персонажами,
|
|
||||||
чомпером и двумя факелами в три растра не укладывается без одного из трёх
|
|
||||||
решений:
|
|
||||||
|
|
||||||
1. **P13** — переход на objtable и отложенные таблицы, как в оригинале
|
|
||||||
(единственный резерв нужного размера, но это переписывание слоя фона);
|
|
||||||
2. **осознанное расхождение с оригиналом** — например, не перерисовывать
|
|
||||||
передний слой персонажа, пока не изменились ни персонаж, ни тайлы под
|
|
||||||
ним (гейт по сигнатуре футпринта);
|
|
||||||
3. **принять 4 растра** как рабочий режим для сцен такой плотности и
|
|
||||||
выравнивать период, чтобы не было рывков 4/5.
|
|
||||||
|
|
||||||
Решение за пользователем — это выбор между точностью порта и скоростью.
|
|
||||||
|
|
||||||
### Как воспроизвести сцену (важно для следующей сессии)
|
|
||||||
|
|
||||||
Сборка стартует прямо в ней: `make` (дефолты `LEVEL=11 ROOM=15 POS=2`) →
|
|
||||||
`make hdd` → **полный рестарт MAME** (`chdman -f` даёт новый inode, memory
|
|
||||||
`mame_hdd_rebuild_restart`) → в DSS набрать `d:` и `roomtest`. Кид встаёт в
|
|
||||||
(0,2) лицом к чомперу, справа факел и страж — та самая сцена замеров.
|
|
||||||
Штатный старт уровня возвращается через `make ROOM=`.
|
|
||||||
|
|
||||||
Проверка, что программа ЖИВА, обязательна перед любым чтением памяти:
|
|
||||||
`cur_room` (0x97AA) должен лежать в 1..24 — на этом уже был сорван один
|
|
||||||
замер (прочитаны два случайных байта остановленной машины).
|
|
||||||
|
|
||||||
### Метод замера
|
|
||||||
|
|
||||||
Зонды — `out (_io_border)` в `roomtest.c` (база модуля 0x42AD) плюс
|
|
||||||
резидентные пустышки `pop_dbg_m*` из `pop_state.c`. Адреса брать ЗАНОВО из
|
|
||||||
`.sprinter-cc-roomtest/roomtest.map` после каждой пересборки. Скрипты
|
|
||||||
сессии: `perfrun.py <out> <сек> tag=addr ...` и `parseseq.py <файл> ПОСЛЕД`.
|
|
||||||
|
|
||||||
Цену отдельной РЕЗИДЕНТНОЙ функции можно снять вообще без пересборки:
|
|
||||||
`bpset <вход>,1,{temp0=totalcycles; g}` плюс `bpset <точка>,1,{printf "…
|
|
||||||
%d",totalcycles-temp0; g}`. Так разложен блит в §1.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Сводка: что сколько даёт в 11/15
|
|
||||||
|
|
||||||
| # | приём | эффект | тип оценки | риск |
|
|
||||||
|---|---|---:|---|---|
|
|
||||||
| P1 | чомпер: только anim-слой | −160 000 | модель | низкий |
|
|
||||||
| P3 | Кид перестанет будиться | −70 000 | модель | следствие P1 |
|
|
||||||
| P2 | логика двух Char | −40 000 … −70 000 | гипотеза | ? |
|
|
||||||
| P4 | накладные блита (4 правки) | −28 000 | модель | низкий |
|
|
||||||
| P5 | `loose_tick` без плит | ✅ −33 840 | ФАКТ | сделано |
|
|
||||||
| P6 | цикл `process_trobs` | −20 000 … −40 000 | гипотеза | ? |
|
|
||||||
| P10 | футпринт из физики | −23 000 | замер | средний |
|
|
||||||
| P11 | мелочи (4 штуки) | −26 000 | замер | низкий |
|
|
||||||
| P12 | снять оснастку | −6 000 | замер | нулевой |
|
|
||||||
| P7 | раскол `draw_tile` | 0 здесь (−50 000 в 13/23) | оценка | средний |
|
|
||||||
| P8/P9 | HEAL-WIDTH / G8 | 0 здесь (сцены с плитами) | оценка | низкий/средний |
|
|
||||||
|
|
||||||
Верхняя часть списка (P1 + P3 + P4 + P5) — **около −286 000 из 801 768, то
|
|
||||||
есть 36 % работы кадра**, и вся она низкого риска. Этого хватит, чтобы
|
|
||||||
сцена ушла с 1,86 растрового кадра до ~1,2 — но НЕ хватит, чтобы период
|
|
||||||
кадра упал с 4 растров до 3: для этого работа должна уложиться в 430 000,
|
|
||||||
то есть нужны ещё ~90 000 сверху (P2 или P6).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5б. Цианная фаза разложена [ЗАМЕР 2026-08-19]
|
|
||||||
|
|
||||||
Фаза 188 004 тактов, и она НЕ менялась ни от P1, ни от P5, ни от P2b.
|
|
||||||
|
|
||||||
| участок | такты |
|
|
||||||
|---|---:|
|
|
||||||
| `check_mirror` | 3 198 |
|
|
||||||
| `loose_mob_draw` + `guard_over_kid` + `skip_mask` | **34 374** |
|
|
||||||
| **`pop_char_draw(KID)`** | **204** |
|
|
||||||
| **соперник: `char_draw` + `char_fore`** | **145 896** |
|
|
||||||
| `fore_needed` + `hp_draw` | 2 598 |
|
|
||||||
| `char_fore(KID)` + борта | 1 758 |
|
|
||||||
|
|
||||||
**Кид уже пропускается** — 204 такта, то есть надежда P3 всё-таки сбылась
|
|
||||||
после P1: метка от чомпера до него больше не дотягивается. А страж
|
|
||||||
перерисовывается каждый кадр, и это ЧЕСТНО: пламя правого факела (0,7)
|
|
||||||
рисуется в ячейке (0,8), где он стоит, и реально накрывает ему голову
|
|
||||||
(пламя занимает y 5..22, страж 12..62).
|
|
||||||
|
|
||||||
Отрисовка стража (148 302) по частям:
|
|
||||||
|
|
||||||
| участок | такты | доля |
|
|
||||||
|---|---:|---:|
|
|
||||||
| **`pop_char_fore`** (два трамплина в банк 2 + обход тайлов) | **62 778** | 42 % |
|
|
||||||
| клинок: `pop_sword_draw` + `cd_overlay_add` + `cd_clip_add` | 27 522 | 19 % |
|
|
||||||
| блит спрайта + `clip_char_right` | 20 982 | 14 % |
|
|
||||||
| загрузка кадра и геометрия | 12 696 | 9 % |
|
|
||||||
| **`pop_clip_char_top`** (трамплин банк 4 → банк 3) | 8 658 | 6 % |
|
|
||||||
| снимок прямоугольника + `cd_clip_add` | 7 890 | 5 % |
|
|
||||||
| `gfx_w0_unmap` + `cd_sig_make` | 4 968 | 3 % |
|
|
||||||
| вход + `cd_heal` | 2 946 | 2 % |
|
|
||||||
|
|
||||||
**Вывод: крупного лишнего здесь нет.** Единственная явно лишняя статья —
|
|
||||||
трамплин `clip_char_top` (8 658), и убрать его непросто: функции нужны
|
|
||||||
`get_tile` и таблицы деления из банка 3, а перенос в резидент вернёт тот же
|
|
||||||
трамплин внутрь. Всё остальное — работа, которую персонаж действительно
|
|
||||||
делает: рисует себя, клинок и передний слой поверх себя.
|
|
||||||
|
|
||||||
## 6. Иерархия референсов (уточнена 2026-08-19)
|
|
||||||
|
|
||||||
Сравнение трёх реализаций луча видимости показало, что источники не
|
|
||||||
равноценны, и это важно для ЛЮБОЙ будущей оптимизации:
|
|
||||||
|
|
||||||
| источник | что берём | чего НЕ берём |
|
|
||||||
|---|---|---|
|
|
||||||
| **Apple II** (`Prince-of-Persia-Apple-II`) | как это делается на 8 битах: таблицы вместо делений, борьба за такты | ничего — но код на 6502, читать сложнее |
|
|
||||||
| **SDLPoP** | эталон ПОВЕДЕНИЯ (декомпиляция DOS-версии) | реализацию: она нарочно «расслаблена» под 32 бита |
|
|
||||||
| **mininim** | разбор краевых случаев, второе мнение о замысле | алгоритмы — переписан с нуля, механика местами своя |
|
|
||||||
|
|
||||||
Доказательство на конкретном месте: `get_tile_div_mod` в SDLPoP содержит
|
|
||||||
комментарий
|
|
||||||
|
|
||||||
```c
|
|
||||||
// DOS PoP does this:
|
|
||||||
// obj_xl = tile_mod_tbl[xpos];
|
|
||||||
// return tile_div_tbl[xpos];
|
|
||||||
```
|
|
||||||
|
|
||||||
а вместо этого делает `x % TILE_SIZEX` и `x / TILE_SIZEX`. Таблицы в файле
|
|
||||||
лежат, но нужны только для эмуляции чтения DOS-версии ЗА ГРАНИЦЕЙ массива.
|
|
||||||
Apple II (`CTRLSUBS.S`, `GETBLOCKX`) читает ровно `BlockTable[x]`.
|
|
||||||
|
|
||||||
**Правило:** сверять поведение по SDLPoP, а реализацию под 8 бит — по
|
|
||||||
Apple II и по комментариям вида «DOS PoP does this» в самом SDLPoP.
|
|
||||||
|
|
||||||
## 7. Повторяющийся источник цены: банковый трамплин в цикле
|
|
||||||
|
|
||||||
Уже трижды крупнейшей статьёй оказывался не алгоритм, а вызов `__banked`-
|
|
||||||
функции ИЗ ЦИКЛА, идущего в другом банке:
|
|
||||||
|
|
||||||
| место | цена | лечение |
|
|
||||||
|---|---:|---|
|
|
||||||
| луч видимости: `pop_tile_at` по колонке (P2b) | 36 786 → 13 002 | один вызов на весь отрезок |
|
|
||||||
| `pop_clip_char_top` — банк 4 → банк 3 ради одной проверки | 8 892 | не сделано (P11) |
|
|
||||||
| `pop_trob_modif(room)` на каждый trob в цикле | не мерено | не сделано (P6a) |
|
|
||||||
|
|
||||||
**Что проверять в первую очередь при новом «дорогом» месте:** не сколько
|
|
||||||
там арифметики, а сколько раз за кадр пересекается граница банка.
|
|
||||||
|
|
||||||
## Фиксированный логический кадр (2026-08-19) — МЕНЯЕТ ВСЕ ЦЕЛЕВЫЕ ЧИСЛА
|
|
||||||
|
|
||||||
Период логического кадра больше не `ceil(W) + 2`, а `max(n, ceil(W))`
|
|
||||||
(`roomtest/pop_pace.c`, разбор — `frame_pacing_plan.md`). Поэтому:
|
|
||||||
|
|
||||||
- **Бюджет кадра вырос с 430 080 до 1 290 240 тактов** (n = 3, режим
|
|
||||||
FASTEST по умолчанию). Все записи этого реестра, где «работа сверх
|
|
||||||
430 000 стоит сразу целого растра», СЧИТАТЬ УСТАРЕВШИМИ.
|
|
||||||
- 13/23 (максимум работы 911 862) теперь укладывается в период 3 растра —
|
|
||||||
проверено, ни одного кадра длиннее. Прежний профиль был 3/4/5.
|
|
||||||
- Оптимизация из спешной стала плановой: смысл резать такты остался
|
|
||||||
(режим NORMAL при n=4 и бой при n=5 дают ещё больше запаса, а FASTEST —
|
|
||||||
верхнюю планку скорости), но «свалиться за растр» больше не обрыв.
|
|
||||||
- Цена самого пейсинга — ≈4 000 тактов на кадр (0,9 %), замерено A/B.
|
|
||||||
|
|
||||||
Приоритет P9 (G8) и остальных позиций от этого не меняется, но их
|
|
||||||
СРОЧНОСТЬ падает: они больше не спасают от скачка периода.
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
# Генераторы псевдослучайных чисел: запасные варианты
|
|
||||||
|
|
||||||
Что сейчас стоит в порте, какие есть альтернативы и сколько на них реально
|
|
||||||
можно выиграть. Заготовка на случай, если упрёмся в бюджет кадра —
|
|
||||||
**сейчас менять ничего не нужно**.
|
|
||||||
|
|
||||||
## Что стоит сейчас
|
|
||||||
|
|
||||||
`pop_geom.c`, ветка `POP_PRANDOM_EXACT=1` (по умолчанию) — LCG оригинала
|
|
||||||
`s = s*214013 + 2531011`, шаг написан на Z80-ассемблере (единственное такое
|
|
||||||
место в порте). Схема Горнера по разреженной записи константы:
|
|
||||||
|
|
||||||
```
|
|
||||||
214013 = ((((1<<1)+1)<<2 + 1)<<4 + 1)<<10 - 3
|
|
||||||
```
|
|
||||||
|
|
||||||
17 удвоений, три сложения, одно вычитание; величина `3*s`, нужная в конце,
|
|
||||||
попадается по дороге на втором шаге. Тело — **≈1 020 тактов** по статическому
|
|
||||||
подсчёту. Бит-в-бит совместим с SDLPoP, поэтому по картинке можно сверяться
|
|
||||||
с эталоном.
|
|
||||||
|
|
||||||
Вторая ветка, `POP_PRANDOM_EXACT=0` — xorshift16 + шаг Вейля на C.
|
|
||||||
Совместимость теряется.
|
|
||||||
|
|
||||||
Замер в MAME, комната 3, 175 кадров (медиана кадра):
|
|
||||||
|
|
||||||
| вариант | кадр | prandom → torch_draw |
|
|
||||||
|---|---|---|
|
|
||||||
| C, бит-в-бит (16-битные половины) | 403 632 | 10 933 |
|
|
||||||
| C, xorshift16 + Вейль | 397 986 | 7 927 |
|
|
||||||
| **asm, бит-в-бит (сейчас)** | **400 800** | **9 331** |
|
|
||||||
|
|
||||||
## Вариант A — комбинированный LFSR + LCG, ~148 тактов
|
|
||||||
|
|
||||||
Период > 4 млрд (lcm(65536, 65535) ≈ 4.29e9), младшие биты не вырождены.
|
|
||||||
|
|
||||||
```z80
|
|
||||||
prng16:
|
|
||||||
seed1=$+1
|
|
||||||
ld hl, 9999
|
|
||||||
ld b, h
|
|
||||||
ld c, l
|
|
||||||
add hl, hl
|
|
||||||
add hl, hl
|
|
||||||
inc l
|
|
||||||
add hl, bc
|
|
||||||
ld (seed1), hl
|
|
||||||
seed2=$+1
|
|
||||||
ld hl, 987
|
|
||||||
add hl, hl
|
|
||||||
sbc a, a
|
|
||||||
and 101101b
|
|
||||||
xor l
|
|
||||||
ld l, a
|
|
||||||
ld (seed2), hl
|
|
||||||
add hl, bc
|
|
||||||
ret
|
|
||||||
```
|
|
||||||
|
|
||||||
Устройство: `seed1` — LCG `x = 5x + 1` (по модулю 2^16; `inc l` вместо
|
|
||||||
`inc hl` — экономия байта, на период не влияет). `seed2` — 16-битный
|
|
||||||
LFSR Галуа: сдвиг влево, и если выехала единица, XOR младшего байта с маской
|
|
||||||
`0x2D` (примитивный многочлен `x^16 + x^5 + x^3 + x^2 + 1`). На выходе
|
|
||||||
сумма обоих состояний — она и разрушает регулярность младших бит LCG.
|
|
||||||
|
|
||||||
**Что мешает взять как есть:** сиды зашиты в код (SMC), а нам нужны ДВЕ
|
|
||||||
независимые последовательности — раскладка кладки и анимация тайлов.
|
|
||||||
Пришлось бы передавать состояние через указатель, как сейчас у `pop_prandom`
|
|
||||||
(это +20…40 тактов, не принципиально).
|
|
||||||
|
|
||||||
## Вариант B — xorshift(7,9,8), ~86 тактов
|
|
||||||
|
|
||||||
Самый быстрый, период 65535.
|
|
||||||
|
|
||||||
```z80
|
|
||||||
xrnd:
|
|
||||||
ld hl, 1 ; seed must not be 0
|
|
||||||
ld a, h
|
|
||||||
rra
|
|
||||||
ld a, l
|
|
||||||
rra
|
|
||||||
xor h
|
|
||||||
ld h, a
|
|
||||||
ld a, l
|
|
||||||
rra
|
|
||||||
ld a, h
|
|
||||||
rra
|
|
||||||
xor l
|
|
||||||
ld l, a
|
|
||||||
xor h
|
|
||||||
ld h, a
|
|
||||||
ld (xrnd+1), hl
|
|
||||||
ret
|
|
||||||
```
|
|
||||||
|
|
||||||
**Две оговорки.** Ноль — неподвижная точка, а сид раскладки кладки у нас
|
|
||||||
считается как `номер комнаты + смещение ряда + колонка` и вполне может
|
|
||||||
оказаться нулём: нужен либо guard, либо шаг Вейля поверх. И тот же SMC-сид,
|
|
||||||
что в варианте A.
|
|
||||||
|
|
||||||
## Чего НЕ брать: RND из Apple II
|
|
||||||
|
|
||||||
Оригинальный `Prince-of-Persia-Apple-II`:
|
|
||||||
|
|
||||||
```
|
|
||||||
RNDseed := (5 * RNDseed + 23) mod 256
|
|
||||||
```
|
|
||||||
|
|
||||||
```asm
|
|
||||||
RND
|
|
||||||
lda RNDseed
|
|
||||||
asl
|
|
||||||
asl
|
|
||||||
clc
|
|
||||||
adc RNDseed
|
|
||||||
clc
|
|
||||||
adc #23
|
|
||||||
sta RNDseed
|
|
||||||
rts
|
|
||||||
```
|
|
||||||
|
|
||||||
Полный период 256 (`a ≡ 1 mod 4`, `c` нечётное), и для своего движка он
|
|
||||||
работал. Нам не годится: у LCG по модулю 256 младшие биты вырождены — бит 0
|
|
||||||
просто чередуется. Наши вызовы это увидят: раскладка кладки берёт
|
|
||||||
`prandom(1)` (ОДИН бит) и `prandom(4)`, то есть вместо шума получилась бы
|
|
||||||
аккуратная шахматка.
|
|
||||||
|
|
||||||
## Сколько реально можно выиграть
|
|
||||||
|
|
||||||
Меньше, чем кажется по числам 86/148 против 1 020. Тело генератора — уже не
|
|
||||||
весь расход: остаются обёртка `pop_prandom`, приведение к диапазону
|
|
||||||
`pop_rnd_fit` и ABI вызова. Верхняя граница выигрыша видна из замера выше:
|
|
||||||
между нынешним asm-LCG и самым дешёвым из проверенных вариантов разница
|
|
||||||
**2 814 тактов за кадр (0.65 %)** при двух вызовах за кадр, и это ПОТОЛОК —
|
|
||||||
любой из вариантов A/B ниже него не опустится.
|
|
||||||
|
|
||||||
Порядок действий, если понадобится:
|
|
||||||
|
|
||||||
1. Сначала убрать обёртки: слить `pop_rnd_fit` в ту же asm-процедуру, чтобы
|
|
||||||
на вызов приходился один `call`, а не три. Это ничего не ломает и не
|
|
||||||
трогает совместимость с эталоном.
|
|
||||||
2. И только если этого мало — менять генератор, начиная с варианта A
|
|
||||||
(качество последовательности у него не хуже LCG, в отличие от B).
|
|
||||||
|
|
||||||
Важно помнить: число вызовов вырастет с боёвкой. Сейчас их два за кадр
|
|
||||||
(факелы), а `guard_advance` / `guard_block` / `guard_strike` дёргают
|
|
||||||
`prandom(255)` каждый по разу за кадр боя — то есть при драке станет 5–6, и
|
|
||||||
цена вопроса вырастет во столько же раз.
|
|
||||||
@@ -1,324 +0,0 @@
|
|||||||
# QuickSave / QuickLoad — разбор оригинала и план реализации
|
|
||||||
|
|
||||||
Статус: **план, код не начат** (2026-08-17). Задача на доске —
|
|
||||||
[`../roomtest/TASKS_OPEN.md#qsave`](../roomtest/TASKS_OPEN.md#qsave).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 0. Важная оговорка об «оригинале»
|
|
||||||
|
|
||||||
**В оригинальном PoP 1989 года (DOS/Apple II) QuickSave/QuickLoad НЕТ.**
|
|
||||||
Там вообще нет сохранения посреди уровня: игра рассчитана на один заход в
|
|
||||||
60 минут, а «продолжение» — это только пароль/чекпоинт уровня 7. Поэтому
|
|
||||||
`Prince-of-Persia-Apple-II/` и `MSDOS/` тут не источники — искать в них
|
|
||||||
нечего.
|
|
||||||
|
|
||||||
Источник истины — **SDLPoP**, где быстрое сохранение добавлено как
|
|
||||||
enhancement: `seg000.c`, блок `#ifdef USE_QUICKSAVE`, клавиши **F6** (save)
|
|
||||||
и **F9** (load). Ниже разобран именно он. Это значит, что правило
|
|
||||||
«расхождение с SDLPoP = баг у нас» здесь работает мягче: мы не обязаны
|
|
||||||
повторять его байт-в-байт, но обязаны повторить его **устройство**, потому
|
|
||||||
что оно решает ровно те проблемы, которые возникнут и у нас.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Как это устроено в SDLPoP
|
|
||||||
|
|
||||||
### 1.1 Точка вызова — отдельная фаза кадра, не обработчик клавиши
|
|
||||||
|
|
||||||
Клавиша только взводит флаг (`need_quick_save` / `need_quick_load`,
|
|
||||||
`seg000.c:558`), а вся работа делается в `check_quick_op()` — она вызывается
|
|
||||||
из главного цикла **между кадрами**, когда движок в согласованном состоянии.
|
|
||||||
Это принципиально: загрузка посреди тика переписала бы `Char` под ногами у
|
|
||||||
`play_seq`.
|
|
||||||
|
|
||||||
Отказ штатный, не фатальный: `quick_save()`/`quick_load()` возвращают
|
|
||||||
успех/неуспех, и игра печатает `QUICKSAVE` / `NO QUICKLOAD` внизу экрана и
|
|
||||||
продолжается.
|
|
||||||
|
|
||||||
### 1.2 Формат — плоская последовательность переменных, без структуры
|
|
||||||
|
|
||||||
```c
|
|
||||||
#define process(x) ok = ok && process_func(&(x), sizeof(x))
|
|
||||||
```
|
|
||||||
|
|
||||||
Один макрос и один и тот же список обходится **и на запись, и на чтение**
|
|
||||||
(`quick_process(process_save)` / `quick_process(process_load)`). Поля
|
|
||||||
пишутся встык, без имён и тегов; совместимость держится ровно одним
|
|
||||||
средством — **строкой версии в начале файла**:
|
|
||||||
|
|
||||||
```c
|
|
||||||
const char quick_version[] = "V1.16b4 ";
|
|
||||||
```
|
|
||||||
|
|
||||||
При загрузке она сравнивается, и при несовпадении файл просто отвергается
|
|
||||||
(`quick_load`, возврат 0). То есть формат нарочно хрупкий и нарочно
|
|
||||||
одноразовый — это снимок конкретной сборки, а не сейв-формат.
|
|
||||||
|
|
||||||
**Это стоит перенять целиком.** Мы платим за версионирование одним байтом
|
|
||||||
и получаем право менять состав снимка при каждой правке движка.
|
|
||||||
|
|
||||||
### 1.3 Что именно сохраняется
|
|
||||||
|
|
||||||
Полный список — `quick_process`, `seg000.c:257-366`. По смыслу он делится
|
|
||||||
на пять групп:
|
|
||||||
|
|
||||||
| группа | поля |
|
|
||||||
|---|---|
|
|
||||||
| уровень | `level` (2305 Б целиком), `checkpoint`, `upside_down`, `drawn_room`, `current_level`, `next_level`, `leveldoor_open` |
|
|
||||||
| анимируемые объекты | `mobs_count`, `mobs[14]`, `trobs_count`, `trobs[30]` |
|
|
||||||
| Кид | `Kid`, `hitp_curr/max/beg_lev`, `grab_timer`, `holding_sword`, `united_with_shadow`, `have_sword`, `kid_sword_strike`, `pickup_obj_type`, `offguard` |
|
|
||||||
| соперник | `Guard`, `Char`, `Opp`, `guardhp_curr/max`, `demo_index`, `demo_time`, `curr_guard_color`, `guard_notice_timer`, `guard_skill`, `shadow_initialized`, `guard_refrac`, `justblocked`, `droppedout`, `is_guard_notice`, `can_guard_see_kid` |
|
|
||||||
| прочее | кэш коллизии (`*_row_coll_room/flags`, `prev_collision_row`), вспышка (`flash_color/time`), звук (`is_screaming`, `is_feather_fall`, …), **`random_seed`**, время (`rem_min`, `rem_tick`), весь блок управления (`control_*`, `ctrl1_*`) |
|
|
||||||
|
|
||||||
Два наблюдения, важные для нас:
|
|
||||||
|
|
||||||
1. **Состояние ОТРИСОВКИ не сохраняется вообще.** Ни экранных буферов, ни
|
|
||||||
пометок перерисовки, ни того, что уже нарисовано. Вместо этого при
|
|
||||||
загрузке комната перерисовывается с нуля. Это резко упрощает задачу и
|
|
||||||
ровно то, что нам нужно при дабл-буфере.
|
|
||||||
2. **`random_seed` сохраняется.** Без него загрузка не воспроизводима:
|
|
||||||
после неё факелы, чомперы и `prandom` в боёвке пойдут иначе.
|
|
||||||
|
|
||||||
### 1.4 Что делается при загрузке
|
|
||||||
|
|
||||||
`restore_room_after_quick_load()` (`seg000.c:395`) — это и есть вся
|
|
||||||
«сложность» операции:
|
|
||||||
|
|
||||||
- `load_lev_spr(current_level)` — **перезагрузка графики уровня** (тайлсет
|
|
||||||
мог смениться: подземелье/дворец);
|
|
||||||
- `different_room = 1`, `next_room = drawn_room = Kid.room` — принудительно
|
|
||||||
«мы в другой комнате», чтобы движок перерисовал всё;
|
|
||||||
- `load_room_links()` — связи комнат заново;
|
|
||||||
- `draw_game_frame()` — отрисовать кадр (важно для состояния падения);
|
|
||||||
- `hitp_delta = guardhp_delta = 1` — принудительный редрой полос HP;
|
|
||||||
- если `Guard.room != drawn_room` — стража «выключить» (`direction =
|
|
||||||
dir_56_none`, `guardhp_curr = 0`), как в `clear_char()`;
|
|
||||||
- `loadkid_and_opp()` — восстановить окно `Char`/`Opp`;
|
|
||||||
- сбросить таймеры текста и `exit_room_timer`.
|
|
||||||
|
|
||||||
Плюс визуальный приём: перед загрузкой экран заливается чёрным на 5 тиков —
|
|
||||||
чтобы переход читался глазом и не выглядел «дёрганием».
|
|
||||||
|
|
||||||
### 1.5 Чего в SDLPoP решили НЕ восстанавливать
|
|
||||||
|
|
||||||
- звуки — просто `stop_sounds()`;
|
|
||||||
- перо (`is_feather_fall`) — без фикса `fix_quicksave_during_feather`
|
|
||||||
сохранение под пером запрещено вовсе, а при загрузке эффект гасится;
|
|
||||||
- есть опциональный **штраф**: `USE_QUICKLOAD_PENALTY` отнимает минуту
|
|
||||||
игрового времени за квиклоад. Нам не нужен (у нас пока нет игрового
|
|
||||||
таймера).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Чем наша архитектура отличается
|
|
||||||
|
|
||||||
| | SDLPoP | у нас | следствие для задачи |
|
|
||||||
|---|---|---|---|
|
|
||||||
| уровень в памяти | `level_type` в ОЗУ, 2305 Б, мутабельный | EMM-страница (`pop_lvl_page`), плюс рабочая копия комнаты в W2 | снимок читает страницу через W0-маппинг, а не `memcpy` |
|
|
||||||
| модификаторы тайлов | внутри `level.bg` | отдельный `room_modif[24][30]` в `pop_trob.c` (**static**) | нужен экспортируемый сериализатор из банка 6 |
|
|
||||||
| код | один бинарник | 8 банков + резидент | сериализатор обязан жить там же, где данные, и зваться через трамплин |
|
|
||||||
| экран | один буфер | **дабл-буфер**, у каждой страницы своя теневая копия | после загрузки перерисовать ОБЕ страницы, иначе через кадр мелькнёт старое |
|
|
||||||
| ОЗУ | сколько угодно | куча 2969 Б, стек 1279 Б | буфер снимка целиком в ОЗУ не положить — писать потоком |
|
|
||||||
| диск | `fopen` | DSS: 8 манипуляторов, 9-й ВЕШАЕТ систему ([[dss_fd_limit]]) | закрывать файл гарантированно, гард уже есть в libc |
|
|
||||||
| ГСЧ | один `random_seed` | **три** независимых: `pop_t_seed`, `trob_seed`, `pop_fight_seed` | сохранять все три, иначе загрузка невоспроизводима |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Инвентаризация нашего состояния
|
|
||||||
|
|
||||||
Собрано по `.sprinter-cc-roomtest/roomtest.map` (данные всех модулей, включая
|
|
||||||
банковые, лежат в W2 — банк влияет только на код). Отмечено, что глобально
|
|
||||||
(видно снаружи), а что `static` и требует аксессора.
|
|
||||||
|
|
||||||
### 3.1 Мутабельные данные уровня
|
|
||||||
|
|
||||||
| что | где | размер | доступ |
|
|
||||||
|---|---|---|---|
|
|
||||||
| тайлы `fg` (провалившиеся плиты, открытые двери, съеденные предметы) | EMM-страница уровня | 720 Б | `pop_level_set_tile` пишет; чтения наружу нет — **нужен аксессор** |
|
|
||||||
| `room_modif[24][30]` | `pop_trob.c`, static | 720 Б | **нужен сериализатор** (банк 6) |
|
|
||||||
| `room_seen[24]` | `pop_trob.c`, static | 24 Б | там же |
|
|
||||||
| `trobs[30]` + `trobs_count` | `pop_trob.c`, static | 91 Б | там же |
|
|
||||||
| `trob_seed` | `pop_trob.c`, static | 4 Б | там же |
|
|
||||||
| `mobs[14]` | `pop_room.c`, **глобален** | 210 Б | напрямую |
|
|
||||||
| `mobs_live` | `pop_room.c`, static | 1 Б | аксессор |
|
|
||||||
|
|
||||||
### 3.2 Персонажи и бой
|
|
||||||
|
|
||||||
`Kid`, `Char`, `Opp` (`pop_kid.c`), `Guard` (`pop_guard.c`) — по 16 Б,
|
|
||||||
все глобальные. Рядом: `hitp_curr/max/beg_lev/delta`, `guardhp_curr/max/delta`,
|
|
||||||
`guard_skill`, `guard_refrac`, `justblocked`, `kid_sword_strike`, `offguard`,
|
|
||||||
`holding_sword`, `can_guard_see_kid`, `is_guard_notice`,
|
|
||||||
`pop_guard_notice_timer`, `pop_guard_hurt`, `pop_united_shadow`,
|
|
||||||
`pop_shadow_init`, `pop_fight_seed`, `knock`.
|
|
||||||
|
|
||||||
### 3.3 Прогресс и физика
|
|
||||||
|
|
||||||
`pop_current_level`, `pop_next_level`, `pop_checkpoint`, `pop_have_sword`,
|
|
||||||
`pop_item_taken`, `pop_leveldoor_open`, `pop_leveldoor_right`,
|
|
||||||
`pop_leveldoor_ybottom`, `pop_kid_dead`, `pop_kid_hurt`, `pop_feather`,
|
|
||||||
`pop_upside` / `pop_upside_want`, `pop_flash_time` / `pop_flash_color`,
|
|
||||||
`pop_droppedout`, `pop_fell_out`, `pop_leave_dir`, `pop_leave_timer`,
|
|
||||||
`pop_loose_*`, `pop_ceil_modif`, `pop_ceil_fell`, `pop_debris_at`,
|
|
||||||
`pop_seamless`, `pop_jumped_mirror`.
|
|
||||||
|
|
||||||
### 3.4 Ввод
|
|
||||||
|
|
||||||
`control_x/y/shift/forward/backward/up/down/shift2` (`pop_state.c`) — как в
|
|
||||||
SDLPoP, сохраняются.
|
|
||||||
|
|
||||||
### 3.5 Что НЕ сохранять (восстанавливается перерисовкой)
|
|
||||||
|
|
||||||
`room_fg/room_bg`, `lcol_*`/`rcol_*`/`below_fg`/`above_*`, `seam_*`,
|
|
||||||
`cur_room`, `pop_t_*` (весь кэш слоя фона, окна клипа, `pop_cd_*`),
|
|
||||||
`trob_drawn`, `mob_spr`, слоты `pop_cd`, метки `pop_redraw`, запечки
|
|
||||||
(`bake_pg`). Всё это — производное; после загрузки оно обязано быть
|
|
||||||
сброшено и пересчитано, а не восстановлено.
|
|
||||||
|
|
||||||
**Оценка объёма снимка: ≈ 1,9 КБ** (720 + 720 + 210 + 91 + 64 + ~60
|
|
||||||
скаляров + запас).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Куда писать снимок: файл, а не EMM-страница
|
|
||||||
|
|
||||||
Рекомендация: **основной путь — файл `QUICKSAVE.SAV`; EMM-страница —
|
|
||||||
необязательный второй слот.**
|
|
||||||
|
|
||||||
> Пересмотрено 2026-08-17 по вопросу пользователя «почему EMM, а не файл».
|
|
||||||
> Первая редакция плана рекомендовала EMM — это была ошибка: она взвешивала
|
|
||||||
> скорость и недооценивала главный сценарий использования. Разбор оставлен
|
|
||||||
> целиком, потому что довод переносится и на другие «положить в память
|
|
||||||
> вместо диска» решения.
|
|
||||||
|
|
||||||
**Решающий довод: EMM-страница не переживает рестарт программы,** а именно
|
|
||||||
рестарт — тот случай, ради которого QuickSave и нужен. Пример из этого же
|
|
||||||
проекта: сцену каскада плит на 13/23 воспроизводит ТОЛЬКО `ESC` → запуск
|
|
||||||
заново ([`perf_l13_room23.md`](perf_l13_room23.md) §1, где перечислено,
|
|
||||||
почему не годятся ни возврат в комнату, ни рестарт уровня, ни запись
|
|
||||||
состояния отладчиком). Тем более снимок в ОЗУ не переживает перезапуск
|
|
||||||
MAME, обязательный после каждой пересборки образа.
|
|
||||||
|
|
||||||
| сценарий | EMM | файл |
|
|
||||||
|---|---|---|
|
|
||||||
| «переиграть это место ещё раз» | работает, мгновенно | работает, на HDD быстро |
|
|
||||||
| «вернуться к багу после рестарта» | **не работает** | **работает** |
|
|
||||||
|
|
||||||
Второй сценарий не закрывается ничем другим; первый закрывается обоими, и
|
|
||||||
разница в скорости там некритична — 1,9 КБ на HDD ([[mame_hdd_test_disk]] —
|
|
||||||
быстрый путь против дискеты) не заметны на фоне полной перерисовки комнаты,
|
|
||||||
которая при загрузке делается в любом случае и стоит дороже.
|
|
||||||
|
|
||||||
Доводы за EMM, которые при перепроверке оказались слабыми: лимит
|
|
||||||
манипуляторов DSS ни при чём (открываем и закрываем ровно один файл, гард
|
|
||||||
`_fd_guard` в libc и так стоит), а «не нужен путь и права» — экономия одной
|
|
||||||
строки.
|
|
||||||
|
|
||||||
Что остаётся за EMM: мгновенный слот для «переиграть» без обращения к диску.
|
|
||||||
Делается тем же сериализатором и добавляется, если понадобится. Поэтому
|
|
||||||
обход состояния писать сразу так, чтобы «куда» было параметром — как у
|
|
||||||
SDLPoP через `process_func`.
|
|
||||||
|
|
||||||
**Проверить ДО кодинга:** пишется ли `test_hdd.chd` из-под MAME. Если образ
|
|
||||||
только на чтение, файловый путь упрётся в это на первом же шаге и порядок
|
|
||||||
работ придётся менять. Проверка дешёвая — записать пробный файл на `D:` из
|
|
||||||
roomtest.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Формат снимка
|
|
||||||
|
|
||||||
```
|
|
||||||
+0 "PQS1" 4 Б магия
|
|
||||||
+4 версия сборки 1 Б (инкремент при ЛЮБОМ изменении состава)
|
|
||||||
+5 pop_current_level 1 Б
|
|
||||||
+6 длина полезной части 2 Б (контроль, что обход совпал)
|
|
||||||
+8 ... поля встык, ОДИН порядок на запись и на чтение ...
|
|
||||||
```
|
|
||||||
|
|
||||||
Версия проверяется первой; несовпадение — отказ, как в SDLPoP. Никаких
|
|
||||||
тегов и выравнивания: снимок одноразовый и живёт ровно одну сборку.
|
|
||||||
|
|
||||||
Обход — один список и один макрос, как `process(x)`:
|
|
||||||
|
|
||||||
```c
|
|
||||||
static void qs_walk(qs_io_t io) /* io = запись или чтение */
|
|
||||||
{
|
|
||||||
QS(pop_current_level); QS(pop_checkpoint); ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Так состав нельзя рассинхронизировать между сохранением и загрузкой —
|
|
||||||
единственная реальная опасность плоского формата.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Что делать при загрузке (наш аналог `restore_room_after_quick_load`)
|
|
||||||
|
|
||||||
Порядок важен, каждый пункт закрывает конкретный отказ:
|
|
||||||
|
|
||||||
1. **Сменился уровень?** → `pop_level_load_num()`, `pop_bg_load(tileset)`,
|
|
||||||
атласы стража по типу. Это дорого, но ровно тот же путь, что при
|
|
||||||
переходе уровня (`pop_level_switch`), — переиспользовать его, а не писать
|
|
||||||
заново.
|
|
||||||
2. Залить экран чёрным (приём SDLPoP: переход должен читаться глазом).
|
|
||||||
3. Восстановить состояние обходом `qs_walk`.
|
|
||||||
4. **Сбросить всё производное:** `pop_trob_reset` (но НЕ трогая
|
|
||||||
восстановленные `room_modif`/`trobs` — нужен отдельный «мягкий» сброс,
|
|
||||||
только `trob_drawn` + метки), `pop_redraw_reset`, слоты `pop_cd`,
|
|
||||||
`pop_bake_forget`, `pop_cd_clear`, сигнатуры пропуска перерисовки.
|
|
||||||
5. `pop_room_load(Kid.room)` — рабочая копия комнаты и срезы соседей.
|
|
||||||
6. **Полная отрисовка комнаты в ОБЕ страницы дабл-буфера.** Это наше
|
|
||||||
главное отличие от SDLPoP: одной перерисовки мало, вторая страница
|
|
||||||
останется со старой картинкой и мигнёт через кадр.
|
|
||||||
7. Принудительный редрой полос HP (`hitp_delta = guardhp_delta = 1`).
|
|
||||||
8. Если `Guard.room != Kid.room` — выключить стража
|
|
||||||
(`Guard.direction = DIR_56_NONE`, `guardhp_curr = 0`), как `clear_char`.
|
|
||||||
9. `pop_loadkid_and_opp()` — согласовать окно `Char`/`Opp`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Разбиение на шаги
|
|
||||||
|
|
||||||
| шаг | что | критерий готовности |
|
|
||||||
|---|---|---|
|
|
||||||
| **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) | файл создался и читается обратно после рестарта программы |
|
|
||||||
| **QS2** | Ядро: `qs_walk` + запись/чтение файла `QUICKSAVE.SAV`, магия и версия, отказ при несовпадении | сохранение и загрузка **в той же комнате, без движения** — картинка и состояние не изменились |
|
|
||||||
| **QS3** | Восстановление отрисовки (§6), включая обе страницы дабл-буфера | загрузка после перехода в другую комнату; нет мерцания через кадр |
|
|
||||||
| **QS4** | Клавиши **F6/F9** (или свободные из `pop_cheat.h`) через `<kbd_raw.h>`, флаги `need_quick_save/load`, обработка **между кадрами** | загрузка посреди боя/падения не ломает `play_seq` |
|
|
||||||
| **QS5** | Загрузка с **другого уровня** (перезагрузка уровня и атласов) | сохранить на ур. 2, уйти на ур. 12, загрузить — тайлсет и стражи верные |
|
|
||||||
| **QS6** | Опционально: второй слот в EMM-странице тем же сериализатором | мгновенное «переиграть» без обращения к диску |
|
|
||||||
|
|
||||||
Порядок не переставлять: QS0 первым (он может изменить весь план), QS3 без
|
|
||||||
QS2 нечего проверять, а QS5 обязан идти после QS3 — иначе смена тайлсета
|
|
||||||
замаскирует ошибки восстановления.
|
|
||||||
|
|
||||||
**Главный критерий приёмки всей задачи:** сохранить состояние, выйти по
|
|
||||||
`ESC`, запустить roomtest заново, загрузить — и оказаться там же. Именно
|
|
||||||
этого сценария сейчас нет ничем, и ради него задача и делается.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Риски и открытые вопросы
|
|
||||||
|
|
||||||
1. **`static` в банковых модулях.** Их нет в карте символов, то есть
|
|
||||||
отладчиком снимок не проверить. Возможно, стоит сделать `room_modif` и
|
|
||||||
`trobs` НЕ-static — так же, как уже сделано с `mobs` в `pop_room.c` и
|
|
||||||
ровно по той же мотивации (там это записано прямым комментарием).
|
|
||||||
2. **Место в банке 6.** `pop_trob` занимает 3802/16384 — запас есть, но
|
|
||||||
сериализатор лучше писать компактным обходом, а не 30 отдельными
|
|
||||||
вызовами.
|
|
||||||
3. **Три ГСЧ.** Проверить, что сохранены ВСЕ: пропуск любого даст
|
|
||||||
«загрузилось, но играется иначе» — самый неприятный класс бага, потому
|
|
||||||
что выглядит как случайность.
|
|
||||||
4. **Согласованность `Char` и `Kid`.** У нас окно `Char` — отдельная копия;
|
|
||||||
если сохранить их рассогласованными (снимок посреди тика), загрузка
|
|
||||||
воскресит рассогласование. Отсюда требование QS4: только между кадрами.
|
|
||||||
Урок свежий — ровно на этом стыке жил
|
|
||||||
[BUG-CHEAT-IMM-1](../roomtest/BUGS_CLOSED.md#bug-cheat-imm-1).
|
|
||||||
5. **Дабл-буфер.** Самый вероятный источник «почти работает»: забыть вторую
|
|
||||||
страницу. Симптом — мерцание через кадр
|
|
||||||
(см. `roomtest/CLAUDE.md`, раздел про дабл-буфер).
|
|
||||||
6. **Открытый вопрос:** нужен ли снимок в файле вообще, или EMM-страницы
|
|
||||||
достаточно. Решать после QS3, по факту использования.
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
# PoP roomtest — модель `kid_room ≠ drawn_room` (баг #4)
|
|
||||||
|
|
||||||
> **Статус: ЖИВОЙ ПЛАН, сделан частично (сверено 2026-08-01).**
|
|
||||||
> - **S1 — сделан:** `kid_room` заведён отдельно от `cur_room`,
|
|
||||||
> `update_kid_render_dx()` (`roomtest.c`) даёт рендер-смещение ∓140, а
|
|
||||||
> `pop_kid_set_render_dx` применяет его в отрисовке. Фактически это пока
|
|
||||||
> каркас: `enter_room` держит `kid_room == cur_room`, так что смещение
|
|
||||||
> всегда 0.
|
|
||||||
> - **S2/S3/S4 — не сделаны и не срочны.** Исходный повод (баг #4,
|
|
||||||
> пинг-понг у шва) закрыт иначе — поправкой odd-pixel в
|
|
||||||
> `char_x_forward_edge` + `pop_leave_timer` (разбор корня —
|
|
||||||
> `../roomtest/BUGS_CLOSED.md`, BUG-SEAM-PINGPONG).
|
|
||||||
>
|
|
||||||
> **Зачем документ остаётся.** Полная straddle-модель понадобится для:
|
|
||||||
> (а) читов осмотра соседних комнат `H/J/U/N` (`levels_plan.md` §4),
|
|
||||||
> (б) сцен, где Кид и страж в разных комнатах кадра, (в) остатков окклюзии у
|
|
||||||
> шва (S4). Брать из `../roomtest/TASKS_OPEN.md`, когда дойдёт очередь.
|
|
||||||
|
|
||||||
Порт straddle-модели SDLPoP: персонаж может находиться в СОСЕДНЕЙ комнате,
|
|
||||||
пока на экране ещё ТЕКУЩАЯ (drawn_room). Источник истины — SDLPoP.
|
|
||||||
|
|
||||||
## Факты из SDLPoP (подтверждено чтением исходника)
|
|
||||||
|
|
||||||
- `Char.room` (реальная комната персонажа) ≠ `drawn_room` (отрисованная) —
|
|
||||||
штатное состояние.
|
|
||||||
- **Коллизия через ±140:** `xpos_in_drawn_room()` (seg004:0405) сдвигает
|
|
||||||
xpos на `±TILE_SIZEX*SCREEN_TILECOUNTX = ±140`, когда `curr_room` колонки
|
|
||||||
(`curr_row_coll_room[col]`) ≠ `drawn_room` (room_L/room_BL → −140,
|
|
||||||
room_R/room_BR → +140). Т.е. коллизия строится по РЕАЛЬНЫМ тайлам соседей.
|
|
||||||
- **Смена экрана:** `check_the_end()` (seg000:0FBD): `if (next_room!=0 &&
|
|
||||||
next_room!=drawn_room) { drawn_room=next_room; load_room_links; redraw }`.
|
|
||||||
`next_room` ставится в `exit_room()` (= `Char.room` ПОСЛЕ успешного
|
|
||||||
`leave_room`). Значит drawn_room следует за Char.room, но Char.room меняется
|
|
||||||
ТОЛЬКО при реальном пересечении шва (leave_room, seg002:0504) на «легальном»
|
|
||||||
кадре/действии (не turn/climb/standup).
|
|
||||||
- **Отрисовка левого соседа:** только `load_leftroom()` (col9 левого соседа в
|
|
||||||
левую кромку); правый сосед НЕ рисуется (изометрия). Окклюзия ворот на шве —
|
|
||||||
только левая (seg008:696).
|
|
||||||
- **Ceiling-полоса:** `draw_room` рисует доп. ряд из `room_A` (row2, draw_main_y
|
|
||||||
=-1). (Уже реализовано, баг #3.)
|
|
||||||
|
|
||||||
## Текущее состояние нашего движка (до #4)
|
|
||||||
|
|
||||||
`cur_room` (=drawn_room) ВСЕГДА == комната Kid. Шов подделан: Kid остаётся в
|
|
||||||
drawn_room с `curr_col=-1/10` + снапшоты соседей `g_lcol/g_rcol` (коллизия ±1
|
|
||||||
кол) / `lcol_bg` (openness ворот). Уход из комнаты — `pop_leave_dir`/`enter_room`
|
|
||||||
МГНОВЕННО при пересечении порога `char_x`. Отсюда #4: экран переключается
|
|
||||||
раньше, чем в оригинале (Kid должен «отступить» за кромку, оставив старую
|
|
||||||
комнату).
|
|
||||||
|
|
||||||
## План (инкременты, каждый проверяется в MAME)
|
|
||||||
|
|
||||||
### S1. Данные + рендер-смещение Kid
|
|
||||||
- Ввести `kid_room` (реальная комната Kid) отдельно от `cur_room`(=drawn_room).
|
|
||||||
- `kid_x_offset()` = разница комнат: kid_room == left(drawn) → лог. x Kid −140
|
|
||||||
(рисуется за левой кромкой); right → +140; равны → 0. (порт
|
|
||||||
xpos_in_drawn_room).
|
|
||||||
- `kid_draw`/heal/fore используют смещение (Kid рисуется частично за кромкой).
|
|
||||||
- Проверка: Kid у шва рисуется со сдвигом, экран не дёргается.
|
|
||||||
|
|
||||||
### S2. Коллизия по kid_room
|
|
||||||
- Коллизионный контекст (`g_fg`/edges/`g_room`/modif в pop_map) следует за
|
|
||||||
`kid_room`, а не за drawn_room. Когда kid_room≠drawn_room — грузим
|
|
||||||
соседа как коллизионную комнату (curr_col 0..9 в кадре kid_room).
|
|
||||||
- Отрисовка (room_fg и т.п.) остаётся по drawn_room.
|
|
||||||
- Порт `curr_row_coll_room[]`/`xpos_in_drawn_room` можно упростить: держим
|
|
||||||
ОДИН коллизионный room (kid_room) + существующие снапшоты кромок для ±1 кол.
|
|
||||||
|
|
||||||
### S3. Отложенная смена drawn_room
|
|
||||||
- Уход (`check_leave`/`check_leave_below`): ставит `kid_room=сосед`,
|
|
||||||
репроецирует Kid (x∓140, col∓10) — но drawn_room НЕ меняет сразу.
|
|
||||||
- `check_the_end`-эквивалент в главном цикле: `if (kid_room != drawn_room &&
|
|
||||||
<условие коммита>) enter_room(kid_room)`. Условие коммита — по SDLPoP:
|
|
||||||
как только Char.room сменилась легальным leave (не bumped/turn). Для
|
|
||||||
«bumped назад за кромку» drawn_room остаётся (симптом #4).
|
|
||||||
- Проверка сценариев #4/#5 в MAME.
|
|
||||||
|
|
||||||
### S4. Полировка
|
|
||||||
- Окклюзия/ceiling у шва при straddle, BUG-OCCL-1 (глубина), правый край.
|
|
||||||
|
|
||||||
## Связанные баги — все ЗАКРЫТЫ (`../roomtest/BUGS_CLOSED.md`)
|
|
||||||
BUG-CEIL-1 (руки при прыжке вверх), BUG-CEIL-2 (loose в потолке),
|
|
||||||
BUG-CEIL-3 (потолок над анимируемыми воротами), BUG-OCCL-1 (тень дальней
|
|
||||||
колонны) — починены без полной straddle-модели. То есть S4 «полировка
|
|
||||||
окклюзии» осталась актуальной только для окклюзии У ШВА при straddle.
|
|
||||||
Memory: `pop_seam_room_model`.
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
# Комнаты для отладочного телепорта (`+` / `-`)
|
|
||||||
|
|
||||||
Считано скриптом прямо по `../SDLPoP/data/LEVELS/res20NN.bin`, 2026-08-13.
|
|
||||||
Задача: обход комнат читом идёт последовательно, и в КАЖДОЙ комнате Киду
|
|
||||||
должно найтись место для материализации без падения и смерти.
|
|
||||||
|
|
||||||
## Критерии пропуска
|
|
||||||
|
|
||||||
| причина | как определяется |
|
|
||||||
|---|---|
|
|
||||||
| **недостижима** | до комнаты нельзя дойти от стартовой обходом связей (BFS по left/right/up/down) |
|
|
||||||
| **нет пола** | ни одного тайла, на котором можно стоять (`tile_is_floor`, seg004) |
|
|
||||||
| **пол опасный** | стоять есть на чём, но обычного пола (код 1) нет — только пики, расшатанные плиты и прочее |
|
|
||||||
|
|
||||||
Про достижимость важно: считать «сколько комнат на неё ссылаются» НЕ
|
|
||||||
годится. На уровне 1 комнаты **13** и **18** ссылаются только друг на
|
|
||||||
друга (13.down = 18, 18.up = 13), то есть входящая связь у каждой есть, а
|
|
||||||
из остального уровня в них не попасть. Ловит это только обход от старта.
|
|
||||||
|
|
||||||
Чит ищет место снизу вверх: сперва обычный пол (код 1), потом любой
|
|
||||||
проходимый тайл (`pop_dbg_roomnav`, `roomtest_cold.c`). Поэтому «пол
|
|
||||||
опасный» — это комнаты, где он свалится на fallback и Кид может погибнуть.
|
|
||||||
|
|
||||||
## Таблица по уровням
|
|
||||||
|
|
||||||
| ур. | старт | достижимо | пропускать |
|
|
||||||
|---|---|---|---|
|
|
||||||
| 1 | 1 | 21/24 | **13** (недостижима), **18** (недостижима), **24** (недостижима) |
|
|
||||||
| 2 | 5 | 24/24 | **14** (пол опасный), **17** (пол опасный) |
|
|
||||||
| 3 | 9 | 22/24 | **17** (пол опасный), **19** (пол опасный), **20** (нет пола), **21** (пол опасный), **23** (недостижима/нет пола), **24** (недостижима/нет пола) |
|
|
||||||
| 4 | 1 | 24/24 | **19** (нет пола), **20** (нет пола) |
|
|
||||||
| 5 | 7 | 19/24 | **1** (недостижима), **3** (недостижима), **5** (недостижима), **6** (недостижима), **19** (недостижима/нет пола), **21** (пол опасный), **22** (нет пола) |
|
|
||||||
| 6 | 24 | 14/24 | **2** (пол опасный), **3** (нет пола), **4** (недостижима), **7** (нет пола), **8** (недостижима/нет пола), **11** (пол опасный), **13** (недостижима), **14** (недостижима), **16** (недостижима), **17** (недостижима), **19** (недостижима/нет пола), **20** (недостижима), **21** (недостижима/пол опасный), **22** (недостижима), **23** (нет пола) |
|
|
||||||
| 7 | 17 | 24/24 | **3** (пол опасный), **17** (нет пола), **21** (пол опасный) |
|
|
||||||
| 8 | 1 | 21/24 | **9** (нет пола), **10** (пол опасный), **11** (недостижима), **15** (недостижима), **17** (пол опасный), **19** (недостижима), **20** (пол опасный), **21** (нет пола) |
|
|
||||||
| 9 | 11 | 24/24 | **8** (нет пола), **18** (пол опасный) |
|
|
||||||
| 10 | 1 | 19/24 | **3** (нет пола), **4** (пол опасный), **6** (недостижима), **9** (нет пола), **11** (пол опасный), **13** (нет пола), **18** (нет пола), **20** (нет пола), **21** (недостижима), **22** (недостижима), **23** (недостижима), **24** (недостижима) |
|
|
||||||
| 11 | 6 | 23/24 | **3** (нет пола), **5** (нет пола), **9** (нет пола), **10** (нет пола), **11** (нет пола), **12** (нет пола), **17** (нет пола), **18** (недостижима), **23** (нет пола) |
|
|
||||||
| 12 | 3 | 24/24 | **5** (нет пола), **6** (пол опасный), **8** (пол опасный), **10** (нет пола), **11** (нет пола), **17** (нет пола), **18** (пол опасный), **19** (пол опасный), **22** (нет пола) |
|
|
||||||
| 13 | 23 | 11/24 | **2** (нет пола), **4** (пол опасный), **5** (недостижима), **6** (недостижима/пол опасный), **7** (недостижима), **8** (недостижима/пол опасный), **9** (недостижима), **12** (недостижима), **14** (недостижима), **15** (недостижима), **18** (недостижима/пол опасный), **19** (недостижима/пол опасный), **20** (недостижима), **21** (недостижима), **22** (недостижима/нет пола) |
|
|
||||||
| 14 | 4 | 6/24 | **7** (недостижима/нет пола), **8** (недостижима), **9** (недостижима), **10** (недостижима), **11** (недостижима), **12** (недостижима), **13** (недостижима), **14** (недостижима/пол опасный), **15** (недостижима), **16** (недостижима), **17** (недостижима/пол опасный), **18** (недостижима), **19** (недостижима), **20** (недостижима), **21** (недостижима), **22** (недостижима), **23** (недостижима), **24** (недостижима) |
|
|
||||||
| 15 | 6 | 6/24 | **1** (недостижима), **2** (недостижима), **7** (нет пола), **9** (недостижима/пол опасный), **10** (недостижима/пол опасный), **11** (недостижима/пол опасный), **12** (недостижима/пол опасный), **13** (недостижима/пол опасный), **14** (недостижима/пол опасный), **15** (недостижима/пол опасный), **16** (недостижима/пол опасный), **17** (недостижима/пол опасный), **18** (недостижима/пол опасный), **19** (недостижима/пол опасный), **20** (недостижима/пол опасный), **21** (недостижима/пол опасный), **22** (недостижима/пол опасный), **23** (недостижима/пол опасный), **24** (недостижима/пол опасный) |
|
|
||||||
|
|
||||||
## Спецкомнаты — пропускать независимо от таблицы
|
|
||||||
|
|
||||||
| ур. | комн. | почему |
|
|
||||||
|---|---|---|
|
|
||||||
| 12 | 23 | **seamless exit**: попадание МЕНЯЕТ УРОВЕНЬ на 13-й. Чит-триггер придержан (`pop_nav_hold`), но в обходе комнате делать нечего |
|
|
||||||
| 6 | 1 | **falling exit**: Кид проваливается вниз и уходит на 7-й уровень |
|
|
||||||
| 7 | 17 | **falling entry**: экран сразу переводится на комнату НИЖЕ (в таблице уже «нет пола») |
|
|
||||||
| 7 | 14 | проходится СВЕРХУ ВНИЗ; чит для неё уже особый — ставит Кида в ряд 0 |
|
|
||||||
| 5 | 24 | тень ждёт в колонке −1 ряда 0; посадка рядом начинает схватку, которой там быть не должно |
|
|
||||||
| 13 | 23, 16 | вход роняет гряду плит (`check_fall_flo`) — материализация под падающей плитой |
|
|
||||||
|
|
||||||
## Уровень 15
|
|
||||||
|
|
||||||
Экран защиты от копирования, не часть сюжета (см.
|
|
||||||
[`levels_12_15_plan.md`](levels_12_15_plan.md) §4). Портировать не
|
|
||||||
планируем — **пропускать целиком**.
|
|
||||||
|
|
||||||
## Как применять
|
|
||||||
|
|
||||||
Список — данные, а не логика: держать таблицей в отладочном коде рядом с
|
|
||||||
`pop_dbg_roomnav` и пропускать помеченные комнаты, чтобы `+` всегда попадал
|
|
||||||
в пригодную. Пересчитывать скриптом, если поменяются данные уровней.
|
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
# Атлас Тени — разбор и план
|
|
||||||
|
|
||||||
Дата: 2026-08-20. Статус: **ЗАКРЫТО.** Ш0-Ш4 сделаны, прогон
|
|
||||||
пользователем на уровнях **4, 5, 6 и 12** — всё корректно.
|
|
||||||
|
|
||||||
Задача: сейчас Тень рисуется спрайтами Кида (вне боя) и спрайтами стража
|
|
||||||
(в бою) как есть, поэтому от Кида она не отличается. В оригинале её вид
|
|
||||||
даёт наложение спрайта на себя со сдвигом; наш пакетный блит такого не
|
|
||||||
умеет, поэтому запекаем результат в отдельный атлас заранее.
|
|
||||||
|
|
||||||
## 1. Что делает оригинал (проверено по SDLPoP, не по памяти)
|
|
||||||
|
|
||||||
`draw_objtable_item`, `seg008.c:1600`:
|
|
||||||
|
|
||||||
```c
|
|
||||||
case 1: // shadow
|
|
||||||
add_midtable(obj_chtab, obj_id + 1, obj_xh, obj_xl, obj_y, blitters_2_or, 1);
|
|
||||||
add_midtable(obj_chtab, obj_id + 1, obj_xh, obj_xl + 1, obj_y, blitters_3_xor, 1);
|
|
||||||
```
|
|
||||||
|
|
||||||
Тот же самый спрайт кладётся ДВАЖДЫ: первый проход в x, второй в x+1.
|
|
||||||
Два уточнения, которые меняют алгоритм запекания:
|
|
||||||
|
|
||||||
1. **`blitters_2_or` — это НЕ побитовое ИЛИ.** В SDLPoP он реализован
|
|
||||||
обычным блитом с colour key = индекс 0 (`method_6_blit_img_to_scr`,
|
|
||||||
`seg009.c:3306`: `SDL_SetColorKey(image, SDL_TRUE, 0)`). То есть
|
|
||||||
первый проход — наш обычный прозрачный блит, один в один.
|
|
||||||
2. **`blitters_3_xor` работает по 24-битному RGB, а не по индексам
|
|
||||||
палитры** (`blit_xor`, `seg009.c:3190`: конвертация в 24 бита, затем
|
|
||||||
`*p_dest ^= *p_src` побайтно). Прозрачности у него нет вообще —
|
|
||||||
XOR'ится весь прямоугольник, но прозрачные пиксели спрайта это
|
|
||||||
чёрный 0x000000, а XOR с нулём ничего не меняет.
|
|
||||||
|
|
||||||
Отсюда и берётся необходимость СВОЕЙ палитры: XOR двух цветов игровой
|
|
||||||
палитры даёт цвет, которого в ней нет.
|
|
||||||
|
|
||||||
### Каким набором спрайтов рисуется Тень (проверено 2026-08-20)
|
|
||||||
|
|
||||||
Сначала я решил, что в боевых кадрах Тень рисуется спрайтами СТРАЖА, и
|
|
||||||
записал это в план. **Это было неверно, поправка ниже.**
|
|
||||||
|
|
||||||
Набор выбирает НЕ charid. `load_frame_to_obj` (`seg008.c:1752`):
|
|
||||||
|
|
||||||
```c
|
|
||||||
word chtab_base = id_chtab_2_kid; // жёстко Кид
|
|
||||||
obj_chtab = chtab_base + (cur_frame.sword >> 6); // старшие 2 бита кадра
|
|
||||||
```
|
|
||||||
|
|
||||||
то есть набор берётся из САМИХ ДАННЫХ КАДРА, поле `sword`: младшие 6 бит —
|
|
||||||
картинка меча, старшие два — смещение chtab относительно Кида
|
|
||||||
(`types.h:361`). Проверил обе таблицы:
|
|
||||||
|
|
||||||
- `frame_table_kid` — **все 241 кадра** имеют `sword & 0xC0 == 0` → chtab_2.
|
|
||||||
Значит **Кид всегда рисуется своими спрайтами**, боевые кадры 150..189 не
|
|
||||||
исключение;
|
|
||||||
- `frame_tbl_guard` — все кадры имеют `0xC0` → chtab_5.
|
|
||||||
|
|
||||||
Тень берёт `frame_tbl_guard` для кадров 150..189 (`seg006.c:533`), значит в
|
|
||||||
бою она идёт через chtab_5. **Но chtab_5 — это не «страж», это «соперник
|
|
||||||
уровня»**: он грузится из `tbl_guard_dat[tbl_guard_type[уровень]]`
|
|
||||||
(`seg000.c:1117`), а `tbl_guard_type[12] == 4` → **SHADOW.DAT**.
|
|
||||||
|
|
||||||
Открыл SHADOW.DAT: его палитра **побайтно равна палитре Кида** (у GUARD.DAT
|
|
||||||
там серая рампа под перекраску), а спрайты — Кид в боевых позах, не страж.
|
|
||||||
Отрендерил тройками «Кид / SHADOW.DAT / GUARD.DAT» для кадров
|
|
||||||
151/153/158/161/167: первые две колонки — один и тот же персонаж, третья —
|
|
||||||
серый страж в тюрбане.
|
|
||||||
|
|
||||||
**Вывод: Тень ВСЕГДА выглядит Кидом, и ощущение пользователя верно.**
|
|
||||||
Спрайты при этом лежат в двух файлах: не-боевые кадры в chtab_2 (KID),
|
|
||||||
боевые — в chtab_5, заполненном SHADOW.DAT.
|
|
||||||
|
|
||||||
### Можно ли взять для боя собственные кадры Кида
|
|
||||||
|
|
||||||
Механически да: `frame_table_kid` покрывает 150..189 со своей геометрией,
|
|
||||||
и хватило бы снять спецветку для `charid_1_shadow` в `load_frame`. Но
|
|
||||||
кадры НЕ совпадают: из 34 боевых у 31 отличается габарит (на 1-6 px), у 3
|
|
||||||
отличаются пиксели. Это разная графика, а не одна и та же в двух файлах.
|
|
||||||
Поэтому берём SHADOW.DAT — он и есть «кадры Кида для Тени», подготовленные
|
|
||||||
авторами.
|
|
||||||
|
|
||||||
## 2. Единственное место, где запечка отличается от оригинала
|
|
||||||
|
|
||||||
XOR идёт по тому, что УЖЕ на экране, то есть результат зависит от фона.
|
|
||||||
Разбор по пикселям показывает, что зависимость узкая:
|
|
||||||
|
|
||||||
| пиксель | первый проход | второй проход | зависит от фона? |
|
|
||||||
|---|---|---|---|
|
|
||||||
| спрайт непрозрачен в x | закрашен цветом спрайта | XOR с цветом из x−1 | **нет** |
|
|
||||||
| прозрачен в x, непрозрачен в x−1 | фон | фон XOR цвет | **да** |
|
|
||||||
| прозрачен в обоих | фон | фон | нет (не рисуем) |
|
|
||||||
|
|
||||||
То есть от фона зависит только **кайма в один пиксель по левым кромкам
|
|
||||||
силуэта**. На чёрном фоне (а Тень почти всегда на нём — уровень 4 у
|
|
||||||
зеркала, уровень 6, бой на 12-м) `фон XOR цвет == цвет`, и запечка точна.
|
|
||||||
На светлом фоне оригинал подкрасит эту кайму, мы — нет.
|
|
||||||
|
|
||||||
**Это осознанное расхождение, в `impl_diff.md` при реализации.**
|
|
||||||
|
|
||||||
## 3. Замеры на реальных ассетах
|
|
||||||
|
|
||||||
Прогон алгоритма по правильным наборам (`SDLPoP/data/KID` +
|
|
||||||
`SDLPoP/data/SHADOW`):
|
|
||||||
|
|
||||||
| набор | кадров | макс. габарит | разных цветов |
|
|
||||||
|---|---:|---|---:|
|
|
||||||
| Кид (chtab_2) | 219 | 56×57 | 44 |
|
|
||||||
| SHADOW.DAT (chtab_5 на ур. 12) | 32 | 49×38 | 22 |
|
|
||||||
| **вместе** | **251** | 57×57 (с учётом +1 px сдвига) | **45** |
|
|
||||||
|
|
||||||
Цветов вместе почти столько же, сколько у одного Кида: SHADOW.DAT сидит на
|
|
||||||
той же палитре, новых сочетаний XOR почти не даёт. Всего пикселей во всех
|
|
||||||
кадрах Тени — 91 045.
|
|
||||||
|
|
||||||
### Насколько заметна подмена
|
|
||||||
|
|
||||||
«Затронуто» само по себе ничего не говорит — важно, НА СКОЛЬКО сместился
|
|
||||||
цвет. Порог различимости на плоской заливке ~30-40 единиц евклида в RGB
|
|
||||||
(максимум возможного — 441). Пробовал три стратегии подбора:
|
|
||||||
|
|
||||||
- **A** — оставить N самых частых цветов ТОЧНО, остальные в ближайший;
|
|
||||||
- **B** — взвешенный k-means по всем цветам (двигает вообще все);
|
|
||||||
- **C** — гибрид: часть слотов под точные частые, остаток — кластеры хвоста.
|
|
||||||
|
|
||||||
| палитра | стратегия | изменено px | заметно (40..90) | сильно (>90) | худшая |
|
|
||||||
|---|---|---:|---:|---:|---:|
|
|
||||||
| 16 | A самые частые | 959 (1,05 %) | 569 | 381 | 128 |
|
|
||||||
| **16** | **C гибрид 5+11** | 6 984 (7,67 %) | 1 078 | **76** | 114 |
|
|
||||||
| 32 | A самые частые | 50 (0,05 %) | 40 | 4 | 113 |
|
|
||||||
| 32 | C гибрид 28+4 | 73 (0,08 %) | 58 | **0** | 90 |
|
|
||||||
|
|
||||||
Читается так. При 32 цветах всё практически идеально: 73 пикселя на 251
|
|
||||||
кадр, грубых промахов нет вовсе. При 16 цветах выбор стратегии виден:
|
|
||||||
«самые частые» трогает меньше пикселей (959), но 381 из них уезжает СИЛЬНО;
|
|
||||||
гибрид размазывает ошибку — грубых остаётся 76, то есть примерно **0,3
|
|
||||||
пикселя на кадр**.
|
|
||||||
|
|
||||||
### Решение: **16 цветов, стратегия C (гибрид 5 + 11)**
|
|
||||||
|
|
||||||
Числа выше — про пиксели, а решает глаз. Отрендерил одни и те же кадры в
|
|
||||||
трёх видах (точный цвет / 32 / 16) и сравнил в увеличении ×3: **отличий
|
|
||||||
не видно**. Объяснение в самих числах: перцептивно значимых пикселей при
|
|
||||||
16 цветах гибридом — 1 154 на 251 кадр, это ~4,6 пикселя на кадр при
|
|
||||||
~1 000 видимых, и они РАССЫПАНЫ по контуру, а не собраны в пятно.
|
|
||||||
|
|
||||||
Поэтому берём 16, а не 32: экономим блок палитры (пригодится под будущие
|
|
||||||
наборы — принцесса, визирь, мышь), а разница неразличима.
|
|
||||||
|
|
||||||
**Но стратегия обязана быть гибридной.** При 16 цветах «взять самые
|
|
||||||
частые» впятеро хуже по грубым промахам (381 пиксель против 76), и это
|
|
||||||
единственное место, где выбор стратегии виден. Гибрид: 5 самых частых
|
|
||||||
берём ТОЧНО, оставшиеся 11 слотов отдаём под взвешенные кластеры хвоста.
|
|
||||||
|
|
||||||
Если в реальной игре кайма всё же будет резать глаз — переход на 32 цвета
|
|
||||||
это одна константа в упаковщике и один блок палитры, данные не меняются.
|
|
||||||
|
|
||||||
## 4. Палитра: что занято и куда класть
|
|
||||||
|
|
||||||
| блок | кто | примечание |
|
|
||||||
|---|---|---|
|
|
||||||
| 0x30..0x3F | VGA-16 | общая; из неё цвет вспышки, пузырьки зелий, отладочная метка |
|
|
||||||
| 0x40..0x4F | chtab_1 | зелья, пламя |
|
|
||||||
| 0x50..0x5F | env тайлсета | **меняется** подземелье/дворец |
|
|
||||||
| 0x60..0x6F | wall тайлсета | **меняется** подземелье/дворец |
|
|
||||||
| 0x70..0x7F | chtab_2 | Кид |
|
|
||||||
| 0x80..0x8F | chtab_0 | меч в руке |
|
|
||||||
| 0x90..0x9F | chtab_5 | страж, перезаливается цветом стража |
|
|
||||||
|
|
||||||
Занято 112 слотов из 256, **свободно 144** — девять выровненных блоков по
|
|
||||||
16. Оговорки: 0xFF в наших атласах это маркер прозрачности, а запись 0
|
|
||||||
правит `flash_bg`, так что блоки 0x00 и 0xF0 лучше не трогать.
|
|
||||||
|
|
||||||
**Берём 0xA0..0xAF** (16 слотов) — сразу за стражем, персонажи остаются
|
|
||||||
сгруппированы, а блок 0xB0 остаётся свободным (под 32 цвета Тени, если
|
|
||||||
понадобится, или под будущие наборы).
|
|
||||||
|
|
||||||
Важно: в наших атласах прозрачность кодируется байтом 0xFF, а исходный
|
|
||||||
индекс 0 в них означает «прозрачно». У Тени **чёрный — настоящий цвет**
|
|
||||||
(это XOR-погашенная середина силуэта, 51 % всех её пикселей), поэтому у
|
|
||||||
неё маппинг свой: «нет пикселя» → 0xFF, цвет k → 0xA0 + k, и слот 0xA0 =
|
|
||||||
чёрный НЕПРОЗРАЧНЫЙ.
|
|
||||||
|
|
||||||
## 5. Объём
|
|
||||||
|
|
||||||
251 кадр против 219 у Кида — по страницам EMM примерно как нынешний
|
|
||||||
набор Кида (28 страниц), плюс пара на кадры из SHADOW.DAT. При 215 свободных
|
|
||||||
страницах на старте (memory `sprinter_emm_budget`) это не проблема.
|
|
||||||
|
|
||||||
Кадры смерти («убитый Кид») пока НЕ вырезаем — экономия несколько
|
|
||||||
страниц, а риск промахнуться мимо нужного кадра реальный: Тень на 12-м
|
|
||||||
уровне умирает.
|
|
||||||
|
|
||||||
## 6. План работ
|
|
||||||
|
|
||||||
**Ш0. Довезти SHADOW.DAT.** Его у нас нет вовсе (см. BUG-SHADOW-SET) —
|
|
||||||
добавить каталог ассетов и правило в Makefile рядом с GUARD/SKEL/VIZIER.
|
|
||||||
|
|
||||||
**Ш1. Упаковщик** `toolchain/pop_pack_shadow.py`: прогнать оба набора
|
|
||||||
через алгоритм §1, собрать 16-цветную палитру гибридом (10 точных + 6
|
|
||||||
кластеров хвоста), выдать
|
|
||||||
`poc/res/shadow/shadow0..N.atl` + `shadow.pal` + `pop_shadow_atlas.h`.
|
|
||||||
Критерий: предпросмотр PNG совпадает с видом Тени в SDLPoP.
|
|
||||||
|
|
||||||
**Ш2. Загрузка**: `pop_shadow_load()` рядом с `pop_kid_load`, палитра в
|
|
||||||
0xA0..0xAF, и обе страницы дабл-буфера (как `bg_load_tile_pal`).
|
|
||||||
Грузить ЛЕНИВО — только когда на уровне есть Тень (4, 5, 6, 12), иначе
|
|
||||||
28 страниц EMM висят зря.
|
|
||||||
|
|
||||||
**Ш3. Отрисовка**: одно место — `pop_cdraw.c:551..568`, где выбирается
|
|
||||||
`pages`. Сейчас там для соперника берётся `gp`, а для не-боевых кадров
|
|
||||||
Тени подменяется на `kidp`; станет «charid == CHARID_1_SHADOW → shadowp»
|
|
||||||
БЕЗ подмены: обе половины (кадры Кида и кадры SHADOW.DAT) лежат в ОДНОМ
|
|
||||||
атласе Тени, так что ветка становится проще нынешней.
|
|
||||||
|
|
||||||
**Ш4. Проверка в MAME**: уровень 4 (Тень у зеркала), уровень 6 (Тень
|
|
||||||
крадёт зелье), уровень 12 (бой с Тенью — там она в боевых кадрах, то
|
|
||||||
есть проверяется вторая половина набора).
|
|
||||||
|
|
||||||
## 7. Что проверить артефактом до Ш3
|
|
||||||
|
|
||||||
1. Индекс кадра для Тени в боевых кадрах: `frame_tbl_guard` адресуется
|
|
||||||
как `frame + add_frame - 149` (`seg006.c:535`), то есть у нашего
|
|
||||||
атласа Тени нумерация двух половин должна совпадать с тем, что уже
|
|
||||||
делает `pop_frame_tbl_is_guard`.
|
|
||||||
2. Перекраска Тени НЕ нужна: SHADOW.DAT приходит уже в палитре Кида, а
|
|
||||||
`curr_guard_color` у не-стражей равен 0 (`seg002:183`). Ветку
|
|
||||||
`pop_guard_set_palette` для типа 4 звать нельзя — она затрёт палитру
|
|
||||||
Тени палитрой стража.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 8. Как сделали (2026-08-20)
|
|
||||||
|
|
||||||
- `toolchain/pop_pack_shadow.py` — запекает обе половины, собирает
|
|
||||||
16-цветную палитру гибридом (получилось 5 частых точно + 11 кластеров
|
|
||||||
хвоста), пишет `poc/res/shadow/sk0..27.atl`, `sf0..3.atl` и
|
|
||||||
`roomtest/pop_shadow_atlas.h`. Итог: **251 спрайт, 32 EMM-страницы,
|
|
||||||
225 178 Б**.
|
|
||||||
- `roomtest/pop_shadow.c/.h` (банк 8) — загрузка. **Грузим один раз при
|
|
||||||
старте**, а не по уровням: страниц EMM с запасом, а забыть перезагрузку
|
|
||||||
на границе легко — ровно так и появился BUG-SHADOW-SET.
|
|
||||||
- `pop_cdraw.c` — одна ветка выбора атласа; заодно брызги урона теперь
|
|
||||||
берут атлас отдельным указателем (`spl`), потому что у оригинала они
|
|
||||||
всегда из «своего» chtab, а у Тени кадр может прийти из другой
|
|
||||||
половины набора.
|
|
||||||
|
|
||||||
## Грабля, стоившая одного прогона
|
|
||||||
|
|
||||||
Набор загрузился, силуэт нарисовался правильной формы — и **целиком
|
|
||||||
чёрный**. Причина: `gfx_pal_fload("KID\\kid.pal")` заливает ВСЕ 256
|
|
||||||
записей палитры и затирает любые слоты, выставленные до него. Ровно та
|
|
||||||
же беда уже была с тайлсетом — сразу за этим вызовом стоит
|
|
||||||
`pop_bg_pal_apply`. Поэтому палитра Тени вынесена в отдельный
|
|
||||||
`pop_shadow_pal_apply()` и зовётся там же, а не внутри загрузки атласов.
|
|
||||||
|
|
||||||
**Правило на будущее: любой новый набор палитровых слотов красится ПОСЛЕ
|
|
||||||
kid.pal, рядом с pop_bg_pal_apply.**
|
|
||||||
|
|
||||||
## Проверено
|
|
||||||
|
|
||||||
Уровни 4 (рождение из зеркала), 5 (кража зелья), 6 (прыжок через
|
|
||||||
пропасть) и 12 (бой — там работает вторая половина набора, `sf*`) —
|
|
||||||
прогон пользователя 2026-08-20, расхождений не найдено.
|
|
||||||
|
|
||||||
Расхождение из §2 (кайма в один пиксель по левым кромкам на НЕчёрном
|
|
||||||
фоне) записано в `impl_diff.md`.
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
# Отрисовка Тени (charid_1_shadow) — изыскания, отложено
|
|
||||||
|
|
||||||
Статус на 2026-08-11: **отложено по решению пользователя.** Тень пока
|
|
||||||
рисуется как обычный персонаж — простой копией из атласов Кида
|
|
||||||
(`pop_cdraw.c`, банк 0x5C, аппаратная прозрачность `#FF`). Вернуться к
|
|
||||||
«правильному» виду, когда будут сделаны все уровни: тогда будет известно,
|
|
||||||
какими именно кадрами тень вообще пользуется.
|
|
||||||
|
|
||||||
Этот файл собирает всё, что уже выяснено, чтобы не переоткрывать.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Как тень выглядит в оригинале
|
|
||||||
|
|
||||||
Тень рисуется **двумя блитами ОДНОГО И ТОГО ЖЕ спрайта Кида**
|
|
||||||
(`seg008:1602`, `add_objtable`):
|
|
||||||
|
|
||||||
```c
|
|
||||||
case 1: // shadow
|
|
||||||
add_midtable(obj_chtab, obj_id + 1, obj_xh, obj_xl, obj_y, blitters_2_or, 1);
|
|
||||||
add_midtable(obj_chtab, obj_id + 1, obj_xh, obj_xl + 1, obj_y, blitters_3_xor, 1);
|
|
||||||
```
|
|
||||||
|
|
||||||
OR на месте, XOR со сдвигом на пиксель вправо. XOR гасит совпавшее,
|
|
||||||
остаются края — отсюда «контурный» вид. Это ЗАМЫСЕЛ оригинала, а не
|
|
||||||
артефакт SDLPoP: подтверждено печатью из живого SDLPoP (метка `DBGMIRROR`
|
|
||||||
в `add_objtable`) — и тень, и отражение идут из `chtab=2` (собственные
|
|
||||||
спрайты Кида), `swordbits=0`, обычными кадрами:
|
|
||||||
|
|
||||||
```
|
|
||||||
type=4 chtab=2 img=40 dir=0 clipL=137 clipT=3 charid=0 frame=41 <- отражение
|
|
||||||
type=1 chtab=2 img=41 dir=0 clipL=137 clipT=3 charid=1 frame=42 <- тень
|
|
||||||
```
|
|
||||||
|
|
||||||
Единственное различие между отражением и тенью — блиттер.
|
|
||||||
|
|
||||||
## 2. Чем мы располагаем
|
|
||||||
|
|
||||||
Блочные AND/OR/XOR/NOT акселератора подняты в libbgi 2026-08-11 (полный
|
|
||||||
набор строками и колонками, `gfx_blit_op` / `gfx_blit_part_op` /
|
|
||||||
`gfx_blit_cols_op` / `gfx_blit_cols_part_wx_op`; регресс — `tests/accop`,
|
|
||||||
10/10 PASS). Механика и ловушки — memory `accel_block_ops` и шапка
|
|
||||||
`libbgi/common/_gfx_blit_full_op.c`. То есть примитивов достаточно, дело
|
|
||||||
не в них.
|
|
||||||
|
|
||||||
## 3. Две причины, по которым «в лоб» не получается
|
|
||||||
|
|
||||||
### 3.1 XOR несовместим с нашей прозрачностью `#FF`
|
|
||||||
|
|
||||||
Аппаратная прозрачность (бит 3 видеобанка) подавляет запись байта `#FF`,
|
|
||||||
то есть смотрит на **результат** операции:
|
|
||||||
|
|
||||||
| операция | прозрачный пиксель источника | итог |
|
|
||||||
|---|---|---|
|
|
||||||
| AND | `#FF & bg = bg` | работает даром |
|
|
||||||
| OR | `#FF \| bg = #FF`, запись подавляется | работает даром |
|
|
||||||
| XOR | `#FF ^ bg = ~bg`, подавления нет | **инверсия фона по всему футпринту** |
|
|
||||||
|
|
||||||
Совпадение с «ничего не делать» у XOR получается только там, где фон равен
|
|
||||||
0 (`#FF ^ 0 = #FF` → подавляется). В DOS-оригинале прозрачный индекс = 0 —
|
|
||||||
нейтральный и для OR, и для XOR, поэтому там оба блиттера работают на одном
|
|
||||||
наборе спрайтов. У нас прозрачный `0xFF` (`pop_pack_kid.py`: `0 -> 0xFF`,
|
|
||||||
`i -> 0x70 + i`).
|
|
||||||
|
|
||||||
Замаскировать `#FF` внутри операции нельзя в принципе: побитовые AND/OR/XOR
|
|
||||||
не умеют «выбрать по условию», а `#FF` — нейтраль только для AND. Значит
|
|
||||||
источнику XOR-прохода нужен **прозрачный `0x00`**, то есть отдельный набор
|
|
||||||
спрайтов.
|
|
||||||
|
|
||||||
### 3.2 Операция читает ОЗУ-копию экрана, а не видео-ОЗУ
|
|
||||||
|
|
||||||
Чтение страниц `#50..#5F` всегда отдаёт ОЗУ-копию (memory
|
|
||||||
`sprinter_vram_transparency`), а персонажи рисуются банком `0x5C` («не
|
|
||||||
писать в копию» — на этом держится даровой heal). Поэтому второй проход
|
|
||||||
**не увидит результат первого**: два блита оригинала выродились бы в
|
|
||||||
«просто XOR», контурного эффекта не будет.
|
|
||||||
|
|
||||||
Лечится не банком `0x50` (он ломает heal — копия перестанет быть чистым
|
|
||||||
фоном), а **однопроходным композитом**: всё складывается в буфере
|
|
||||||
акселератора за один проход по колонке j футпринта
|
|
||||||
|
|
||||||
```
|
|
||||||
буфер := s[j] ; спрайт
|
|
||||||
буфер |= bg[j] ; вертикальное чтение экрана
|
|
||||||
буфер ^= s[j-1] ; тот же спрайт, предыдущая колонка = сдвиг на +1 px
|
|
||||||
запись ; вертикальная запись колонки
|
|
||||||
```
|
|
||||||
|
|
||||||
что **точно эквивалентно** двум блитам оригинала (крайние колонки: `x` —
|
|
||||||
только OR, `x+w` — только XOR) и вдобавок дешевле их: 4 burst'а на колонку
|
|
||||||
против 6. Такому композиту тоже нужен источник с прозрачным `0x00` — уже
|
|
||||||
на обоих шагах.
|
|
||||||
|
|
||||||
## 4. Сколько стоит подготовить источник с прозрачным `0x00`
|
|
||||||
|
|
||||||
Замер 2026-08-11 (`tests/convbench`, watchpoint по IO-записи в MAME, кадр =
|
|
||||||
430 080 тактов). Цикл безветвочный (`ADD A,A / SBC A,A / CPL / AND` —
|
|
||||||
маска из бита 7: прозрачный `#FF` отличается от цветов Кида `0x70..0x7F`
|
|
||||||
именно им), 59 номинальных T-states на байт, по факту **145.3 такта/байт**
|
|
||||||
(2.5× wait-state'ов ОЗУ):
|
|
||||||
|
|
||||||
| объём | кадров | секунд |
|
|
||||||
|---|---|---|
|
|
||||||
| 1 страница атласа, 16 КБ | 5.5 | 0.11 |
|
|
||||||
| весь атлас Кида, 28 страниц × 16 КБ = 448 КБ | 155 | 3.2 |
|
|
||||||
| он же **по реальному размеру данных (186 КБ)** | 64 | **1.3** |
|
|
||||||
|
|
||||||
Последняя строка — замечание пользователя: 28 атласов занимают 186 КБ, а не
|
|
||||||
448 КБ; обрабатывать по фактическому размеру ленты вместо целой страницы
|
|
||||||
даёт 2.4× (ценой проверки границы в цикле). Потолок разгона самого цикла —
|
|
||||||
ещё примерно вдвое (раскрутка убирает `djnz`, чтение через SP парами +
|
|
||||||
таблица 256 Б вместо арифметики), то есть **~0.7 с** на 186 КБ. Порядок
|
|
||||||
величины при этом не меняется.
|
|
||||||
|
|
||||||
**Окна:** источник и приёмник — разные EMM-страницы, а окно под атласы одно
|
|
||||||
(W0), поэтому конвертация гоняется «страница-источник в W0 →
|
|
||||||
страница-приёмник в W3» целыми страницами; побайтно переключать окно нельзя.
|
|
||||||
EMM-бюджет: +28 страниц (448 КБ) из ~3440 КБ свободных — не проблема
|
|
||||||
(memory `sprinter_emm_budget`), и он одинаков в любом из вариантов.
|
|
||||||
|
|
||||||
## 5. Варианты (когда вернёмся)
|
|
||||||
|
|
||||||
1. **Конвертация в рантайме при загрузке уровня с тенью** (4, 5, 6, 12):
|
|
||||||
диск и упаковщик не трогаем, цена — 1.3 с (или 0.7 с после разгона) на
|
|
||||||
загрузку такого уровня.
|
|
||||||
2. **Лениво, постранично** — 0.11 с (5.5 кадра) при первом обращении тени к
|
|
||||||
странице; рывок один раз на страницу, суммарно меньше, чем вариант 1.
|
|
||||||
3. **Второй набор `.atl` от упаковщика** (`pop_pack_kid.py`, прозрачный
|
|
||||||
`0x00`): 0 с рантайма, +186 КБ на образе и вторая ветка в загрузчике
|
|
||||||
атласов.
|
|
||||||
4. **Только OR-проход** (то, чем можно обойтись бесплатно): OR с нашим
|
|
||||||
`#FF`-атласом работает как есть, тень получается сплошным силуэтом в
|
|
||||||
палитре Кида, без контурного эффекта. Расхождение с оригиналом — тогда
|
|
||||||
записью в `docs/impl_diff.md`.
|
|
||||||
|
|
||||||
**Ключ к выбору — какие кадры тень вообще использует.** Предположение
|
|
||||||
пользователя: только бег, длинный прыжок (из зеркала), питьё зелья и
|
|
||||||
боёвка; прыжки с места и подтягивания — нет. Если так, конвертировать
|
|
||||||
(или паковать) нужно единицы страниц, а не 28, и разница между вариантами
|
|
||||||
почти исчезает. Список снимать по факту — когда уровни 5/6/12 будут
|
|
||||||
проходиться.
|
|
||||||
|
|
||||||
## 6. Что ещё придётся проверить глазами
|
|
||||||
|
|
||||||
Палитра. У нас индексы разложены группами по 16 (`pop_pack_bg.py`):
|
|
||||||
`0x30` VGA16, `0x40` chtab_1, `0x50` env, `0x60` wall, `0x70` kid, `0x80`
|
|
||||||
sword, `0x90` guard. Отсюда ожидания (аналитические, в MAME НЕ
|
|
||||||
проверялись):
|
|
||||||
|
|
||||||
- OR-проход ложится удачно: `0x7X | 0x5Y = 0x7Z` — результат остаётся в
|
|
||||||
палитре Кида, а младший ниббл получается ровно тот же, что дал бы DOS
|
|
||||||
(там OR шёл по 4-битным индексам внутри одной палитры);
|
|
||||||
- XOR-проход уводит результат в группы `0x0Z` (поверх OR-результата) и
|
|
||||||
`0x2Z` (по чистому фону) — **обе группы палитры у нас не заполнены**, то
|
|
||||||
есть контур рискует оказаться просто чёрным.
|
|
||||||
|
|
||||||
Значит к «посмотреть глазами» добавляется вопрос, чем заполнять `0x00..0x0F`
|
|
||||||
и `0x20..0x2F` — по сути это и будет выбор цветов тени. В DOS такого
|
|
||||||
вопроса не было: XOR двух 4-битных индексов всегда оставался внутри той же
|
|
||||||
16-цветной палитры.
|
|
||||||
@@ -1,643 +0,0 @@
|
|||||||
# Звук в порте PoP — разбор и план
|
|
||||||
|
|
||||||
Дата: 2026-08-20. Статус: **разбор, кода нет.**
|
|
||||||
|
|
||||||
Задача пользователя: добавить звук. Приоритет — эффекты; музыку, если
|
|
||||||
найдётся способ. Эффекты — **обязательно WAV, а не PC-спикер**
|
|
||||||
(уточнение 2026-08-20; см. §1а — оказалось, что они и так все в WAV). Ниже — что реально лежит в ассетах, что умеет железо, и
|
|
||||||
почему получившийся план вышел проще, чем ожидалось.
|
|
||||||
|
|
||||||
## 1. Главный вывод
|
|
||||||
|
|
||||||
**Ни MIDI разбирать, ни ноты сочинять не придётся, и ресэмплировать тоже.**
|
|
||||||
|
|
||||||
- Эффекты уже лежат **8-битным беззнаковым PCM на 11 000 Гц**, а у CBL есть
|
|
||||||
режим **10 937,5 Гц** — расхождение 0,6 %, на слух неразличимо. Формат
|
|
||||||
сэмпла совпадает с нашим CBL байт в байт (`cbl.h`: 8 бит, беззнаковый,
|
|
||||||
центр 0x80). То есть данные играются **как есть**, без конверсии.
|
|
||||||
- Музыка есть в виде **списков нот PC-спикера — 7 КБ на всю игру**, а нота
|
|
||||||
там задана прямо в ГЕРЦАХ. Пересчёт в делитель AY — одно деление.
|
|
||||||
- AY и COVOX на Sp2000 сведены в **один ЦАП TDA1543** (док Ивана Мака,
|
|
||||||
§5), значит музыка на AY и эффекты через CBL звучат ОДНОВРЕМЕННО, и
|
|
||||||
смешивать их программно не надо.
|
|
||||||
|
|
||||||
## 1а. Уточнение после разбора ВСЕХ наборов MS-DOS версии (2026-08-20)
|
|
||||||
|
|
||||||
Пользователь попросил, чтобы эффекты были не PC-спикером, а WAV, и заодно
|
|
||||||
посмотреть `mt32snd[1-2].dat`. Разобрал все восемь `.dat` из `MSDOS/`.
|
|
||||||
Ответ короткий: **эффекты И ТАК все до одного есть в WAV, а вот у музыки
|
|
||||||
WAV нет ни в одном наборе.**
|
|
||||||
|
|
||||||
| набор | формат | какие звуки | сколько |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `digisnd1..3` | **WAV**, 8 бит PCM | эффекты 0..23, 44..49, 51 | **31** |
|
|
||||||
| `mt32snd1..2` | MIDI для Roland MT-32 | ТЕ ЖЕ эффекты 0..23, 44..51 | 31 |
|
|
||||||
| `midisnd1..2` | MIDI (AdLib/GM) | **музыка** 24..43, 50, 52..56 | 22 |
|
|
||||||
| `ibm_snd1..2` | ноты PC-спикера | **всё подряд, 0..56** | 57 |
|
|
||||||
|
|
||||||
Здесь пряталась ловушка: `mt32snd` по имени похож на «музыку получше», а
|
|
||||||
на деле это набор ЭФФЕКТОВ для владельцев MT-32 — те же id, что у
|
|
||||||
`digisnd`. Музыки в нём нет вовсе.
|
|
||||||
|
|
||||||
Частоты WAV: 28 звуков на 11 000 Гц, по одному на 8 200, 14 000 и 2 750.
|
|
||||||
Итого 112 922 сэмпла = **11,4 с, ~110 КБ ≈ 6,7 EMM-страниц**.
|
|
||||||
|
|
||||||
Только PC-спикером, без альтернатив, остаются четыре id: 31, 34, 42
|
|
||||||
(пустые) и **38 `blink`** — четыре ноты. То есть на весь звук игры
|
|
||||||
спикер нужен ровно для одного писка.
|
|
||||||
|
|
||||||
### Музыка: WAV нет, есть три пути
|
|
||||||
|
|
||||||
| путь | данные | что получится | цена |
|
|
||||||
|---|---|---|---|
|
|
||||||
| **A. Ноты PC-спикера на AY** | 7 КБ | один квадратный голос — ровно то, что слышали на IBM PC 1989 | секвенсор на полсотни строк |
|
|
||||||
| **B. MIDI -> AY, три голоса** | 27 КБ исходника | богаче: бас + мелодия + арпеджио | разбор MIDI + раскладка по каналам |
|
|
||||||
| **C. MIDI -> WAV на хосте, стрим через CBL** | см. ниже | настоящее звучание, любое | нужен синтезатор на хосте + место |
|
|
||||||
|
|
||||||
Про объём для пути C (замерено по длительностям треков):
|
|
||||||
|
|
||||||
| группа | треков | длительность | WAV 11 кГц |
|
|
||||||
|---|---:|---:|---|
|
|
||||||
| звучат ПО ХОДУ игры (гимн уровня, смерть, зелья, перо, победа) | 12 | 74,7 с | **803 КБ = 50 EMM-страниц** |
|
|
||||||
| заставки и титры | 10 | 248,1 с | 2 665 КБ = 167 страниц |
|
|
||||||
|
|
||||||
Игровая половина в EMM **влезает** (при ~215 свободных страницах), а
|
|
||||||
заставочная — нет, её пришлось бы стримить с диска. Но заставки идут
|
|
||||||
тогда, когда игра ничего не рисует, так что стрим там как раз уместен.
|
|
||||||
|
|
||||||
**Предложение:** начинать с A (7 КБ, работает сразу, ноль рисков), а C
|
|
||||||
держать как отдельную фазу — она ортогональна: проигрыватель WAV для
|
|
||||||
музыки это тот же `cbl_push`, что и для эффектов, только длиннее буфер.
|
|
||||||
B имеет смысл только если C окажется неподъёмным по месту.
|
|
||||||
|
|
||||||
## 1б. РЕШЕНИЯ (пользователь, 2026-08-20)
|
|
||||||
|
|
||||||
1. **Эффекты — WAV через CBL, 8 бит, МОНО, единая частота.** Проверено по
|
|
||||||
`convert_digi_sound` (`seg009.c:2358`): один байт на кадр, то есть
|
|
||||||
моно, и байт беззнаковый (`(b | b<<8) - 32768`), центр 0x80 — ровно
|
|
||||||
формат нашего CBL. Стерео в данных нет вовсе: каналы у оригинала
|
|
||||||
размножаются уже на выходе (`digi_audiospec->channels`).
|
|
||||||
2. **Музыка, первый заход — путь A** (ноты спикера на AY).
|
|
||||||
3. **Заставки и титры — потом WAV.** Конфликта с эффектами там нет:
|
|
||||||
одновременно они не звучат.
|
|
||||||
4. **Музыка ПО ХОДУ игры** (она может совпасть с эффектом) — открыто, два
|
|
||||||
варианта: либо тоже WAV с ГАШЕНИЕМ эффектов на время музыки (музыка
|
|
||||||
важнее — **проверить на слух**), либо путь B (MIDI -> три голоса AY).
|
|
||||||
5. **Все эффекты привести к одной частоте.**
|
|
||||||
6. Синтезатор для MIDI -> WAV — решать ближе к делу; годятся и онлайн-
|
|
||||||
конвертеры, хоть вручную, если fluidsynth/timidity не поставится.
|
|
||||||
|
|
||||||
### Про единую частоту (замер)
|
|
||||||
|
|
||||||
Приводим не к 11 000, а ровно к **10 937,5 Гц — частоте CBL**
|
|
||||||
(`CBL_FREQ_10K9`). Тогда тон точен, а не «на 0,6 % ниже»: проигрывание
|
|
||||||
11 000 Гц данных на 10 937,5 даёт сдвиг **−9,9 цента**, что на коротком
|
|
||||||
эффекте не слышно, но бесплатно избавиться от него всё равно приятно —
|
|
||||||
пересчитывать три файла всё равно придётся.
|
|
||||||
|
|
||||||
| id | звук | было | станет | дельта |
|
|
||||||
|---|---|---|---|---:|
|
|
||||||
| 15 | `leveldoor_sliding` | 2 750 Гц, 4 436 сэмплов | 17 643 | **+13 207 Б** |
|
|
||||||
| 23 | `footstep` | 8 200 Гц, 996 | 1 329 | +333 Б |
|
|
||||||
| 51 | `princess_door_opening` | 14 000 Гц, 6 188 | 4 834 | −1 354 Б |
|
|
||||||
| — | остальные 28 (11 000 Гц) | — | ×0,9943 | −588 Б |
|
|
||||||
|
|
||||||
Итог: **112 922 -> 124 531 Б, 6,9 -> 7,6 EMM-страниц.** Рост целиком от
|
|
||||||
`leveldoor_sliding`: источник у него 2 750 Гц, вчетверо реже целевой, и
|
|
||||||
апсэмплинг не улучшит звучание — только уравняет формат. Платим 0,8
|
|
||||||
страницы за то, что **CBL открывается ОДИН раз и частоту менять не надо
|
|
||||||
никогда** — ни между эффектами, ни при переходе на музыку-WAV.
|
|
||||||
|
|
||||||
(Альтернатива для него — хранить как есть и повторять каждый сэмпл
|
|
||||||
четырежды в рантайме: 2 750 × 4 = 11 000 ровно. Это код в `fill()` ради
|
|
||||||
13 КБ; не стоит того, но если место когда-нибудь прижмёт — вариант есть.)
|
|
||||||
|
|
||||||
## 1в. Бюджет памяти EMM (живой замер 2026-08-20)
|
|
||||||
|
|
||||||
Замерено `mem_info` из работающей программы (уровень 1), а не посчитано на
|
|
||||||
бумаге: инструментовка ставилась временно и откатана.
|
|
||||||
|
|
||||||
| | страниц | КБ |
|
|
||||||
|---|---:|---:|
|
|
||||||
| всего в машине | 256 | 4 096 |
|
|
||||||
| система (DSS) + сам exe: база + 8 банков кода | **43** | 688 |
|
|
||||||
| наши ассеты | **81** | 1 296 |
|
|
||||||
| **занято** | **124** | 1 984 |
|
|
||||||
| **свободно** | **132** | **2 112 (2,06 МБ)** |
|
|
||||||
|
|
||||||
Разбивка 81 страницы ассетов (сходится точно):
|
|
||||||
|
|
||||||
| набор | страниц |
|
|
||||||
|---|---:|
|
|
||||||
| **Тень** (`sk*` 28 + `sf*` 4) | **32** |
|
|
||||||
| Кид (`kid0..27`) | 28 |
|
|
||||||
| фон тайлсета (env 10 + wall 1 + fore 1) | 12 |
|
|
||||||
| страж | 5 |
|
|
||||||
| зелья (chtab_1), меч, `kid_data.bin`, страница уровня | по 1 |
|
|
||||||
|
|
||||||
Самый крупный потребитель теперь — **набор Тени, 32 страницы**, больше
|
|
||||||
самого Кида. Если место когда-нибудь прижмёт, там есть очевидный резерв
|
|
||||||
(кадры смерти и позы, в которых Тень не бывает), но при 132 свободных
|
|
||||||
страницах трогать незачем.
|
|
||||||
|
|
||||||
### Что из этого следует для звука
|
|
||||||
|
|
||||||
| статья | страниц | останется свободно |
|
|
||||||
|---|---:|---:|
|
|
||||||
| эффекты WAV, все 31, 10 937,5 Гц | **8** | 124 |
|
|
||||||
| музыка ПО ХОДУ игры в WAV (путь C, 12 треков) | 50 | 74 |
|
|
||||||
| заставки и титры в WAV (10 треков, 248 с) | 167 | **не влезает** |
|
|
||||||
|
|
||||||
То есть эффекты — капля, игровая музыка в WAV тоже поместится, а
|
|
||||||
заставочную придётся стримить с диска в любом случае (что и планировалось:
|
|
||||||
во время заставок игра ничего не рисует).
|
|
||||||
|
|
||||||
Оговорка: 132 свободных страницы — это на уровне 1. На уровне 9 добавятся
|
|
||||||
зеркальные наборы (`pop_vflip_load_all`: 28 Кид + 5 страж + меч = 34
|
|
||||||
страницы), останется ~98. Проверять запас надо ИМЕННО ТАМ.
|
|
||||||
|
|
||||||
## 1г. MSDOS против SDLPoP: чем отличаются наборы (сверено 2026-08-20)
|
|
||||||
|
|
||||||
У нас лежат ДВЕ копии звука — оригинальные `.dat` в `MSDOS/` (версия
|
|
||||||
1.3/1.4) и распакованные ассеты `SDLPoP/data/` (версия 1.0/1.1). Разница
|
|
||||||
есть, и она влияет на выбор источника.
|
|
||||||
|
|
||||||
### Оцифровка: берём MSDOS
|
|
||||||
|
|
||||||
Заголовок разный (`digi_new_type` против `digi_type`), но **28 звуков из
|
|
||||||
31 совпадают побайтно**. Различаются три, и все не в пользу SDLPoP:
|
|
||||||
|
|
||||||
| id | звук | MSDOS | SDLPoP |
|
|
||||||
|---|---|---:|---:|
|
|
||||||
| 10 | `sword_vs_sword` | 5 020 сэмплов | 3 504 |
|
|
||||||
| 11 | `sword_moving` | 1 172 | 1 172, но **другие байты** |
|
|
||||||
| 48 | `spiked` | 5 069 | **7** — то есть звука нет |
|
|
||||||
|
|
||||||
`spiked` в наборе SDLPoP фактически пустой. Поэтому упаковщик читает
|
|
||||||
`MSDOS/digisnd*.dat`, а не распакованные ассеты — в отличие от графики,
|
|
||||||
где источник наоборот SDLPoP.
|
|
||||||
|
|
||||||
### MIDI: если дойдём до музыки — брать SDLPoP
|
|
||||||
|
|
||||||
Здесь всё наоборот. Содержимое музыкально то же (деление 480, те же
|
|
||||||
каналы 0..7 плюс ударные), но:
|
|
||||||
|
|
||||||
| | MSDOS | SDLPoP |
|
|
||||||
|---|---|---|
|
|
||||||
| формат MIDI | **0** — всё слито в ОДНУ дорожку | **1** — 8-9 дорожек |
|
|
||||||
| размер (звук 24) | 327 Б | 448 Б |
|
|
||||||
| размер (звук 56) | 13 587 Б | 12 773 Б |
|
|
||||||
|
|
||||||
Формат 1 с отдельной дорожкой на инструмент — это готовое разделение
|
|
||||||
голосов. Для пути B (MIDI -> три канала AY) оно решает половину задачи:
|
|
||||||
дорожки можно выбирать напрямую (бас / мелодия / гармония), а не
|
|
||||||
разбирать слитый поток и догадываться, что чем было.
|
|
||||||
|
|
||||||
**Итог: эффекты из MSDOS, музыка (когда дойдёт) из SDLPoP.**
|
|
||||||
|
|
||||||
## 2. Что лежит в ассетах (замерено, а не по памяти)
|
|
||||||
|
|
||||||
Звук в PoP адресуется как ресурс `10000 + N`, N = 0..56 — 57 звуков
|
|
||||||
(`load_sound`, `seg009.c:2289`). Наборов три, и они ПАРАЛЛЕЛЬНЫЕ: один и
|
|
||||||
тот же звук есть в нескольких видах.
|
|
||||||
|
|
||||||
| набор | что это | объём | покрытие |
|
|
||||||
|---|---|---:|---|
|
|
||||||
| `DIGISND1..3.DAT` | оцифровка, 8 бит PCM | 103 941 Б | **31 звук** (эффекты) |
|
|
||||||
| `MIDISND1..2.DAT` | MIDI-музыка | 27 776 Б | музыка |
|
|
||||||
| `IBM_SND1..2` (распакованы) | ноты PC-спикера | **7 КБ** | **все 57** |
|
|
||||||
|
|
||||||
### 2.1 Оцифровка (эффекты)
|
|
||||||
|
|
||||||
Разбор контейнера: индекс по 8 байт на запись (id, offset, size), **первый
|
|
||||||
байт ресурса — контрольная сумма**, тело за ней (спецификация
|
|
||||||
`POP-DAT-FormatSpecifications`, §3.1.2 — на этом я сначала споткнулся и
|
|
||||||
читал мусор). Тело — `digi_type`: `word rate, word count, word unk,
|
|
||||||
byte size`, дальше сэмплы.
|
|
||||||
|
|
||||||
- 31 звук, все 8-битные;
|
|
||||||
- частоты: **28 звуков на 11 000 Гц**, по одному на 8 200 и 14 000;
|
|
||||||
- 103 701 сэмпл = **9,4 секунды**, **103 941 Б ≈ 6,3 EMM-страницы**.
|
|
||||||
|
|
||||||
### 2.2 Ноты PC-спикера (и эффекты, и музыка)
|
|
||||||
|
|
||||||
Формат: `byte type(=0), word tempo`, дальше тройки `word frequency,
|
|
||||||
byte length`; `frequency <= 1` — пауза, `0x12` — конец. Ключевое, что
|
|
||||||
пришлось смотреть в `play_speaker_sound`/`speaker_callback` (`seg009.c`):
|
|
||||||
|
|
||||||
- **`frequency` — это ГЕРЦЫ напрямую** (`generate_square_wave(stream,
|
|
||||||
(float)note->frequency, ...)`), а не делитель PIT, как кажется по
|
|
||||||
маленьким числам;
|
|
||||||
- длительность ноты = `length / tempo` СЕКУНД.
|
|
||||||
|
|
||||||
Замеры по всем 57 звукам: **2212 нот**, частоты 16..65507 Гц,
|
|
||||||
длительности 1,74..2571 мс.
|
|
||||||
|
|
||||||
Из них 23 звука — те, у которых оцифровки НЕТ, то есть вся музыка:
|
|
||||||
заставки, гимны уровней, смерть, победа, титры (id 24..43, 50..56).
|
|
||||||
**1469 нот**, и вот их длительности:
|
|
||||||
|
|
||||||
| длительность ноты | нот | доля |
|
|
||||||
|---|---:|---:|
|
|
||||||
| < 3 мс | 0 | 0 % |
|
|
||||||
| 3..12 мс | 2 | 0,1 % |
|
|
||||||
| 12..25 мс | 140 | 9,5 % |
|
|
||||||
| > 25 мс | 1327 | 90,3 % |
|
|
||||||
|
|
||||||
Это число решает вопрос про таймер — см. §4.
|
|
||||||
|
|
||||||
## 3. Что умеет железо (док Ивана Мака §5 + MAME)
|
|
||||||
|
|
||||||
- **AY-3-8910/8912** в ПЛМ, «программируется по стандартным описаниям» —
|
|
||||||
то есть ZX-порты; в MAME он заведён как `AY8910(config, "ay8912",
|
|
||||||
X_SP/24)`, то есть **тактовая 1,75 МГц**. Период канала = 109375 /
|
|
||||||
частота(Гц), 12 бит (макс 4095) → снизу берутся частоты от ~27 Гц.
|
|
||||||
Точные Z80-адреса портов идут через таблицу DCP, а не напрямую —
|
|
||||||
**проверить артефактом до кодинга** (ожидаем ZX-стандарт 0xFFFD/0xBFFD).
|
|
||||||
- **CBL** — COVOX с буфером 256 Б, две половины по 128; бит 7 порта 0xFE
|
|
||||||
показывает играющую половину, порт управления 0x4E. Прерывание —
|
|
||||||
когда половина сменилась.
|
|
||||||
- **Бипер** (бит 5 порта 0xFE) — туда же в ЦАП. Нам не нужен.
|
|
||||||
- Всё это **сведено в один ЦАП**, поэтому AY и CBL звучат вместе.
|
|
||||||
|
|
||||||
У нас уже есть готовая обвязка CBL (`libc/include/cbl.h`): callback
|
|
||||||
`fill(n)`, выдача блока через `cbl_push_otir()` (порт 0x4F) или через
|
|
||||||
акселератор, коды частот, счётчик недоливов. Своего кольца библиотека не
|
|
||||||
держит — данные пропихиваются прямо из наших EMM-страниц.
|
|
||||||
|
|
||||||
## 4. Предлагаемая архитектура
|
|
||||||
|
|
||||||
```
|
|
||||||
эффекты (31 шт, 8 бит 11 кГц) музыка (23 шт, ноты)
|
|
||||||
│ │
|
|
||||||
EMM-страницы (6,3) 7 КБ нот в банке
|
|
||||||
│ │
|
|
||||||
cbl_push_otir из fill() запись 3 регистров AY
|
|
||||||
│ │
|
|
||||||
CBL (10,9 кГц) ──────┐ ┌────────── AY (1,75 МГц)
|
|
||||||
▼ ▼
|
|
||||||
TDA1543 (аппаратное смешивание)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Один источник прерываний — CBL.** Его callback приходит каждые 128
|
|
||||||
сэмплов = **11,7 мс** при 10,9 кГц, и он же двигает секвенсор музыки.
|
|
||||||
Отдельный таймер (CTC) НЕ нужен: по таблице из §2.2 короче 12 мс всего
|
|
||||||
2 ноты из 1469 — они растянутся на один тик, чего не слышно.
|
|
||||||
|
|
||||||
Почему это важно: `irq_ctc_install` сейчас требует кода в W2 (tiny/big),
|
|
||||||
а roomtest — huge, и попадёт ли туда CTC-трамплин, зависит от раскладки.
|
|
||||||
Обойтись без него — значит не открывать этот фронт вовсе.
|
|
||||||
|
|
||||||
**Пейсинг кадра при этом не страдает.** Наш темп считается ПО ЛУЧУ
|
|
||||||
(`pop_pace.h`), а не по кадровым прерываниям, поэтому то, что CBL-ветка
|
|
||||||
трамплина делает приватный RETI и съедает кадровые прерывания, нам
|
|
||||||
безразлично. Если бы пейсинг остался на прерываниях — звук бы его сломал.
|
|
||||||
|
|
||||||
## 5. Объём работ
|
|
||||||
|
|
||||||
| фаза | что | оценка |
|
|
||||||
|---|---|---|
|
|
||||||
| **З1** | распаковщик `pop_pack_sound.py`: DAT → `.snd`-страницы EMM (эффекты) + `.not` (ноты музыки) | формат уже разобран |
|
|
||||||
| **З2** | `pop_sfx.c`: `cbl_open` + `fill()`, таблица «звук → страница/смещение/длина», `pop_sfx_play(id)` | ядро |
|
|
||||||
| **З3** | развесить вызовы: 66 мест `play_sound` в оригинале; у нас часть уже помечена TODO (`pop_ctrl.c:408`, `guards.c:992`, `pop_map.c:3035/3107`, …). **Плюс бесплатный кусок**: опкод `SEQ_SOUND` в seqtbl уже разбирается нашим `play_seq` (`pop_kid.c:326`) — шаги, приземления и прочее поедут сами | механическая |
|
|
||||||
| **З4** | `pop_music.c`: секвенсор нот на AY (путь A), тик из CBL-callback | небольшая |
|
|
||||||
| **З6** | заставки и титры — WAV-музыка потоком (эффекты в это время не звучат) | после З1-З4 |
|
|
||||||
| **З7** | музыка по ходу игры: WAV с гашением эффектов ЛИБО путь B — решать по итогам З6 | открыто |
|
|
||||||
| **З5** | приоритеты и вытеснение: у оригинала `play_sound` глушит предыдущий (`stop_sounds`), музыка и эффект — разные каналы | правила из seg009 |
|
|
||||||
|
|
||||||
## 6. Что проверить артефактом ДО кодинга
|
|
||||||
|
|
||||||
1. **Порты AY на Sprinter** — записать в 0xFFFD/0xBFFD и убедиться, что
|
|
||||||
MAME отдаёт звук (порты идут через DCP-таблицу, «стандартные ZX» —
|
|
||||||
это ожидание, а не факт).
|
|
||||||
2. **CBL в режиме huge.** Шапка `cbl.h` говорит «код/данные в W2
|
|
||||||
(tiny/big)», но это скорее всего устаревшая оговорка: IM2-трамплин
|
|
||||||
давно переделан на all-modes, и наш кадровый путь в huge работает.
|
|
||||||
Проверить `cbl_open` из roomtest.
|
|
||||||
3. **Совместное владение портом 0xFE.** Бит 5 (луч) у нас держится через
|
|
||||||
`_cbl_port_ref` «немым» кодом частоты; когда откроется НАСТОЯЩИЙ CBL,
|
|
||||||
владение переходит к нему. Убедиться, что пейсинг переживает
|
|
||||||
`cbl_open`/`cbl_close`.
|
|
||||||
4. **Цена fill() в кадре.** 128 байт через `cbl_push_otir` раз в 11,7 мс
|
|
||||||
— замерить тем же способом, что и остальное (брейкпоинт + totalcycles).
|
|
||||||
|
|
||||||
## 7. Про MIDI — почему не он
|
|
||||||
|
|
||||||
Музыка в MIDISND — настоящие MThd/MTrk чанки, 27 КБ. Чтобы играть их на
|
|
||||||
AY, нужен разбор MIDI, раскладка каналов на три голоса и таблица
|
|
||||||
инструментов — это отдельный проект, и звучать он будет НЕ так, как
|
|
||||||
оригинал на PC. А набор PC-спикера — это ровно то, что слышал игрок на
|
|
||||||
IBM PC 1989 года: один квадратный голос. Он у нас есть целиком, весит
|
|
||||||
7 КБ и ложится на AY напрямую.
|
|
||||||
|
|
||||||
Если позже захочется богаче — материал уже будет разобран, и можно
|
|
||||||
разложить те же мелодии на три канала AY (бас/мелодия/арпеджио), не трогая
|
|
||||||
ни данные, ни секвенсор.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Разводка вызовов по коду (сделано 2026-08-20)
|
|
||||||
|
|
||||||
Портированы ВСЕ места `play_sound()` SDLPoP, у которых есть оцифровка
|
|
||||||
(id 0..23, 44..49, 51 — остальные id это музыка, у них в
|
|
||||||
`pop_sound_tbl.h` длина 0, и вызов просто глушит текущий эффект).
|
|
||||||
|
|
||||||
| id | что | где у нас | оригинал |
|
|
||||||
|----|-----|-----------|----------|
|
|
||||||
| 0 | разбился насмерть | `pop_map.c` land | seg005 |
|
|
||||||
| 1 | крик падения | `pop_map.c` do_fall | seg005:39 |
|
|
||||||
| 2 | плита рухнула | `pop_room.c` (обе ветки посадки) | seg007 |
|
|
||||||
| 3 | кнопка нажата | `pop_trob.c` | seg007 |
|
|
||||||
| 4/5/6/7 | ворота: закрываются / открываются / рухнули / стоп | `pop_trob.c` | seg007 |
|
|
||||||
| 8 | удар о стену | `pop_map.c` bumped_fall/bumped_floor + seqtbl | seg004/seg006 |
|
|
||||||
| 9 | зацеп за карниз | `pop_map.c` check_grab | seg006 |
|
|
||||||
| 11 | свист клинка мимо | `guards.c` check_hurting | seg002:0DAE |
|
|
||||||
| 12/13 | ранен соперник / Кид | `guards.c` hurt_by_sword | seg002:0C1F |
|
|
||||||
| 13 | Кид ранен зельем | `pop_map.c` ветка «злого» зелья | seg006:1894 |
|
|
||||||
| 14/15 | дверь уровня: закрывается / едет | `pop_trob.c` | seg007 |
|
|
||||||
| 16 | средняя посадка; толчок о стража | `pop_map.c` land / bump_into_opponent | seg005/seg003:0654 |
|
|
||||||
| 17 | мягкая посадка | `pop_map.c` land | seg005 |
|
|
||||||
| 18 | пьёт | seqtbl (SEQ_SOUND) | seg006 |
|
|
||||||
| 19 | вынул меч | `pop_ctrl.c` | seg005:945 |
|
|
||||||
| 20/21/22 | дрожит плита | `pop_map.c` loose_shake | seg007:0E55 |
|
|
||||||
| 23 | шаг | seqtbl (SEQ_SOUND) | seg006 |
|
|
||||||
| 44 | скелет оживает | `guards.c` pop_check_skel | seg002:106D |
|
|
||||||
| 45 | прыжок в зеркало | `pop_map.c` jump_through_mirror | seg003:0617 |
|
|
||||||
| 46 | сожрал чомпер | `pop_map.c` | seg004 |
|
|
||||||
| 47 | чомпер щёлкнул | `pop_trob.c` (кадр 2) | seg007 |
|
|
||||||
| 48 | напоролся на пики | `pop_map.c` | seg005 |
|
|
||||||
| 49 | пики пошли | `pop_map.c` start_anim_spike | seg007:08F6 |
|
|
||||||
|
|
||||||
Что осталось не разведено — только МУЗЫКА (25 презентация, 26 объятия,
|
|
||||||
27/35/40 заставки, 29 встреча Джафара, 30/33 зелья, 32/41 конец уровня,
|
|
||||||
36 время вышло, 37 победа, 43 смерть Джафара, 50/52/53 сюжетные вставки)
|
|
||||||
и 51 (дверь принцессы, тоже из заставки). Их черёд — фаза «музыка».
|
|
||||||
|
|
||||||
**Квирк, за которым следить.** Звук ворот у оригинала звучит не всегда, а
|
|
||||||
по условию видимости (`play_door_sound_if_visible`, seg007:1250): либо
|
|
||||||
ворота в комнате слева и стоят в 9-й колонке, либо ворота в НАРИСОВАННОЙ
|
|
||||||
комнате и колонка не 9-я. У нас это параметр `audible` у `animate_door`.
|
|
||||||
|
|
||||||
**Тряска плиты — свой домен prandom.** Оригинал берёт номер сэмпла (20/21/22)
|
|
||||||
из общего генератора и вдобавок «сжигает» один бросок ради совместимости с
|
|
||||||
DOS-версией; у нас последовательности разведены по доменам
|
|
||||||
(`impl_diff.md`), поэтому у тряски свой сид, а холостой бросок не делаем —
|
|
||||||
на розыгрыши физики и кладки это не влияет.
|
|
||||||
|
|
||||||
## 9. Цена звука в тактах (замер 2026-08-20, MAME)
|
|
||||||
|
|
||||||
Вопрос был поставлен так: звук идёт по прерываниям, значит размазан по всем
|
|
||||||
фазам кадра — и если фазы укладываются, всё хорошо? Да, но проверять это
|
|
||||||
надо не по фазам, а по двум числам, потому что **нагрузка от звука
|
|
||||||
постоянная и от сцены не зависит вовсе**.
|
|
||||||
|
|
||||||
### 9.1 Одно прерывание CBL
|
|
||||||
|
|
||||||
Зонды: `bpset` на входе трамплина (`_irq_tramp`) и на `reti` ветки CBL,
|
|
||||||
разница `totalcycles`. 398 замеров в сцене 11/15.
|
|
||||||
|
|
||||||
| величина | значение |
|
|
||||||
|---|---:|
|
|
||||||
| цена одного прерывания | **7 825 тактов ровно**, с разбросом до 8 299 (среднее 8 001) |
|
|
||||||
| период между прерываниями | 245 759 тактов (= 128 сэмплов на 10 937,5 Гц) |
|
|
||||||
| **доля процессорного времени** | **8 001 / 245 759 = 3,26 %** |
|
|
||||||
|
|
||||||
Цена постоянная, потому что работа фиксированная: OTIR ровно 128 байт плюс
|
|
||||||
скобка сохранения контекста. Ветвлений по данным в насосе нет.
|
|
||||||
|
|
||||||
### 9.2 Дрожание обслуживания — риск для ЗВУКА, не для кадра
|
|
||||||
|
|
||||||
Период плавает 228 804 … 262 734, то есть прерывание опаздывает максимум на
|
|
||||||
**~17 000 тактов = 0,8 мс**. Это самая длинная DI-скобка в коде
|
|
||||||
(акселератор режется по 16 строк, memory `sprinter_wait_states_2x`).
|
|
||||||
Буфер CBL — 128 сэмплов = **11,7 мс**, запас **14×**. Недолива быть не
|
|
||||||
может; счётчик `cbl_underruns()` это подтверждает косвенно (наш `fill`
|
|
||||||
всегда возвращает 1, поэтому он ловит только отсутствие данных, не
|
|
||||||
опоздание).
|
|
||||||
|
|
||||||
### 9.3 A/B в одном прогоне (Ctrl+S), сцена 11/15
|
|
||||||
|
|
||||||
Один и тот же кадр, звук выключается на ходу — сравнение чистое.
|
|
||||||
|
|
||||||
| | работа min | работа max | работа avg | прерываний CBL на кадр |
|
|
||||||
|---|---:|---:|---:|---:|
|
|
||||||
| звук ВКЛ | 453 132 | 572 532 | **498 064** | 1,40 |
|
|
||||||
| звук ВЫКЛ | 444 348 | 559 434 | **487 372** | 0,00 |
|
|
||||||
| разница | +8 784 | +13 098 | **+10 692 (+2,2 %)** | |
|
|
||||||
|
|
||||||
Разница на лёгком кадре (+8 784) — ровно одно прерывание, сходится с §9.1.
|
|
||||||
`CBL/кадр = 0` при выключенном звуке подтверждает, что Ctrl+S реально
|
|
||||||
ЗАКРЫВАЕТ CBL, а не глушит сэмпл: иначе насос продолжал бы отдавать блоки
|
|
||||||
тишины и платить те же 3,26 %.
|
|
||||||
|
|
||||||
### 9.4 Укладываемся ли
|
|
||||||
|
|
||||||
Логический кадр (`pop_pace.h`): NORMAL = 4 растра вне боя = **1 720 000
|
|
||||||
тактов**, FASTEST = 3 растра = **1 290 000**.
|
|
||||||
|
|
||||||
| | работа | доля NORMAL | доля FASTEST |
|
|
||||||
|---|---:|---:|---:|
|
|
||||||
| 11/15, обычная позиция | 498 064 | 29 % | 39 % |
|
|
||||||
| 11/15, тяжёлая позиция (Кид на 7 px правее) | 750 066 макс | 44 % | 58 % |
|
|
||||||
|
|
||||||
Период кадра за все прогоны: 1 719 936 … 1 720 752 — ровно 4 растра, ни
|
|
||||||
одного проскока. **Звук занимает 1,1 % бюджета NORMAL и 1,5 % FASTEST.**
|
|
||||||
|
|
||||||
### 9.5 Где 3,26 % МОГЛИ БЫ стоить дорого
|
|
||||||
|
|
||||||
Ответ «всё размазано, если фазы влезли — ок» верен с одной оговоркой.
|
|
||||||
Пейсинг квантован растром: работа 1,00 растра и 1,02 растра дают РАЗНЫЙ
|
|
||||||
период кадра (3 против 4 интервалов), то есть скачок сразу на 20 мс.
|
|
||||||
Значит звук опасен ровно в одной ситуации — когда сцена стоит в пределах
|
|
||||||
~8 000 тактов НИЖЕ кратного растру порога. Сейчас ближайший запас — 540 000
|
|
||||||
тактов до порога FASTEST, то есть в 60 раз больше цены звука. Проверять
|
|
||||||
эту оговорку заново стоит только если работа кадра подберётся к 430 000 или
|
|
||||||
860 000 вплотную.
|
|
||||||
|
|
||||||
## 10. Мусор при включении и щелчок на выходе (разбор 2026-08-20)
|
|
||||||
|
|
||||||
Жалоба: «при старте, когда разрешается звук, проходит кусок мусора».
|
|
||||||
Разобрано записью выхода MAME в WAV (`-wavwrite`) — по огибающей и
|
|
||||||
автокорреляции, а не на слух.
|
|
||||||
|
|
||||||
### 10.1 На старте мусора НЕТ; это настоящие звуки
|
|
||||||
|
|
||||||
От `cbl_open` до первого эффекта в записи **точная цифровая тишина**
|
|
||||||
(размах 1 при разрешении 16 бит). Дальше — два штатных звука:
|
|
||||||
|
|
||||||
| что | когда | длительность |
|
|
||||||
|---|---|---|
|
|
||||||
| `gate_closing_fast` (6) — решётка в комнате СЛЕВА | +0,30 с после `cbl_open` | обрывается на 80 мс |
|
|
||||||
| `soft_land` (17) — Кид приземляется | +0,38 с | 383 мс |
|
|
||||||
|
|
||||||
Опознаны корреляцией огибающих с оригинальными сэмплами `digisnd`:
|
|
||||||
звук 6 даёт +0,72 с начала записи, звук 17 — +0,32 со сдвигом 80 мс.
|
|
||||||
Приземление на старте КОРРЕКТНО: `start_pos` уровня 1 — тайл (0,0), а он
|
|
||||||
`space`, то есть Кид падает на ряд ниже, на площадку с факелами.
|
|
||||||
|
|
||||||
Обрыв первого звука вторым — тоже поведение оригинала, а не наш дефект:
|
|
||||||
`play_digi_sound` (seg009.c:2402) начинается с `stop_digi()`, голос ОДИН.
|
|
||||||
Ощущение «мусора» даёт именно 80-мс огрызок скрежещущей решётки.
|
|
||||||
|
|
||||||
Звук кнопки (3) при этом не слышен: `pop_sfx_play(3)` случается ДО
|
|
||||||
`cbl_open` и глохнет. С SDLPoP совпадает (там на старте тоже только
|
|
||||||
решётка), но держится это на порядке вызовов — если поднимать звук раньше
|
|
||||||
`pop_start_level`, щелчок кнопки станет слышен.
|
|
||||||
|
|
||||||
### 10.2 Незалитый буфер CBL — дефект есть, но в MAME он немой
|
|
||||||
|
|
||||||
Буфер CBL (256 слотов) железо не чистит ни сбросом, ни записью в порт
|
|
||||||
управления, а эта запись сразу пускает воспроизведение с нулевого слота.
|
|
||||||
Значит первые 256 сэмплов (23,4 мс) — то, что лежало раньше. Разбор по
|
|
||||||
`sprinter.cpp`: `case 0x89` делает `m_cbl_cnt = 0; m_cbl_wa = 0`, а
|
|
||||||
прерывание «долей половину» приходит только на 128-м слоте и ставит
|
|
||||||
указатель на ПРОТИВОПОЛОЖНУЮ половину — своими данными звук идёт лишь с
|
|
||||||
третьей половины.
|
|
||||||
|
|
||||||
В MAME это не слышно: эмулируемый буфер стартует нулями, а ЦАП
|
|
||||||
двухдополнительный, то есть 0 = середина шкалы. На ЖЕЛЕЗЕ там
|
|
||||||
неинициализированное ОЗУ — ровно тот мусор, который ловился ещё на
|
|
||||||
тестовых примерах CBL. Лечение — `_cbl_prime` в `cbl_open`: сразу после
|
|
||||||
включения 256 записей байта тишины в порт данных (заранее нельзя, запись
|
|
||||||
проходит только при поднятом bit7). Стоит 6 400 тактов один раз за
|
|
||||||
открытие; за это время таймер уходит на два-три слота.
|
|
||||||
|
|
||||||
### 10.3 Щелчок на выходе — ГОЛОДАНИЕ насоса, вылечено
|
|
||||||
|
|
||||||
На выходе по ESC в записи было **ровно 11 мс шума на полной громкости**
|
|
||||||
(размах 33 671 — громче всего в прогоне), потом мгновенная тишина. 11 мс
|
|
||||||
= один блок CBL (128 сэмплов = 11,7 мс), то есть один пропущенный долив:
|
|
||||||
`pop_shutdown` звал `closegraph`/`pop_bg_free`/`pop_kid_free` (а это
|
|
||||||
ESTEX на каждый атлас) ПРИ ОТКРЫТОМ звуке, насос не успевал, и железо
|
|
||||||
доигрывало несвежую половину.
|
|
||||||
|
|
||||||
Лечение: `pop_sfx_close()` первым действием `pop_shutdown`. Проверено
|
|
||||||
второй записью — всплеска на выходе больше нет. Механизм тот же, из-за
|
|
||||||
которого звук глушится на время загрузки уровня.
|
|
||||||
|
|
||||||
## 11. Приоритеты и перебиваемость: звук у оригинала НЕ «всегда перебивать»
|
|
||||||
|
|
||||||
Пользователь услышал расхождение: у нас решётка обрывалась приземлением
|
|
||||||
Кида, в SDLPoP — доигрывала до звонкого конца, а приземления не было
|
|
||||||
слышно вовсе. Разбор исходника показал, что мы упустили ЦЕЛЫЙ МЕХАНИЗМ.
|
|
||||||
|
|
||||||
### 11.1 Модель оригинала
|
|
||||||
|
|
||||||
```
|
|
||||||
play_sound(id) seg000:12C5 — только НОМИНИРУЕТ кандидата на кадр:
|
|
||||||
if next < 0 || prio[id] <= prio[next]: next = id
|
|
||||||
|
|
||||||
play_next_sound() seg000:1304 — раз в кадр решает, запускать ли:
|
|
||||||
if next >= 0:
|
|
||||||
if !играет_что_то ||
|
|
||||||
(перебиваем[текущий] && prio[next] <= prio[текущий]):
|
|
||||||
текущий = next; запустить
|
|
||||||
next = -1 // НЕ запустили -> номинант ВЫБРОШЕН, очереди нет
|
|
||||||
```
|
|
||||||
|
|
||||||
Три следствия, каждое слышно:
|
|
||||||
|
|
||||||
- **Неперебиваемый звук доигрывает целиком.** У `gate_closing_fast` (6)
|
|
||||||
`interruptible = 0`, поэтому приземление Кида (17) в этот момент
|
|
||||||
пропадает совсем — не откладывается, а именно теряется.
|
|
||||||
- **Внутри кадра выживает важнейший.** Меньше `prio` — важнее; при
|
|
||||||
равенстве побеждает ПОСЛЕДНИЙ (сравнение `<=`).
|
|
||||||
- **Два источника не «чередуются как получится».** Челюсти (47, prio
|
|
||||||
0x10) всегда важнее решётки (4, prio 0x32): решётка не может перебить
|
|
||||||
укус, а укус решётку — может. Отсюда и картина на ур. 9 к. 9, где
|
|
||||||
решётка звучит только в паузах между укусами.
|
|
||||||
|
|
||||||
### 11.2 Что сделано у нас
|
|
||||||
|
|
||||||
`pop_sfx_play` теперь только номинирует; запуск — в `pop_sfx_tick`,
|
|
||||||
который зовётся раз в кадр в конце отрисовки (там же, где оригинал зовёт
|
|
||||||
`play_next_sound`, seg000:954). Таблицы `snd_prio` (57 байт) и битовая
|
|
||||||
карта `snd_intr` (8 байт) — в резиденте, значения из SDLPoP С УЧЁТОМ
|
|
||||||
`fix_sound_priorities()`: в `config.h` SDLPoP `FIX_SOUND_PRIORITIES`
|
|
||||||
определён безусловно, значит сравниваемся мы с исправленным вариантом
|
|
||||||
(звук 10 → 0x0D, 48 → 0x15, 49 перебиваем).
|
|
||||||
|
|
||||||
Створка двери уровня (15) — единственная запись, которую оригинал правит
|
|
||||||
на ходу: перебиваема при закрытии, нет при открытии (seg007:442/464).
|
|
||||||
Держим отдельным байтом `pop_sfx_slide_intr`, чтобы таблица осталась в
|
|
||||||
ПЗУ. Там же добавлен пропущенный `stop_sounds()` на завершении открытия
|
|
||||||
двери (seg007:455) — без него неперебиваемый съезд (1,6 с) блокировал бы
|
|
||||||
очередь.
|
|
||||||
|
|
||||||
Звуки без оцифровки (музыка, длина 0) не номинируются вовсе — порт
|
|
||||||
проверки `if (NULL == sound_pointers[id]) return;`. Раньше такой id
|
|
||||||
глушил живой эффект.
|
|
||||||
|
|
||||||
### 11.3 Проверка
|
|
||||||
|
|
||||||
Записью MAME, старт уровня 1:
|
|
||||||
|
|
||||||
| | всплески | что это |
|
|
||||||
|---|---|---|
|
|
||||||
| до | 135 мс + 210 мс | решётка, обрезанная приземлением на 80 мс |
|
|
||||||
| после | **один, 455 мс** | решётка целиком, корреляция огибающей со звуком 6 **+0,889** |
|
|
||||||
|
|
||||||
Цена: резидент +~250 Б (таблицы + логика), куча ужалась с 347 до 134 Б —
|
|
||||||
довод в пользу давно назревшей реорганизации базовой памяти.
|
|
||||||
|
|
||||||
## 12. Ворота: гейт слышимости и «решётка встала» (2026-08-20)
|
|
||||||
|
|
||||||
Проверка на сцене, которую предложил пользователь — уровень 9, комната 9:
|
|
||||||
кнопка (1,8), челюсти (1,2), а ворота **в комнате 4, тайл (1,9)**, то есть
|
|
||||||
в комнате СЛЕВА. Нашлись три расхождения сразу.
|
|
||||||
|
|
||||||
### 12.1 Гейт слышимости был неверный
|
|
||||||
|
|
||||||
У нас стояло `audible = (room == cur_room)`. У оригинала
|
|
||||||
(`play_door_sound_if_visible`, seg007:1239) правило другое:
|
|
||||||
|
|
||||||
- ворота в комнате СЛЕВА и в колонке 9 — СЛЫШНЫ (створка видна в шве);
|
|
||||||
- ворота в отрисованной комнате и НЕ в колонке 9 — слышны;
|
|
||||||
- особый случай: уровень 3, комната 2 — слышны всегда.
|
|
||||||
|
|
||||||
Сцена 9/9 попадает ровно в первый пункт, поэтому спуск решётки у нас
|
|
||||||
молчал. Подъём при этом совпадал с оригиналом — потому что звук открытия
|
|
||||||
(5) идёт БЕЗ гейта (seg007:386, прямой `play_sound`). Эта асимметрия и была
|
|
||||||
подсказкой.
|
|
||||||
|
|
||||||
Взят вариант под `FIX_GATE_SOUNDS` (условия через ИЛИ): в config.h SDLPoP
|
|
||||||
он определён безусловно.
|
|
||||||
|
|
||||||
### 12.2 Потерян звук «решётка встала» (7)
|
|
||||||
|
|
||||||
`gate_stop()` (seg007:05E3) зовётся из ТРЁХ мест `animate_door` и каждый раз
|
|
||||||
играет звук 7 через гейт слышимости: конец закрытия, открытие насовсем и
|
|
||||||
ветка «уже 0xFF». У нас во всех трёх стояло только `*type = -1` без звука.
|
|
||||||
Добавлено. Лязг после ОБЫЧНОГО открытия (seg007:395) остаётся без гейта —
|
|
||||||
там оригинал зовёт `play_sound` напрямую.
|
|
||||||
|
|
||||||
### 12.3 Кнопка: у оригинала есть параметр playsound
|
|
||||||
|
|
||||||
`trigger_button(playsound, ...)` — в трёх местах он нулевой: вход на уровень
|
|
||||||
(seg003:170), выход Джаффара (seg002:520) и зелье «открыть» (seg006:1890, у
|
|
||||||
нас не портировано). Мы играли щелчок всегда. Добавлен параметр `snd`.
|
|
||||||
|
|
||||||
### 12.4 Почему щелчок кнопки слышно через раз — это НЕ баг
|
|
||||||
|
|
||||||
Бюджет сцены 9/9 (длительности после пересчёта на 10 937,5 Гц):
|
|
||||||
|
|
||||||
| звук | длительность | prio |
|
|
||||||
|---|---:|---:|
|
|
||||||
| челюсти (47) | 465 мс | 0x10 |
|
|
||||||
| решётка вниз (4) | 97 мс | 0x32 |
|
|
||||||
| решётка вверх (5) | 123 мс | 0x37 |
|
|
||||||
| решётка встала (7) | 75 мс | 0x30 |
|
|
||||||
| кнопка (3) | 106 мс | 0x66 |
|
|
||||||
|
|
||||||
Цикл челюстей — 15 кадров = 1229 мс (замерено брейкпоинтом на номинации:
|
|
||||||
25 805 000 тактов между укусами). Значит укус занимает 465 мс, пауза 764 мс.
|
|
||||||
Кнопка (prio 0x66) перебить челюсти не может (0x66 > 0x10), поэтому слышна
|
|
||||||
только если нажатие попало в паузу — примерно в 6 случаях из 10.
|
|
||||||
Подтверждено пользователем на живой сцене.
|
|
||||||
|
|
||||||
### 12.5 Грабли сцены
|
|
||||||
|
|
||||||
Если игра стартует ПРЯМО в комнате с челюстями, они не заводятся сами:
|
|
||||||
нужно сходить Кидом на левую кнопку и вернуться. Это поведение оригинала
|
|
||||||
(trob челюстей создаётся событием), а не наш дефект — учитывать при
|
|
||||||
постановке автотестов.
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
# PoC порта Prince of Persia (applications/PoP/docs/PORT_PLAN.md §5).
|
|
||||||
# --memory huge, БЕЗ --bank.
|
|
||||||
#
|
|
||||||
# ПОЧЕМУ huge, а НЕ small (исправлено 2026-07-16): poc использует
|
|
||||||
# raw-клавиатуру (kbd_raw_open → IM2-таблица). Буферы IM2 (_irq_vec_buf,
|
|
||||||
# BSS) ОБЯЗАНЫ жить в W2 (0x8000-0xBFFF) — во время прерывания W1/W3
|
|
||||||
# могут быть перемаплены DSS (см. libc/irq/_irq_table.c, memory/
|
|
||||||
# fps_divider: «verified tiny/big/huge, small=EINVAL»). --memory small
|
|
||||||
# пулит W1+W2 в плоские ~32КБ и чейнит DATA за CODE — при небольшом CODE
|
|
||||||
# BSS уезжает в W1 (<0x8000), и _irq_table_ref отдаёт EINVAL →
|
|
||||||
# kbd_raw_open молча возвращал -1, poc печатал ошибку УЖЕ в графическом
|
|
||||||
# режиме (невидимо) и выходил в prompt. huge кладёт CODE в W1, а
|
|
||||||
# DATA/BSS/STACK/HEAP жёстко в W2 → IM2 работает. Цена: раздел 16КБ
|
|
||||||
# CODE / 16КБ DATA вместо общего 32КБ-пула small — сейчас влезает с
|
|
||||||
# запасом; при росте настоящего PoP CODE>16КБ понадобится банк под код.
|
|
||||||
#
|
|
||||||
# --bank НЕ нужен: gfx_blit_part()/atlas_load() сами временно трогают W3
|
|
||||||
# (видеобанк / чтение атласа) — банк room.c в W3 давал вероятностный
|
|
||||||
# «снег»; банк в W1 несовместим с CODE=W1 (трамплин переключения W1 сам
|
|
||||||
# бы уехал). sprintf() заменён на ручное hex-форматирование пути в
|
|
||||||
# tile_atlas_load() (единственный потребитель printf, ~2.9КБ).
|
|
||||||
|
|
||||||
PROJ_ROOT := $(abspath $(CURDIR)/../../..)
|
|
||||||
EXAMPLE := poc
|
|
||||||
MEMORY ?= huge
|
|
||||||
EXTRA_FLAGS ?= --gfx 256
|
|
||||||
EXTRA_SRCS := room.c
|
|
||||||
TILE_ATLASES := res/tiles/tile01.atl res/tiles/tile14.atl res/tiles/tile03.atl \
|
|
||||||
res/tiles/tile13.atl res/tiles/tile0e.atl res/tiles/tile0b.atl
|
|
||||||
EXTRA_DATA := tools/kid.atl tools/room.pal res/room1.dat $(TILE_ATLASES)
|
|
||||||
include $(PROJ_ROOT)/app.mk
|
|
||||||
|
|
||||||
LEVEL1_BIN := $(PROJ_ROOT)/applications/PoP/SDLPoP/data/LEVELS/res2001.bin
|
|
||||||
|
|
||||||
tools/kid.raw tools/kid.pal: tools/gen_kid_placeholder.py
|
|
||||||
cd tools && python3 gen_kid_placeholder.py
|
|
||||||
|
|
||||||
tools/kid.atl: tools/kid.raw
|
|
||||||
python3 $(PROJ_ROOT)/toolchain/mkatlas.py $@ tools/kid.raw:16x16:1x12
|
|
||||||
|
|
||||||
res/room1.dat: tools/extract_room.py $(LEVEL1_BIN)
|
|
||||||
python3 tools/extract_room.py $(LEVEL1_BIN) 1 res/room1.dat
|
|
||||||
|
|
||||||
res/tiles/1F-0.png: tools/gen_tile_placeholders.py
|
|
||||||
cd tools && python3 gen_tile_placeholders.py
|
|
||||||
|
|
||||||
tools/room.pal $(TILE_ATLASES): tools/kid.pal res/tiles/1F-0.png tools/build_room_palette.py
|
|
||||||
cd tools && python3 build_room_palette.py
|
|
||||||
|
|
||||||
# make_disk.py упаковывает EXTRA_DATA на диск ПОД БАЗОВЫМ ИМЕНЕМ
|
|
||||||
# (плоская ФС) — tileNN.atl/room1.dat оказываются в корне рядом с
|
|
||||||
# kid.atl/room.pal, room.c/poc.c грузят их без пути.
|
|
||||||
$(EXAMPLE).exe: room.c tools/kid.atl tools/room.pal res/room1.dat $(TILE_ATLASES)
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
/*
|
|
||||||
* level.h — структуры уровня PoP под наш движок.
|
|
||||||
*
|
|
||||||
* Формат — POP-DAT-FormatSpecifications.pdf §3.4 (DAT 1.0): комната =
|
|
||||||
* 30 тайлов (10 колонок x 3 ряда), foretable даёт тип тайла,
|
|
||||||
* backtable — модификатор/состояние (семантика зависит от типа).
|
|
||||||
* Адресация тайла: tile = (room-1)*30 + tileOffset, tileOffset 0-9 =
|
|
||||||
* верхний ряд, 10-19 = средний, 20-29 = нижний, слева направо.
|
|
||||||
*
|
|
||||||
* Фаза 1 (applications/PoP/docs/PORT_PLAN.md §5, сузили объём):
|
|
||||||
* только геометрия (пол/стены/провалы) для коллизий — двери, факелы,
|
|
||||||
* ловушки, гарды НЕ используются (структуры под них здесь тоже
|
|
||||||
* упрощены/оставлены как заготовка на потом, см. §7 плана Фаза 2).
|
|
||||||
*/
|
|
||||||
#ifndef LEVEL_H
|
|
||||||
#define LEVEL_H
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
/* 1 (не 24) — PoC грузит и рисует только комнату 1; расширить, когда
|
|
||||||
* появится настоящий level_load() на несколько комнат. */
|
|
||||||
#define LEVEL_ROOMS 1 /* комнаты нумеруются 1..24 в файле,
|
|
||||||
* здесь индекс 0..23 = комната N+1 */
|
|
||||||
#define ROOM_COLS 10
|
|
||||||
#define ROOM_ROWS 3
|
|
||||||
#define ROOM_TILES (ROOM_COLS * ROOM_ROWS) /* 30 */
|
|
||||||
|
|
||||||
/* --- Типы тайлов (Table 7 спецификации) --- */
|
|
||||||
#define TILE_TYPE(byte) ((uint8_t)((byte) & 0x1F))
|
|
||||||
#define TILE_MODIFIER(byte) ((uint8_t)(((byte) >> 5) & 1))
|
|
||||||
|
|
||||||
enum {
|
|
||||||
TILE_EMPTY = 0x00,
|
|
||||||
TILE_FLOOR = 0x01,
|
|
||||||
TILE_SPIKES = 0x02,
|
|
||||||
TILE_PILLAR = 0x03,
|
|
||||||
TILE_GATE = 0x04,
|
|
||||||
TILE_STUCK_BUTTON = 0x05,
|
|
||||||
TILE_DROP_BUTTON = 0x06,
|
|
||||||
TILE_TAPESTRY = 0x07,
|
|
||||||
TILE_PILLAR_BOTTOM = 0x08,
|
|
||||||
TILE_PILLAR_TOP = 0x09,
|
|
||||||
TILE_POTION = 0x0A,
|
|
||||||
TILE_LOOSE = 0x0B,
|
|
||||||
TILE_TAPESTRY_TOP = 0x0C,
|
|
||||||
TILE_MIRROR = 0x0D,
|
|
||||||
TILE_DEBRIS = 0x0E,
|
|
||||||
TILE_RAISE_BUTTON = 0x0F,
|
|
||||||
TILE_EXIT_LEFT = 0x10,
|
|
||||||
TILE_EXIT_RIGHT = 0x11,
|
|
||||||
TILE_CHOPPER = 0x12,
|
|
||||||
TILE_TORCH = 0x13,
|
|
||||||
TILE_WALL = 0x14,
|
|
||||||
TILE_SKELETON = 0x15,
|
|
||||||
TILE_SWORD = 0x16,
|
|
||||||
TILE_BALCONY_LEFT = 0x17,
|
|
||||||
TILE_BALCONY_RIGHT = 0x18,
|
|
||||||
TILE_LATTICE_PILLAR = 0x19,
|
|
||||||
TILE_LATTICE_SUPPORT= 0x1A,
|
|
||||||
TILE_LATTICE_SMALL = 0x1B,
|
|
||||||
TILE_LATTICE_LEFT = 0x1C,
|
|
||||||
TILE_LATTICE_RIGHT = 0x1D,
|
|
||||||
TILE_TORCH_DEBRIS = 0x1E,
|
|
||||||
TILE_NULL = 0x1F
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Твёрдые тайлы — Фаза 1 (только геометрия); классификация наша, для
|
|
||||||
* коллизий движка, не часть исходного формата. Двери/шипы/дробилки и
|
|
||||||
* т.п. сознательно исключены из объёма Фазы 1 (см. §5 плана) — при
|
|
||||||
* встрече в реальных данных трактовать как проходимые до Фазы 2.
|
|
||||||
*
|
|
||||||
* tile_is_solid() — "есть опора сверху" (вертикальный смысл: можно
|
|
||||||
* стоять НА этом тайле) — Floor ТОЖЕ solid в этом смысле! Для
|
|
||||||
* горизонтальной коллизии (можно ли ВОЙТИ в эту клетку сбоку) нужен
|
|
||||||
* ОТДЕЛЬНЫЙ предикат — см. tile_blocks_side ниже. Баг 2026-07-16:
|
|
||||||
* col_blocked() в poc.c ошибочно звал tile_is_solid() для бокового
|
|
||||||
* упора — Floor блокировал сам себя, Кид не мог сдвинуться с места
|
|
||||||
* стоя на полу. */
|
|
||||||
static inline uint8_t tile_is_solid(uint8_t byte)
|
|
||||||
{
|
|
||||||
switch (TILE_TYPE(byte)) {
|
|
||||||
case TILE_FLOOR:
|
|
||||||
case TILE_PILLAR:
|
|
||||||
case TILE_PILLAR_BOTTOM:
|
|
||||||
case TILE_PILLAR_TOP:
|
|
||||||
case TILE_WALL:
|
|
||||||
case TILE_BALCONY_LEFT:
|
|
||||||
case TILE_BALCONY_RIGHT:
|
|
||||||
return 1;
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* tile_blocks_side() — настоящая преграда СБОКУ (нельзя войти в
|
|
||||||
* клетку по горизонтали): Wall/Pillar-семейство. Floor/Balcony НЕ
|
|
||||||
* блокируют — по ним идёшь (тайл под ногами, не впереди). Lattice-
|
|
||||||
* колонны (0x19-0x1D) — узкие, Кид физически проходит мимо (см.
|
|
||||||
* gen_tile_placeholders.py draw_lattice_like) — тоже НЕ блокируют. */
|
|
||||||
static inline uint8_t tile_blocks_side(uint8_t byte)
|
|
||||||
{
|
|
||||||
switch (TILE_TYPE(byte)) {
|
|
||||||
case TILE_PILLAR:
|
|
||||||
case TILE_PILLAR_BOTTOM:
|
|
||||||
case TILE_PILLAR_TOP:
|
|
||||||
case TILE_WALL:
|
|
||||||
return 1;
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Тайл и комната --- */
|
|
||||||
typedef struct {
|
|
||||||
uint8_t type; /* foretable byte (rrmccccc — см. TILE_TYPE/MODIFIER) */
|
|
||||||
uint8_t state; /* backtable byte — модификатор/состояние */
|
|
||||||
} tile_t;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
tile_t tiles[ROOM_TILES]; /* индекс = tileOffset 0..29 */
|
|
||||||
uint8_t link_left, link_right; /* links-блок: 0 = нет соседа */
|
|
||||||
uint8_t link_up, link_down;
|
|
||||||
uint8_t guard_location; /* 0..29; 30 (и выше) = нет гарда */
|
|
||||||
int8_t guard_direction; /* 0 = вправо, -1 = влево */
|
|
||||||
uint8_t guard_skill; /* 0..9 */
|
|
||||||
uint8_t guard_colour; /* индекс палитры, Table 11 */
|
|
||||||
/* door I/II (событийные цепочки) — Фаза 2, не здесь */
|
|
||||||
} room_t;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
room_t rooms[LEVEL_ROOMS]; /* индекс 0 = комната 1 (файл 1-based) */
|
|
||||||
uint8_t start_room; /* 1..24 */
|
|
||||||
uint8_t start_location; /* 0..29 */
|
|
||||||
int8_t start_direction; /* 0 = вправо, -1 = влево */
|
|
||||||
} level_t;
|
|
||||||
|
|
||||||
/* Тайл по (room 1-based, col 0-9, row 0-2). */
|
|
||||||
static inline tile_t *level_tile(level_t *lv, uint8_t room, uint8_t col, uint8_t row)
|
|
||||||
{
|
|
||||||
return &lv->rooms[room - 1].tiles[row * ROOM_COLS + col];
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Загружает ОДНУ комнату + стартовую позицию уровня из файла в формате
|
|
||||||
* tools/extract_room.py (63 Б: foretable[30]+backtable[30] той комнаты
|
|
||||||
* + start_room+start_pos+start_dir, вырезанные из res20NN.bin — layout
|
|
||||||
* подтверждён декодом байт 2026-07-15/16: файл начинается СРАЗУ с
|
|
||||||
* foretable[720], потом backtable[720], без заголовка; start_position
|
|
||||||
* — смещение 2112, сверено со структурой level_type в SDLPoP/src/
|
|
||||||
* types.h). Заполняет lv->start_room/start_location/start_direction.
|
|
||||||
* 0 — OK, -1 — файл не найден/короче 63 Б. */
|
|
||||||
int level_load_room(level_t *lv, uint8_t room, const char *path);
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,264 +0,0 @@
|
|||||||
/*
|
|
||||||
* poc.c — PoC порта Prince of Persia (applications/PoP/docs/PORT_PLAN.md
|
|
||||||
* §5): проверяем управление (raw-клавиатура, held-state) + коллизию
|
|
||||||
* по краям экрана + анимацию ходьбы + прыжок/присед поверх готового
|
|
||||||
* спрайтового движка (sprite.h).
|
|
||||||
*
|
|
||||||
* ВАЖНО: персонаж — ВРЕМЕННАЯ ЗАГЛУШКА (лицензированный спрайт-пак
|
|
||||||
* third_party/16x16-RPG-characters через tools/gen_kid_placeholder.py,
|
|
||||||
* тот же источник, что уже использует examples/rpgwalk), НЕ графика
|
|
||||||
* оригинальной Prince of Persia — см. §5 и §8.5 плана. У заглушки нет
|
|
||||||
* отдельных поз прыжка/приседа — механика (тайминг дуги, состояние,
|
|
||||||
* коллизия с полом) проверяется на том же спрайте без смены позы;
|
|
||||||
* визуально это упрощение, не финальный вид.
|
|
||||||
*
|
|
||||||
* Дуга прыжка (jump_height[]) — СВОЯ, приблизительная (не таблица
|
|
||||||
* смещений оригинала — см. §6 плана: авторские таблицы кадров решено
|
|
||||||
* не переносить, только код/структуры).
|
|
||||||
*
|
|
||||||
* Нет ещё (следующие итерации): реальный уровень/фон по BLUETYPE,
|
|
||||||
* рывок вбок при прыжке с разбега, зацепление за уступ.
|
|
||||||
*/
|
|
||||||
#include <graphics.h>
|
|
||||||
#include <gfx.h>
|
|
||||||
#include <sprite.h>
|
|
||||||
#include <kbd_raw.h>
|
|
||||||
#include <conio.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include "level.h"
|
|
||||||
#include "room.h"
|
|
||||||
|
|
||||||
/* Комната 1 уровня 1 — РЕАЛЬНАЯ геометрия (foretable/backtable),
|
|
||||||
* вырезана tools/extract_room.py из applications/PoP/SDLPoP/data/
|
|
||||||
* LEVELS/res2001.bin (level_load_room(), см. level.h/room.c) — не
|
|
||||||
* плейсхолдер. Верхний ряд (row0) — floor-уступ на cols 3-7 (там
|
|
||||||
* реально стоит персонаж в оригинале), стены по cols 8-9; ряды 1-2 —
|
|
||||||
* ниже уступа (торч/колонны/пол — Фаза 1 просто их отрисовывает по
|
|
||||||
* тем же типам, без многоуровневой физики падения). */
|
|
||||||
static level_t test_level;
|
|
||||||
|
|
||||||
#define CHAR_ROW 0 /* ряд, где стоит персонаж (floor-уступ room1) */
|
|
||||||
#define ROW_Y(r) ((r) * 64) /* room_row_h все по 64 */
|
|
||||||
#define GROUND_Y ROW_Y(CHAR_ROW + 1) /* низ ряда CHAR_ROW = верх пола */
|
|
||||||
#define KIDY (GROUND_Y - 16) /* y спрайта (16 px высотой) */
|
|
||||||
#define SPEED 2 /* px/кадр — заглушка, не авторский темп */
|
|
||||||
|
|
||||||
/* Настоящая преграда (Wall/Pillar) слева/справа от кандидата x в
|
|
||||||
* CHAR_ROW — блокирует движение (грубая проверка по краям хитбокса
|
|
||||||
* 16px, без под-тайловой подгонки — для PoC достаточно, см.
|
|
||||||
* PORT_PLAN.md §6). tile_blocks_side(), НЕ tile_is_solid(): Floor —
|
|
||||||
* тайл, на котором Кид СТОИТ (тот же CHAR_ROW), tile_is_solid() его
|
|
||||||
* тоже считает "твёрдым" (можно стоять сверху) — если проверять им же
|
|
||||||
* боковую преграду, Кид не мог сдвинуться с собственного пола (баг,
|
|
||||||
* найден 2026-07-16). */
|
|
||||||
static uint8_t col_blocked(int x)
|
|
||||||
{
|
|
||||||
uint8_t c0, c1;
|
|
||||||
|
|
||||||
if (x < 0 || x + 15 >= ROOM_COLS * ROOM_TILE_W)
|
|
||||||
return 1;
|
|
||||||
c0 = (uint8_t)(x / ROOM_TILE_W);
|
|
||||||
c1 = (uint8_t)((x + 15) / ROOM_TILE_W);
|
|
||||||
if (tile_blocks_side(level_tile(&test_level, 1, c0, CHAR_ROW)->type))
|
|
||||||
return 1;
|
|
||||||
if (tile_blocks_side(level_tile(&test_level, 1, c1, CHAR_ROW)->type))
|
|
||||||
return 1;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ленты атласа: dir*3+frame, dir 0=вниз/1=влево/2=вправо/3=вверх,
|
|
||||||
* 3 кадра маятника на направление (см. tools/gen_kid_placeholder.py). */
|
|
||||||
#define DIR_DOWN 0
|
|
||||||
#define DIR_LEFT 1
|
|
||||||
#define DIR_RIGHT 2
|
|
||||||
|
|
||||||
/* Дуга прыжка: своя, приблизительная (не авторская таблица, см. шапку
|
|
||||||
* файла) — высота над полом (px) по кадрам 0..19, УЖЕ ПОЛНЫЙ горб
|
|
||||||
* (подъём 2→22 к элементу 10, спуск обратно к 2 к элементу 19) — БЕЗ
|
|
||||||
* зеркалирования в коде, массив читается один раз целиком. БАГ,
|
|
||||||
* найденный пользователем 2026-07-15: раньше код ЕЩЁ РАЗ зеркалил
|
|
||||||
* этот уже полный горб на 40 кадров — получалось два полных прыжка
|
|
||||||
* подряд от одного триггера (не проблема клавиатуры/декодера, чистая
|
|
||||||
* рассинхронизация данных и комментария). 20 кадров @ 50 Гц ~= 0.4 с. */
|
|
||||||
static const uint8_t jump_height[20] = {
|
|
||||||
2, 4, 7, 10, 13, 16, 18, 20, 21, 22,
|
|
||||||
22, 21, 20, 18, 16, 13, 10, 7, 4, 2
|
|
||||||
};
|
|
||||||
#define JUMP_FRAMES 20
|
|
||||||
|
|
||||||
static atlas_t at;
|
|
||||||
static sprite_t kid;
|
|
||||||
static uint8_t facing = DIR_DOWN; /* текущее направление анимации */
|
|
||||||
static uint8_t jumping = 0; /* 0 = на земле */
|
|
||||||
static uint8_t jump_t = 0; /* кадр дуги, 0..JUMP_FRAMES-1 */
|
|
||||||
static uint8_t crouching = 0;
|
|
||||||
/* Прыжок — level-triggered НАМЕРЕННО (не edge-detect): если UP всё ещё
|
|
||||||
* зажат к моменту приземления — следующий прыжок стартует СРАЗУ (цепочка
|
|
||||||
* прыжков, пока держишь); отпустил раньше — второй прыжок не начнётся
|
|
||||||
* сам, только по следующему нажатию. Раньше здесь были up_prev/
|
|
||||||
* jump_cooldown — попытка "починить" ровно ЭТО поведение, приняв его
|
|
||||||
* за баг; убрано 2026-07-15 после уточнения желаемого поведения. */
|
|
||||||
|
|
||||||
static void draw_room(void)
|
|
||||||
{
|
|
||||||
setfillstyle(SOLID_FILL, BLACK);
|
|
||||||
bar(0, 0, 319, 255);
|
|
||||||
room_draw(&test_level, 1);
|
|
||||||
setcolor(LIGHTGRAY);
|
|
||||||
outtextxy(60, 4, "PoP PoC: hold LEFT/RIGHT to walk, ESC to quit");
|
|
||||||
outtextxy(4, 14, "(placeholder tiles -- not original PoP art)");
|
|
||||||
}
|
|
||||||
|
|
||||||
/* HUD-плашка состояния (нет отдельной позы прыжка/приседа — статус
|
|
||||||
* текстом, рисуется банком 0x50, heal спрайтового движка её не
|
|
||||||
* трогает — как fps-плашка в examples/rpgwalk). */
|
|
||||||
static void show_state(uint8_t jump, uint8_t crouch)
|
|
||||||
{
|
|
||||||
setfillstyle(SOLID_FILL, BLACK);
|
|
||||||
bar(0, 24, 60, 32);
|
|
||||||
setcolor(YELLOW);
|
|
||||||
if (jump)
|
|
||||||
outtextxy(0, 24, "JUMP");
|
|
||||||
else if (crouch)
|
|
||||||
outtextxy(0, 24, "CROUCH");
|
|
||||||
}
|
|
||||||
|
|
||||||
static void set_facing(uint8_t dir)
|
|
||||||
{
|
|
||||||
if (facing == dir)
|
|
||||||
return;
|
|
||||||
facing = dir;
|
|
||||||
sprite_anim(&kid, (uint8_t)(dir * 3), (uint8_t)(dir * 3 + 2),
|
|
||||||
6, ANIM_PINGPONG);
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(void)
|
|
||||||
{
|
|
||||||
uint8_t page, hidden;
|
|
||||||
int x;
|
|
||||||
|
|
||||||
if (level_load_room(&test_level, 1, "room1.dat") != 0 &&
|
|
||||||
level_load_room(&test_level, 1, "a:\\room1.dat") != 0) {
|
|
||||||
puts("room1.dat not found");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
/* Стартовая позиция. В данных room1 start_location = tileOffset 0
|
|
||||||
* (row0/col0) — а там EMPTY (провал, без пола); в Фазе 1 нет физики
|
|
||||||
* падения, поэтому для PoC ставим Кида на floor-уступ (col 3, где он
|
|
||||||
* реально стоит в оригинале). Когда появится многоуровневая физика —
|
|
||||||
* брать col из start_location. start_direction: -1 = влево, 0 =
|
|
||||||
* вправо (level.h). */
|
|
||||||
x = 3 * ROOM_TILE_W;
|
|
||||||
facing = (test_level.start_direction < 0) ? DIR_LEFT : DIR_RIGHT;
|
|
||||||
|
|
||||||
if (atlas_load(&at, "kid.atl") != 0 &&
|
|
||||||
atlas_load(&at, "a:\\kid.atl") != 0) {
|
|
||||||
puts("kid.atl not found");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (tile_atlas_load("") == 0 && tile_atlas_load("a:\\") == 0) {
|
|
||||||
puts("tile atlases not found");
|
|
||||||
atlas_free(&at);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
initgraph();
|
|
||||||
/* room.pal = EGA16 + Kid + Floor + Wall — ОДНА палитра на всё,
|
|
||||||
* собрана tools/build_room_palette.py (см. --seed-pal в
|
|
||||||
* toolchain/png_strip.py) — Kid и тайлы на экране одновременно,
|
|
||||||
* их "свои" цвета обязаны жить в одной таблице. */
|
|
||||||
if (gfx_pal_fload(0, "room.pal") < 0)
|
|
||||||
gfx_pal_fload(0, "a:\\room.pal");
|
|
||||||
gfx_pal_sync();
|
|
||||||
gfx_sprite_clip(0); /* коллизия по краям гарантирует границы */
|
|
||||||
|
|
||||||
/* kid.atl — ОДНА лента (12 кадров вертикально: dir*3+кадр, см. шапку
|
|
||||||
* файла и examples/rpgwalk); индекс atlas_sprite_init — это НОМЕР
|
|
||||||
* ЛЕНТЫ (персонажа), а не кадра. Лента одна → всегда 0. Стартовый
|
|
||||||
* кадр направления выставляем sprite_frame (вертикальная лента, fh=16:
|
|
||||||
* кадр N на sy=N*16). Баг Соннета (найден 2026-07-16): здесь стоял
|
|
||||||
* facing*3 как индекс ЛЕНТЫ — при старте лицом влево (idx 3) читался
|
|
||||||
* мусор за каталогом атласа → мусорные w/h/src → блит спрайта заливал
|
|
||||||
* пол-экрана «снегом». */
|
|
||||||
atlas_sprite_init(&kid, &at, 0);
|
|
||||||
sprite_frame(&kid, 0, (int)(facing * 3) * 16);
|
|
||||||
kid.x = x;
|
|
||||||
kid.y = KIDY;
|
|
||||||
sprite_show(&kid);
|
|
||||||
|
|
||||||
for (page = 0; page < 2; page++) { /* фон + спрайт на обе страницы */
|
|
||||||
gfx_set_draw_page(page);
|
|
||||||
draw_room();
|
|
||||||
sprite_update(&kid, 1);
|
|
||||||
}
|
|
||||||
gfx_set_visible_page(0);
|
|
||||||
|
|
||||||
/* kbd_raw требует BSS в W2 (IM2-таблица) — недоступно в --memory small
|
|
||||||
* (там BSS может уехать в W1 → EINVAL); poc собирается --memory huge
|
|
||||||
* (CODE в W1, DATA/BSS в W2). closegraph ДО puts — иначе сообщение
|
|
||||||
* ушло бы в графический режим (невидимо), а программа молча вышла бы
|
|
||||||
* в prompt (баг Соннета, найден 2026-07-16). */
|
|
||||||
if (kbd_raw_open() != 0) {
|
|
||||||
closegraph();
|
|
||||||
puts("kbd_raw_open failed (need memory mode with BSS in W2)");
|
|
||||||
atlas_free(&at);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (;;) {
|
|
||||||
uint8_t moving = 0;
|
|
||||||
int y = KIDY;
|
|
||||||
|
|
||||||
if (jumping) {
|
|
||||||
/* дуга идёт сама; направлением можно скользить вбок,
|
|
||||||
* поза не меняется (нет отдельного кадра прыжка) */
|
|
||||||
if (kbd_raw_down(KBD_LEFT)) {
|
|
||||||
if (!col_blocked(x - SPEED)) x -= SPEED;
|
|
||||||
} else if (kbd_raw_down(KBD_RIGHT)) {
|
|
||||||
if (!col_blocked(x + SPEED)) x += SPEED;
|
|
||||||
}
|
|
||||||
y = KIDY - jump_height[jump_t];
|
|
||||||
jump_t++;
|
|
||||||
if (jump_t >= JUMP_FRAMES) {
|
|
||||||
jumping = 0;
|
|
||||||
y = KIDY;
|
|
||||||
}
|
|
||||||
} else if (crouching) {
|
|
||||||
if (!kbd_raw_down(KBD_DOWN))
|
|
||||||
crouching = 0;
|
|
||||||
} else if (kbd_raw_down(KBD_UP)) {
|
|
||||||
jumping = 1;
|
|
||||||
jump_t = 0;
|
|
||||||
} else if (kbd_raw_down(KBD_DOWN)) {
|
|
||||||
crouching = 1;
|
|
||||||
} else if (kbd_raw_down(KBD_LEFT)) {
|
|
||||||
if (!col_blocked(x - SPEED)) x -= SPEED;
|
|
||||||
set_facing(DIR_LEFT);
|
|
||||||
moving = 1;
|
|
||||||
} else if (kbd_raw_down(KBD_RIGHT)) {
|
|
||||||
if (!col_blocked(x + SPEED)) x += SPEED;
|
|
||||||
set_facing(DIR_RIGHT);
|
|
||||||
moving = 1;
|
|
||||||
}
|
|
||||||
if (!moving && !jumping && (sprite_anim_status(&kid) & SPR_ANIM_ON))
|
|
||||||
sprite_anim_stop(&kid, (int8_t)(facing * 3));
|
|
||||||
|
|
||||||
sprite_move(&kid, x, y);
|
|
||||||
|
|
||||||
if (kbd_raw_down(KBD_ESC))
|
|
||||||
break;
|
|
||||||
|
|
||||||
hidden = gfx_get_visible_page() ^ 1;
|
|
||||||
gfx_set_draw_page(hidden);
|
|
||||||
show_state(jumping, crouching);
|
|
||||||
sprite_update(&kid, 1);
|
|
||||||
gfx_wait_vsync();
|
|
||||||
gfx_set_visible_page(hidden);
|
|
||||||
}
|
|
||||||
|
|
||||||
kbd_raw_close();
|
|
||||||
closegraph();
|
|
||||||
tile_atlas_free();
|
|
||||||
atlas_free(&at);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,47 +0,0 @@
|
|||||||
/* pop_bg_atlas.h — раскладка атласов статического фона PoP.
|
|
||||||
* Сгенерировано toolchain/pop_pack_bg.py — НЕ править вручную.
|
|
||||||
*
|
|
||||||
* Прямая адресация (ноль remap-таблиц в W2):
|
|
||||||
* ENV фон id N -> atlas env_bg[N>>4], idx N&15
|
|
||||||
* WALL id N -> atlas wall, idx N
|
|
||||||
* FORE id N -> atlas fore, idx N
|
|
||||||
*
|
|
||||||
* ДВА ТАЙЛСЕТА (порт tbl_envir_ki[tbl_level_type], seg000:1108):
|
|
||||||
* 0 = подземелье (pop_*), 1 = дворец (pal_*). Набор id и раскладка
|
|
||||||
* у них ОДНИ И ТЕ ЖЕ — меняются только файлы и 32 записи палитры
|
|
||||||
* (env 0x50..0x5F, wall 0x60..0x6F), см. *tile.pal.
|
|
||||||
*/
|
|
||||||
#ifndef POP_BG_ATLAS_H
|
|
||||||
#define POP_BG_ATLAS_H
|
|
||||||
|
|
||||||
#define POP_ENV_SHIFT 4
|
|
||||||
#define POP_ENV_MASK 15
|
|
||||||
#define POP_ENV_PAGES 10
|
|
||||||
#define POP_TILESETS 2
|
|
||||||
|
|
||||||
/* Палитра: env-слоты, wall-слоты (спрайт-пиксель i -> база+i). */
|
|
||||||
#define POP_PAL_ENV 0x50
|
|
||||||
#define POP_PAL_WALL 0x60
|
|
||||||
|
|
||||||
/* Имена файлов атласов по тайлсету (грузятся atlas_load). Таблицы
|
|
||||||
* видны только тому, кто попросил POP_BG_ATLAS_NAMES: иначе копия
|
|
||||||
* строк уедет в каждый включивший заголовок модуль. */
|
|
||||||
#ifdef POP_BG_ATLAS_NAMES
|
|
||||||
static const char *const pop_env_atl[POP_TILESETS][POP_ENV_PAGES] = {
|
|
||||||
{ "pop_env0.atl", "pop_env1.atl", "pop_env2.atl", "pop_env3.atl", "pop_env4.atl", "pop_env5.atl", "pop_env6.atl", "pop_env7.atl", "pop_env8.atl", "pop_env9.atl" },
|
|
||||||
{ "pal_env0.atl", "pal_env1.atl", "pal_env2.atl", "pal_env3.atl", "pal_env4.atl", "pal_env5.atl", "pal_env6.atl", "pal_env7.atl", "pal_env8.atl", "pal_env9.atl" },
|
|
||||||
};
|
|
||||||
static const char *const pop_wall_atl[POP_TILESETS] = { "pop_wall.atl", "pal_wall.atl" };
|
|
||||||
static const char *const pop_fore_atl[POP_TILESETS] = { "pop_fore.atl", "pal_fore.atl" };
|
|
||||||
static const char *const pop_tile_pal[POP_TILESETS] = { "pop_tile.pal", "pal_tile.pal" };
|
|
||||||
#endif /* POP_BG_ATLAS_NAMES */
|
|
||||||
#define POP_POT_ATL "pop_pot.atl" /* chtab_1: зелья, от набора не зависит */
|
|
||||||
#define POP_PAL_POT 0x40
|
|
||||||
/* Пузырёк зелья: красный набор = id 16..22 (кадры оригинала),
|
|
||||||
зелёный (перо/переворот) и синий (вред/открыть) = те же кадры
|
|
||||||
под id 30..36 и 40..46 (draw_tile_anim, seg008:652). */
|
|
||||||
#define POP_POT_BUBB_GREEN 30
|
|
||||||
#define POP_POT_BUBB_BLUE 40
|
|
||||||
#define POP_BG_PAL "pop_bg.pal"
|
|
||||||
|
|
||||||
#endif
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user