docs(PoP): документация к актуальному статусу + план следующих уровней
Документы отстали от кода: PORT_PLAN писал «PoC не начат», хотя играется весь уровень 1, а четыре плана были исполнены целиком. - PORT_PLAN: таблица статусов по разделам; фазы 0-3 сделаны, 4-6 нет; риски §8 п.1/п.3 закрыты, п.2 переформулирован под реальный движок (спрайтовый движок для персонажей не используется, лимит «21 спрайт» неприменим), п.4 — найдено расхождение таймингов: оригинал считает логический кадр за 5 тиков при BASE_FPS=60 (83.3 мс, в бою 100 мс), а мы ждём три vsync (60 мс) — игра идёт примерно на 39 % быстрее эталона. - levels_plan.md — новый: машинерия перехода между уровнями, второй тайлсет (palace), потабличные различия и читы SDLPoP, которые окупаются сразу. Инвентарь тайлов снят прямо с res200N.bin: уровень 2 не требует ни одного нового ассета и ни одной новой механики. - roomtest/TASKS.md — новый: доска текущих задач с критериями готовности. - Удалены как исполненные и перекрытые кодом: clip_char_plan, double_buffer_plan, loose_floors_plan, size_optimization_plan. Его §8 (замеры скорости отрисовки) не был перекрыт — перенесён в layout_plan_v2 §9, чтобы не потерять цифры. - KID_PLAN / gates_spikes_plan — шапки «реализовано, оставлено справочником»; room_model_plan — «S1 сделан, остальное не срочно». - docs/README.md стал индексом с отметками актуальности. - ideas_backlog: зелье переворота экрана — оригинал переворачивает готовый буфер построчно, спрайты не трогает; по данным уровней тип 4 встречается только на уровне 9, до него механика не нужна. - examples/scroll: ссылка на удалённый план вела к неверному факту «теневая копия одна — общая»; заменено на подтверждённое «у каждой страницы своя». Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -59,11 +59,14 @@ Princed для DAT v1.0 (контейнер/индекс/чек-сумма, ко
|
|||||||
|
|
||||||
## Структура папки
|
## Структура папки
|
||||||
|
|
||||||
- `docs/` — планы и форматы: `PORT_PLAN.md` (общий план фаз),
|
- `docs/` — планы и форматы; **индекс с отметками актуальности —
|
||||||
`KID_PLAN.md`, `double_buffer_plan.md`, `loose_floors_plan.md`, форматы
|
`docs/README.md`**, начинать чтение оттуда. Ключевое:
|
||||||
ресурсов. Начинать чтение отсюда.
|
`levels_plan.md` (следующий этап), `layout_plan_v2.md` (раскладка кода по
|
||||||
- `roomtest/` — **активная разработка**: комната 1 + Kid (анимация, ввод,
|
окнам/банкам + скорость отрисовки), `PORT_PLAN.md` (карта фаз со
|
||||||
коллизия, падение, зацеп, fore-окклюзия, loose-полы). Свой `CLAUDE.md`.
|
статусами).
|
||||||
|
- `roomtest/` — **активная разработка**: уровень 1 целиком (Kid, стражи,
|
||||||
|
ловушки, ворота, loose-полы). Свой `CLAUDE.md`; текущие задачи —
|
||||||
|
`roomtest/TASKS.md`, баги — `roomtest/bug_list.md`.
|
||||||
- `poc/` — ранний proof-of-concept (снег/атлас/kbd_raw); ассеты в `poc/res/`.
|
- `poc/` — ранний proof-of-concept (снег/атлас/kbd_raw); ассеты в `poc/res/`.
|
||||||
- `bgtest/`, `coltest/` — отдельные проверки фона/коллизии.
|
- `bgtest/`, `coltest/` — отдельные проверки фона/коллизии.
|
||||||
- `toolchain/` — python-упаковщики ассетов + эталонные PNG (`1.1-2.png`).
|
- `toolchain/` — python-упаковщики ассетов + эталонные PNG (`1.1-2.png`).
|
||||||
|
|||||||
@@ -4,15 +4,22 @@
|
|||||||
target-слоя SDCC (sprinter-cc / libc / libbgi). Цель — 320×256×256 (режим
|
target-слоя SDCC (sprinter-cc / libc / libbgi). Цель — 320×256×256 (режим
|
||||||
0x81), VGA-256 ассеты оригинала переносятся почти впрямую.
|
0x81), VGA-256 ассеты оригинала переносятся почти впрямую.
|
||||||
|
|
||||||
Общий план и статус фаз — [`docs/PORT_PLAN.md`](docs/PORT_PLAN.md).
|
**Состояние (2026-08-01): играется весь уровень 1** — комнаты и переходы,
|
||||||
Правила работы для ИИ-сессий — [`CLAUDE.md`](CLAUDE.md).
|
Kid со всем набором действий, ловушки, ворота, дверь уровня, меч и бой,
|
||||||
|
стражи с ИИ, HP и зелья. Нет: перехода на следующий уровень, звука,
|
||||||
|
таймера/HUD, сохранений.
|
||||||
|
|
||||||
|
- Что в работе прямо сейчас — [`roomtest/TASKS.md`](roomtest/TASKS.md).
|
||||||
|
- Следующий этап (уровни 2+) — [`docs/levels_plan.md`](docs/levels_plan.md).
|
||||||
|
- Общий план и статус фаз — [`docs/PORT_PLAN.md`](docs/PORT_PLAN.md).
|
||||||
|
- Правила работы для ИИ-сессий — [`CLAUDE.md`](CLAUDE.md).
|
||||||
|
|
||||||
## Что где
|
## Что где
|
||||||
|
|
||||||
| Папка | Назначение |
|
| Папка | Назначение |
|
||||||
|-------|-----------|
|
|-------|-----------|
|
||||||
| `roomtest/` | **Активная разработка.** Комната 1 уровня 1 живой композицией тайлов + Kid: анимация (seqtbl), управление с клавиатуры, коллизия, падение, зацеп/подтягивание, fore-окклюзия, проваливающиеся полы. Свой README/CLAUDE. |
|
| `roomtest/` | **Активная разработка.** Уровень 1 целиком: фон композицией тайлов, Kid (seqtbl-анимация, ввод, коллизия, падение, зацеп, окклюзия), ловушки, ворота, стражи, бой. Свой README/CLAUDE/TASKS. |
|
||||||
| `docs/` | Планы (`PORT_PLAN`, `KID_PLAN`, `double_buffer_plan`, `loose_floors_plan`) и разбор форматов ресурсов Apple II / DOS. |
|
| `docs/` | Планы и разбор форматов ресурсов Apple II / DOS — см. индекс в [`docs/README.md`](docs/README.md). |
|
||||||
| `toolchain/` | Python-упаковщики ассетов под Sprinter (рендер комнат, атласы тайлов/Kid, извлечение данных анимации) + эталонные скриншоты. |
|
| `toolchain/` | Python-упаковщики ассетов под Sprinter (рендер комнат, атласы тайлов/Kid, извлечение данных анимации) + эталонные скриншоты. |
|
||||||
| `poc/` | Ранний proof-of-concept (снег, атлас, raw-клавиатура). Ассеты в `poc/res/`. |
|
| `poc/` | Ранний proof-of-concept (снег, атлас, raw-клавиатура). Ассеты в `poc/res/`. |
|
||||||
| `bgtest/`, `coltest/` | Точечные проверки фона и коллизии. |
|
| `bgtest/`, `coltest/` | Точечные проверки фона и коллизии. |
|
||||||
|
|||||||
@@ -1,6 +1,21 @@
|
|||||||
# Prince of Persia — Kid (персонаж): анализ и план
|
# Prince of Persia — Kid (персонаж): анализ и план
|
||||||
|
|
||||||
Статус: план (2026-07-16). Опирается на разбор `SDLPoP/src/seg006.c`
|
> **Статус: РЕАЛИЗОВАНО (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.md`.
|
||||||
|
|
||||||
|
Составлен 2026-07-16. Опирается на разбор `SDLPoP/src/seg006.c`
|
||||||
(ядро физики/управления Kid), `seqtbl.c` (таблицы последовательностей),
|
(ядро физики/управления Kid), `seqtbl.c` (таблицы последовательностей),
|
||||||
`types.h` (char_type, seq_*, SEQ_*, actions_*), `SDLPoP/data/KID` (спрайты).
|
`types.h` (char_type, seq_*, SEQ_*, actions_*), `SDLPoP/data/KID` (спрайты).
|
||||||
Фон уже готов и проверен на MAME (`applications/PoP/roomtest`, см.
|
Фон уже готов и проверен на MAME (`applications/PoP/roomtest`, см.
|
||||||
|
|||||||
@@ -1,8 +1,29 @@
|
|||||||
# Prince of Persia на ZX Sprinter — план порта
|
# Prince of Persia на ZX Sprinter — план порта
|
||||||
|
|
||||||
Статус: план (2026-07-15). §2 (A: kbd_mod_state / B: kbd_raw) —
|
## СТАТУС (обновлено 2026-08-01)
|
||||||
РЕАЛИЗОВАНО и частично проверено в MAME (tests/kbdraw, 2026-07-15,
|
|
||||||
подробности в §2.2); PoC (§5) и остальные фазы — не начаты. Опирается на
|
Документ составлен 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.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.md` (что в работе), `levels_plan.md`
|
||||||
|
(следующие уровни), `layout_plan_v2.md` (раскладка кода по окнам и банкам).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Опирается на
|
||||||
`APPLEII_RESOURCE_FORMAT.md` / `MSDOS_RESOURCE_FORMAT.md` / `README.md` в
|
`APPLEII_RESOURCE_FORMAT.md` / `MSDOS_RESOURCE_FORMAT.md` / `README.md` в
|
||||||
этой папке, на текущий sprinter-cc/libc/libbgi (см. §1) и на локальные копии
|
этой папке, на текущий sprinter-cc/libc/libbgi (см. §1) и на локальные копии
|
||||||
`applications/PoP/SDLPoP` (github.com/NagyD/SDLPoP, GPLv3) и
|
`applications/PoP/SDLPoP` (github.com/NagyD/SDLPoP, GPLv3) и
|
||||||
@@ -227,6 +248,13 @@ dirty-биты, heal против фона через ОЗУ-копию); `room_
|
|||||||
|
|
||||||
## 5. Proof-of-Concept — цель: доказать, что порт вообще ощущается как PoP
|
## 5. Proof-of-Concept — цель: доказать, что порт вообще ощущается как PoP
|
||||||
|
|
||||||
|
> **Закрыт (исторический раздел).** PoC в `poc/` свою задачу выполнил и
|
||||||
|
> дальше не развивается: управление ощущается как PoP, held-state работает.
|
||||||
|
> Всё, что ниже про плейсхолдер-персонажа и приблизительную дугу прыжка,
|
||||||
|
> — уже неправда для активной ветки: в `roomtest/` стоит настоящая графика
|
||||||
|
> Кида и авторские таблицы кадров (§6). Раздел оставлен ради истории
|
||||||
|
> решений (в частности §5.1 — почему сначала был плейсхолдер).
|
||||||
|
|
||||||
**Объём**: одна комната (например Level 1, экран старта Кида), без
|
**Объём**: одна комната (например Level 1, экран старта Кида), без
|
||||||
переходов между экранами, без стражников (стретч-цель, не обязательна).
|
переходов между экранами, без стражников (стретч-цель, не обязательна).
|
||||||
|
|
||||||
@@ -364,45 +392,53 @@ memory/png_strip_padding_tradeoff.
|
|||||||
## 7. Полноценное приложение — фазы (после PoC)
|
## 7. Полноценное приложение — фазы (после PoC)
|
||||||
|
|
||||||
Порядок — по риску и зависимостям, не по геймплейной важности.
|
Порядок — по риску и зависимостям, не по геймплейной важности.
|
||||||
|
**Отметки статуса — на 2026-08-01.**
|
||||||
|
|
||||||
**Фаза 0 — инфраструктура порта** (расширяет PoC, не переписывает):
|
**Фаза 0 — инфраструктура порта** — **СДЕЛАНА**, но иначе, чем задумано:
|
||||||
- Хелд-стейт клавиатуры — финальное решение и реализация по §2 (после
|
- Хелд-стейт клавиатуры по §2 — сделан.
|
||||||
подтверждения пользователем).
|
- Конвертер уровней не понадобился: `res200N.bin` из `SDLPoP/data/LEVELS`
|
||||||
- Полный конвертер уровней (все 15 файлов `levels.dat`/`LEVELn`) → бинарный
|
кладётся на образ как есть и читается по офсетам в рантайме
|
||||||
формат приложения (можно 1-в-1 raw dump, читать по офсетам в рантайме —
|
(`roomtest/pop_level.c`), уровень живёт в EMM-странице.
|
||||||
не обязательно разворачивать в C-struct с указателями).
|
- Конвертер фона в растры **отменён осознанно** (§4): фон собирается
|
||||||
- Полный конвертер фона (24 экрана × N уровней) в растры + конвертер
|
тайлами в рантайме. Спрайты — `toolchain/pop_pack_bg.py` /
|
||||||
спрайт-лент Кид/стражник/скелет/тень/Джаффар в атласы `.atl` (расширение
|
`pop_pack_kid.py` / `pop_pack_guard.py` → атласы `.atl` (Kid — 28
|
||||||
`conv_sprites.py`/формата `.atl`, если частот кадров/атласов на актора не
|
страниц, риск §8 п.3 закрыт).
|
||||||
хватит текущего лимита — см. риск в §8).
|
|
||||||
|
|
||||||
**Фаза 1 — Кид, полный набор действий**: стоять/идти/бежать/тормозить/
|
**Фаза 1 — Кид, полный набор действий** — **СДЕЛАНА**: стоять/идти/бежать/
|
||||||
разворот/прыжок (на месте, вперёд, «прыжок с разбега»)/повисание на
|
тормозить/разворот/прыжки/повисание/подтягивание/спуск/приседание/
|
||||||
краю/подтягивание/спуск по свисанию/приседание/питьё зелья/смерть от
|
осторожный шаг/питьё зелья/смерть от провала и от пик; переходы между
|
||||||
провала. Переходы между экранами (`MAP`-граф, `INFO.KidStartScrn`).
|
комнатами во все четыре стороны. Осталось: **старт по данным уровня**
|
||||||
|
(`pop_level_start_*` реализованы, но не подключены) — задача L1-START в
|
||||||
|
`../roomtest/TASKS.md`.
|
||||||
|
|
||||||
**Фаза 2 — мир и ловушки**: нажимные плиты/двери через граф
|
**Фаза 2 — мир и ловушки** — **СДЕЛАНА**: кнопки/ворота через
|
||||||
`LINKLOC`/`LINKMAP` (см. `APPLEII_RESOURCE_FORMAT.md` §1.2), шипы
|
`LINKLOC`/`LINKMAP`, шипы, loose-полы (тряска, обрушение, щебень, пробой
|
||||||
(выдвижение/втягивание/заклинивание), шаткие плиты (loose, обрушение),
|
потолка), зелья, дверь уровня (открывается), факелы. Подробности и
|
||||||
зелья (эффект по `BLUESPEC×32`), стартовые позиции по `INFO`.
|
справочник — `gates_spikes_plan.md`.
|
||||||
|
|
||||||
**Фаза 3 — бой**: подбор/выхватывание меча, состояние стойки, парирование/
|
**Фаза 3 — бой** — **СДЕЛАНА в объёме обычного стражника**: подбор и
|
||||||
удар, коллизия клинков — по логике `AUTO.S`/`seg003-006.c` (референс, не
|
выхватывание меча, стойка, удар/парирование, коллизия клинков, HP обеих
|
||||||
копия). Стражник: базовое AI-поведение по `GdStartProg` (несколько
|
сторон, смерть; ИИ стража (замечает Кида, подходит, боевые ветки),
|
||||||
шаблонов программ), Y-сортировка слоями уже есть в движке для «кто
|
персистентность трупа между комнатами.
|
||||||
спереди/сзади».
|
|
||||||
|
|
||||||
**Фаза 4 — разнообразие противников**: скелет, тень (копия анимации Кида —
|
**Фаза 4 — разнообразие противников** — **НЕ НАЧАТА**. Скелет нужен на
|
||||||
подтверждено побайтовым совпадением данных, см. `MSDOS_RESOURCE_FORMAT.md`
|
уровне 3, толстый — на 6, тень — на 12, визирь — на 13; привязка
|
||||||
§3), толстый стражник/визирь (общая база анимации с визирем).
|
«уровень → тип стража» (`tbl_guard_type`) описана в `levels_plan.md` §1.
|
||||||
|
|
||||||
**Фаза 5 — звук**: CBL-эффекты (шаги, удары, двери, падение) из
|
**Фаза 5 — звук** — **НЕ НАЧАТА**: CBL-эффекты (шаги, удары, двери,
|
||||||
`digisnd*.dat`→PCM; PC-спикер тройки (`ibm_snd*.dat`) как опциональный
|
падение) из `digisnd*.dat`→PCM; PC-спикер тройки (`ibm_snd*.dat`) как
|
||||||
дешёвый бипер без CBL, если формат подтвердится простым парсингом.
|
опциональный дешёвый бипер без CBL, если формат подтвердится простым
|
||||||
|
парсингом. Опкод `SOUND` в `play_seq` пока просто съедает свой аргумент —
|
||||||
|
точки вызова уже на месте.
|
||||||
|
|
||||||
**Фаза 6 — оболочка**: титры, меню/выбор уровня, HUD (таймер/жизни),
|
**Фаза 6 — оболочка** — **НЕ НАЧАТА**: титры, меню/выбор уровня, HUD
|
||||||
сохранение прогресса (FILE*), финальные катсцены — по минимуму,
|
(таймер/жизни), сохранение прогресса (FILE*), финальные катсцены — по
|
||||||
геймплейно не критично.
|
минимуму, геймплейно не критично. Полоса HP — единственное, что уже есть.
|
||||||
|
|
||||||
|
**Между Фазами 4 и 5 вклинивается то, чего в этом плане не было:
|
||||||
|
переход между УРОВНЯМИ** (загрузка следующего уровня, второй тайлсет
|
||||||
|
palace, потабличные различия уровней). Отдельный документ —
|
||||||
|
`levels_plan.md`.
|
||||||
|
|
||||||
**Фаза 7 — стабилизация**: полный прогон всех 14 уровней в MAME
|
**Фаза 7 — стабилизация**: полный прогон всех 14 уровней в MAME
|
||||||
(`mame_interactive.py`), затем на реальном железе; профилирование бюджета
|
(`mame_interactive.py`), затем на реальном железе; профилирование бюджета
|
||||||
@@ -418,23 +454,33 @@ tiny/small.
|
|||||||
|
|
||||||
(по правилу `defer_unexplained_quirks` — не гадать, проверять)
|
(по правилу `defer_unexplained_quirks` — не гадать, проверять)
|
||||||
|
|
||||||
1. **Held-state клавиатуры** (§2) — блокирует даже PoC, если решать
|
1. ~~**Held-state клавиатуры** (§2)~~ — **закрыт** (`<kbd_raw.h>`). Открытый
|
||||||
«правильно»; иначе PoC на компромиссном варианте 2 (таймаут-эвристика).
|
остаток — не «есть ли held-state», а потеря байт при аккордах
|
||||||
2. **Бюджет спрайтов на насыщенный экран** — сцена с 2+ стражниками +
|
Shift+стрелка: `../roomtest/TASKS.md`, KBD-1.
|
||||||
несколько анимированных ловушек может приблизиться к лимиту
|
2. **Бюджет кадра** — риск подтвердился, но не в том виде, в каком ожидался:
|
||||||
~21 спрайт/кадр (`sprite_engine_perf`) — нужна прикидка по реальным
|
спрайтовый движок для персонажей не используется, поэтому лимит
|
||||||
уровням (сколько объектов одновременно активно в худшем экране).
|
«~21 спрайт/кадр» неприменим. Реальный бюджет упирается в heal+блиты и
|
||||||
3. **Ёмкость одного атласа/страницы EMM на актора** — у Кида ~220 кадров
|
перерисовку тайлов; замер 2026-07-30 — типичный кадр ~371 К тактов
|
||||||
(все действия) против 4×12 у `rpgwalk` — потребуется либо несколько
|
(~86 % периода). Инструмент замера уже в коде: полосы бордюра `PROF()`
|
||||||
атласов на актора с переключением по фазе действия (стоять/идти отдельно
|
в `roomtest.c`. План выжимания — `../roomtest/TASKS.md` (CLIP-1) и
|
||||||
от боя), либо расширение формата `.atl`/загрузчика на мульти-страничные
|
`../roomtest/bug_list.md` (T-1/T-2).
|
||||||
атласы — оценить фактический байтовый вес конвертированных кадров Кида
|
3. ~~**Ёмкость атласа на актора**~~ — **закрыт**: Kid разложен на 28
|
||||||
прежде чем проектировать.
|
атласов-страниц по 8 спрайтов (`pop_pack_kid.py`), страж — на 5;
|
||||||
4. **Тайминг оригинала** — сверить логическую частоту кадров анимации
|
мульти-страничного формата `.atl` не потребовалось. Побочно
|
||||||
оригинала (Apple II ~60 Гц NTSC / DOS — фиксированный таймер) с 50 Гц
|
подтвердился компромисс паддинга (§6.1).
|
||||||
Sprinter; если оригинал считался на другой частоте — потребуется
|
4. **Тайминг оригинала** — **ОТКРЫТ, и сверка 2026-08-01 показывает
|
||||||
коэффициент пересчёта смещений кадров (§6), иначе прыжки/бег будут
|
расхождение.** Цифры оригинала (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) — иначе замедление
|
||||||
|
спрячет проблему бюджета вместо того, чтобы её показать.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,38 @@
|
|||||||
# Форматы ресурсов Prince of Persia — сводка
|
# `applications/PoP/docs` — индекс + сводка по форматам ресурсов
|
||||||
|
|
||||||
|
## Индекс документов (актуальность на 2026-08-01)
|
||||||
|
|
||||||
|
**Живые планы — читать перед работой:**
|
||||||
|
|
||||||
|
| Документ | О чём |
|
||||||
|
|----------|-------|
|
||||||
|
| [`../roomtest/TASKS.md`](../roomtest/TASKS.md) | **Что берётся в работу сейчас** (не в этой папке, но входная точка) |
|
||||||
|
| [`levels_plan.md`](levels_plan.md) | Следующий этап: уровни 2+, второй тайлсет, читы SDLPoP |
|
||||||
|
| [`layout_plan_v2.md`](layout_plan_v2.md) | Раскладка кода по окнам/банкам/страницам + замеры скорости отрисовки |
|
||||||
|
| [`room_model_plan.md`](room_model_plan.md) | `kid_room ≠ drawn_room` (straddle): сделан S1, остальное впереди |
|
||||||
|
| [`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.pdf`
|
||||||
(+ текстовая конверсия `POP-DAT-FormatSpecifications.txt` для grep/цитирования):
|
(+ текстовая конверсия `POP-DAT-FormatSpecifications.txt` для grep/цитирования):
|
||||||
@@ -70,7 +104,11 @@ DOS-упаковщика). Это значит: раскладку `BLUETYPE`/`B
|
|||||||
дворца/подземелий) практичнее взять их оттуда напрямую, чем писать свой
|
дворца/подземелий) практичнее взять их оттуда напрямую, чем писать свой
|
||||||
декодер сжатия пикселей DOS `.DAT`.
|
декодер сжатия пикселей DOS `.DAT`.
|
||||||
|
|
||||||
## Что дальше (не сделано в этом заходе)
|
## Что дальше по форматам (не сделано и пока не нужно)
|
||||||
|
|
||||||
|
Порт читает уровень напрямую из `res200N.bin` (`roomtest/pop_level.c`), а
|
||||||
|
графику берёт из распакованных PNG `SDLPoP/data/` — поэтому ни один пункт
|
||||||
|
ниже сейчас не блокирует работу.
|
||||||
|
|
||||||
1. Точный кодек сжатия пикселей спрайтов в сыром DOS `.DAT` (нужен только
|
1. Точный кодек сжатия пикселей спрайтов в сыром DOS `.DAT` (нужен только
|
||||||
если понадобится читать именно нашу локальную копию `MSDOS/*.dat`
|
если понадобится читать именно нашу локальную копию `MSDOS/*.dat`
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
# План: порт `clip_char()` — обрезка спрайта персонажа
|
|
||||||
|
|
||||||
Статус: в работе с 2026-07-28. Контекст: баг «спуск Кида с кнопки в комнате 8»
|
|
||||||
(Kid просвечивает в щель между кнопкой и ближним столбом, мусор на кромке).
|
|
||||||
|
|
||||||
## Симптом
|
|
||||||
|
|
||||||
room 8, Kid спускается (climbdown) с тайла-кнопки у ближней колонны. Спрайт
|
|
||||||
Кида нарисован ЦЕЛИКОМ, включая часть, которая в оригинале обрезана по линии
|
|
||||||
пола: видно «просвет» Кида в щели между кнопкой и колонной и мусор на кромке.
|
|
||||||
|
|
||||||
## Корень
|
|
||||||
|
|
||||||
Не портирован `clip_char()` (SDLPoP `seg006.c:1749`, вызывается из
|
|
||||||
`add_kid_to_objtable`/`add_guard_to_objtable`, `seg008.c:1671/1690`, ПОСЛЕ
|
|
||||||
`set_char_collision`/`set_objtile_at_char`/`redraw_at_char*` и ПЕРЕД
|
|
||||||
`add_objtable`). Оригинал кладёт в objtable не только позицию спрайта, но и
|
|
||||||
прямоугольник клипа `obj_clip_{top,bottom,left,right}`; блиттер рисует только
|
|
||||||
внутри него. Мы рисуем без клипа вообще.
|
|
||||||
|
|
||||||
## Что делает оригинал (дословно)
|
|
||||||
|
|
||||||
`reset_obj_clip()` → `left=0, top=0, right=320, bottom=192`.
|
|
||||||
|
|
||||||
Дальше (упрощая C-трюк с глобалью `curr_tile2`, которую ставит каждый
|
|
||||||
`get_tile`: `X == wall || tile_is_floor(curr_tile2)` — это «тайл X = стена
|
|
||||||
ИЛИ пол»):
|
|
||||||
|
|
||||||
```
|
|
||||||
T_L = get_tile(room, char_col_left, char_top_row)
|
|
||||||
T_R = get_tile(room, char_col_right, char_top_row)
|
|
||||||
if (T_L — стена или пол) &&
|
|
||||||
( (action == stand && (frame == 79 || frame == 81)) /* прыжок вверх / зацеп */
|
|
||||||
|| (T_R — стена или пол) )
|
|
||||||
{
|
|
||||||
clip_row = Char.curr_row + 1;
|
|
||||||
clip_y = y_clip[clip_row]; /* y_clip[] = {-60, 3, 66, 129, 192} */
|
|
||||||
if (clip_row == 1 || (clip_y < obj_y && clip_y - 15 < char_top_y))
|
|
||||||
obj_clip_top = char_top_y = clip_y;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Смысл: `y_clip[row+1]` — верхняя граница СВОЕЙ полосы ряда. Если над головой
|
|
||||||
пол/стена — всё, что выше этой линии, не рисуется (персонаж «уходит под пол»).
|
|
||||||
Для ряда 0 (`clip_row == 1`) клип применяется БЕЗУСЛОВНО.
|
|
||||||
|
|
||||||
Метрики из `set_char_collision` (seg006:0723):
|
|
||||||
- `char_x_left = obj_x/2 + 58` (и `-= char_width_half`, если смотрит вправо)
|
|
||||||
- `char_x_right = char_x_left + char_width_half`, `char_width_half = (w+1)/2`
|
|
||||||
- `char_top_y = obj_y - h + 1`; если `>= 192` → `0`
|
|
||||||
- `char_top_row = y_to_row_mod4(char_top_y)`
|
|
||||||
- `char_col_left = MAX(get_tile_div_mod(char_x_left), 0)`,
|
|
||||||
`char_col_right = MIN(get_tile_div_mod(char_x_right), 9)`
|
|
||||||
|
|
||||||
Второй блок `clip_char` — `obj_clip_right` (doortop / стена / зеркало для
|
|
||||||
виса-полёта-подъёма при взгляде влево, кадры 137..139) и ветка кадров 224..228
|
|
||||||
(выход в дверь уровня). **Фаза 2**, не в этом заходе.
|
|
||||||
|
|
||||||
## Наши точки касания
|
|
||||||
|
|
||||||
- `roomtest/pop_kid.c` `kid_draw()` — сам считает `obj_x/obj_y/w/h` и блитит
|
|
||||||
через `gfx_blit_cols(bx, top + POP_YOFF, img, flip)`; запоминает
|
|
||||||
прямоугольник в `kid_l{x,y,w,h}[page]` для `kid_heal()`.
|
|
||||||
- `roomtest/pop_map.c` — там `get_tile()`, `tile_is_floor()`, `y_to_row()`,
|
|
||||||
`get_tile_div_mod_m7()`, Kid-структура. Аналогичный расчёт габарита уже
|
|
||||||
есть в `check_spike_below()` (строка ~1198) — брать за образец.
|
|
||||||
- `libbgi/common/gfx_blit_cols.c` — клип по экрану есть (при `y < 0`
|
|
||||||
пропускает `sy` верхних строк колонки), но произвольной верхней границы нет.
|
|
||||||
|
|
||||||
## Шаги
|
|
||||||
|
|
||||||
1. **libbgi**: `gfx_blit_cols_part(int x, int y, const void *img, uint8_t flip,
|
|
||||||
int sy, int h)` — «пропустить `sy` верхних строк спрайта, нарисовать `h`».
|
|
||||||
Реализация = тело `gfx_blit_cols` с предустановленными `sy`/`h` (источник
|
|
||||||
`+= sy`, экранный `y += sy`). Прототип в `libbgi/include/gfx.h`.
|
|
||||||
Стоимость: 0 в общем пути (обычный `gfx_blit_cols` вызывает то же ядро).
|
|
||||||
2. **pop_map.c**: `int pop_clip_char_top(int obj_x, int obj_y, uint16_t w,
|
|
||||||
uint16_t h)` — порт первого блока `clip_char`; возвращает КОМНАТНЫЙ y
|
|
||||||
(0 = клипа нет). Экспорт в `pop_map.h`.
|
|
||||||
3. **pop_kid.c**: в `kid_draw()` после расчёта `top` —
|
|
||||||
`ct = pop_clip_char_top(...)`; если `ct > top` → `sy = ct - top`,
|
|
||||||
рисовать `gfx_blit_cols_part(bx, ct + POP_YOFF, img, flip, sy, h - sy)`;
|
|
||||||
в `kid_l*[page]` класть ОБРЕЗАННЫЙ прямоугольник (иначе heal чистит лишнее
|
|
||||||
и стирает кромку пола). То же для `kid_draw_splash` — там оригинал делает
|
|
||||||
`reset_obj_clip()`, т.е. splash НЕ клипится (ничего не менять).
|
|
||||||
4. **Сборка**: `make -C libbgi`, `make -C applications/PoP/roomtest`,
|
|
||||||
`make size-check`; вывести свободное место в W2/W3 (порог 512 Б).
|
|
||||||
5. **Проверка в MAME** (`mame_hdd_test_disk`, полный рестарт после
|
|
||||||
пересборки образа): комната 6 → переход в 8 → влезть на кнопку → спуск.
|
|
||||||
Сверять с эталоном: живой SDLPoP той же позой (см. `pop_check_sdlpop_first`).
|
|
||||||
|
|
||||||
## Риски / что проверить отдельно
|
|
||||||
|
|
||||||
- `char_top_row` считается `y_to_row_mod4` — у нас `y_to_row()` даёт −1 для
|
|
||||||
полосы у потолка; `get_tile(row −1)` уже умеет ряд 2 верхнего соседа.
|
|
||||||
- Kid ниже комнаты (`char_top_y >= 192` → `0`) — обязателен ресет, иначе клип
|
|
||||||
прыгнет.
|
|
||||||
- `clip_row == 1` (ряд 0) — клип БЕЗУСЛОВНЫЙ: проверить, что не режет Кида в
|
|
||||||
обычной стойке на ряду 0 (условие внешнего `if` про пол/стену над головой
|
|
||||||
должно отсекать).
|
|
||||||
- Клип меняет прямоугольник heal → возможен «хвост» на второй странице
|
|
||||||
дабл-буфера: проверять оба кадра (SPACE — выключить дабл-буфер).
|
|
||||||
|
|
||||||
## Дальше (Фаза 2, отдельно)
|
|
||||||
|
|
||||||
`obj_clip_right` (doortop/стена/зеркало) — нужен для виса и подъёма при
|
|
||||||
взгляде влево; и `obj_clip_left` для зеркала (уровень 4). Требует клипа по
|
|
||||||
колонкам в `gfx_blit_cols` (обрезка справа = уменьшить `w`) — дёшево, но
|
|
||||||
без тестовой сцены проверять нечем.
|
|
||||||
|
|
||||||
См. memory: `pop_clip_char_todo`, `pop_backtable_vs_midtable`,
|
|
||||||
`pop_check_sdlpop_first`, `gfx_blit_noclip_fast`.
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
# Double buffer (два экрана + флип) — план
|
|
||||||
|
|
||||||
Цель: убрать мерцание/тиринг при перерисовке слоёв (Kid ↔ fore/пол-оверлей)
|
|
||||||
и гарантировать, что на экране ВСЕГДА готовый кадр с правильным порядком
|
|
||||||
слоёв. Нужно для отладки fore-слоя (видно момент композиции, а не
|
|
||||||
промежуточные состояния heal-рендера). **Тумблер обязателен** —
|
|
||||||
однобуферный режим удобнее для отладки багов рисования.
|
|
||||||
|
|
||||||
## Что уже готово (libbgi — трогать НЕ нужно)
|
|
||||||
|
|
||||||
- Две графических страницы 0/1: `gfx_set_draw_page(p)` (двигает
|
|
||||||
`_gfx_addr_base` 0xC000/0xC140 — рисуют все примитивы),
|
|
||||||
`gfx_set_visible_page(p)` (ESTEX $54 SELPAGE — display-учёт DSS).
|
|
||||||
- `gfx_wait_vsync()` — луч (bit5 порта 0xFE), момент без разрывов.
|
|
||||||
- Паттерн из gfx.h: `set_draw_page(hidden); draw(); wait_vsync();
|
|
||||||
set_visible_page(hidden);`
|
|
||||||
- Замечание gfx.h: у каждой страницы СВОЯ плоскость палитры
|
|
||||||
(page0→pal0, page1→pal1) — для seamless грузить одну палитру в ОБЕ.
|
|
||||||
|
|
||||||
## Текущая модель рендера roomtest (однобуфер)
|
|
||||||
|
|
||||||
`roomtest.c`: `draw_page=0`, `visible_page=0` фиксированы. Фон комнаты
|
|
||||||
рисуется ОДИН раз в видео-ОЗУ + теневую копию (GFX_BANK_TRANSPARENT).
|
|
||||||
Цикл: 3× `gfx_wait_vsync` (пейсинг) → `pop_ctrl_tick` → `kid_heal()`
|
|
||||||
(восстановить прямоугольник Кида из тени) → `kid_tick`/`pop_phys_tick`
|
|
||||||
→ `kid_draw` → `pop_fore_over_kid`. Мерцание = heal+draw+fore длиннее
|
|
||||||
бланка, луч ловит промежуток.
|
|
||||||
|
|
||||||
## Работа на стороне PoP
|
|
||||||
|
|
||||||
1. **Инициализация обеих страниц**: `pop_room_draw` в page 0 И page 1
|
|
||||||
(теневая копия одна — общая, из неё heal'ит любая страница).
|
|
||||||
2. **Палитра в обе плоскости**: сейчас `gfx_pal_fload(0,...)` + sync.
|
|
||||||
Продублировать в plane 1 (проверить сигнатуру gfx_pal_fload/sync —
|
|
||||||
plane-параметр).
|
|
||||||
3. **Per-page heal-история** (ядро): вынести `kid_lx/ly/lw/lh` в
|
|
||||||
массивы `[2]`, индекс = страница, в которую рисуем. `kid_heal(page)`
|
|
||||||
восстанавливает СВОЙ прошлый прямоугольник (кадр -2, т.к. рисуем
|
|
||||||
через страницу). То же для fore/пол-оверлея, если они рисуют вне
|
|
||||||
футпринта Кида.
|
|
||||||
4. **Ping-pong в цикле**:
|
|
||||||
```
|
|
||||||
uint8_t back = dbuf ? (front ^ 1) : 0;
|
|
||||||
gfx_set_draw_page(back);
|
|
||||||
kid_heal(back); kid_tick; phys; kid_draw; fore;
|
|
||||||
gfx_wait_vsync();
|
|
||||||
if (dbuf) { gfx_set_visible_page(back); front = back; }
|
|
||||||
```
|
|
||||||
5. **Тумблер** `dbuf`: off → draw==visible==0, без флипа, heal[0] —
|
|
||||||
бит-в-бит текущее поведение. Управление — клавишей (напр. F2) или
|
|
||||||
compile-флагом.
|
|
||||||
6. **Пейсинг**: сейчас 3× vsync/лог.кадр. При флипе — один vsync перед
|
|
||||||
свопом; недостающий пейсинг добрать `gfx_set_fps_div(3)` или ручным
|
|
||||||
счётом кадров, чтобы скорость игры не изменилась.
|
|
||||||
|
|
||||||
## Порядок
|
|
||||||
|
|
||||||
- D1: обе страницы + палитра в обе плоскости; ping-pong без per-page
|
|
||||||
heal (проверить, что флип работает, фон корректен на обеих).
|
|
||||||
- D2: per-page heal-история (kid_l*[2]) — убрать «хвост» Кида.
|
|
||||||
- D3: тумблер dbuf + сверка однобуферного пути с текущим (регресс-нет).
|
|
||||||
- D4: пейсинг (fps_div) — вернуть исходную скорость.
|
|
||||||
|
|
||||||
## Связанные
|
|
||||||
|
|
||||||
Рендер-модель — [[pop_fore_layer]], [[pop_background_strategy]];
|
|
||||||
heal — kid_heal/gfx_heal. Будущие динамические слои (loose-floors
|
|
||||||
[[двойной буфер требует их per-page перерисовки]]) должны рисоваться в
|
|
||||||
обе страницы по той же дисциплине.
|
|
||||||
@@ -1,8 +1,23 @@
|
|||||||
# Интерактивные объекты (кнопки/гейты/пики) + HP/смерть — ПОДРОБНЫЙ план
|
# Интерактивные объекты (кнопки/гейты/пики) + HP/смерть — ПОДРОБНЫЙ план
|
||||||
|
|
||||||
Статус: **план** (2026-07-20). Реализация — отдельной сессией. Документ
|
> **Статус: РЕАЛИЗОВАНО (2026-08-01).** Все фазы плана (P0 персистентный
|
||||||
самодостаточный: рассчитан на старт «с чистого листа» (пустой контекст).
|
> per-room `room_modif`, S пики, B кнопки+ворота) сделаны и играются:
|
||||||
Всё сверено с `applications/PoP/SDLPoP/src/` и данными `res2001.bin`.
|
> `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.md`.
|
||||||
|
|
||||||
|
Составлен 2026-07-20. Документ самодостаточный: рассчитан на старт
|
||||||
|
«с чистого листа» (пустой контекст). Всё сверено с
|
||||||
|
`applications/PoP/SDLPoP/src/` и данными `res2001.bin`.
|
||||||
|
|
||||||
Правило проекта (см. `applications/PoP/CLAUDE.md`): **SDLPoP — источник истины**,
|
Правило проекта (см. `applications/PoP/CLAUDE.md`): **SDLPoP — источник истины**,
|
||||||
перед кодингом читать соответствующий код seg*.c, не гадать.
|
перед кодингом читать соответствующий код seg*.c, не гадать.
|
||||||
|
|||||||
@@ -4,6 +4,54 @@
|
|||||||
гипотеза с причиной, по которой её стоит проверить, и с тем, что мешает
|
гипотеза с причиной, по которой её стоит проверить, и с тем, что мешает
|
||||||
сделать это прямо сейчас.
|
сделать это прямо сейчас.
|
||||||
|
|
||||||
|
## Зелье «переворот экрана» (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 тактов), бит-в-бит
|
Сейчас стоит LCG оригинала, шаг на ассемблере (~1 020 тактов), бит-в-бит
|
||||||
|
|||||||
@@ -1,8 +1,26 @@
|
|||||||
# roomtest — план v2: размер кода и раскладка по окнам/банкам/страницам
|
# roomtest — план v2: размер кода и раскладка по окнам/банкам/страницам
|
||||||
|
|
||||||
|
> **Замер 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 (`pop_gdraw.c`) 2 236 — все из 16 384.
|
||||||
|
> **Резидента `--w3` больше нет**: отрисовка уехала в банк 2, и это сняло
|
||||||
|
> главное ограничение резидента (из банка его было не достать) — банк→банк
|
||||||
|
> работает, трамплин сохраняет страницу окна на стеке. Отрисовка стража
|
||||||
|
> вынесена из банка 2 в собственный банк 4, потому что банк 2 подошёл к
|
||||||
|
> потолку (16 021 из 16 384) — коммит `2f3e854`.
|
||||||
|
>
|
||||||
|
> **Свободного места в банке 2 осталось ~2.6 КБ**, а туда же просятся
|
||||||
|
> чомперы, зеркало и второй тайлсет (palace). Прежде чем начинать
|
||||||
|
> `levels_plan.md` §3 — посчитать, куда это ляжет. Свободные номера банков
|
||||||
|
> есть, гранулярность — файл.
|
||||||
|
>
|
||||||
|
> Из плана ниже **не сделаны шаги 5 (данные: `room_modif`, `dl1/dl2`,
|
||||||
|
> `_kbdraw_down`; потенциал ~1.5 КБ) и 7 (дедуп `draw_tile`, отложен по
|
||||||
|
> решению пользователя)**.
|
||||||
|
|
||||||
Статус: **план для отдельной сессии**, составлен 2026-07-29 по свежему замеру.
|
Статус: **план для отдельной сессии**, составлен 2026-07-29 по свежему замеру.
|
||||||
Заменяет `size_optimization_plan.md` (v1, 2026-07-21): часть его пунктов уже
|
Заменял `size_optimization_plan.md` (v1, 2026-07-21) — тот удалён 2026-08-01
|
||||||
сделана, часть опиралась на неверную модель банкинга. Документ самодостаточный
|
как полностью перекрытый этим документом. Документ самодостаточный
|
||||||
— рассчитан на старт с пустого контекста.
|
— рассчитан на старт с пустого контекста.
|
||||||
|
|
||||||
Повод: перед стражами и боёвкой (новый код ~5–8 КБ) надо понять, куда он
|
Повод: перед стражами и боёвкой (новый код ~5–8 КБ) надо понять, куда он
|
||||||
@@ -442,5 +460,53 @@ dir_behind`, `y_to_row`, `char_dx_forward`, `get_tile_div_mod(_m7)`,
|
|||||||
`bank_local_data_pattern`, `sprinter_memory_modes`, `memory_modes_implemented`,
|
`bank_local_data_pattern`, `sprinter_memory_modes`, `memory_modes_implemented`,
|
||||||
`sprinter_emm_budget`, `mame_mcp_bridge`, `avoid_32bit_arith_z80`,
|
`sprinter_emm_budget`, `mame_mcp_bridge`, `avoid_32bit_arith_z80`,
|
||||||
`libc_one_function_per_module`.
|
`libc_one_function_per_module`.
|
||||||
- `applications/PoP/docs/size_optimization_plan.md` — v1 (замер 2026-07-21,
|
- `applications/PoP/roomtest/TASKS.md` — что из этого берётся в работу сейчас.
|
||||||
раздел §8 про скорость отрисовки актуален и не дублируется здесь).
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.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.md`) длинные DI-окна и есть причина потери байт
|
||||||
|
клавиатуры. Батчинг эту проблему УХУДШИТ, если делать его вслепую.
|
||||||
|
2. **Решётка ворот одним спрайтом** — `draw_gate_back` рисует бары по одному
|
||||||
|
(до 7 блитов). Сгенерировать в атласе «столб решётки» и выводить одним
|
||||||
|
`gfx_blit_part` с обрезкой по фазе `gate_bot_y & 7`: 7 блитов → 1.
|
||||||
|
3. **Не перерисовывать статичные части шва** — грань ворот, пол и кромка при
|
||||||
|
анимации решётки не меняются (см. OPT-1 в `../roomtest/bug_list.md`).
|
||||||
|
4. **T-1 / T-2** (`../roomtest/bug_list.md`) — перерисовка пик по причине и
|
||||||
|
idle-skip Кида: самый большой оставшийся резерв, потому что убирает работу
|
||||||
|
целиком, а не удешевляет её.
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
# План: от одного уровня к нескольким (загрузка, переходы, тайлсеты)
|
||||||
|
|
||||||
|
Статус: план, 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.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 | обычный | — |
|
||||||
|
| 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)
|
||||||
|
|
||||||
|
Порядок именно такой; каждый пункт проверяем в 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+)
|
||||||
|
|
||||||
|
**Ассеты.** `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,143 +0,0 @@
|
|||||||
# Loose floors (проваливающиеся полы) — план порта
|
|
||||||
|
|
||||||
Разбор SDLPoP (seg007 loose/trob/mob, seg008 draw_loose). ПЛАН, ещё не
|
|
||||||
реализовано. Тайл в комнате 1: `[2,6] = 0x0B = tiles_11_loose`.
|
|
||||||
|
|
||||||
## 1. Хранение состояния
|
|
||||||
|
|
||||||
- **Тип тайла**: `curr_room_tiles[tilepos] & 0x1F == 11` (tiles_11_loose).
|
|
||||||
Бит `0x20` = «solid» loose (авто-падающий вариант, ур.13 — от шага НЕ
|
|
||||||
падает). После падения тайл → `0` (tiles_0_empty).
|
|
||||||
- **Модификатор** `curr_room_modif[tilepos]` = состояние анимации:
|
|
||||||
- `0` — покой (обычный loose-пол);
|
|
||||||
- `0x80..0x83` — **трясётся** (бит7); за ~4 кадра затухает обратно в 0;
|
|
||||||
- `1..11` — **обратный отсчёт до падения** (на нём что-то стоит);
|
|
||||||
достигает `loose_floor_delay = 11` → падает.
|
|
||||||
|
|
||||||
## 2. Анимация тряски (shake) — когда включается
|
|
||||||
|
|
||||||
- **Триггер = do_knock** (seg007:0FE0): на ЖЁСТКОМ приземлении в кадрах
|
|
||||||
посадки играет `SEQ_KNOCK_DOWN` → взводит `knock` → `check_knock()` →
|
|
||||||
`do_knock(room, curr_row − (knock>0))`.
|
|
||||||
- `do_knock(room, row)`: по всем колонкам ряда — если тайл loose →
|
|
||||||
`loose_make_shake()`.
|
|
||||||
- `loose_make_shake()` (seg007:0FB4): если `modif==0` (и не ур.13) →
|
|
||||||
`modif = 0x80`, `add_trob(type 1)`.
|
|
||||||
- **Отсюда кейс пользователя**: Kid падает/приземляется на `[2,4]` →
|
|
||||||
do_knock трясёт ВСЕ loose-тайлы ряда 2 → `[2,6]` трясётся. (Через
|
|
||||||
knock-смещение ряда может задеть и соседний ряд.)
|
|
||||||
- `animate_loose` (кадрово): `++modif`; при бите7 трясёт до `>=0x84` →
|
|
||||||
сброс в 0, `trob.type=-1`. `loose_shake()` играет звук
|
|
||||||
(sound 20/21/22) по таблице `loose_sound[]`.
|
|
||||||
|
|
||||||
## 3. Анимация падения (fall) — когда включается
|
|
||||||
|
|
||||||
- **Триггер = make_loose_fall(1)** (seg007:0EF6), вызывается когда:
|
|
||||||
- Kid СТОИТ на loose-тайле — `check_press()` (seg006): кадр с
|
|
||||||
FRAME_NEEDS_FLOOR над loose → make_loose_fall(1);
|
|
||||||
- зацеп/подтягивание на loose (`check_grab`, `check_jump_up`);
|
|
||||||
- пробой сверху: кадр 79 (jumphang) над loose → make_loose_fall(1);
|
|
||||||
- авто-падающие (ур.13) — `make_loose_fall(-(prandom&0x0F))`.
|
|
||||||
- `make_loose_fall(modifier)`: если НЕ solid (`tiles & 0x20 == 0`) и
|
|
||||||
`(sbyte)modif <= 0` → `modif = modifier`, `add_trob(type 0)`.
|
|
||||||
- `animate_loose`: `++modif`; когда `modif >= 11` (loose_floor_delay) →
|
|
||||||
`remove_loose()` (тайл → empty) + `add_mob()` (спавн падающего куска).
|
|
||||||
|
|
||||||
## 4. Падающий кусок (mob)
|
|
||||||
|
|
||||||
- `add_mob()` кладёт `curmob` в `mobs[]` (до 14). `do_mobs()` каждый
|
|
||||||
кадр: `move_mob()` (гравитация, y растёт) + `check_loose_fall_on_kid()`
|
|
||||||
(урон Киду/страже, если попал).
|
|
||||||
- Приземление куска → тайл под ним `curr_room_tiles[...] = tiles_14_debris`
|
|
||||||
(seg007 move_mob:1053). Т.е. **loose(11) упал → сверху empty(0), снизу
|
|
||||||
debris(14)**.
|
|
||||||
|
|
||||||
## 5. Отрисовка по статусу
|
|
||||||
|
|
||||||
- Куски тайла: `loose_fram_left[]={41,69,41,70,70,41,41,41,70,70,70,0}`,
|
|
||||||
`loose_fram_right[]={42,71,...}`, `loose_fram_bottom[]={43,73,...}`
|
|
||||||
(env-спрайты, seg008:518/596/608).
|
|
||||||
- Индекс кадра = `get_loose_frame(modifier)` (seg008): `0` = ровный
|
|
||||||
(41/42/43); `1..10` = дрожащие варианты (69–74); при бите7/большой
|
|
||||||
задержке — низкие индексы.
|
|
||||||
- **До падения**: рисуем loose с `get_loose_frame(modif)` (0 = ровно,
|
|
||||||
иначе колеблется). **После**: сверху empty, снизу debris(14) — обычная
|
|
||||||
статическая отрисовка (у нас уже есть tile 0x0E/14 debris в tile_table).
|
|
||||||
|
|
||||||
## 6. Что нужно в нашем движке (сейчас НЕТ)
|
|
||||||
|
|
||||||
Наш `pop_bg` рисует комнату СТАТИЧЕСКИ один раз. Loose-полы требуют
|
|
||||||
**динамического тайлового слоя**:
|
|
||||||
1. **Массив модификаторов** `room_modif[30]` (у нас есть `bg[30]` — можно
|
|
||||||
переиспользовать/рядом) — состояние каждого тайла.
|
|
||||||
2. **Очередь trob** (список анимируемых тайлов) + `animate_loose` пер-кадр
|
|
||||||
→ перерисовка ТОЛЬКО изменившихся тайлов (как heal-прямоугольник Kid).
|
|
||||||
3. **make_loose_fall / do_knock / loose_make_shake** — триггеры (из
|
|
||||||
физики Kid: приземление→knock, стойка на loose→fall).
|
|
||||||
4. **mob-система** (падающий кусок): минимум 1–2 mob'а, гравитация,
|
|
||||||
приземление → debris. Урон Киду (`check_loose_fall_on_kid`) — можно
|
|
||||||
Фазой 2.
|
|
||||||
5. **Перерисовка тайла**: `draw_tile(row,col)` у нас уже умеет loose
|
|
||||||
(`code==11`, `loose_fram_*` в env) — нужно вызывать его выборочно с
|
|
||||||
текущим модификатором (сейчас draw_tile берёт статический bg).
|
|
||||||
|
|
||||||
**Порядок реализации (предложение):**
|
|
||||||
- L1: room_modif[] + выборочная перерисовка тайла по модификатору
|
|
||||||
(draw_loose с get_loose_frame) — статика→динамика одного тайла.
|
|
||||||
- L2: trob-очередь + animate_loose (тряска по do_knock на приземлении).
|
|
||||||
- L3: make_loose_fall (стойка на loose) + отсчёт + remove → empty.
|
|
||||||
- L4: mob (падающий кусок → debris снизу).
|
|
||||||
- L5: урон Киду от падающего куска.
|
|
||||||
|
|
||||||
## Конкретика из SDLPoP (сверено 2026-07-18, готово к реализации)
|
|
||||||
|
|
||||||
Таблицы (seg008.c), индекс = `get_loose_frame(modif)`:
|
|
||||||
- `loose_fram_left[] = {41,69,41,70,70,41,41,41,70,70,70,0}`
|
|
||||||
- `loose_fram_right[] = {42,71,42,72,72,42,42,42,72,72,72,0}`
|
|
||||||
- `loose_fram_bottom[]= {43,73,43,74,74,43,43,43,74,74,74,0}`
|
|
||||||
- `get_loose_frame(m)`: если `(m&0x80)` (или delay>11) → `m&=0x7F; if(m>10) return 1;` → `return m;`
|
|
||||||
- `y_loose_land[] = {2,65,128,191,254}` (mob), `loose_floor_delay = 11`.
|
|
||||||
|
|
||||||
Триггеры (call-sites):
|
|
||||||
- **make_loose_fall(modifier=1)** — из `check_press()` (seg006): когда Kid
|
|
||||||
СТОИТ на тайле (FRAME_NEEDS_FLOOR, action < hang_climb / turn / bumped) и
|
|
||||||
`get_tile_at_char()==11`; ИЛИ `frame==79` (прыжок вверх) и
|
|
||||||
`get_tile_above_char()==11` (пробой сверху). `tile_is_floor(11)==1` —
|
|
||||||
Kid стоит на loose (start_fall НЕ зовётся). Тело:
|
|
||||||
`if(!(tile&0x20) && (sbyte)modif<=0){ modif=modifier; add_trob(type0); }`
|
|
||||||
- **do_knock(row)** — на ЖЁСТКОМ приземлении (SEQ_KNOCK_DOWN→check_knock,
|
|
||||||
seg003): по всем колонкам ряда `if(tile==11) loose_make_shake()`
|
|
||||||
(`if(modif==0){ modif=0x80; add_trob(type1); }`).
|
|
||||||
- **animate_loose** (каждый кадр, seg007:816): `++modif`; если `&0x80`
|
|
||||||
(тряска): `if(modif>=0x84){modif=0; trob=-1;}`; иначе (отсчёт):
|
|
||||||
`if(modif>=11){ remove_loose(tile→0); trob=-1; add_mob(); } else shake`.
|
|
||||||
|
|
||||||
## Интеграция в наш движок (roomtest) — подход
|
|
||||||
|
|
||||||
Наш движок ПЕКЁТ комнату один раз (двойной буфер: своя ОЗУ-копия на
|
|
||||||
страницу). Loose требует динамики + общего состояния pop_map↔pop_bg:
|
|
||||||
1. **Общая МУТАБЕЛЬНАЯ копия комнаты**: roomtest.c держит `uint8_t
|
|
||||||
room_fg[30]` (копия room1_fg) и передаёт ОДИН указатель и в
|
|
||||||
`pop_room_draw`, и в `pop_map_set` → мутации loose видны обоим.
|
|
||||||
(Сейчас g_fg — `const`; сделать неконстантным.)
|
|
||||||
2. **Состояние**: `uint8_t pop_loose_modif[30]` (0 / 0x80.. / 1..11).
|
|
||||||
3. **Модель** (pop_map): `check_press()` в `pop_phys_tick` (make_loose_fall
|
|
||||||
при стоянии на 11), `animate` каждый кадр.
|
|
||||||
4. **Перерисовка** (pop_bg, на back-странице каждый кадр):
|
|
||||||
- тряска/отсчёт (код всё ещё 11): `gfx_heal(tile rect)` (вернуть
|
|
||||||
печёный фон) + нарисовать loose-кадр (left/right/bottom по
|
|
||||||
get_loose_frame) банком SPRITE;
|
|
||||||
- падение (11→0): mutate `g_fg[pos]=0` + «запечь пустоту» на ОБЕИХ
|
|
||||||
страницах (bake-счётчик 2: чёрный bar + draw_tile empty банком NORMAL
|
|
||||||
на текущей странице 2 кадра подряд) → дальше heal показывает пусто.
|
|
||||||
5. **L4 mob**: падающий кусок → debris(14) снизу (y_loose_land); пока
|
|
||||||
отложено — упавший loose = пусто. **L5** урон — позже.
|
|
||||||
|
|
||||||
Риск: перерисовка динамического тайла в двойном буфере (per-page heal +
|
|
||||||
bake) — единственное тонкое место; остальное — прямой порт логики выше.
|
|
||||||
|
|
||||||
## Связанные
|
|
||||||
|
|
||||||
Триггеры завязаны на физику Kid ([[pop_hang_state]] check_press/check_grab,
|
|
||||||
приземление land/SEQ_KNOCK_DOWN). Отрисовка — [[pop_fore_layer]] /
|
|
||||||
[[pop_background_strategy]] (draw_tile уже знает loose_fram_*).
|
|
||||||
@@ -1,5 +1,21 @@
|
|||||||
# PoP roomtest — модель `kid_room ≠ drawn_room` (баг #4)
|
# 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/bug_list.md`,
|
||||||
|
> BUG-SEAM-PINGPONG).
|
||||||
|
>
|
||||||
|
> **Зачем документ остаётся.** Полная straddle-модель понадобится для:
|
||||||
|
> (а) читов осмотра соседних комнат `H/J/U/N` (`levels_plan.md` §4),
|
||||||
|
> (б) сцен, где Кид и страж в разных комнатах кадра, (в) остатков окклюзии у
|
||||||
|
> шва (S4). Брать из `../roomtest/TASKS.md`, когда дойдёт очередь.
|
||||||
|
|
||||||
Порт straddle-модели SDLPoP: персонаж может находиться в СОСЕДНЕЙ комнате,
|
Порт straddle-модели SDLPoP: персонаж может находиться в СОСЕДНЕЙ комнате,
|
||||||
пока на экране ещё ТЕКУЩАЯ (drawn_room). Источник истины — SDLPoP.
|
пока на экране ещё ТЕКУЩАЯ (drawn_room). Источник истины — SDLPoP.
|
||||||
|
|
||||||
|
|||||||
@@ -1,335 +0,0 @@
|
|||||||
# roomtest — план оптимизации по размеру + переход на huge/banking
|
|
||||||
|
|
||||||
Статус: **план для отдельной сессии** (2026-07-21). Документ самодостаточный
|
|
||||||
(рассчитан на старт с пустого контекста). Цель — освободить место: сейчас
|
|
||||||
`applications/PoP/roomtest` в режиме `small` почти упёрся в потолок 32 КБ.
|
|
||||||
|
|
||||||
Правило проекта (`applications/PoP/CLAUDE.md`): механику/раскладку памяти
|
|
||||||
сверять с исходником и с memory (`sprinter_memory_modes`, `sdcc_banking`,
|
|
||||||
`bank_local_data_pattern`, `pop_banking_architecture`). Перед оптимизацией —
|
|
||||||
`make size-check`-подобный замер до/после (здесь — руками по `.map`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 0. Как мерить
|
|
||||||
|
|
||||||
- Сборка: `cd applications/PoP/roomtest && make roomtest.exe` (режим `small`,
|
|
||||||
`--gfx 256`). Карта символов — `.sprinter-cc-roomtest/roomtest.map`
|
|
||||||
(адреса сдвигаются при каждой пересборке!).
|
|
||||||
- Размеры областей — из `.map` (`_CODE`, `_DATA`, `_BSS`).
|
|
||||||
- Вклад модулей в `_CODE` — атрибуция диапазонов между символами по модулю
|
|
||||||
(скрипт-однострочник на python в истории; группировать символы `.map` по
|
|
||||||
3-й колонке-модулю и суммировать `addr[i+1]-addr[i]`).
|
|
||||||
- MAME-проверка после изменений раскладки ОБЯЗАТЕЛЬНА (режимы памяти —
|
|
||||||
типовой источник «молча ломается», см. `sprinter_memory_modes`).
|
|
||||||
|
|
||||||
## 1. ТЕКУЩЕЕ СОСТОЯНИЕ (замер 2026-07-21)
|
|
||||||
|
|
||||||
Режим `small` = единое пространство **W1+W2 = 0x4000..0xBFFF (32 КБ)**; CODE с
|
|
||||||
0x4100, DATA/BSS/heap цепляются ЗА CODE автоматически (`--data-loc 0` =
|
|
||||||
linker chains), стек — вверху W2.
|
|
||||||
|
|
||||||
| Область | Размер | Диапазон |
|
|
||||||
|---------|--------|----------|
|
|
||||||
| `_CODE` | ~27 250 Б (0x6A6F) | 0x4100–0xAB6F |
|
|
||||||
| `_HOME` | 227 Б | 0xAB6F |
|
|
||||||
| `_DATA` | 3 449 Б (0x0D79) | 0xAC78–0xB9F1 |
|
|
||||||
| `_BSS` | 290 Б | |
|
|
||||||
|
|
||||||
**Образ ≈ 31.2 КБ; до верха W2 (0xBFFF) остаётся ≈ 1.3 КБ на кучу+стек.**
|
|
||||||
Куча в roomtest почти не используется (атласы/уровень — в EMM-страницах),
|
|
||||||
но запас критично мал.
|
|
||||||
|
|
||||||
### Вклад модулей в _CODE (по .map, приблизительно)
|
|
||||||
```
|
|
||||||
7003 pop_bg (вся отрисовка тайлов/слоёв/wall_pattern)
|
|
||||||
6326 pop_kid (из них ~3745 Б — СТАТ. ТАБЛИЦЫ kid_data.h, см. ниже)
|
|
||||||
3762 pop_map (коллизия/физика/пики)
|
|
||||||
1455 pop_trob (кнопки/ворота/пики-каркас)
|
|
||||||
1224 pop_level (загрузка уровня, doorlink)
|
|
||||||
914 roomtest (главный цикл)
|
|
||||||
~7000 libc/libbgi (gfx_blit*, atlas_load, kbd_raw, open/read, irq, div/mul…)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Крупные СТАТИЧЕСКИЕ данные (сейчас в _CODE как `const`)
|
|
||||||
- **`kid_data.h` — самый большой кусок, ~3.7 КБ**, живёт в _CODE (атрибутируется
|
|
||||||
pop_kid):
|
|
||||||
- `kid_seqtbl[2310]` — байткод последовательностей (play_seq).
|
|
||||||
- `kid_frames[241]` × 5 Б = 1205 Б — таблица кадров (image,dx,dy,flags,sword).
|
|
||||||
- `kid_seq_off[115]` × 2 Б = 230 Б — смещения seq.
|
|
||||||
- `pop_bg`: `tile_table[31]`×12 = 372 Б + ~20 мелких const-таблиц (COL_XH,
|
|
||||||
WALL_FRAM_*, SPIKES_FRAM_{RIGHT,LEFT,FORE}, LOOSE_FRAM_*, DOOR_FRAM_SLICE,
|
|
||||||
BLUELINE_*, LPOS/RPOS, FLOOR_LEFT_OVERLAY) — суммарно ~0.5–0.7 КБ.
|
|
||||||
- `pop_map`: `x_bump[20]`, `y_land[5]`, `wall_dl/dr`, `dir_front/behind` — ~100 Б.
|
|
||||||
- В `_DATA` (W2, не CODE): `room_modif[24][30]`=720 Б + копии LINKLOC/LINKMAP=512 Б
|
|
||||||
(pop_trob/pop_level) + рабочие массивы roomtest.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. ПУТЬ A — оптимизация КОДА (без смены модели)
|
|
||||||
|
|
||||||
1. **Компиляторные флаги** (`bin/sprinter-cc`): попробовать `--opt-code-size`
|
|
||||||
у SDCC и подобрать `--max-allocs` (сейчас дефолт 100000; меньше = мельче код,
|
|
||||||
но медленнее компиляция; см. `mdview2_size_budget` — там `--max-allocs`
|
|
||||||
давал −1.4 КБ). Замерить каждый модуль отдельно.
|
|
||||||
2. **Дедуп подстановки нажатой кнопки**: логика `opener→floor / closer→stuck`
|
|
||||||
по таймеру связи ПРОДУБЛИРОВАНА в `draw_tile` и `fore_tile` (pop_bg.c).
|
|
||||||
Вынести в `static inline`/helper `subst_pressed_button(code,mod)`.
|
|
||||||
3. **wall_pattern / prandom** (pop_bg): 32-битный LCG (`unsigned long`) —
|
|
||||||
пользователь не любит 32-бит (см. `avoid_32bit_arith_z80`); но это PRNG
|
|
||||||
оригинала (нужен для совпадения раскладки стен) — трогать осторожно, только
|
|
||||||
если найдётся 16-битный эквивалент, дающий ТУ ЖЕ последовательность.
|
|
||||||
4. **Ревизия дублей**: `y_to_row` определён в pop_bg И pop_map; мелкие
|
|
||||||
геометрические хелперы дублируются — свести в один internal-модуль.
|
|
||||||
5. `/simplify`-проход по последним правкам Фазы B (pop_trob/pop_bg).
|
|
||||||
|
|
||||||
Ожидаемый выигрыш пути A: единицы–первые сотни байт на пункт; в сумме,
|
|
||||||
оптимистично, ~1–2 КБ. Недостаточно как единственная мера.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. ПУТЬ B — вынос СТАТ. ДАННЫХ в EMM-страницы (с атласами / с level)
|
|
||||||
|
|
||||||
**Идея (по замечанию пользователя):** EMM-страницы атласов и уровня
|
|
||||||
использованы лишь частично (страница 16 КБ, данных меньше), в «хвосте» —
|
|
||||||
свободное место. Часть `const`-таблиц можно хранить ТАМ, а не в _CODE/_DATA,
|
|
||||||
если таблица читается ИМЕННО ТОГДА, когда нужная страница уже в W0.
|
|
||||||
|
|
||||||
**Механика W0:** атласы блитятся из W0 (`_gfx_w0_state`: `_gfx_w0_cur` —
|
|
||||||
спрайт-страница в W0; ISR-стаб `_gfx_w0_isr` возвращает её после прерывания).
|
|
||||||
Уровень (pop_level) маппит свою страницу в W0 на время извлечения
|
|
||||||
(`gfx_w0_map`/`gfx_w0_unmap`). → пока страница в W0, CPU может читать и
|
|
||||||
данные из неё по адресам 0x0000..0x3FFF.
|
|
||||||
|
|
||||||
**Категоризация таблиц по W0-контексту (задача сессии — уточнить по каждой):**
|
|
||||||
|
|
||||||
- **(a) Читается, когда в W0 АТЛАС** → хранить в свободном хвосте атлас-страницы.
|
|
||||||
Кандидаты — таблицы, которые нужны В МОМЕНТ блита конкретного атласа.
|
|
||||||
ГРАБЛИ: `draw_tile` читает `tile_table`/`COL_XH` ДО блита (чтобы решить, какой
|
|
||||||
спрайт/куда) — в этот момент в W0 может быть ДРУГАЯ страница (DSS/предыдущий
|
|
||||||
атлас). Т.е. большинство draw-таблиц читаются ВНЕ W0-атлас-контекста →
|
|
||||||
«в лоб» не переносятся. Нужен аудит КАЖДОГО чтения: гарантирована ли нужная
|
|
||||||
страница в W0 в этот тик.
|
|
||||||
- **(b) Читается, когда в W0 LEVEL** → хранить с уровнем (в его странице; там
|
|
||||||
~13.7 КБ свободно из 16). Кандидаты: константы декода doorlink, разбор
|
|
||||||
комнат — всё, что pop_level делает под `gfx_w0_map(lvl_page)`.
|
|
||||||
- **(c) Нужна и там, и там** → дублировать в обеих страницах ЛИБО оставить
|
|
||||||
резидентной (если дубли дороже экономии).
|
|
||||||
- **(d) Читается в чистой ЛОГИКЕ (W0 не важен)** → перенос требует ЯВНОГО
|
|
||||||
`gfx_w0_map` на каждое чтение (дорого, особенно в горячих циклах) → как
|
|
||||||
правило оставить резидентной.
|
|
||||||
|
|
||||||
**Отдельно `kid_data.h` (3.7 КБ — самый жирный кандидат):**
|
|
||||||
- `kid_frames`/`kid_seqtbl` читаются в `play_seq` (ЧИСТАЯ логика, каждый тик) И
|
|
||||||
в `kid_draw` (блит из kid-атласа, kid-страница в W0). Т.е. частично (a),
|
|
||||||
частично (d). Перенос всей таблицы в kid-атлас-страницу заставит `play_seq`
|
|
||||||
делать `gfx_w0_map` на каждый шаг байткода → замерить стоимость (может убить
|
|
||||||
бюджет спрайтов, см. `sprite_engine_perf`). Вариант: держать в EMM отдельной
|
|
||||||
страницей данных Kid и маппить один раз на кадр вокруг kid_tick+kid_draw.
|
|
||||||
- Это самый большой одиночный выигрыш (−3.7 КБ из _CODE), но и самый рискованный
|
|
||||||
по скорости — приоритетный к ПРОТОТИПИРОВАНИЮ и замеру.
|
|
||||||
|
|
||||||
**Паттерн переноса writable/const данных в банк/страницу:** см. memory
|
|
||||||
`bank_local_data_pattern` (--codeseg/--constseg/--dataseg BANKn + trampoline-fix
|
|
||||||
+ mkexe -p 0) и `sdcc_static_storage_gotcha`.
|
|
||||||
|
|
||||||
### 3.1 Свободное место в страницах (замер 2026-07-21, страница = 16384 Б)
|
|
||||||
```
|
|
||||||
BG-атласы: размер свободно
|
|
||||||
pop_env0.atl 10578 5806
|
|
||||||
pop_env1.atl 12449 3935 <- САМАЯ ТЕСНАЯ из bg
|
|
||||||
pop_env2.atl 10798 5586
|
|
||||||
pop_env3.atl 5032 11352 <- много места
|
|
||||||
pop_env4.atl 8498 7886
|
|
||||||
pop_wall.atl 11543 4841
|
|
||||||
pop_fore.atl 7763 8621
|
|
||||||
Kid-атласы (28 стр): free min=6161 max=15452 avg=9722
|
|
||||||
Level (res2001.bin): данные 2305, свободно ~13823 (16384 − 0x100 стаб − 2305)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Выводы по вместимости:**
|
|
||||||
- **Макс. данных в ОДНОМ атлас-банке = свободный хвост ЭТОЙ страницы** (см.
|
|
||||||
таблицу). Связывающее ограничение — самая тесная нужная страница (env1 =
|
|
||||||
3935 Б; не перегружать её).
|
|
||||||
- Если страница будет маппиться в **W0** — минус ~0x100 Б на ISR-стаб (как
|
|
||||||
level). Атлас-страницы стаб УЖЕ содержат (atlas_load патчит) → данные класть
|
|
||||||
в хвост ПОСЛЕ атласа.
|
|
||||||
- **`kid_data.h` (3.7 КБ) влезает в kid-страницу** (min free 6161) или в
|
|
||||||
отдельную выделенную страницу данных Kid — предпочтительно отдельную (маппить
|
|
||||||
раз на кадр, не конфликтуя с kid-атласами блита).
|
|
||||||
- **Level-таблицы** — вагон места в level-странице (~13.8 КБ).
|
|
||||||
- **BG draw-таблицы** (~0.7 КБ) влезут в env3/fore/env4 (много free), НО см.
|
|
||||||
граблю W0-контекста в §3(a) — читаются ли они, когда нужная страница в W0.
|
|
||||||
- **Выделенная страница ТОЛЬКО под данные** (не делить с атласом) = до ~16 КБ
|
|
||||||
(−0x100 стаб при W0-маппинге). EMM-бюджет это позволяет (см.
|
|
||||||
`sprinter_emm_budget`: 215/3440 КБ free на старте).
|
|
||||||
- **Принудительно уменьшать макс. атлас (репак мельче) — КРАЙНИЙ случай:** это
|
|
||||||
резко поднимет число атлас-банков (сейчас 5 env-страниц адресуются как id>>5;
|
|
||||||
дробление ломает эту адресацию и множит страницы). Сначала использовать
|
|
||||||
СУЩЕСТВУЮЩИЙ свободный хвост и отдельные data-страницы.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. ПУТЬ C — переход на huge (banked code)
|
|
||||||
|
|
||||||
### 4.1 Что такое huge сейчас (`bin/sprinter-cc`, `runtime/crt0_banked`)
|
|
||||||
- `--memory huge`: `MODE_CODE_LOC=0x4100`, **`MODE_DATA_LOC=0x8000` (ФИКС.)**,
|
|
||||||
banked code в W3. crt0_banked, как crt0_small, авто-детектит W2.
|
|
||||||
Помечено `[TODO]` — не обкатано.
|
|
||||||
- Отличие от small: small цепляет DATA сразу за CODE (`--data-loc 0`); huge
|
|
||||||
ФИКСИРУЕТ DATA на 0x8000.
|
|
||||||
|
|
||||||
### 4.2 ТРЕБОВАНИЕ (по пользователю): huge должен переносить DATA динамически
|
|
||||||
Сейчас huge жёстко кладёт DATA на 0x8000. Если РЕЗИДЕНТНЫЙ CODE вылезет за
|
|
||||||
0x8000 (W1 = только 0x4000..0x7FFF ≈ 16 КБ; резидент > 16 КБ лезет в W2) →
|
|
||||||
коллизия с DATA. **Надо научить huge класть DATA динамически ЗА резидентным
|
|
||||||
CODE (как small: `--data-loc 0` + crt0 считает старт), а не на фикс 0x8000.**
|
|
||||||
Тогда huge = «small-раскладка резидента (W1+W2, DATA за CODE) + ДОП. код в
|
|
||||||
банках W3». Это первый пункт работ по huge.
|
|
||||||
|
|
||||||
### 4.3 КОНФЛИКТ: графика тоже хочет W3 (ключевой риск)
|
|
||||||
`pop_banking_architecture` прямо говорит: **графику нельзя в W3** (блиты/атласы
|
|
||||||
используют окна; см. §4.5). Поэтому в банки W3 можно выносить ТОЛЬКО
|
|
||||||
НЕ-графические блоки, и такой банк НЕ должен во время своего исполнения держать
|
|
||||||
графику в W3. Если W3-банкованная функция ЗОВЁТ графику (которой нужен W3),
|
|
||||||
трамплин обязан сохранить/восстановить банк вокруг вызова (проверить, что
|
|
||||||
banking-ABI это делает — `sdcc_banking`). Альтернатива без этого риска —
|
|
||||||
**big + BANK_W1** (банк кода в W1, не W3), рекомендованная в
|
|
||||||
`pop_banking_architecture` именно из-за W3-графики. Сессия должна выбрать:
|
|
||||||
huge(W3) с аккуратным save/restore ИЛИ big(BANK_W1).
|
|
||||||
|
|
||||||
### 4.4 Какие блоки МОЖНО вынести (не работают с графикой напрямую)
|
|
||||||
Замер graphics-ref по модулям (grep `gfx_|blit|env_b|wall_b|fore_b|setfillstyle|
|
|
||||||
bar(|GFX_BANK|initgraph`):
|
|
||||||
```
|
|
||||||
pop_bg.c : 83 — РЕЗИДЕНТ (вся отрисовка)
|
|
||||||
roomtest.c : 23 — РЕЗИДЕНТ (главный цикл + флип страниц)
|
|
||||||
pop_level.c : 17 — использует gfx_w0_map (W0, не W3-блиты) — ПОГРАНИЧНЫЙ
|
|
||||||
pop_kid.c : 12 — kid_draw = графика; НО play_seq — чистая логика (можно split)
|
|
||||||
pop_ctrl.c : 0 — КАНДИДАТ В БАНК (ввод/диспетчер control)
|
|
||||||
pop_map.c : 0 — КАНДИДАТ В БАНК (коллизия/физика, ~3.8 КБ) — лучший по объёму
|
|
||||||
pop_trob.c : 0 — КАНДИДАТ В БАНК (кнопки/ворота/пики-логика)
|
|
||||||
```
|
|
||||||
- **Лучшие кандидаты в W3-банк(и): pop_map + pop_trob + pop_ctrl** (нет прямой
|
|
||||||
графики; вместе ~5.3 КБ CODE). Освобождают резидент → он влезает в W1.
|
|
||||||
- **Осторожно с межбанковыми вызовами:** pop_map/pop_trob ЗОВУТ pop_bg
|
|
||||||
(перерисовка loose/пик/кнопок/шва) и pop_kid (play_seq/kid_set_seq). Это
|
|
||||||
кросс-банк вызовы через трамплин (`sdcc_banking`: стек +3 байта, виртуальный
|
|
||||||
24-битный адрес). Правило `pop_banking_architecture`: «один файл = один банк
|
|
||||||
= прямые вызовы», main резидентен. Проверить, что трамплин сохраняет W3
|
|
||||||
вокруг вызова в графический pop_bg (см. §4.3).
|
|
||||||
- **pop_kid split** (по желанию): вынести play_seq/seqtbl-интерпретатор
|
|
||||||
(логика + таблицы kid_data.h) в банк, оставить kid_draw/kid_heal резидентными.
|
|
||||||
Даёт и −код, и −данные из резидента, но требует аккуратного разделения TU
|
|
||||||
(1 функция = 1 модуль, см. `libc_one_function_per_module`).
|
|
||||||
- **pop_level: пограничный** — не блитит, но маппит уровень в W0; банковать
|
|
||||||
можно, если W0-логика совместима с трамплином (проверить ISR-стаб взаимодействие).
|
|
||||||
|
|
||||||
### 4.5 Почему графику нельзя в W3 (контекст)
|
|
||||||
Блиттер держит спрайт-страницу атласа в **W0** (`_gfx_w0_state`,
|
|
||||||
`_gfx_w0_isr`). Ускоритель/адресация видео — отдельная тема (см.
|
|
||||||
`sprinter_accelerator`, `sprinter_graphics`). W3 в banked-раскладке — окно
|
|
||||||
кода-банка; смешивать с окном, которое графика перемапливает, нельзя без
|
|
||||||
save/restore. Детально — `pop_banking_architecture`, `graphics_constraints`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. РЕКОМЕНДУЕМЫЙ ПОРЯДОК РАБОТ (для след. сессии)
|
|
||||||
|
|
||||||
1. **Замер-базлайн** (CODE/DATA/BSS + per-module) — зафиксировать до.
|
|
||||||
2. **Путь A** дешёвые пункты (флаги, дедуп кнопки, дедуп y_to_row) — быстрый −1..2 КБ.
|
|
||||||
3. **huge §4.2**: научить huge класть DATA динамически (как small) — инфраструктурный
|
|
||||||
пререквизит, без него банкинг не даст гибкости. Обкатать в MAME на текущем
|
|
||||||
резиденте (пока без выноса — просто huge-раскладка = small + пустой W3).
|
|
||||||
4. **huge §4.4**: вынести pop_map (+pop_trob, +pop_ctrl) в W3-банк(и); проверить
|
|
||||||
кросс-банк вызовы в pop_bg (§4.3) в MAME. ЛИБО выбрать big+BANK_W1.
|
|
||||||
5. **Путь B** (по остатку нужды): прототип выноса `kid_data.h` в EMM-страницу
|
|
||||||
Kid с маппингом раз на кадр; замерить скорость (`sprite_engine_perf`).
|
|
||||||
Затем аудит draw-таблиц по W0-контексту (§3 a/b/c/d).
|
|
||||||
|
|
||||||
## 6. Ссылки
|
|
||||||
- `bin/sprinter-cc` (§162+ — резолв memory-mode → CODE_LOC/DATA_LOC).
|
|
||||||
- `runtime/crt0_small.*`, `runtime/crt0_banked.*`, `runtime/bank.s`.
|
|
||||||
- memory: `sprinter_memory_modes`, `memory_modes_implemented`,
|
|
||||||
`setwin2_for_w2_alloc`, `sdcc_banking`, `bank_local_data_pattern`,
|
|
||||||
`pop_banking_architecture`, `avoid_32bit_arith_z80`,
|
|
||||||
`libc_one_function_per_module`, `sprite_engine_perf`, `mdview2_size_budget`.
|
|
||||||
- `applications/PoP/roomtest/bug_list.md` — открытые баги Фазы B (не блокируют
|
|
||||||
оптимизацию, но держать в уме при рефакторе pop_map/pop_bg).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Лишние блиты в горячем пути (добавлено 2026-07-27)
|
|
||||||
|
|
||||||
Найдено при разборе окклюзии по эталону SDLPoP: **наш «передний слой» рисовал
|
|
||||||
спрайты, которых в оригинале там нет** — это и артефакты, и лишняя работа
|
|
||||||
каждый кадр. Исправлено: `fore_tile` (вызывается для КАЖДОГО тайла футпринта
|
|
||||||
Kid, обычно 2–4 за кадр) рисовал ещё и `bottom_id` — переднюю кромку пола; в
|
|
||||||
оригинале `draw_tile_fore` (seg008:690) добавляет только `add_foretable`-часть,
|
|
||||||
а `bottom` идёт через `draw_tile_bottom` в backtable (ПОД персонажем).
|
|
||||||
Итог: −2..4 блита за кадр, `_CODE` −388 Б, ушла «тень» у основания колонны.
|
|
||||||
|
|
||||||
**Что проверить тем же методом (по одному вопросу к каждому месту: а есть ли
|
|
||||||
этот спрайт в оригинале в ЭТОЙ таблице?):**
|
|
||||||
|
|
||||||
1. `pop_room_draw`/`draw_tile` — вызовы на входе в комнату не критичны по
|
|
||||||
скорости, но по ним стоит сверить состав слоёв (backtable vs foretable).
|
|
||||||
2. `overlay_mid_tile` — сейчас точный порт midtable-части `draw_tile2`;
|
|
||||||
проверить, не рисуем ли `base_id` там, где оригинал его не рисует
|
|
||||||
(loose: base=0, потому что кадр плиты идёт через `draw_loose` в backtable).
|
|
||||||
3. `pop_loose_mob_tick` — перерисовка соседнего тайла (`draw_tile(mob_row,
|
|
||||||
mob_col+1)`) КАЖДЫЙ кадр падения: в оригинале это `set_redraw_full` на
|
|
||||||
один кадр; можно ограничить только тайлом, который реально пересекается
|
|
||||||
с куском.
|
|
||||||
4. `pop_ceil_shake_draw` — heal 64×8 + два `draw_tile(-1,·)` на кадр тряски;
|
|
||||||
проверить, нужен ли второй тайл (правую грань loose в полосе потолка
|
|
||||||
оригинал не рисует вовсе — `draw_tile_aboveroom` без `draw_tile_anim_right`).
|
|
||||||
5. `fore_only_tile` для полосы потолка: вызывается для всех колонок габарита,
|
|
||||||
а оригинал (`redraw_needed_above`) — только для колонок с флагом
|
|
||||||
`redraw_frames_above`; сузить до колонок, реально задетых спрайтом.
|
|
||||||
6. `wall_pattern` внутри fore/overlay — тяжёлая (PRNG + до 4 блитов); проверить,
|
|
||||||
не зовём ли её там, где оригинал ограничивается `wall_fram_main`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Скорость отрисовки: замеры и запас (2026-07-27)
|
|
||||||
|
|
||||||
Профилирование в MAME (маркеры в порт 0xFE + `wpiset … totalcycles`, приём из
|
|
||||||
memory `mame_mcp_bridge`). Кадр Sprinter = **430 080 тактов**.
|
|
||||||
|
|
||||||
**Стоимость блита почти НЕ зависит от размера** — платим за проход по цепочке
|
|
||||||
`gfx_blit → gfx_blit_part → _gfx_blit_full` (16-битная арифметика, клип,
|
|
||||||
пересчёт src, нарезка полос >256), а не за пиксели:
|
|
||||||
|
|
||||||
| путь (спрайт 32×3) | тактов |
|
|
||||||
|---|---|
|
|
||||||
| `gfx_blit` (общее ядро, с клипом) | 13 288 |
|
|
||||||
| линейное спрайтовое ядро без клипа (`putsprite` при `gfx_sprite_clip(0)`) | 4 617 |
|
|
||||||
|
|
||||||
Отсюда `draw_tile(0,0)` тайла шва (9 блитов) стоил **183 690 тактов = 43 %
|
|
||||||
кадра**; сам `bar` — только 13 308.
|
|
||||||
|
|
||||||
**СДЕЛАНО (шаг 1):** в libbgi добавлен `gfx_blit_noclip()`
|
|
||||||
(`common/gfx_blit_noclip.c`, прототип в `include/gfx.h`) — блит без клипа в
|
|
||||||
ТЕКУЩЕМ банке через линейное ядро; `pop_bg.blit_b` уходит на него, когда
|
|
||||||
спрайт целиком на экране и не нужен `g_clip_top`. Выигрыш ~2.9× на каждом
|
|
||||||
фоновом блите (подтверждено в MAME).
|
|
||||||
**ВАЖНО:** W3-скобку (`_bgi_begin/_bgi_end`) ставит САМА libbgi — вызывать её
|
|
||||||
из модуля, собранного с `--w3`, нельзя: после `_bgi_begin` окно W3 занято
|
|
||||||
видеобанком и код вызывающего исчезает из адресного пространства (проверено:
|
|
||||||
белый экран).
|
|
||||||
|
|
||||||
**ЗАПАС (шаг 2), когда перестанет хватать бюджета кадра:**
|
|
||||||
|
|
||||||
1. **Батчинг W3-скобки** — одна `_bgi_begin/_bgi_end` на весь `draw_tile`
|
|
||||||
вместо скобки на блит; нужен публичный batch-API в libbgi (как у
|
|
||||||
спрайтового движка). Осторожно: между begin/end стоит `DI` — длинная
|
|
||||||
серия задержит кадровое прерывание.
|
|
||||||
2. **Решётка ворот одним спрайтом** — `draw_gate_back` рисует бары по одному
|
|
||||||
(`env 52`, до 7 блитов). Сгенерировать в атласе «столб решётки» (повтор
|
|
||||||
бара на высоту тайла) и выводить одним `gfx_blit_part` с обрезкой по фазе
|
|
||||||
`gate_bot_y & 7`: 7 блитов → 1.
|
|
||||||
3. **Не перерисовывать статичные части шва** — грань ворот (env 47, 26×62),
|
|
||||||
пол (41) и кромка (43) при анимации решётки не меняются; если стирать
|
|
||||||
только полосу баров, уйдут ещё 3 блита из 9.
|
|
||||||
4. См. также §7 (лишние блиты, которых нет в оригинале).
|
|
||||||
@@ -11,6 +11,10 @@
|
|||||||
по памяти и не угадывай константы/порядок слоёв. Расхождение с SDLPoP =
|
по памяти и не угадывай константы/порядок слоёв. Расхождение с SDLPoP =
|
||||||
по умолчанию баг у нас.
|
по умолчанию баг у нас.
|
||||||
|
|
||||||
|
**Что в работе сейчас — `TASKS.md`** (доска задач: приоритеты, критерии
|
||||||
|
готовности). Баги — `bug_list.md` (его шапка честно говорит, что список
|
||||||
|
отстал). План следующих уровней — `../docs/levels_plan.md`.
|
||||||
|
|
||||||
## Сборка и запуск
|
## Сборка и запуск
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -39,9 +39,14 @@ ESC выход.
|
|||||||
- `room1_data.h` — карта комнаты 1; `kid_data.h` — данные анимации Kid
|
- `room1_data.h` — карта комнаты 1; `kid_data.h` — данные анимации Kid
|
||||||
(оба генерируются скриптами `../toolchain/`).
|
(оба генерируются скриптами `../toolchain/`).
|
||||||
|
|
||||||
## Статус
|
## Статус (2026-08-01)
|
||||||
|
|
||||||
Готово и проверено в MAME: статический фон комнаты 1; Kid — анимация,
|
Играется весь уровень 1: комнаты и переходы между ними, Kid (анимация,
|
||||||
управление, коллизия/падение, зацеп/подтягивание/спуск, fore-окклюзия
|
управление, коллизия/падение, зацеп/подтягивание/спуск, окклюзия), кнопки и
|
||||||
пола/стены над Kid. В работе: проваливающиеся полы (loose floors) — см.
|
ворота, пики, проваливающиеся полы, дверь уровня, меч и бой, стражи с ИИ,
|
||||||
`../docs/loose_floors_plan.md`.
|
HP и зелья.
|
||||||
|
|
||||||
|
Не сделано: выход в следующий уровень (дверь открывается, но войти в неё
|
||||||
|
нельзя), звук, таймер/HUD времени, сохранения. **Что берём в работу
|
||||||
|
сейчас — [`TASKS.md`](TASKS.md)**; план следующих уровней —
|
||||||
|
[`../docs/levels_plan.md`](../docs/levels_plan.md).
|
||||||
|
|||||||
@@ -0,0 +1,329 @@
|
|||||||
|
# roomtest — доска текущих задач (обновлено 2026-08-01)
|
||||||
|
|
||||||
|
Не список багов (он в `bug_list.md`) и не план фаз (`../docs/PORT_PLAN.md`,
|
||||||
|
`../docs/layout_plan_v2.md`, `../docs/levels_plan.md`), а то, **что берём в
|
||||||
|
работу сейчас и в каком порядке**. Каждая запись: что сделать, почему
|
||||||
|
именно сейчас, чем подтверждать результат.
|
||||||
|
|
||||||
|
Правило проекта в силе: механику сверять с `../SDLPoP/src/` ДО кодинга;
|
||||||
|
диагноз платформы подтверждать артефактом (брейкпоинт/дамп/.asm), а не
|
||||||
|
гипотезой (memory `defer_unexplained_quirks`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P0 — делаем сейчас
|
||||||
|
|
||||||
|
### KBD-1. Shift + стрелки: нажатия теряются — определить причину
|
||||||
|
|
||||||
|
**Симптом (пользователь, 2026-08-01).** Залипаний почти нет, но при
|
||||||
|
УДЕРЖИВАЕМОМ Shift первые один-два нажатия ← дают осторожные шаги, дальше
|
||||||
|
нажатия ← не отрабатываются, пока Shift не отпустишь.
|
||||||
|
|
||||||
|
**Рабочая гипотеза (механизм, а не догадка «что-то с клавиатурой»).**
|
||||||
|
Три известных факта складываются в одну картину:
|
||||||
|
|
||||||
|
1. **PS/2 Set 2, «fake shift».** При зажатом Shift нажатие РАСШИРЕННОЙ
|
||||||
|
клавиши (стрелки — `E0`-коды) обрамляется фиктивным отпусканием/нажатием
|
||||||
|
шифта: нажатие ← шлёт `E0 F0 12` + `E0 6B` = **5 байт** (без шифта было
|
||||||
|
бы 2), отпускание — `E0 F0 6B` + `E0 12` = **5 байт** (было 3). То есть
|
||||||
|
ровно в связке Shift+стрелка трафик удваивается.
|
||||||
|
2. **Приёмный FIFO SIO — 3 байта.** Пачка в 5 байт переживает только то,
|
||||||
|
что мы успеваем вычерпывать её по ходу. Потерянный make стрелки =
|
||||||
|
«нажатие не отработало»; потерянный break = залипание (его лечит
|
||||||
|
`kbd_raw_sync`, но ценой сброса всех немодификаторных клавиш).
|
||||||
|
3. **Импульс IRQ клавиатуры в MAME живёт 32 такта CPU.**
|
||||||
|
`mame/sources/MAME/src/mame/sinclair/sprinter.cpp`: `on_kbd_data()`
|
||||||
|
выставляет `m_irqs->in_set<1>()` НА КАЖДЫЙ принятый байт (то есть старая
|
||||||
|
запись в `docs/TODO.md` «MAME не даёт per-byte INT» — неверна), но тут же
|
||||||
|
заводит `m_irq_off_timer` на 32 такта, а `irq_off()` снимает линию.
|
||||||
|
**Если в эти 32 такта мы под `DI` — прерывание пропало насовсем**, байт
|
||||||
|
остаётся в FIFO до следующего IRQ (следующий байт или кадровый 50 Гц).
|
||||||
|
4. **Наши DI-окна длинные.** Ядра акселератора держат `di` на ВЕСЬ блит
|
||||||
|
(`libbgi/bgi256/_bgi_blit_cols_raw.c:47` — «один DI на весь блит»);
|
||||||
|
порядок цены прохода — 13.6 К тактов (`libbgi/include/gfx.h`), это
|
||||||
|
сотни микросекунд, на порядки больше 32-тактового импульса.
|
||||||
|
|
||||||
|
Отсюда: **обе версии пользователя — про одно и то же.** Логика ввода
|
||||||
|
(`pop_ctrl.c`, порт `read_user_control`/`safe_step`) сверена с SDLPoP и
|
||||||
|
выглядит корректной: `safe_step()` ставит `control_forward = CONTROL_IGNORE`,
|
||||||
|
и это снимается в `read_user_control()` при ОТПУСКАНИИ стрелки — то есть
|
||||||
|
повторные тапы ← при зажатом Shift обязаны работать. Не работают они
|
||||||
|
потому, что до нас не доезжает либо make, либо break стрелки.
|
||||||
|
|
||||||
|
**План проверки — по шагам, каждый даёт артефакт:**
|
||||||
|
|
||||||
|
1. Счётчики в MAME: брейк на `_kbdraw_overrun` (запись) и на ветке
|
||||||
|
`tr_kbd_drain` — сколько overrun'ов за 10 с при «Shift зажат, тапаю ←»
|
||||||
|
против «тапаю ← без Shift». Ожидание по гипотезе: с Shift кратно больше.
|
||||||
|
2. Замер максимального DI-окна кадра: брейкпоинты на `di`/`ei` в
|
||||||
|
`_bgi_blit_cols_raw` + `{printf totalcycles; g}` — получить реальную длину
|
||||||
|
в тактах и в микросекундах.
|
||||||
|
3. **Спайк «блит без DI».** `docs/new/06-accel.md §6.6`: новая прошивка
|
||||||
|
допускает работу акселератора при EI (по приходу прерывания он
|
||||||
|
отключается, по `RETI` включается обратно); старая — нет. Собрать libbgi
|
||||||
|
с убранным `di` в блит/heal-ядрах, прогнать roomtest в MAME: (а) не
|
||||||
|
рушится ли картинка, (б) падает ли счётчик overrun из п.1. Если да —
|
||||||
|
причина подтверждена, и дальше это вопрос «какая прошивка на живом
|
||||||
|
железе» (по умолчанию оставить DI, режим без DI — опцией libbgi).
|
||||||
|
4. **Независимо от п.3 — `kbd_raw_poll()`.** Вычерпывание FIFO ОПРОСОМ
|
||||||
|
(порт `0x19` бит 0 → читать `0x18`, тот же декодер make/break, что в
|
||||||
|
трамплине) из главного цикла 2–4 раза за кадр между фазами `PROF()`.
|
||||||
|
Снимает зависимость от «поймали ли мы импульс IRQ» вообще, стоит сотни
|
||||||
|
тактов, графику не трогает. Реализация: вынести drain-цикл из
|
||||||
|
`libc/irq/_irq_tramp.c` в общий кусок либо продублировать в
|
||||||
|
`libc/kbd/kbd_raw_poll.c`; тело обязано идти под `DI` (гонка с ISR за
|
||||||
|
деструктивное чтение порта 0x18).
|
||||||
|
5. Побочно сюда же играет **T-2 (idle-skip)** из `bug_list.md`: не
|
||||||
|
перерисовывать Кида, пока поза/координаты не менялись, — это минус
|
||||||
|
heal+blit (то есть минус DI-окна) в самых спокойных кадрах, где как раз
|
||||||
|
и тапают Shift+стрелку.
|
||||||
|
6. Только если после 3–4 симптом жив — копать логику
|
||||||
|
`control_shift2`/`CONTROL_IGNORE` против `seg005.c:374..390`.
|
||||||
|
|
||||||
|
**Критерий готовности:** при зажатом Shift десять тапов ← дают десять
|
||||||
|
осторожных шагов (проверка в MAME через `:kbd:ms_naturl:*` напрямую, НЕ
|
||||||
|
через `press_key` — тот дёргает обе клавиатуры, см. `docs/libc-reference.md`
|
||||||
|
`<kbd_raw.h>`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### KBD-1: ЧТО ИЗМЕРЕНО (сессия 2026-08-01) — гипотеза про DI НЕ подтвердилась
|
||||||
|
|
||||||
|
**Методика.** Симптом «нажатие не отработало» переведён в счётчики, чтобы не
|
||||||
|
спорить с глазами. Нажимается **Home** — тоже расширенная клавиша (тот же
|
||||||
|
`E0`-префикс и тот же «fake shift», что у стрелок), но игрой игнорируется,
|
||||||
|
поэтому Кид стоит на месте и рельеф комнаты на результат не влияет.
|
||||||
|
Брейкпоинты с действием `{ b@ADDR = b@ADDR + 1 ; g }` (счёт без остановки
|
||||||
|
машины) в трёх точках: вход клавиатурной ветки трамплина, чтение порта 0x18
|
||||||
|
внутри drain-цикла, запись make-бита для кода `0x6C`. Скратч-байты — хвост
|
||||||
|
`ovr_tile[]` (в этом сценарии не используется).
|
||||||
|
|
||||||
|
**Симптом воспроизведён скриптом:** при зажатом Shift 10 нажатий → до
|
||||||
|
декодера дошло 9 make-байт. Без Shift потерь нет — ровно как сообщил
|
||||||
|
пользователь.
|
||||||
|
|
||||||
|
| Прогон | make дошло / нажато | overrun |
|
||||||
|
|--------|---------------------|---------|
|
||||||
|
| игра идёт, `kbd_raw_poll` ВКЛ | 9 / 10 | 3 |
|
||||||
|
| игра идёт, `kbd_raw_poll` ВЫКЛ (патч `ret` в точке входа) | 9 / 10 | 4 |
|
||||||
|
| игра ЗАМОРОЖЕНА клавишей «1» (блитов нет вообще, значит и длинных DI нет) | **8 / 10** | 6 |
|
||||||
|
|
||||||
|
**Вывод 1: наши DI-окна ни при чём.** В замороженном кадре, где блитов нет
|
||||||
|
и прерывания разрешены практически всё время, потерь НЕ меньше, а больше.
|
||||||
|
|
||||||
|
**Вывод 2: `kbd_raw_poll()` в текущей расстановке бесполезен** — 9/10 и с
|
||||||
|
ним, и без. Причина понятна задним числом: шесть вызовов стоят В ТЕХ ЖЕ
|
||||||
|
точках, где прерывания и так разрешены, то есть добавляют ровно то, что
|
||||||
|
трамплин сделал бы сам. Вызовы из `roomtest.c` убраны; сама функция в libc
|
||||||
|
оставлена — она корректна и нужна как заготовка под «плотный опрос» (см.
|
||||||
|
ниже), но в горячем цикле её держать не за что.
|
||||||
|
|
||||||
|
**Вывод 3 (главный): байт теряется НИЖЕ нашего кода.** Счётчик чтений порта
|
||||||
|
0x18: 5 нажатий Shift+Home должны дать ровно 50 байт (нажатие `E0 F0 12` +
|
||||||
|
`E0 6C`, отпускание `E0 F0 6C` + `E0 12` = по 10 на цикл). Насчитано **49**
|
||||||
|
— и ровно один make потерян. То есть до процессора байт не доехал вообще,
|
||||||
|
декодер тут ни при чём.
|
||||||
|
|
||||||
|
**Вывод 4: прерывание на байт теряется примерно в 44 % случаев.** На тех же
|
||||||
|
49 прочитанных байтах — только **28 входов** в клавиатурную ветку трамплина
|
||||||
|
(1.75 байта за вход). То есть больше сорока процентов импульсов запроса
|
||||||
|
не были обслужены, и байты копятся в трёхбайтовом FIFO вплотную к его
|
||||||
|
потолку; одна неудачная пауза — и байт потерян.
|
||||||
|
|
||||||
|
### KBD-1: ПОТОЛОК ПЛОТНОГО ОПРОСА ИЗМЕРЕН — приём лечит полностью
|
||||||
|
|
||||||
|
`tests/kbdpoll` — программа, которая не делает НИЧЕГО, кроме
|
||||||
|
`kbd_raw_poll()` в бесконечном цикле (ни графики, ни vsync, ни вывода:
|
||||||
|
любая работа разредила бы опрос и испортила замер). Это физический
|
||||||
|
максимум плотности. Тот же счётный метод, те же брейкпоинты-счётчики.
|
||||||
|
|
||||||
|
| Прогон | нажатий | make дошло | байт прочитано / ожидалось |
|
||||||
|
|--------|---------|-----------|-----------------------------|
|
||||||
|
| контроль: Shift зажат 4 с, нажатий нет | 0 | 0 | 0 (Shift сам ничего не шлёт — автоповтора у модификатора нет) |
|
||||||
|
| Shift + Home | **25** | **25** | **250 / 250** |
|
||||||
|
|
||||||
|
**Ни одного потерянного байта.** Для сравнения: в игре при шести вызовах
|
||||||
|
за кадр терялся 1 байт из 50. При такой частоте потерь вероятность
|
||||||
|
случайно получить ноль потерь на 250 байтах ≈ 0.6 %, так что результат не
|
||||||
|
совпадение.
|
||||||
|
|
||||||
|
**Вывод: опрос — рабочее решение, вопрос только в ПЛОТНОСТИ.** Нужно
|
||||||
|
опрашивать примерно раз в 0.5 мс (≈10 000 тактов), а шесть вызовов за
|
||||||
|
60-мс кадр давали один раз в 10 мс — в двадцать раз реже необходимого.
|
||||||
|
|
||||||
|
**Где взять частоту:** логический тик = 60 мс, из них ~18 мс занято
|
||||||
|
работой и **~42 мс процессор простаивает внутри `gfx_wait_vsync`**, опрашивая
|
||||||
|
луч. Опрос там стоит ноль и покрывает две трети периода с запасом по
|
||||||
|
плотности. Остаётся слепым только тело одного accel-блита под DI (до
|
||||||
|
~650 мкс) — разорвать его нельзя (см. «что НЕ делать»).
|
||||||
|
|
||||||
|
**Почему нужна именно такая частота (вопрос «PS/2 же не даёт больше 30
|
||||||
|
нажатий в секунду»).** Частота опроса определяется НЕ темпом нажатий, а
|
||||||
|
темпом байт ВНУТРИ одного нажатия и глубиной FIFO. Одно нажатие при
|
||||||
|
зажатом Shift — это 5 байт подряд (`E0 F0 12`, `E0 6C`), отпускание — ещё 5,
|
||||||
|
и клавиатура выдаёт их со скоростью провода: 11 бит на байт при ~10–16 кГц
|
||||||
|
= ~0.7–1.1 мс на байт. Воронка — 3 байта. Значит между двумя вычерпываниями
|
||||||
|
имеют право прийти максимум два байта, то есть вычерпывать надо не реже чем
|
||||||
|
раз в ~1.5–2 мс (0.5 мс взято с запасом). **Даже ОДНО нажатие в секунду
|
||||||
|
переполнит FIFO**, если в эти несколько миллисекунд его никто не разгребает.
|
||||||
|
Замер это подтверждает: 1.75 байта за одно вычерпывание — уже 58 % ёмкости.
|
||||||
|
В норме разгребает прерывание; опрос понадобился только потому, что ~44 %
|
||||||
|
импульсов здесь теряется.
|
||||||
|
|
||||||
|
**Альтернатива, которая убирает опрос совсем — уменьшить трафик, а не
|
||||||
|
ускорять разгребание.** BIOS `$EA` (`FN_KBD_OUT`, `docs/new/09-input.md`
|
||||||
|
§9.2) шлёт байт НА клавиатуру, то есть ей можно скомандовать:
|
||||||
|
- **Scan Code Set 3** — нет ни «fake shift», ни `E0`-префиксов: make = 1 байт,
|
||||||
|
break = 2. Нажатие с шифтом перестаёт превышать FIFO в принципе.
|
||||||
|
- либо хотя бы отключить typematic (`0xF5`/`0xF7`).
|
||||||
|
|
||||||
|
**Но проверить это в MAME НЕЛЬЗЯ:** в `sprinter.cpp` подключено только
|
||||||
|
направление клавиатура→SIO (`m_kbd->out_data_cb() → rxa_w`); обратный путь
|
||||||
|
(SIO→клавиатура) не разведён вовсе, так что команда просто уйдёт в никуда.
|
||||||
|
Плюс пришлось бы переписать все наши константы кодов под Set 3. Значит это
|
||||||
|
кандидат на «когда дойдём до реального железа», а не на сейчас.
|
||||||
|
|
||||||
|
**Что делать (в порядке зависимостей):**
|
||||||
|
1. Idle-хук в libbgi: `gfx_set_idle_hook(fn)`, вызывается в цикле ожидания
|
||||||
|
луча внутри `gfx_wait_vsync`. Приложение ставит туда `kbd_raw_poll`.
|
||||||
|
Полезен не только нам — любой программе даёт «качать» что-то в ожидании
|
||||||
|
кадра. Осторожно с регистрами: цикл ждёт на BC-таймауте, вокруг вызова
|
||||||
|
нужен push/pop, а сам таймаут в итерациях станет длиннее по времени.
|
||||||
|
**Важно про цену: это НЕ новая нагрузка.** Опрос ставится ровно туда,
|
||||||
|
где процессор и так впустую крутит `in a,(#0xFE)` — 42 мс из 60. Полезной
|
||||||
|
работы не отнимается нисколько.
|
||||||
|
**Ограничитель области, если «постоянный опрос» всё равно не нравится:**
|
||||||
|
потери случаются ТОЛЬКО при зажатом модификаторе (замерено; без Shift
|
||||||
|
потерь нет). Значит хук можно взводить лишь пока нажат Shift/Ctrl/Alt —
|
||||||
|
тогда опрос работает исключительно в той ситуации, ради которой заведён.
|
||||||
|
2. Вернуть вызовы в занятую треть кадра (они бесплатны, просто сами по себе
|
||||||
|
ничего не решали).
|
||||||
|
3. Перемерить тем же счётным методом уже в roomtest: цель — 25/25.
|
||||||
|
|
||||||
|
**Куда смотреть дальше, если плотного опроса не хватит.**
|
||||||
|
1. **Драйвер MAME — НЕ ТРОГАЕМ** (решение пользователя: пересборка MAME на
|
||||||
|
его машине занимает часы). Для протокола, подозрение осталось:
|
||||||
|
`sinclair/sprinter.cpp` держит запрос от клавиатуры ровно **32 такта
|
||||||
|
CPU**, и `m_irq_off_timer` — **один на два источника** (`irq_on()` экрана
|
||||||
|
заводит его же, `irq_off()` гасит разом обе линии). То есть кадровое
|
||||||
|
прерывание способно обрезать клавиатурный импульс — правдоподобное
|
||||||
|
объяснение «44 % пропущенных импульсов».
|
||||||
|
2. **Реальное железо.** Если п.1 — чисто эмуляционный артефакт, на железе
|
||||||
|
проблемы может не быть вовсе. Проверять при первом прогоне на живом
|
||||||
|
Sprinter.
|
||||||
|
|
||||||
|
**Про совпадение кадрового и клавиатурного прерываний** (вопрос
|
||||||
|
пользователя, 2026-08-01). Документация Sprinter: оба приходят с вектором
|
||||||
|
`0FFh`, различать по биту приёма байта в порту клавиатуры — «не пришёл,
|
||||||
|
значит экран»; совпадение возможно, но «исключительно редкий случай»
|
||||||
|
(в новой версии обещают развести жёстче через ПЛМ). То есть наш трамплин
|
||||||
|
делает ровно предписанное. Известный побочный эффект: при совпадении мы
|
||||||
|
обслуживаем клавиатуру и `reti`, пропуская кадровую цепочку и DSS — на
|
||||||
|
потерю байт это не влияет (линия кадрового остаётся взведённой и вызывает
|
||||||
|
повторный вход), но кадровый тик может пропасть. Отдельная мелкая правка,
|
||||||
|
в KBD-1 не входит.
|
||||||
|
|
||||||
|
**Что НЕ делать (проверено, стоило времени):**
|
||||||
|
- **Снимать `di` в accel-ядрах libbgi нельзя.** Патч `di`→`nop` в
|
||||||
|
`_bgi_blit_cols_raw`/`_bgi_heal_rows_raw`/`_bgi_blit_rows_raw` прямо в
|
||||||
|
памяти **уронил машину в перезагрузку**. То есть режим «акселератор
|
||||||
|
работает при EI» из `docs/new/06-accel.md §6.6` в этой прошивке/эмуляции
|
||||||
|
недоступен — вопрос закрыт артефактом, а не рассуждением.
|
||||||
|
- Дробить DI-окна по колонкам смысла тоже нет: см. вывод 1.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CLIP-1. Аудит блитов: где клип не нужен
|
||||||
|
|
||||||
|
**Зачем сейчас.** Кадр занят на ~86 %; подготовка клипающего варианта
|
||||||
|
стоит ~5.6 К тактов на вызов, а общее ядро против линейного — 13 288 против
|
||||||
|
4 617 тактов на спрайт 32×3 (`libbgi/include/gfx.h`). Это самая дешёвая
|
||||||
|
оставшаяся оптимизация: не переписывание логики, а выбор ядра.
|
||||||
|
|
||||||
|
**Что уже правильно** (шаблон, который надо распространить): блиты Кида,
|
||||||
|
стража, клинка и брызг спрашивают `pop_onscreen_cols()` и уходят в
|
||||||
|
`gfx_blit_cols_part_noclip`, иначе в клипающий вариант
|
||||||
|
(`pop_kid.c:86,591,676`, `pop_gdraw.c:105`).
|
||||||
|
|
||||||
|
**Что чинить:**
|
||||||
|
|
||||||
|
- `kid_heal()` (`pop_kid.c:613,615`) и `pop_guard_heal()`
|
||||||
|
(`pop_gdraw.c:65,66`) зовут `gfx_heal` — **всегда с клипом**, хотя
|
||||||
|
`gfx_heal_noclip` существует и `pop_bg.c:147` им уже пользуется по тому же
|
||||||
|
тесту. Это heal 2–4 прямоугольников КАЖДЫЙ кадр; замер общего ядра —
|
||||||
|
11 658 тактов на heal 22×22. Тест onscreen у нас уже посчитан рядом.
|
||||||
|
- `pop_room_clip_borders()` (`pop_bg.c:1193,1194`) — `gfx_heal(0,0,320,…)`:
|
||||||
|
noclip требует w,h ≤ 255, значит либо два куска по 160, либо оставить как
|
||||||
|
есть (зовётся по гейту, только в кадрах падения — проверить, что гейт
|
||||||
|
действительно редкий, прежде чем трогать).
|
||||||
|
|
||||||
|
**Что обязано остаться с клипом** (зафиксировать в комментарии, чтобы потом
|
||||||
|
не «оптимизировать» повторно):
|
||||||
|
- кромочные тайлы фона `pop_bg.c:132` — тайл у края экрана режется по
|
||||||
|
построению;
|
||||||
|
- спрайты при straddle (`kid_render_dx = ∓140`, комната Кида ≠ отрисованной)
|
||||||
|
и при падении ниже поля — фолбэки `pop_kid.c:594,681`, `pop_gdraw.c:107`.
|
||||||
|
|
||||||
|
**Порядок работы:** (1) выписать полный список вызовов
|
||||||
|
`gfx_blit*`/`gfx_heal*` в roomtest с ответом «кто гарантирует on-screen»;
|
||||||
|
(2) перевести то, что можно, на noclip по существующему тесту; (3) замерить
|
||||||
|
кадр полосами бордюра ДО/ПОСЛЕ (`PROF()` уже в `roomtest.c`) и брейкпоинтом
|
||||||
|
на конкретной функции — числом, а не «стало плавнее»; (4) `make size-check`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P1 — сразу после P0 (закрываем уровень 1 как ИГРУ, а не стенд)
|
||||||
|
|
||||||
|
### L1-START. Старт по данным уровня
|
||||||
|
`roomtest.c` жёстко стартует `START_ROOM 1 / COL 3 / ROW 0`, хотя
|
||||||
|
`pop_level_start_room()` / `pop_level_start_pos()` / `pop_level_start_dir()`
|
||||||
|
в `pop_level.h` уже реализованы и НИКЕМ не вызываются. Перевести старт и
|
||||||
|
респавн на них; `#define ROOMNAV` оставить, но выключенным по умолчанию.
|
||||||
|
|
||||||
|
### L1-EXIT. Выход с уровня (дверь уровня)
|
||||||
|
Сейчас: дверь открывается (`animate_leveldoor`), но войти в неё нельзя —
|
||||||
|
`up_pressed()` (`pop_ctrl.c:188`) не проверяет `tiles_16_level_door_left`, а
|
||||||
|
опкод `0xF1 END_LEVEL` в `play_seq` (`pop_kid.c:418`) пустой. Портировать
|
||||||
|
`up_pressed`-ветку + `go_up_leveldoor()` (`seg005.c:410..500`) и завести
|
||||||
|
`pop_next_level`, который взводит `END_LEVEL` (`seg006.c:662`). **Это же
|
||||||
|
первый шаг плана следующих уровней** — см. `../docs/levels_plan.md`.
|
||||||
|
|
||||||
|
### L1-TRIAGE. Ревизия `bug_list.md`
|
||||||
|
Список отстал от кода: BUG-1/BUG-2 (боковой переход, ping-pong) закрываются
|
||||||
|
`pop_leave_timer` + `char_x_forward_edge` (`pop_map.c:1402..1432,1553`),
|
||||||
|
BUG-3 (окклюзия climb-up на кнопке) — фиксом `tile_code_drawn` от 2026-07-28,
|
||||||
|
но все три по-прежнему стоят как **Critical**. Пройти их в MAME, закрыть
|
||||||
|
подтверждённые, оставшиеся (BUG-CEIL-1/2/3, BUG-OCCL-1 — косметика окклюзии)
|
||||||
|
переклассифицировать. Заодно закрыть таблицу обхода 24 комнат — она
|
||||||
|
заполнена на 5 строк из 24, а инструмент (`ROOMNAV`) готов.
|
||||||
|
|
||||||
|
### L1-SPEED. Игра идёт быстрее оригинала (найдено 2026-08-01)
|
||||||
|
Сверка таймингов: оригинал — `BASE_FPS = 60` при `base_speed = 5` тиков на
|
||||||
|
логический кадр (`SDLPoP/src/types.h:1373`, `data.h:869`) = **83.3 мс**, в бою
|
||||||
|
`fight_speed = 6` = **100 мс**. У нас `roomtest.c` ждёт **три** `gfx_wait_vsync()`
|
||||||
|
= 60 мс, и отдельной скорости боя нет — то есть примерно **+39 % к скорости
|
||||||
|
эталона**. Соответствие: 4 ожидания (80 мс) обычно, 5 (100 мс) в бою.
|
||||||
|
**Делать ПОСЛЕ CLIP-1**: замедление кадра спрячет проблемы бюджета вместо
|
||||||
|
того, чтобы их показать. Проверка — секундомером по одинаковому отрезку
|
||||||
|
рядом с живым SDLPoP, не «на глаз».
|
||||||
|
|
||||||
|
### L1-PASS. Сквозное прохождение уровня 1
|
||||||
|
От старта до двери уровня одним заходом: подбор меча, страж, кнопки/ворота,
|
||||||
|
пики, loose-полы, зелье, падения. Это приёмка этапа 1 и одновременно
|
||||||
|
регресс-база для уровня 2.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Отложено осознанно (не брать, пока не появится причина)
|
||||||
|
|
||||||
|
- **Звук** (CBL-эффекты, Фаза 5 `PORT_PLAN.md`) — геймплей не блокирует.
|
||||||
|
- **Таймер уровня / HUD времени / меню / сохранения** — Фаза 6.
|
||||||
|
- **T-1** (пики: перерисовка по причине) — `bug_list.md`; отдаётся почти
|
||||||
|
бесплатно после T-2, отдельно не окупается.
|
||||||
|
- **BUG-CEIL-2** (loose-плита в потолке из комнаты сверху) — требует
|
||||||
|
персистентного per-room modifier соседей, это Фаза P0 из
|
||||||
|
`../docs/gates_spikes_plan.md`.
|
||||||
|
- **Отключение мыши на время игры** и **замена PRNG** —
|
||||||
|
`../docs/ideas_backlog.md` (оба дают доли процента кадра).
|
||||||
|
- **OPT-1** (хирургический редрой шва) — стоимость транзиентная, решение от
|
||||||
|
2026-07-22 «оставляем».
|
||||||
@@ -1,5 +1,14 @@
|
|||||||
# roomtest — список известных багов (Фаза B и смежное)
|
# roomtest — список известных багов (Фаза B и смежное)
|
||||||
|
|
||||||
|
> **Список отстал от кода (ревизия 2026-08-01, задача L1-TRIAGE в
|
||||||
|
> [`TASKS.md`](TASKS.md)).** По исходникам похоже, что закрыты, но в MAME не
|
||||||
|
> переподтверждены: **BUG-1** и **BUG-2** (боковой переход/ping-pong —
|
||||||
|
> `pop_map.c` `pop_leave_timer` + `char_x_forward_edge`, см. также
|
||||||
|
> BUG-SEAM-PINGPONG ниже) и **BUG-3** (окклюзия climb-up на кнопке — фикс
|
||||||
|
> `tile_code_drawn` от 2026-07-28, описан в разделе «Исправлено»). Пока не
|
||||||
|
> проверены — статус **Critical** оставлен как есть, не полагаться на него.
|
||||||
|
> Текущие приоритеты работ — в [`TASKS.md`](TASKS.md), а не здесь.
|
||||||
|
|
||||||
Найдено при тестировании Фазы B (кнопки/ворота) в MAME. **НЕ решаем сейчас**
|
Найдено при тестировании Фазы B (кнопки/ворота) в MAME. **НЕ решаем сейчас**
|
||||||
— задача следующего этапа. Правило проекта: механику сверять с
|
— задача следующего этапа. Правило проекта: механику сверять с
|
||||||
`applications/PoP/SDLPoP/src/` ДО кодинга.
|
`applications/PoP/SDLPoP/src/` ДО кодинга.
|
||||||
|
|||||||
@@ -175,11 +175,14 @@ void gfx_scroll_v(uint8_t src_page, uint8_t dst_page,
|
|||||||
прогон: нарисовать фон, поверх спрайт банком `0x5C`, сделать `scroll_h` на
|
прогон: нарисовать фон, поверх спрайт банком `0x5C`, сделать `scroll_h` на
|
||||||
dx>0, убедиться что спрайт **не размазался** (копировался фон, не VRAM со
|
dx>0, убедиться что спрайт **не размазался** (копировался фон, не VRAM со
|
||||||
спрайтом). Заодно выяснить, нужен ли сброс Port_Y на колонку (§4).
|
спрайтом). Заодно выяснить, нужен ли сброс Port_Y на колонку (§4).
|
||||||
2. **ОЗУ-копия per-page или общая.** `C-Compiler/applications/PoP/docs/double_buffer_plan.md`
|
2. **ОЗУ-копия per-page или общая.** Ранний план дабл-буфера PoP исходил из
|
||||||
пишет «теневая копия одна — общая». Если тень физически одна (а не адресуется
|
«теневая копия одна — общая»; **практикой это опровергнуто**: у каждой
|
||||||
по базе `0xC000/0xC140` как VRAM), page0 и page1 не удержат фон **разных**
|
страницы СВОЯ теневая ОЗУ-копия, heal берёт фон из копии той страницы, в
|
||||||
положений камеры во время скролла → пинг-понг (§8) сломается. Проверить:
|
которую рисуем (`applications/PoP/roomtest/CLAUDE.md`, раздел
|
||||||
записать разный фон в page0 и page1 банком `0x50`, сверить чтение обеих.
|
«Дабл-буфер»; `roomtest.c enter_room` рисует фон в обе страницы по
|
||||||
|
отдельности именно поэтому). Значит пинг-понг (§8) в принципе рабочий,
|
||||||
|
но перед опорой на него — подтвердить на своём коде: записать разный фон
|
||||||
|
в page0 и page1 банком `0x50`, сверить чтение обеих.
|
||||||
3. **(только для `scroll_v`, путь A)** accel-буфер переживает `OUT Port_Y` между
|
3. **(только для `scroll_v`, путь A)** accel-буфер переживает `OUT Port_Y` между
|
||||||
fill и flush одного burst'а. Доковый straight-copy этого не проверяет.
|
fill и flush одного burst'а. Доковый straight-copy этого не проверяет.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user