MEM-BANK5: pop_ctrl.c → банк 5; чит [/] подгонки Кида по X

Три правки едут вместе намеренно: n_banks и --bank обязаны меняться
атомарно, иначе промежуточный коммит — зависание (см. ниже).

MEM-BANK5.  CODE и DATA делят одно 32-КБ пространство W1+W2, поэтому
килобайт кода, уехавший в банк, — это килобайт, доступный данным.
Кандидат выбран не по размеру, а по частоте вызова: диспетчер управления
дёргается раз в кадр на персонажа и горячих банк→банк переходов не
создаёт (в отличие от pop_level, чей pop_level_tile зовётся из банка 2 на
КАЖДЫЙ тайл).

  _CODE   26 780 -> 24 662 Б   (−2 118)
  куча       180 -> 2 298 Б
  банк 5            2 211 / 16 384 (13.5 %)

Шина control_* (8 глобалов) переехала в pop_state.c.  Сегодня она уцелела
бы и в pop_ctrl.c — банки собираются без --bank-data, их писучие данные
остаются в общем _DATA, — но это флаг сборки, а не свойство кода, а шину
трогают уже три банка: 5 пишет с клавиатуры, 1 подаёт синтетический ввод
ИИ (autocontrol_*, seg002), 3 читает через pop_ctrl_shift_held.
Заодно pop_ctrl.c наконец включает собственный заголовок — раньше
объявления жили прямо в нём.

ГРАБЛИ, на которые наступили (стоили дольше самой задачи): n_banks в
roomtest.c захардкожен, и его надо править вместе с числом --bank.  С
n_banks=4 и пятым банком crt0 выделил четыре страницы, _bank_pages[5]
остался нулём, и первый же вызов pop_ctrl_init() через трамплин
отобразил в W3 страницу 0 и прыгнул на 0xC874 в мусор — исполнение
забрело в дисковый код DSS и осталось крутить чтение секторов.  Симптом:
загрузка ресурсов проходит целиком (open=45 — все атласы), комната и Кид
успевают нарисоваться из enter_room, а HP и номер комнаты уже нет, и
kid_tick не вызывается ни разу.  Ровно предупреждение из шапки
runtime/bank.s.  Сверку n_banks с реальным максимальным индексом банка
записал в docs/TODO.md (Auto-banking) — это должно быть ошибкой сборки.

DBG-CHEATS: [ (0x54) и ] (0x5B) двигают Кида на пиксель (seg000:1828),
по фронту нажатия, под pop_cheats.  Нужны потому, что мост MAME теряет
нажатия при быстрой отправке и подогнать Кида в позу скриптом нельзя —
на это упёрлись BUG-LOOSE-2 и BUG-GATE-PASS-1.

ROOMNAV больше не зовёт pop_trob_reset: reset обнуляет room_seen, то есть
чит ОТМАТЫВАЛ МИР (открытые/закрытые ворота, нажатые кнопки).  Навигация
обязана только телепортировать.

