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` (общий план фаз),
|
||||
`KID_PLAN.md`, `double_buffer_plan.md`, `loose_floors_plan.md`, форматы
|
||||
ресурсов. Начинать чтение отсюда.
|
||||
- `roomtest/` — **активная разработка**: комната 1 + Kid (анимация, ввод,
|
||||
коллизия, падение, зацеп, fore-окклюзия, loose-полы). Свой `CLAUDE.md`.
|
||||
- `docs/` — планы и форматы; **индекс с отметками актуальности —
|
||||
`docs/README.md`**, начинать чтение оттуда. Ключевое:
|
||||
`levels_plan.md` (следующий этап), `layout_plan_v2.md` (раскладка кода по
|
||||
окнам/банкам + скорость отрисовки), `PORT_PLAN.md` (карта фаз со
|
||||
статусами).
|
||||
- `roomtest/` — **активная разработка**: уровень 1 целиком (Kid, стражи,
|
||||
ловушки, ворота, loose-полы). Свой `CLAUDE.md`; текущие задачи —
|
||||
`roomtest/TASKS.md`, баги — `roomtest/bug_list.md`.
|
||||
- `poc/` — ранний proof-of-concept (снег/атлас/kbd_raw); ассеты в `poc/res/`.
|
||||
- `bgtest/`, `coltest/` — отдельные проверки фона/коллизии.
|
||||
- `toolchain/` — python-упаковщики ассетов + эталонные PNG (`1.1-2.png`).
|
||||
|
||||
@@ -4,15 +4,22 @@
|
||||
target-слоя SDCC (sprinter-cc / libc / libbgi). Цель — 320×256×256 (режим
|
||||
0x81), VGA-256 ассеты оригинала переносятся почти впрямую.
|
||||
|
||||
Общий план и статус фаз — [`docs/PORT_PLAN.md`](docs/PORT_PLAN.md).
|
||||
Правила работы для ИИ-сессий — [`CLAUDE.md`](CLAUDE.md).
|
||||
**Состояние (2026-08-01): играется весь уровень 1** — комнаты и переходы,
|
||||
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. |
|
||||
| `docs/` | Планы (`PORT_PLAN`, `KID_PLAN`, `double_buffer_plan`, `loose_floors_plan`) и разбор форматов ресурсов Apple II / DOS. |
|
||||
| `roomtest/` | **Активная разработка.** Уровень 1 целиком: фон композицией тайлов, Kid (seqtbl-анимация, ввод, коллизия, падение, зацеп, окклюзия), ловушки, ворота, стражи, бой. Свой README/CLAUDE/TASKS. |
|
||||
| `docs/` | Планы и разбор форматов ресурсов Apple II / DOS — см. индекс в [`docs/README.md`](docs/README.md). |
|
||||
| `toolchain/` | Python-упаковщики ассетов под Sprinter (рендер комнат, атласы тайлов/Kid, извлечение данных анимации) + эталонные скриншоты. |
|
||||
| `poc/` | Ранний proof-of-concept (снег, атлас, raw-клавиатура). Ассеты в `poc/res/`. |
|
||||
| `bgtest/`, `coltest/` | Точечные проверки фона и коллизии. |
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
# 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` (таблицы последовательностей),
|
||||
`types.h` (char_type, seq_*, SEQ_*, actions_*), `SDLPoP/data/KID` (спрайты).
|
||||
Фон уже готов и проверен на MAME (`applications/PoP/roomtest`, см.
|
||||
|
||||
@@ -1,8 +1,29 @@
|
||||
# Prince of Persia на ZX Sprinter — план порта
|
||||
|
||||
Статус: план (2026-07-15). §2 (A: kbd_mod_state / B: kbd_raw) —
|
||||
РЕАЛИЗОВАНО и частично проверено в MAME (tests/kbdraw, 2026-07-15,
|
||||
подробности в §2.2); PoC (§5) и остальные фазы — не начаты. Опирается на
|
||||
## СТАТУС (обновлено 2026-08-01)
|
||||
|
||||
Документ составлен 2026-07-15 как план «с нуля» и с тех пор во многом
|
||||
исполнен. Читать его надо так:
|
||||
|
||||
| Раздел | Что с ним сейчас |
|
||||
|--------|------------------|
|
||||
| §1 возможности библиотек | актуально как обзор, но **спрайтовый движок `sprite.h` для персонажей НЕ используется**: Kid/страж рисуются прямыми блитами атласов (`gfx_blit_cols_part*`) с ручным heal — так требует модель оригинала (§6) |
|
||||
| §2 held-state клавиатуры | **сделано** (`kbd_mod_state`, `<kbd_raw.h>`). Открытая проблема — потеря байт при аккордах Shift+стрелка; диагноз и план в `../roomtest/TASKS.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` в
|
||||
этой папке, на текущий sprinter-cc/libc/libbgi (см. §1) и на локальные копии
|
||||
`applications/PoP/SDLPoP` (github.com/NagyD/SDLPoP, GPLv3) и
|
||||
@@ -227,6 +248,13 @@ dirty-биты, heal против фона через ОЗУ-копию); `room_
|
||||
|
||||
## 5. Proof-of-Concept — цель: доказать, что порт вообще ощущается как PoP
|
||||
|
||||
> **Закрыт (исторический раздел).** PoC в `poc/` свою задачу выполнил и
|
||||
> дальше не развивается: управление ощущается как PoP, held-state работает.
|
||||
> Всё, что ниже про плейсхолдер-персонажа и приблизительную дугу прыжка,
|
||||
> — уже неправда для активной ветки: в `roomtest/` стоит настоящая графика
|
||||
> Кида и авторские таблицы кадров (§6). Раздел оставлен ради истории
|
||||
> решений (в частности §5.1 — почему сначала был плейсхолдер).
|
||||
|
||||
**Объём**: одна комната (например Level 1, экран старта Кида), без
|
||||
переходов между экранами, без стражников (стретч-цель, не обязательна).
|
||||
|
||||
@@ -364,45 +392,53 @@ memory/png_strip_padding_tradeoff.
|
||||
## 7. Полноценное приложение — фазы (после PoC)
|
||||
|
||||
Порядок — по риску и зависимостям, не по геймплейной важности.
|
||||
**Отметки статуса — на 2026-08-01.**
|
||||
|
||||
**Фаза 0 — инфраструктура порта** (расширяет PoC, не переписывает):
|
||||
- Хелд-стейт клавиатуры — финальное решение и реализация по §2 (после
|
||||
подтверждения пользователем).
|
||||
- Полный конвертер уровней (все 15 файлов `levels.dat`/`LEVELn`) → бинарный
|
||||
формат приложения (можно 1-в-1 raw dump, читать по офсетам в рантайме —
|
||||
не обязательно разворачивать в C-struct с указателями).
|
||||
- Полный конвертер фона (24 экрана × N уровней) в растры + конвертер
|
||||
спрайт-лент Кид/стражник/скелет/тень/Джаффар в атласы `.atl` (расширение
|
||||
`conv_sprites.py`/формата `.atl`, если частот кадров/атласов на актора не
|
||||
хватит текущего лимита — см. риск в §8).
|
||||
**Фаза 0 — инфраструктура порта** — **СДЕЛАНА**, но иначе, чем задумано:
|
||||
- Хелд-стейт клавиатуры по §2 — сделан.
|
||||
- Конвертер уровней не понадобился: `res200N.bin` из `SDLPoP/data/LEVELS`
|
||||
кладётся на образ как есть и читается по офсетам в рантайме
|
||||
(`roomtest/pop_level.c`), уровень живёт в EMM-странице.
|
||||
- Конвертер фона в растры **отменён осознанно** (§4): фон собирается
|
||||
тайлами в рантайме. Спрайты — `toolchain/pop_pack_bg.py` /
|
||||
`pop_pack_kid.py` / `pop_pack_guard.py` → атласы `.atl` (Kid — 28
|
||||
страниц, риск §8 п.3 закрыт).
|
||||
|
||||
**Фаза 1 — Кид, полный набор действий**: стоять/идти/бежать/тормозить/
|
||||
разворот/прыжок (на месте, вперёд, «прыжок с разбега»)/повисание на
|
||||
краю/подтягивание/спуск по свисанию/приседание/питьё зелья/смерть от
|
||||
провала. Переходы между экранами (`MAP`-граф, `INFO.KidStartScrn`).
|
||||
**Фаза 1 — Кид, полный набор действий** — **СДЕЛАНА**: стоять/идти/бежать/
|
||||
тормозить/разворот/прыжки/повисание/подтягивание/спуск/приседание/
|
||||
осторожный шаг/питьё зелья/смерть от провала и от пик; переходы между
|
||||
комнатами во все четыре стороны. Осталось: **старт по данным уровня**
|
||||
(`pop_level_start_*` реализованы, но не подключены) — задача L1-START в
|
||||
`../roomtest/TASKS.md`.
|
||||
|
||||
**Фаза 2 — мир и ловушки**: нажимные плиты/двери через граф
|
||||
`LINKLOC`/`LINKMAP` (см. `APPLEII_RESOURCE_FORMAT.md` §1.2), шипы
|
||||
(выдвижение/втягивание/заклинивание), шаткие плиты (loose, обрушение),
|
||||
зелья (эффект по `BLUESPEC×32`), стартовые позиции по `INFO`.
|
||||
**Фаза 2 — мир и ловушки** — **СДЕЛАНА**: кнопки/ворота через
|
||||
`LINKLOC`/`LINKMAP`, шипы, loose-полы (тряска, обрушение, щебень, пробой
|
||||
потолка), зелья, дверь уровня (открывается), факелы. Подробности и
|
||||
справочник — `gates_spikes_plan.md`.
|
||||
|
||||
**Фаза 3 — бой**: подбор/выхватывание меча, состояние стойки, парирование/
|
||||
удар, коллизия клинков — по логике `AUTO.S`/`seg003-006.c` (референс, не
|
||||
копия). Стражник: базовое AI-поведение по `GdStartProg` (несколько
|
||||
шаблонов программ), Y-сортировка слоями уже есть в движке для «кто
|
||||
спереди/сзади».
|
||||
**Фаза 3 — бой** — **СДЕЛАНА в объёме обычного стражника**: подбор и
|
||||
выхватывание меча, стойка, удар/парирование, коллизия клинков, HP обеих
|
||||
сторон, смерть; ИИ стража (замечает Кида, подходит, боевые ветки),
|
||||
персистентность трупа между комнатами.
|
||||
|
||||
**Фаза 4 — разнообразие противников**: скелет, тень (копия анимации Кида —
|
||||
подтверждено побайтовым совпадением данных, см. `MSDOS_RESOURCE_FORMAT.md`
|
||||
§3), толстый стражник/визирь (общая база анимации с визирем).
|
||||
**Фаза 4 — разнообразие противников** — **НЕ НАЧАТА**. Скелет нужен на
|
||||
уровне 3, толстый — на 6, тень — на 12, визирь — на 13; привязка
|
||||
«уровень → тип стража» (`tbl_guard_type`) описана в `levels_plan.md` §1.
|
||||
|
||||
**Фаза 5 — звук**: CBL-эффекты (шаги, удары, двери, падение) из
|
||||
`digisnd*.dat`→PCM; PC-спикер тройки (`ibm_snd*.dat`) как опциональный
|
||||
дешёвый бипер без CBL, если формат подтвердится простым парсингом.
|
||||
**Фаза 5 — звук** — **НЕ НАЧАТА**: CBL-эффекты (шаги, удары, двери,
|
||||
падение) из `digisnd*.dat`→PCM; PC-спикер тройки (`ibm_snd*.dat`) как
|
||||
опциональный дешёвый бипер без CBL, если формат подтвердится простым
|
||||
парсингом. Опкод `SOUND` в `play_seq` пока просто съедает свой аргумент —
|
||||
точки вызова уже на месте.
|
||||
|
||||
**Фаза 6 — оболочка**: титры, меню/выбор уровня, HUD (таймер/жизни),
|
||||
сохранение прогресса (FILE*), финальные катсцены — по минимуму,
|
||||
геймплейно не критично.
|
||||
**Фаза 6 — оболочка** — **НЕ НАЧАТА**: титры, меню/выбор уровня, HUD
|
||||
(таймер/жизни), сохранение прогресса (FILE*), финальные катсцены — по
|
||||
минимуму, геймплейно не критично. Полоса HP — единственное, что уже есть.
|
||||
|
||||
**Между Фазами 4 и 5 вклинивается то, чего в этом плане не было:
|
||||
переход между УРОВНЯМИ** (загрузка следующего уровня, второй тайлсет
|
||||
palace, потабличные различия уровней). Отдельный документ —
|
||||
`levels_plan.md`.
|
||||
|
||||
**Фаза 7 — стабилизация**: полный прогон всех 14 уровней в MAME
|
||||
(`mame_interactive.py`), затем на реальном железе; профилирование бюджета
|
||||
@@ -418,23 +454,33 @@ tiny/small.
|
||||
|
||||
(по правилу `defer_unexplained_quirks` — не гадать, проверять)
|
||||
|
||||
1. **Held-state клавиатуры** (§2) — блокирует даже PoC, если решать
|
||||
«правильно»; иначе PoC на компромиссном варианте 2 (таймаут-эвристика).
|
||||
2. **Бюджет спрайтов на насыщенный экран** — сцена с 2+ стражниками +
|
||||
несколько анимированных ловушек может приблизиться к лимиту
|
||||
~21 спрайт/кадр (`sprite_engine_perf`) — нужна прикидка по реальным
|
||||
уровням (сколько объектов одновременно активно в худшем экране).
|
||||
3. **Ёмкость одного атласа/страницы EMM на актора** — у Кида ~220 кадров
|
||||
(все действия) против 4×12 у `rpgwalk` — потребуется либо несколько
|
||||
атласов на актора с переключением по фазе действия (стоять/идти отдельно
|
||||
от боя), либо расширение формата `.atl`/загрузчика на мульти-страничные
|
||||
атласы — оценить фактический байтовый вес конвертированных кадров Кида
|
||||
прежде чем проектировать.
|
||||
4. **Тайминг оригинала** — сверить логическую частоту кадров анимации
|
||||
оригинала (Apple II ~60 Гц NTSC / DOS — фиксированный таймер) с 50 Гц
|
||||
Sprinter; если оригинал считался на другой частоте — потребуется
|
||||
коэффициент пересчёта смещений кадров (§6), иначе прыжки/бег будут
|
||||
визуально быстрее/медленнее эталона.
|
||||
1. ~~**Held-state клавиатуры** (§2)~~ — **закрыт** (`<kbd_raw.h>`). Открытый
|
||||
остаток — не «есть ли held-state», а потеря байт при аккордах
|
||||
Shift+стрелка: `../roomtest/TASKS.md`, KBD-1.
|
||||
2. **Бюджет кадра** — риск подтвердился, но не в том виде, в каком ожидался:
|
||||
спрайтовый движок для персонажей не используется, поэтому лимит
|
||||
«~21 спрайт/кадр» неприменим. Реальный бюджет упирается в heal+блиты и
|
||||
перерисовку тайлов; замер 2026-07-30 — типичный кадр ~371 К тактов
|
||||
(~86 % периода). Инструмент замера уже в коде: полосы бордюра `PROF()`
|
||||
в `roomtest.c`. План выжимания — `../roomtest/TASKS.md` (CLIP-1) и
|
||||
`../roomtest/bug_list.md` (T-1/T-2).
|
||||
3. ~~**Ёмкость атласа на актора**~~ — **закрыт**: Kid разложен на 28
|
||||
атласов-страниц по 8 спрайтов (`pop_pack_kid.py`), страж — на 5;
|
||||
мульти-страничного формата `.atl` не потребовалось. Побочно
|
||||
подтвердился компромисс паддинга (§6.1).
|
||||
4. **Тайминг оригинала** — **ОТКРЫТ, и сверка 2026-08-01 показывает
|
||||
расхождение.** Цифры оригинала (SDLPoP): базовый таймер `BASE_FPS = 60`
|
||||
(`types.h:1373`), логический кадр игры — `base_speed = 5` тиков
|
||||
(`data.h:869`), то есть **83.3 мс (12 лог. кадров/с)**; в бою
|
||||
`fight_speed = 6` → **100 мс (10/с)**. У нас (`roomtest.c`) — три
|
||||
ожидания `gfx_wait_vsync()` на итерацию, то есть **60 мс (16.7/с)** и
|
||||
без отдельной скорости боя. Значит **игра идёт примерно на 39 %
|
||||
быстрее эталона**. Точное соответствие даёт 4 ожидания vsync (80 мс
|
||||
против 83.3) и 5 в бою (100 мс — совпадает точно).
|
||||
Проверять не «на глаз», а секундомером по одинаковому отрезку
|
||||
(SDLPoP рядом на том же экране), и только после того, как кадр
|
||||
перестанет иногда вылезать за период (см. п.2) — иначе замедление
|
||||
спрячет проблему бюджета вместо того, чтобы её показать.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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.txt` для grep/цитирования):
|
||||
@@ -70,7 +104,11 @@ DOS-упаковщика). Это значит: раскладку `BLUETYPE`/`B
|
||||
дворца/подземелий) практичнее взять их оттуда напрямую, чем писать свой
|
||||
декодер сжатия пикселей DOS `.DAT`.
|
||||
|
||||
## Что дальше (не сделано в этом заходе)
|
||||
## Что дальше по форматам (не сделано и пока не нужно)
|
||||
|
||||
Порт читает уровень напрямую из `res200N.bin` (`roomtest/pop_level.c`), а
|
||||
графику берёт из распакованных PNG `SDLPoP/data/` — поэтому ни один пункт
|
||||
ниже сейчас не блокирует работу.
|
||||
|
||||
1. Точный кодек сжатия пикселей спрайтов в сыром DOS `.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/смерть — ПОДРОБНЫЙ план
|
||||
|
||||
Статус: **план** (2026-07-20). Реализация — отдельной сессией. Документ
|
||||
самодостаточный: рассчитан на старт «с чистого листа» (пустой контекст).
|
||||
Всё сверено с `applications/PoP/SDLPoP/src/` и данными `res2001.bin`.
|
||||
> **Статус: РЕАЛИЗОВАНО (2026-08-01).** Все фазы плана (P0 персистентный
|
||||
> per-room `room_modif`, S пики, B кнопки+ворота) сделаны и играются:
|
||||
> `roomtest/pop_trob.c` (trob-диспетчер, `LINKLOC`/`LINKMAP`, ворота, дверь
|
||||
> уровня, факелы, зелья), `pop_map.c` (HP, смерть на пиках, урон падения),
|
||||
> `pop_redraw.c` (пометки перерисовки вместо прямых блитов). Ограничения
|
||||
> из §0 закрыты: тайлы персистентны, HP/смерть есть, loose обобщён в trob;
|
||||
> L3-вверх (climb-up в комнату сверху) тоже сделан (`pop_leave_dir = 3`).
|
||||
> Из §5 остаётся открытым только **переход на следующий уровень через дверь
|
||||
> уровня** — он вынесен в `levels_plan.md`.
|
||||
>
|
||||
> **Документ оставлен как СПРАВОЧНИК**, а не как план: §1 (раскладка
|
||||
> объектов уровня 1 по комнатам, декод связей кнопка→цель) и §2 (точные
|
||||
> ссылки на механику SDLPoP) продолжают экономить время при отладке.
|
||||
> Текущие задачи — `../roomtest/TASKS.md`.
|
||||
|
||||
Составлен 2026-07-20. Документ самодостаточный: рассчитан на старт
|
||||
«с чистого листа» (пустой контекст). Всё сверено с
|
||||
`applications/PoP/SDLPoP/src/` и данными `res2001.bin`.
|
||||
|
||||
Правило проекта (см. `applications/PoP/CLAUDE.md`): **SDLPoP — источник истины**,
|
||||
перед кодингом читать соответствующий код 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 тактов), бит-в-бит
|
||||
|
||||
@@ -1,8 +1,26 @@
|
||||
# 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 по свежему замеру.
|
||||
Заменяет `size_optimization_plan.md` (v1, 2026-07-21): часть его пунктов уже
|
||||
сделана, часть опиралась на неверную модель банкинга. Документ самодостаточный
|
||||
Заменял `size_optimization_plan.md` (v1, 2026-07-21) — тот удалён 2026-08-01
|
||||
как полностью перекрытый этим документом. Документ самодостаточный
|
||||
— рассчитан на старт с пустого контекста.
|
||||
|
||||
Повод: перед стражами и боёвкой (новый код ~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`,
|
||||
`sprinter_emm_budget`, `mame_mcp_bridge`, `avoid_32bit_arith_z80`,
|
||||
`libc_one_function_per_module`.
|
||||
- `applications/PoP/docs/size_optimization_plan.md` — v1 (замер 2026-07-21,
|
||||
раздел §8 про скорость отрисовки актуален и не дублируется здесь).
|
||||
- `applications/PoP/roomtest/TASKS.md` — что из этого берётся в работу сейчас.
|
||||
|
||||
---
|
||||
|
||||
## 9. Скорость отрисовки: замеры и запас
|
||||
|
||||
Перенесено из удалённого `size_optimization_plan.md` §8 (замер 2026-07-27) —
|
||||
единственная его часть, которая не была перекрыта этим документом.
|
||||
Профилирование в MAME: маркеры в порт 0xFE + `wpiset … totalcycles` (приём из
|
||||
memory `mame_mcp_bridge`); в самом `roomtest.c` для этого уже стоят полосы
|
||||
бордюра `PROF()`. Кадр Sprinter = **430 080 тактов**.
|
||||
|
||||
**Стоимость блита почти НЕ зависит от размера** — платим за проход по цепочке
|
||||
`gfx_blit → gfx_blit_part → _gfx_blit_full` (16-битная арифметика, клип,
|
||||
пересчёт src, нарезка полос >256), а не за пиксели:
|
||||
|
||||
| путь (спрайт 32×3) | тактов |
|
||||
|---|---|
|
||||
| `gfx_blit` (общее ядро, с клипом) | 13 288 |
|
||||
| линейное ядро без клипа | 4 617 |
|
||||
|
||||
Отсюда `draw_tile(0,0)` тайла шва (9 блитов) стоил **183 690 тактов = 43 %
|
||||
кадра**; сам `bar` — только 13 308.
|
||||
|
||||
**Сделано:** `gfx_blit_noclip()` в libbgi, фоновые блиты `pop_bg` уходят на
|
||||
него, когда спрайт целиком на экране (~2.9×, подтверждено в MAME). Позже
|
||||
тем же приёмом закрыты спрайты персонажей (`gfx_blit_cols_part_noclip`).
|
||||
**Не закрыт heal** — задача CLIP-1 в `../roomtest/TASKS.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)
|
||||
|
||||
> **Статус: ЖИВОЙ ПЛАН, сделан частично (сверено 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: персонаж может находиться в СОСЕДНЕЙ комнате,
|
||||
пока на экране ещё ТЕКУЩАЯ (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 =
|
||||
по умолчанию баг у нас.
|
||||
|
||||
**Что в работе сейчас — `TASKS.md`** (доска задач: приоритеты, критерии
|
||||
готовности). Баги — `bug_list.md` (его шапка честно говорит, что список
|
||||
отстал). План следующих уровней — `../docs/levels_plan.md`.
|
||||
|
||||
## Сборка и запуск
|
||||
|
||||
```
|
||||
|
||||
@@ -39,9 +39,14 @@ ESC выход.
|
||||
- `room1_data.h` — карта комнаты 1; `kid_data.h` — данные анимации Kid
|
||||
(оба генерируются скриптами `../toolchain/`).
|
||||
|
||||
## Статус
|
||||
## Статус (2026-08-01)
|
||||
|
||||
Готово и проверено в MAME: статический фон комнаты 1; Kid — анимация,
|
||||
управление, коллизия/падение, зацеп/подтягивание/спуск, fore-окклюзия
|
||||
пола/стены над Kid. В работе: проваливающиеся полы (loose floors) — см.
|
||||
`../docs/loose_floors_plan.md`.
|
||||
Играется весь уровень 1: комнаты и переходы между ними, Kid (анимация,
|
||||
управление, коллизия/падение, зацеп/подтягивание/спуск, окклюзия), кнопки и
|
||||
ворота, пики, проваливающиеся полы, дверь уровня, меч и бой, стражи с ИИ,
|
||||
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 и смежное)
|
||||
|
||||
> **Список отстал от кода (ревизия 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. **НЕ решаем сейчас**
|
||||
— задача следующего этапа. Правило проекта: механику сверять с
|
||||
`applications/PoP/SDLPoP/src/` ДО кодинга.
|
||||
|
||||
@@ -175,11 +175,14 @@ void gfx_scroll_v(uint8_t src_page, uint8_t dst_page,
|
||||
прогон: нарисовать фон, поверх спрайт банком `0x5C`, сделать `scroll_h` на
|
||||
dx>0, убедиться что спрайт **не размазался** (копировался фон, не VRAM со
|
||||
спрайтом). Заодно выяснить, нужен ли сброс Port_Y на колонку (§4).
|
||||
2. **ОЗУ-копия per-page или общая.** `C-Compiler/applications/PoP/docs/double_buffer_plan.md`
|
||||
пишет «теневая копия одна — общая». Если тень физически одна (а не адресуется
|
||||
по базе `0xC000/0xC140` как VRAM), page0 и page1 не удержат фон **разных**
|
||||
положений камеры во время скролла → пинг-понг (§8) сломается. Проверить:
|
||||
записать разный фон в page0 и page1 банком `0x50`, сверить чтение обеих.
|
||||
2. **ОЗУ-копия per-page или общая.** Ранний план дабл-буфера PoP исходил из
|
||||
«теневая копия одна — общая»; **практикой это опровергнуто**: у каждой
|
||||
страницы СВОЯ теневая ОЗУ-копия, heal берёт фон из копии той страницы, в
|
||||
которую рисуем (`applications/PoP/roomtest/CLAUDE.md`, раздел
|
||||
«Дабл-буфер»; `roomtest.c enter_room` рисует фон в обе страницы по
|
||||
отдельности именно поэтому). Значит пинг-понг (§8) в принципе рабочий,
|
||||
но перед опорой на него — подтвердить на своём коде: записать разный фон
|
||||
в page0 и page1 банком `0x50`, сверить чтение обеих.
|
||||
3. **(только для `scroll_v`, путь A)** accel-буфер переживает `OUT Port_Y` между
|
||||
fill и flush одного burst'а. Доковый straight-copy этого не проверяет.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user