Проверено в MAME: старт уровня 1 рисуется полностью (комната, Кид, HP,
номер), бег вправо и падение на второй ряд отрабатывают, ] даёт x+1 и
[ даёт x−1 по одному нажатию, Shift+→ — осторожный шаг (x 131 -> 142,
колонка 4 -> 5) и при удержании 90 кадров не срывается в бег (x 142 ->
151), то есть pop_ctrl_shift_held работает через границу банк 3 -> банк 5.
Наборы под ucsim: geom 39, grab 53, phys 1723 — все зелёные.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Александр Петров
2026-08-05 13:45:15 +03:00
parent cfc3602375
commit 4737ec323c
8 changed files with 126 additions and 31 deletions
+9 -2
View File
@@ -14,14 +14,21 @@ MEMORY ?= huge # small-раскладка + банки кода в W
# банк1→банк2→банк1 в MAME 2026-07-30). Теперь в банки можно уводить и
# логику, которая рисует. См. docs/layout_plan_v2.md §2, memory
# sdcc_banked_call_rules.
EXTRA_FLAGS ?= --gfx 256 --bank 2=pop_bg.c --bank 4=pop_gdraw.c --bank 3=pop_map.c --bank 1=guards.c $(PROF_FLAGS)
# Банк 5 = pop_ctrl.c (MEM-BANK5). CODE и DATA делят одно 32-КБ
# пространство W1+W2, поэтому килобайт кода, уехавший в банк, — это
# килобайт, доступный данным. Кандидат выбран не по размеру, а по частоте
# вызова: диспетчер управления дёргается один раз за кадр на персонажа и
# горячих банк→банк переходов не создаёт (в отличие от pop_level, чей
# pop_level_tile зовётся из банка 2 на КАЖДЫЙ тайл). Шина control_*
# переехала в pop_state.c — см. там.
EXTRA_FLAGS ?= --gfx 256 --bank 2=pop_bg.c --bank 4=pop_gdraw.c --bank 3=pop_map.c --bank 1=guards.c --bank 5=pop_ctrl.c $(PROF_FLAGS)
# Профилирование полосами бордюра (см. PROF() в roomtest.c) — ВКЛЮЧЕНО по
# умолчанию, пока идёт работа с производительностью; make PROF=0 выключает.
# Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте
# MAME = время фазы в растровых строках.
PROF ?= 1
PROF_FLAGS := -DPROF_BORDER=$(PROF)
EXTRA_SRCS := pop_state.c pop_draw.c pop_kid.c pop_ctrl.c pop_level.c pop_trob.c pop_redraw.c pop_geom.c pop_guard.c
EXTRA_SRCS := pop_state.c pop_draw.c pop_kid.c pop_level.c pop_trob.c pop_redraw.c pop_geom.c pop_guard.c
BG_DIR := $(CURDIR)/../poc/res/bg
KID_DIR := $(CURDIR)/../poc/res/kid
+2 -2
View File
@@ -44,8 +44,8 @@
| 4 | [L3-CHKP](#l3-chkp) | чекпойнт ур. 3 | корректный респавн ур. 3 |
| — | [L1-SPEED](#l1-speed) | игра на ~39 % быстрее оригинала | ощущение от ВСЕХ уровней; берётся в любой момент |
| — | [L1-PASS](#l1-pass) | сквозной прогон ур. 1 + таблица 24 комнат | приёмка ур. 1 |
| | [DBG-CHEATS](#dbg-cheats) | `[`/`]` — подгонка Кида по X | отладка (BUG-GATE-PASS-1) |
| | [MEM-BANK5](#mem-bank5) | новый банк кода: W1/W2 осталось 38 Б кучи | берётся по факту нехватки места |
| | [DBG-CHEATS](#dbg-cheats) | `[`/`]` — подгонка Кида по X | **СДЕЛАНО 2026-08-05** |
| | [MEM-BANK5](#mem-bank5) | `pop_ctrl.c` → банк 5 | **СДЕЛАНО 2026-08-05**: куча 180 Б → 2298 Б |
---
+4 -1
View File
@@ -6,7 +6,8 @@
* просто глобальный флаг: main ставит его при старте (на время разработки
* ВКЛЮЧЁН). Клавиши читов обрабатывает главный цикл.
*
* Реализовано: K убить стража (seg000:786). Остальные читы оригинала
* Реализовано: K убить стража (seg000:786), I/S/Shift+L (наши отладочные),
* [ / ] подгонка Кида по X (seg000:1828). Остальные читы оригинала
* (время, перо, воскрешение, переход по комнатам) пока не портированы.
*/
#ifndef POP_CHEAT_H
@@ -20,6 +21,8 @@ extern uint8_t pop_cheats; /* 0 = выключены, иначе включ
#define KBD_CHEAT_IMMO 0x43 /* I (PS/2 set 2) — бессмертие Кида (toggle) */
#define KBD_CHEAT_SWORD 0x1B /* S (PS/2 set 2) — выдать Киду меч */
#define KBD_CHEAT_NEXTLVL 0x4B /* Shift+L (PS/2 set 2) — следующий уровень */
#define KBD_CHEAT_XDEC 0x54 /* [ (PS/2 set 2) — сдвинуть Кида на 1 px влево */
#define KBD_CHEAT_XINC 0x5B /* ] (PS/2 set 2) — сдвинуть Кида на 1 px вправо */
/* Последний уровень игры (tbl_* в pop_level рассчитаны на 0..15). */
#define POP_LAST_LEVEL 15
+15 -14
View File
@@ -17,6 +17,7 @@
*/
#include <stdint.h>
#include <kbd_raw.h>
#include "pop_ctrl.h" /* свой API (__banked) + объявления шины control_* */
#include "pop_kid.h"
#include "pop_map.h"
#include "pop_guard.h" /* charid, состояние меча, seq стража */
@@ -73,15 +74,15 @@
#define FRAME_109_CROUCH 109
/* ---- Состояние управления (seg006 globals) ------------------------ *
* control_x/y/shift «сырое» состояние осей ЭТОГО кадра (абсолютно:
* X LEFT/-1..RIGHT/+1); *_forward/backward/up/down/shift2 конечный
* автомат held/released/ignore (авто-повтор); ctrl1_* их память между
* кадрами (rest/save). */
int8_t control_x, control_y, control_shift;
/* НЕ static: в оригинале это глобалы, через которые ИИ стража подаёт
* СИНТЕТИЧЕСКИЙ ввод (autocontrol_*, seg002), а дальше работает тот же
* control(), что и у игрока. Поэтому шина общая. */
int8_t control_forward, control_backward, control_up, control_down, control_shift2;
* Сами control_* ЖИВУТ В pop_state.c, а не здесь. Модуль уехал в БАНК 5,
* а шину пишут с трёх сторон (этот банк, guards.c из банка 1) и читают из
* банка 3 (pop_map) это ровно тот случай, для которого pop_state и
* заведён. Сегодня оно работало бы и отсюда: банки собираются БЕЗ
* --bank-data, поэтому их писучие данные остаются в общем _DATA. Но это
* флаг сборки, а не свойство кода: включат --bank-data ради экономии
* W1/W2 и шина молча уедет в страницу банка.
*
* ctrl1_* остаются здесь: их трогает только этот модуль. */
static int8_t ctrl1_forward, ctrl1_backward, ctrl1_up, ctrl1_down, ctrl1_shift2;
/* ---- seqtbl_offset_char: сменить последовательность Kid ------------ *
@@ -549,7 +550,7 @@ static void control_with_sword(void)
}
/* control() — диспетчер по кадру (seg005.c:252) для АКТИВНОГО персонажа. */
void pop_control(void)
void pop_control(void) __banked
{
uint8_t f = Char.frame;
/* seg005 control(), самая первая ветка: персонаж УМИРАЕТ (alive >= 0).
@@ -674,7 +675,7 @@ static void save_ctrl_1(void)
}
/* ---- Публичный API ------------------------------------------------ */
int pop_ctrl_init(void)
int pop_ctrl_init(void) __banked
{
control_forward = control_backward = control_up = control_down = CONTROL_RELEASED;
control_shift2 = CONTROL_RELEASED;
@@ -682,17 +683,17 @@ int pop_ctrl_init(void)
return kbd_raw_open();
}
void pop_ctrl_close(void)
void pop_ctrl_close(void) __banked
{
kbd_raw_close();
}
int8_t pop_ctrl_shift_held(void)
int8_t pop_ctrl_shift_held(void) __banked
{
return (int8_t)(control_shift == CONTROL_HELD);
}
void pop_ctrl_tick(void)
void pop_ctrl_tick(void) __banked
{
kbd_raw_sync(); /* снять залипшее после Rx-overrun SIO */
/* control_kid (seg006:0CD1), ПЕРВАЯ строка: HP кончились — персонаж
+15 -5
View File
@@ -18,19 +18,26 @@
#include <stdint.h>
/* МОДУЛЬ ЖИВЁТ В БАНКЕ 5 (--bank 5=pop_ctrl.c), поэтому всё, что зовут
* снаружи, помечено __banked. Зовут с трёх сторон: главный цикл
* (резидент), guards.c (банк 1) и pop_map.c (банк 3) банкбанк
* работает, трамплин сохраняет страницу окна на стеке. Диспетчер
* дёргается один раз за кадр на персонажа, так что цена перехода
* незаметна. Сама шина control_* лежит в pop_state.c (см. ниже). */
/* Открыть raw-канал клавиатуры и обнулить память авто-повтора.
* 0 OK, -1 ошибка (EBUSY и пр., см. kbd_raw_open). */
int pop_ctrl_init(void);
void pop_ctrl_close(void);
int pop_ctrl_init(void) __banked;
void pop_ctrl_close(void) __banked;
/* Один кадр ввода: прочитать клавиши -> обновить control_* -> прогнать
* диспетчер control() (может сменить Kid.curr_seq). Звать ПЕРЕД
* kid_tick() (play_seq продолжит с новой последовательности). */
void pop_ctrl_tick(void);
void pop_ctrl_tick(void) __banked;
/* control_shift зажат ПРЯМО СЕЙЧАС? — для check_grab в физике (pop_map):
* зацеп за уступ в падении по Shift. Валидно после pop_ctrl_tick. */
int8_t pop_ctrl_shift_held(void);
int8_t pop_ctrl_shift_held(void) __banked;
/* ---- Шина синтетического ввода (порт глобалов control_* из seg005) ----
* В оригинале ИИ соперника (autocontrol_*, seg002) не имеет своего
@@ -42,6 +49,9 @@ int8_t pop_ctrl_shift_held(void);
/* Оси ЭТОГО кадра (абсолютные): X = -1 влево / +1 вправо, Y = -1 вверх /
* +1 вниз. Для ИИ «вперёд/назад» уже в системе персонажа, поэтому оси он
* заполняет как есть, без flip_control_x. */
/* Определены в pop_state.c (резидент W1/W2), а не в pop_ctrl.c: шину
* трогают три банка сразу, а данные банкового модуля живут в _DATA лишь
* пока сборка идёт без --bank-data. */
extern int8_t control_x, control_y, control_shift;
#define POP_CONTROL_HELD_FORWARD (-1)
#define POP_CONTROL_HELD_BACKWARD 1
@@ -53,6 +63,6 @@ extern int8_t control_forward, control_backward, control_up, control_down,
/* Диспетчер (seg005:252) для АКТИВНОГО персонажа Char. Игрок идёт через
* pop_ctrl_tick (тот делает user_control с зеркалом «вперёд»), страж из
* своего тика напрямую. */
void pop_control(void);
void pop_control(void) __banked;
#endif
+15
View File
@@ -11,9 +11,24 @@
* Модуль обязан оставаться в W1/W2: его читают и главный цикл, и банки.
*/
#include "pop_state.h"
#include "pop_ctrl.h" /* объявления шины control_* (живёт здесь) */
uint8_t pop_loose_landed;
/* ---- Шина управления (seg006 globals) ------------------------------ *
* Объявлены в pop_ctrl.h, а ЛЕЖАТ здесь: к ним обращаются ТРИ банка
* pop_ctrl (банк 5) пишет с клавиатуры, guards.c (банк 1) подаёт
* синтетический ввод ИИ (autocontrol_*, seg002), pop_map (банк 3) читает
* через pop_ctrl_shift_held. Сегодня оно уцелело бы и в pop_ctrl.c
* (собираем без --bank-data, данные банков остаются в _DATA), но это
* флаг сборки: включат его и шина уедет в страницу банка 5.
*
* control_x/y/shift «сырое» состояние осей ЭТОГО кадра (абсолютно:
* X LEFT/-1..RIGHT/+1); *_forward/backward/up/down/shift2 конечный
* автомат held/released/ignore (авто-повтор). */
int8_t control_x, control_y, control_shift;
int8_t control_forward, control_backward, control_up, control_down, control_shift2;
/* Чит «бессмертие» (клавиша I). Отладочный: Кид получает анимацию удара,
* но HP не теряет иначе проверять боёвку приходится с трёх попаданий. */
uint8_t pop_immortal;
+37 -7
View File
@@ -49,11 +49,18 @@ __sfr __at (0xFE) io_border;
/* Сколько банков грузит crt0_banked (читает его стартовый код).
* 1 = guards (ИИ/боёвка), 2 = pop_bg (фон, fore, loose), 3 = pop_map
* (коллизия/физика), 4 = pop_gdraw (отрисовка стража + полоса HP).
* (коллизия/физика), 4 = pop_gdraw (отрисовка стража + полоса HP),
* 5 = pop_ctrl (диспетчер управления).
* Отрисовка стража вынесена из банка 2 отдельно: тот подошёл к 16 КБ
* вплотную (16 021 из 16 384), а расти ему ещё есть куда тайлы поздних
* уровней, чомперы, зеркало. */
const uint8_t n_banks = 4;
* уровней, чомперы, зеркало.
*
* ДЕРЖАТЬ В СОГЛАСИИ с числом --bank в Makefile. Забыть это не ошибка
* сборки, а зависание: crt0 выделит меньше страниц, _bank_pages[N] так и
* останется нулём, и трамплин прыгнет по 0xC000 в мусор (см. шапку
* runtime/bank.s). Проверено 2026-08-05: с n_banks=4 и пятым банком игра
* доходила до конца загрузки ресурсов и вставала намертво в DSS. */
const uint8_t n_banks = 5;
/* Режим читов (pop_cheat.h): на время разработки включаем в main. */
uint8_t pop_cheats;
@@ -419,6 +426,7 @@ int main(void)
uint8_t nav_prev = 0;
#endif
uint8_t cheat_prev = 0, immo_prev = 0, sword_prev = 0, nlvl_prev = 0;
uint8_t xdec_prev = 0, xinc_prev = 0;
if (pop_bg_load() != 0) { /* атласы — ДО initgraph (снимает DSS-страницу W0) */
puts("pop_bg_load failed");
@@ -505,21 +513,35 @@ int main(void)
* тестировать уровни 2..15. NB: L без Shift отладочный
* «осторожный шаг вправо» (pop_ctrl KBD_DBG_STEPR); с
* Shift шаг тоже пройдёт, но уровень тут же сменится, так
* что конфликта на практике нет. */
* что конфликта на практике нет;
* [ / ] подвинуть Кида на ПИКСЕЛЬ влево/вправо (seg000:1828,
* `--Char.x` / `++Char.x`). Нужен потому, что мост MAME
* теряет нажатия при быстрой отправке, и подогнать Кида в
* нужную позу скриптом нельзя (на это упёрлись BUG-LOOSE-2
* и BUG-GATE-PASS-1). Пишем прямо в Kid.x: геометрию
* персонажа в этом порте задаёт не диспетчер (грабли
* L1-EXIT). Колонку не трогаем её пересчитает физика
* следующего кадра. */
uint8_t k = kbd_raw_down(KBD_CHEAT_KILL);
uint8_t i = kbd_raw_down(KBD_CHEAT_IMMO);
uint8_t s = kbd_raw_down(KBD_CHEAT_SWORD);
uint8_t nl = (uint8_t)(kbd_raw_down(KBD_CHEAT_NEXTLVL) &&
(kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT)));
uint8_t xd = kbd_raw_down(KBD_CHEAT_XDEC);
uint8_t xi = kbd_raw_down(KBD_CHEAT_XINC);
if (pop_cheats && k && !cheat_prev) pop_guard_kill();
if (pop_cheats && i && !immo_prev) pop_immortal = !pop_immortal;
if (pop_cheats && s && !sword_prev) pop_have_sword = 1;
if (pop_cheats && nl && !nlvl_prev && pop_current_level < POP_LAST_LEVEL)
pop_next_level = (uint8_t)(pop_current_level + 1);
if (pop_cheats && xd && !xdec_prev) Kid.x = (uint8_t)(Kid.x - 1);
if (pop_cheats && xi && !xinc_prev) Kid.x = (uint8_t)(Kid.x + 1);
cheat_prev = k;
immo_prev = i;
sword_prev = s;
nlvl_prev = nl;
xdec_prev = xd;
xinc_prev = xi;
}
if (frozen) {
PROF(0); /* чёрный: ждём vsync */
@@ -530,8 +552,17 @@ int main(void)
#ifdef ROOMNAV
/* Обход комнат по номеру: '+' (0x79 numpad / 0x55 '=') — вперёд,
* '-' (0x7B / 0x4E) назад. Edge по nav_prev: одно нажатие = одна
* комната. pop_trob_reset ДО enter_room каждая комната смотрится в
* исходном состоянии (пики убраны, ворота закрыты). */
* комната.
*
* Здесь БЫЛ pop_trob_reset() перед enter_room «чтобы каждая комната
* смотрелась в исходном состоянии». Убран: reset обнуляет room_seen,
* и модификаторы перечитываются из уровня заново, то есть чит
* ОТМАТЫВАЕТ МИР открытые/закрытые ворота, выдвинутые пики, нажатые
* кнопки. Пока ворота с bg=1 ошибочно стартовали закрытыми, откат был
* не виден; после порта load_alter_mod он выглядит как баг «ворота
* снова открылись» (уровень 1, решётка комнаты 5 (0,9), которую
* закрывает стартовый триггер do_startpos). Навигация обязана только
* телепортировать; исходное состояние даёт перезапуск уровня. */
{
uint8_t nav = (uint8_t)((kbd_raw_down(0x79) || kbd_raw_down(0x55)) |
((kbd_raw_down(0x7B) || kbd_raw_down(0x4E)) << 1));
@@ -539,7 +570,6 @@ int main(void)
uint8_t r = cur_room, i, c, pos = 20, any = 0xFF;
r = (nav & 1) ? (uint8_t)(r == 24 ? 1 : r + 1)
: (uint8_t)(r == 1 ? 24 : r - 1);
pop_trob_reset();
enter_room(r);
/* Предпочитаем обычный пол; иначе — любой tile_is_floor
* (seg001): всё, кроме пустоты/стены/верхов колонны и двери. */
+29
View File
@@ -61,6 +61,35 @@
## Auto-banking (memory/banking_roadmap.md)
- [ ] **Сверять `n_banks` с реальным числом банков — ошибкой сборки.**
Сейчас число банков задаётся ДВАЖДЫ и вручную: флагами `--bank N=…`
у `sprinter-cc` и константой `const uint8_t n_banks = N;` в
приложении (её читает `crt0_banked` при старте, чтобы выделить
EMM-страницы и загрузить в них образы банков). Рассогласование не
даёт ни ошибки компиляции, ни ошибки линковки — только зависание в
рантайме: `_bank_pages[N]` остаётся нулём, трамплин отображает в
окно страницу 0 и прыгает по 0xC000 в мусор (предупреждение об этом
уже есть в шапке `runtime/bank.s`).
**Найдено 2026-08-05** на PoP/roomtest: добавили пятый банк
(`--bank 5=pop_ctrl.c`), забыли `n_banks` — игра доходила до конца
загрузки ресурсов и вставала намертво в дисковом коде DSS. Диагноз
занял заметно больше, чем сама правка: симптом (зависание в чужом
коде) никак не указывает на причину.
Сверять надо не количество, а **максимальный индекс** банка: `--bank`
нумеруются с 1 и в принципе могут идти с дырами, а `crt0_banked`
заполняет `_bank_pages[1..n_banks]` подряд.
Варианты, от простого к правильному:
1. **Проверка после линковки** — вытащить значение `_n_banks` из
`.ihx`/`.map` (это `const` в `_CODE`, адрес известен из карты) и
сравнить с `max(N)` по флагам; не совпало — `sprinter-cc` падает
с внятным сообщением. Ничего не ломает в существующих
приложениях, ставится сразу.
2. **Генерировать самим `sprinter-cc`** — он уже так делает, когда
банков нет (`const unsigned char n_banks = 0;`). Правильнее по
сути (единственный источник истины), но требует убрать константу
из всех банковых приложений, иначе дубль символа: `tests/banked`,
`banktest`, `banklocl`, `bankedbg`, `applications/PoP/roomtest`.
Phase 1 — file-level bin-packing (`toolchain/auto_bank.py`) — когда
проект перерастёт ~30 KB кода: парсинг размеров из .rel/.map,
first-fit-decreasing, уважение `#pragma codeseg BANKn`, перелинковка,