Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6dabe9b4b1 | |||
| a05970cd36 | |||
| 2176c12cc5 | |||
| 4e43890fce | |||
| 25b2db8b0b |
@@ -81,9 +81,18 @@ all: $(EXE)
|
||||
# уже собранный exe (кусало: фикс bank.s не подхватился).
|
||||
RUNTIME_DEPS := $(wildcard $(PROJ_ROOT)/runtime/*.s)
|
||||
|
||||
# ПРОВЕРКА БАНКОВЫХ ВЫЗОВОВ — сразу после линковки, пока артефакты свежие.
|
||||
# Ловит прямой `call` в чужой банк: он собирается МОЛЧА и стреляет диким
|
||||
# переходом в пустой хвост банка (разбор — в шапке скрипта). Запускается
|
||||
# только если банки вообще есть, то есть по наличию каталога сборки с
|
||||
# bankN_*.asm; обычным небанковым программам ничего не стоит.
|
||||
BANK_CHECK := $(PROJ_ROOT)/toolchain/check_bank_calls.py
|
||||
|
||||
$(EXE): $(SOURCES) $(MKEXE) $(LIB) $(RUNTIME_DEPS)
|
||||
$(if $(strip $(BUILD_DIR)),@mkdir -p $(dir $@))
|
||||
$(SPRINTER_CC) $(CC_FLAGS) -o $@ $(SOURCES)
|
||||
@d=$(dir $@).sprinter-cc-$(EXAMPLE); \
|
||||
if ls $$d/bank*_*.asm >/dev/null 2>&1; then python3 $(BANK_CHECK) $$d; fi
|
||||
|
||||
$(MKEXE):
|
||||
$(MAKE) -C $(PROJ_ROOT)/toolchain/mkexe
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
* Left: turn or run left
|
||||
* Right: turn or run right
|
||||
* Up: jump or climb up
|
||||
* Down: crouch or climb down
|
||||
* Down+Left/Right: hop
|
||||
* Shift: pick up things
|
||||
* Shift+Left/Right: careful step
|
||||
* Home or Up+Left: jump left
|
||||
* Page Up or Up+Right: jump right
|
||||
* Up while running: running jump
|
||||
* Shift while falling: grab onto ledge
|
||||
|
||||
* Left/Right: walk (advance or retreat)
|
||||
* Shift: strike (attack)
|
||||
* Up: block (defend)
|
||||
* Down: put sword away; press Shift to draw your sword again.
|
||||
|
||||
===
|
||||
|
||||
* Esc: Pause game.
|
||||
* Space: Show how much time is left.
|
||||
* Ctrl+A: Restart level.
|
||||
|
||||
* Ctrl+R: Return to intro.
|
||||
* Ctrl+S: Sound on/off.
|
||||
* Ctrl+M: Music on/off.
|
||||
* Ctrl+V: Show version of SprPoP.
|
||||
* Ctrl+Q: Quit game.
|
||||
|
||||
* F6: Quicksave: Save the exact state of the game.
|
||||
* F9: Quickload: Load what the last quicksave saved.
|
||||
* F12: Save a screenshot to the screenshots folder.
|
||||
* Backspace: Display the in-game menu. (Esc will also display the menu by default, but you can turn that off.)
|
||||
|
||||
* Shift+L: Go to next level.
|
||||
* -: Decrease remaining time by one minute.
|
||||
* +: Increase remaining time by one minute.
|
||||
* R: Resurrect kid.
|
||||
* K: Kill guard.
|
||||
* Shift+I: Flip the screen upside down.
|
||||
* Shift+W: Slow falling.
|
||||
* Shift+S: Restore a lost hit-point. (Like a small red potion.)
|
||||
* Shift+T: Give more hit-points. (Like a big red potion.)
|
||||
|
||||
===
|
||||
|
||||
* H: Look at the room to the left.
|
||||
* J: Look at the room to the right.
|
||||
* U: Look at the room above.
|
||||
* N: Look at the room below.
|
||||
* Ctrl+B: Go back to the room where the prince is. (Undo H,J,U,N.)
|
||||
|
||||
===
|
||||
|
||||
* [: Shift kid 1 pixel to the left.
|
||||
* ]: Shift kid 1 pixel to the right.
|
||||
* T: Toggle display of timer (remaining minutes:seconds:ticks). Also shows the total elapsed ticks during playback.
|
||||
|
||||
@@ -3630,3 +3630,115 @@ public `_pop_mus_page` по смещениям из `.sym`):
|
||||
брейкпоинте, значения в памяти застывают — картина «`busy=0` при `next=3` из
|
||||
20» выглядела как обрыв чтения с ошибкой, хотя это был просто стоп-кадр.
|
||||
Прежде чем толковать значения, проверять `status`: `state=run` или `stop`.
|
||||
|
||||
---
|
||||
|
||||
<a id="final-bankcall"></a>
|
||||
## FINAL-BANKCALL. Финал убивал программу: прямой вызов в чужой банк
|
||||
|
||||
**Симптом (пользователь, 2026-08-27).** Пройденная игра доходила до
|
||||
таблицы рекордов и умирала: программа исчезала, а машина следом вставала
|
||||
намертво (`di; halt` по адресу 0x0000) либо уходила в reset. Из Flex
|
||||
Navigator и из голого DSS — одинаково.
|
||||
|
||||
**Что оказалось не при чём** (проверено, чтобы не искать заново): Flex
|
||||
Navigator, звук и CBL (с выключенным звуком падало так же), потоковое
|
||||
чтение победной темы, второй `open` поверх открытого файла, фейды с
|
||||
клавиатурным диспетчером, чтение архива PV, межбанковый вызов
|
||||
банк 10 → банк 11, переполнение банка (самый полный — BANK8, 15376 из
|
||||
16384), утечка манипуляторов (`_fd_open_count` = 1) и утечка ссылок на
|
||||
IM2-таблицу (`_irq_refs` = 2, как задумано).
|
||||
|
||||
**Как искали.** Бисекция ключом `HOF=0/2/4/5` (пропустить таблицу целиком
|
||||
/ только `hof_load` / без фона / без фейдов) сузила место до
|
||||
`hof_draw_rows`, а точную инструкцию дала ТРАССИРОВКА MAME на узком
|
||||
участке: `trace` включалась брейкпоинтом на входе в `pop_hof_show`
|
||||
(0x44AB) и выключалась на процедуре завершения процесса DSS (0x1E56).
|
||||
`history` для этого не годится — её 250 записей забиваются обработчиком
|
||||
прерываний, пока процессор ползёт по мусору.
|
||||
|
||||
**Корень.**
|
||||
|
||||
```
|
||||
CD42: ld hl,$B8AB ← _gfx_bank
|
||||
CD45: ld (hl),$58 ← gfx_set_bank(GFX_BANK_TRANSPARENT)
|
||||
CD47: call $E503 ← ПРЯМОЙ вызов; по карте 0009E503 = _pop_text_map, банк 9
|
||||
E503: rst $38 ← а в W3 стоит банк 10, там пустой хвост (0xFF)
|
||||
```
|
||||
|
||||
`pop_ui.h` объявлял группу `pop_text_*_mapped` БЕЗ `__banked`. Пока
|
||||
`pop_hof.c` лежал в банке 9 рядом с `pop_ui.c`, прямой `call` был верен.
|
||||
Когда `pop_hof`/`pop_config`/`pop_pal` перенесли в банк 10 ради разгрузки
|
||||
банка 9, тот же `call` стал уходить в пустоту. Процессор полз по 0xFF до
|
||||
0x0000, где ловушка DSS ставит B=0x27 и сворачивает процесс: закрывает его
|
||||
файлы, возвращает контекст (байт 0x1E48: 2 = шелл, 3 = наша программа) и
|
||||
восстанавливает страницы родителя. Именно это выглядело как «W2 подменили
|
||||
у нас под ногами» — на самом деле подмена была уже уборкой трупа.
|
||||
|
||||
**Фикс.** Группа помечена `__banked`; появилась
|
||||
`toolchain/check_bank_calls.py`, встроенная в `app.mk` и валящая сборку
|
||||
(проверено намеренной поломкой). Правило записано в `CLAUDE.md`.
|
||||
|
||||
---
|
||||
|
||||
<a id="final-hof-garbage"></a>
|
||||
## FINAL-HOF-GARBAGE. Таблица рекордов заливалась знаками вопроса
|
||||
|
||||
**Симптом.** После фикса FINAL-BANKCALL экран HOF рисовался правильно, но
|
||||
с началом ввода имени строку заливало `?` во всю ширину.
|
||||
|
||||
**Корень — вторая мина того же класса, созданная первым фиксом.** Курсор
|
||||
рисовался литералом:
|
||||
|
||||
```c
|
||||
pop_text_draw_mapped(POP_TEXT_BIG_DARK, x, baseline, "_"); /* ___str_3 в _BANK10 */
|
||||
```
|
||||
|
||||
Пока `pop_text_draw_mapped` был в том же банке, литерал был виден. После
|
||||
пометки `__banked` трамплин на время вызова переключает W3 на банк 9 —
|
||||
указатель показывает в чужой банк, функция читает оттуда байты до первого
|
||||
нуля и рисует их знаками вопроса. Сходится и с наблюдением пользователя
|
||||
«сначала рисуется правильно, потом мусор»: строка времени берётся из
|
||||
локального массива (стек, W2 — виден всем), а мусор начинается с мигания
|
||||
курсора.
|
||||
|
||||
**Фикс.** Курсор строится на стеке. Вторая проверка
|
||||
`check_bank_calls.py` предупреждает о таких случаях (проверено: на сборке
|
||||
с возвращённым литералом даёт ровно одно срабатывание).
|
||||
|
||||
---
|
||||
|
||||
<a id="kbd-arrow-phantom"></a>
|
||||
## KBD-ARROW-PHANTOM. Залипшая стрелка вешала ожидания насмерть
|
||||
|
||||
**Симптом.** После игры (особенно 14-й уровень — он почти весь проходится
|
||||
удержанием Left) текстовые экраны переставали прерываться, а «новая игра»
|
||||
вставала намертво: `pop_new_game_load` начинается с
|
||||
`while (kbd_raw_any_down())`. При этом ввод имени в HOF работал —
|
||||
он опрашивает конкретные коды, а не всю карту.
|
||||
|
||||
**Корень (подтверждён дважды: 0x72 24.08, 0x6B 27.08).** Стрелка идёт по
|
||||
проводу как `E0 6B`. При переполнении 3-байтового FIFO SIO теряется
|
||||
префикс, make садится в PLAIN-половину карты как код нумпада (0x6B = KP4),
|
||||
а break приходит уже с префиксом и снимает бит в EXT-половине. PLAIN-бит
|
||||
остаётся зажатым навсегда, `kbd_raw_any_down()` отвечает «да» вечно.
|
||||
Доказано вмешательством: запись 0 в этот байт отладчиком мгновенно
|
||||
перевела состояние 5 (LEVEL_LOAD) → 6 (PLAYING).
|
||||
|
||||
**Фикс.** `kbd_raw_keypad_as_ext()` в libc: `kbd_raw_sync()` переносит
|
||||
биты голых кодов нумпада в EXT-половину и гасит в PLAIN — make и break
|
||||
начинают работать с одним битом. В PS/2 это и есть одни и те же
|
||||
физические клавиши, поэтому нумпад заодно стал управлением (7/8/9, 4/6,
|
||||
2 и 5 — вниз). НЕ лечит обратный случай (префикс потерян у break) — там
|
||||
бит стоит уже в EXT и выглядит как реально зажатая клавиша.
|
||||
|
||||
---
|
||||
|
||||
<a id="f10-gameplay"></a>
|
||||
## F10-GAMEPLAY. Выход по F10 не работал в игре
|
||||
|
||||
F10 проверяется в idle-хуке, а хук висит на `gfx_wait_vsync`. Когда
|
||||
игровой цикл перевели на пейсинг по лучу, он стал ждать через
|
||||
`pop_wait_edge`, который звал `kbd_raw_poll` напрямую — хук не вызывался,
|
||||
и F10 в геймплее умер (в заставках и меню работал). Фикс: `pop_wait_edge`
|
||||
зовёт тот же `pop_idle`.
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
PROJ_ROOT := $(abspath $(CURDIR)/../../..)
|
||||
EXAMPLE := roomtest
|
||||
HDD_IMG := $(CURDIR)/build/hdd/roomtest.chd
|
||||
|
||||
# small: W1+W2 как единое 32КБ-пространство, DATA сразу за CODE (иначе
|
||||
# CODE ~23КБ переполняет W1 и лезет в фиксированный DATA@0x8000 в huge).
|
||||
MEMORY ?= huge # small-раскладка + банки кода в W3 (стражи)
|
||||
@@ -88,7 +90,7 @@ EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/../poc/res/bg --max-allocs $(ALLOCS) \
|
||||
# умолчанию, пока идёт работа с производительностью; make PROF=0 выключает.
|
||||
# Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте
|
||||
# MAME = время фазы в растровых строках.
|
||||
PROF ?= 1
|
||||
PROF ?= 0
|
||||
PROF_FLAGS := -DPROF_BORDER=$(PROF)
|
||||
# Стартовый уровень (отладка): make LEVEL=9 — начать сразу с девятого.
|
||||
# Дефолт держим на отлаживаемом сейчас уровне; для «настоящей» игры — LEVEL=1.
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
# Точка входа для следующей сессии (обновлено 2026-08-12, ночь)
|
||||
|
||||
Файл для старта с чистого контекста: где всё стоит, что делать первым, какие
|
||||
грабли уже собраны. Читать целиком — он короткий. Дальше по ссылкам:
|
||||
`TASKS_OPEN.md` (доска), `BUGS_OPEN.md` (открытые баги), `CLAUDE.md`.
|
||||
|
||||
---
|
||||
|
||||
## 1. СЛЕДУЮЩАЯ ЗАДАЧА: smoke-прогон уровня 9 и регресс 1-8
|
||||
|
||||
Переворот (зелье инверсии) **доведён и принят пользователем 2026-08-12**:
|
||||
фон, персонажи, клинок, полоса HP, факелы, окклюзия передних граней и
|
||||
переходы между комнатами работают зеркально; чит `U` и зелёное зелье идут
|
||||
одним путём. Осталось пройти сам уровень 9 живьём и убедиться, что на
|
||||
уровнях 1-8 ничего не поехало (весь слой фона теперь спрашивает `pop_upside`).
|
||||
|
||||
Что смотреть на прогоне в первую очередь:
|
||||
|
||||
- **вис на кромке и спуск в перевёрнутом виде** — это новый срез `clip_char`
|
||||
(он режет кадр СНИЗУ вместо верха); тело должно уходить за кладку, а не
|
||||
ложиться поверх неё;
|
||||
- **бой со стражем перевёрнутым** — у соперника свой зеркальный набор
|
||||
(`g*_v.atl`), а тень рисуется атласами Кида, отдельных файлов ей не надо;
|
||||
- **зелье в другой комнате** — переворот перерисовывает не комнату, а чистит
|
||||
факелы и отражает страницу; проверить, что после смены комнаты и возврата
|
||||
фон остаётся чистым.
|
||||
|
||||
Открытый риск (пока не проявился, но проверить осознанно): имена зеркальных
|
||||
атласов `kid10_v.atl`…`kid27_v.atl` — **9 символов до точки, а DSS работает
|
||||
по 8.3**. Если часть кадров Кида в перевёрнутом виде рисуется обычными
|
||||
(неперевёрнутыми) — это оно; лечится переименованием в `pop_pack_kid.py`
|
||||
(например `k10v.atl`) плюс те же имена в `pop_kboot.c`.
|
||||
|
||||
Дальше по плану — уровни 10/11 (по анализу нового в них нет) и
|
||||
[MEM-COLD2](TASKS_OPEN.md#mem-cold2) п.3, если снова понадобится место.
|
||||
|
||||
Ощущенческий хвост прежний — [GRAB-KBD-TIMING](BUGS_OPEN.md#grab-kbd-timing),
|
||||
разбирается после готовности всех уровней (решение пользователя).
|
||||
|
||||
## 2. Состояние репозитория
|
||||
|
||||
Всё закоммичено, `main` чист. `tests-host`: **6 наборов** проходят
|
||||
(`[char] 65`, `[geom] 3144`, `[grab] 55`, `[mouse] 17`, `[phys] 1727`,
|
||||
`[wall] 1`).
|
||||
|
||||
Коммиты 2026-08-12 (сверху — свежие):
|
||||
|
||||
| хеш | что |
|
||||
|---|---|
|
||||
| `7f5c99b` | чистка факелов при перевороте — в СТАРОЙ системе координат |
|
||||
| `c9aa6d9` | переворот: не перерисовывать комнату, стирать только запечённое пламя |
|
||||
| `948d8f0` | переворот: перерисовка вместо отражения, переключение на границе кадра, `clip_char` |
|
||||
| `5b6664f` | зеркальные кадры персонажей — готовыми файлами `*_v.atl` |
|
||||
| `c4dd2c7` | отсев тайлов по fore-окну — в ЛОГИЧЕСКИХ координатах |
|
||||
| `7551906` | L9-INVERT II.4: персонажи рисуются перевёрнутыми |
|
||||
| `055d6d7` | чтение файлов нельзя выносить в банк (код банка сам живёт в W3) |
|
||||
| `147cb18` | резидент: `pop_kid`/`pop_guard`/`main` расколоты по частоте вызова |
|
||||
| `5d31ed0` | резидент: `pop_redraw` + холодная половина `pop_level` в банки |
|
||||
| `f541c0a` | клип поля при перевороте (мусор в бортах) |
|
||||
|
||||
Уровни 1-8 проходятся, уровень 9 ждёт прогона.
|
||||
|
||||
## 3. Раскладка кода после разгрузки резидента
|
||||
|
||||
Куча выросла **902 → 5795 Б** (`_CODE` 25 628 → 20 569). Кандидатов выбирали
|
||||
не по размеру, а по тому, КТО зовёт: если единственный вызывающий уже в
|
||||
банке, код едет к нему и трамплин не появляется вовсе.
|
||||
|
||||
| банк | модули |
|
||||
|---|---|
|
||||
| 1 | `guards.c` |
|
||||
| 2 | `pop_bg.c` |
|
||||
| 3 | `pop_map.c` |
|
||||
| 4 | `pop_cdraw.c` + **`pop_kdraw.c`** (клинок, блит по id — прямые вызовы) |
|
||||
| 5 | `pop_ctrl.c` |
|
||||
| 6 | `pop_trob.c` |
|
||||
| 7 | `pop_room.c` + **`pop_redraw.c`** |
|
||||
| 8 | `roomtest_cold.c` + **`pop_level_cold.c`** + **`pop_kboot.c`** + **`pop_guard_cold.c`** |
|
||||
|
||||
Внутренние контракты: `_pop_level.h`, `_pop_kid.h`, `_pop_kdraw.h`. В них же
|
||||
записаны два правила, которые линкер НЕ проверяет:
|
||||
|
||||
1. **Банковый модуль не маппит W3** — он в этом окне и исполняется (через W0
|
||||
можно). Поэтому `pop_level_read_file` и `pop_kid_data_load` остались в
|
||||
резиденте: первый же вызов из банка улетал в halt по мусору.
|
||||
2. **Прямые вызовы между модулями одного банка** корректны только пока они
|
||||
действительно в одном банке; меняешь раскладку в Makefile — проверь пары.
|
||||
|
||||
## 4. Переворот (L9-INVERT) — как он устроен
|
||||
|
||||
- **Флаги:** `pop_upside_want` (пишут зелье, смерть Кида, чит `U`) и
|
||||
`pop_upside` (читают все слои). Переключение — ОДНО место, начало кадра,
|
||||
вместе с `pop_flip_screen`. Оригиналу этого не нужно (он рисует в offscreen
|
||||
неперевёрнутым и зеркалит на выводе) — расхождение записано в
|
||||
`../docs/impl_diff.md`.
|
||||
- **Сам переворот:** чистка запечённого пламени (`pop_torch_wipe`, 0..4
|
||||
тайла) + отражение страницы акселератором + прямая копия во вторую.
|
||||
**904 247 тактов = 2.1 кадра.** Вариант с полной перерисовкой комнаты
|
||||
(как `redraw_screen` в оригинале) стоил 6 245 477 = 14.5 кадра — отвергнут.
|
||||
- **Спрайты персонажей:** column-major, вертикальное зеркало для них не
|
||||
бесплатно, поэтому второй набор файлов делает упаковщик (`vflip_cols` в
|
||||
`pop_pack_kid.py`). Загрузка одной страницы с диска — 970 003 такта,
|
||||
разворот той же страницы в рантайме — 12 645 582; поэтому файлы.
|
||||
`pop_vflip_load_all()` идемпотентна и зовётся откуда угодно (сейчас — старт
|
||||
и смена уровня, если это `POP_UPSIDE_LEVEL`; задел под загрузку в интро).
|
||||
- **Координаты:** у окна fore-клипа ДВЕ пары границ — экранная
|
||||
(`pop_t_fclip_y0/y1`, по ней режется блит) и логическая
|
||||
(`pop_t_fclip_ly0/ly1`, по ней идут ВСЕ отсевы тайлов). Смешать их — и
|
||||
передние грани перестают перекрывать персонажа.
|
||||
|
||||
## 5. Состояние окружения
|
||||
|
||||
- **MAME запущена с мостом**, на HDD — roomtest с `LEVEL=9`. Стартовый
|
||||
уровень теперь **параметр сборки**: `make LEVEL=9` (дефолт 9; для «настоящей»
|
||||
игры — `LEVEL=1`), константа объявлена в `pop_tune.h`.
|
||||
- **ПРАВИЛО ПОЛЬЗОВАТЕЛЯ: перед сборкой образа — `make clean`, потом `make`,
|
||||
потом `make hdd`** (memory `mame_build_clean_rule`). После пересборки
|
||||
образа MAME **обязана** пройти полный рестарт (memory
|
||||
`mame_hdd_rebuild_restart`): остановка через `exit` в дебаггере, затем
|
||||
`run_bridge.sh`.
|
||||
- **Тайминги моста** (правило пользователя, длиннее не ждать; они же в
|
||||
`docs/mame-autotest.md` §10): старт `run_bridge.sh` → **6 с** → `go` →
|
||||
**8 с** → `keyseq d:{ENTER}` + `keyseq roomtest{ENTER}` → **5 с** →
|
||||
программа работает. Весь цикл перезапуска ~20 секунд.
|
||||
- **MCP-инструменты `mame-z80` работают** (в отличие от прошлой сессии):
|
||||
`press_key`, `screenshot`, `read_logical_memory`, `debugger_command`.
|
||||
Профилирование — `bpset <addr>` + `print totalcycles`, затем `out` и снова
|
||||
`print totalcycles`; кадр = 430 000 тактов.
|
||||
- Адреса состояния смещаются после КАЖДОЙ пересборки — брать из
|
||||
`.sprinter-cc-roomtest/roomtest.map` заново. Банковые функции лежат по
|
||||
`0x0008XXXX`; для брейкпоинта брать младшие 16 бит (`0xC912` и т.п.).
|
||||
- **SDLPoP собран с отладочной информацией** (`-O0 -g3`).
|
||||
- **НЕ ПРИБРАНО:** в `SDLPoP/src/seg008.c` диагностический `fprintf` с меткой
|
||||
`DBGMIRROR` в начале `add_objtable`.
|
||||
|
||||
## 6. Остальное открытое
|
||||
|
||||
- **[FORE-DUP](BUGS_OPEN.md#fore-dup)** — передний слой тайла рисуется дважды
|
||||
при перекрытии объектов. Картинку не портит, тратит такты. Сначала
|
||||
замерить, потом чинить.
|
||||
- **TORCH-ANIM-RIGHT** — под запечённым пламенем могут застыть пики/меч/зелье
|
||||
справа от факела.
|
||||
- **[GATE-FORE-KID](BUGS_OPEN.md#gate-fore-kid)** — Кид в проёме ворот виден
|
||||
поверх решётки; портирован только шовный случай.
|
||||
- Долгие: `BUG-SPIKE-1`, `BUG-CHOMP-JUMP-1`, `L3-COLOR`, `L1-SPEED`, `TUNE-1`.
|
||||
- **Вид тени (OR+XOR) отложен** до готовности всех уровней —
|
||||
[`../docs/shadow_render.md`](../docs/shadow_render.md).
|
||||
|
||||
## 7. Грабли, собранные 2026-08-12 (вечер/ночь)
|
||||
|
||||
- **`return 1;` из ветки у SDCC 4.5 может не положить единицу в A** — функция
|
||||
возвращает мусор. Годами маскировался (в A случайно оставался ненулевой
|
||||
байт) и вылез ровно на `__banked`. Репро — `docs/bugs/sdcc-z80-ret-const-lost/`,
|
||||
обход — один выход через переменную. Аудит всех `.asm` roomtest: других
|
||||
мест нет.
|
||||
- **Банковый модуль не может маппить W3** (см. §3). Симптом — halt по мусору
|
||||
в 0xC4xx на первом же вызове.
|
||||
- **Логические и экранные координаты при перевороте — разные системы.** Три
|
||||
бага подряд из одного корня: отсев тайлов по fore-окну, `clip_char`, чистка
|
||||
факелов. Правило: всё, что считается ИЗ РЯДА комнаты (`63*row + …`), —
|
||||
логическое; всё, что режет блит, — экранное.
|
||||
- **Пламя факела ЗАПЕКАЕТСЯ в ОЗУ-копию** (heal'а у него нет, следующий кадр
|
||||
накрывает предыдущий непрозрачно). Любое отражение/копирование страницы
|
||||
тащит его с собой — перед такими операциями звать `pop_torch_wipe`.
|
||||
- **`pop_upside` переключать в середине кадра нельзя**: остаток кадра
|
||||
рисуется зеркально поверх ещё неперевёрнутой картинки, и запечённые
|
||||
транзиенты остаются в комнате навсегда.
|
||||
- **Перенос константы между файлами — потерянное значение.** При выносе
|
||||
`FIRST_LEVEL` в `pop_tune.h` записал дефолт 1, и игра молча стартовала с
|
||||
первого уровня вместо девятого; заметил пользователь. Теперь это параметр
|
||||
сборки (`make LEVEL=N`).
|
||||
- **`make -C tests-host` не пересобирает по изменению заголовков** — при
|
||||
правке прототипов (особенно `__banked`) обязателен `make -C tests-host
|
||||
clean`, иначе стаб и вызывающий разъезжаются по ABI и тест врёт.
|
||||
|
||||
## Грабли 2026-08-10/11
|
||||
|
||||
- **Не оценивать железо по своей же memory-заметке.** Дважды поправлял
|
||||
пользователь: accel умеет не только копирование (есть AND/OR/XOR) и умеет
|
||||
работать с обычной RAM, а не только с видео (`docs/part2/accelerator_doc.txt`:
|
||||
«supports data blocks transfer of RAM and Video-RAM», не поддерживает только
|
||||
ROM и FastRAM). Проверять по драйверу MAME и докам, а не по памяти.
|
||||
- **lldb через FIFO — плохая идея.** Работает надёжно: `fprintf(stderr, …)`
|
||||
прямо в SDLPoP, пересборка (секунды) и чтение stdout.
|
||||
- **Доска отставала от кода на три задачи.** Перед планированием — сверять
|
||||
`TASKS_OPEN.md` с кодом грепом, а не верить записи.
|
||||
- Диапазон `obj_x` = **−416..695** (посчитан из `kid_data.bin`).
|
||||
|
Before Width: | Height: | Size: 482 KiB |
|
Before Width: | Height: | Size: 426 KiB |
|
Before Width: | Height: | Size: 532 KiB |
|
Before Width: | Height: | Size: 413 KiB |
|
Before Width: | Height: | Size: 462 KiB |
|
Before Width: | Height: | Size: 825 KiB |
|
Before Width: | Height: | Size: 825 KiB |
|
Before Width: | Height: | Size: 825 KiB |
@@ -1,6 +0,0 @@
|
||||
Уровень 5, комната 2 - с разбега чуть раньше - иногда попадаем в стену (некорректное отображение падения)
|
||||
|
||||
Уровень 7, комната 9 - падает плита -1,1 - звук потери HP у Кида (причем раз даже когда Кид просто бъет по плите стоя на 0,0)
|
||||
|
||||
/// Уровень 10, комната 1 - когда Кид стоя на кнопке 1,8 роняет плиту 0,8 на кнопку 1,8 - кнопка ломается (превращается в щебень)
|
||||
/// но двери которые она должна открыть остаются закрытыми
|
||||
@@ -155,6 +155,32 @@ Makefile склеивает архивы прямо в `assets/packed/<КАТА
|
||||
| `pop_ctrl.c/.h` | Управление: диспетчер `control()` (`seg005`) + ввод `read_user_control` (`seg006`) поверх `<kbd_raw.h>`. |
|
||||
| `pop_map.c/.h` | Коллизия с картой + физика падения/приземления/стены + loose-полы (`seg005/006/007`). |
|
||||
|
||||
## Банки: две мины, которых компилятор не видит
|
||||
|
||||
Обе стреляют молча, обе поймали нас 2026-08-27 (разбор — в
|
||||
`../PoP/roomtest/BUGS_CLOSED.md`, поиск «FINAL-BANKCALL»).
|
||||
|
||||
**1. Прямой вызов в чужой банк.** Трамплин SDCC выбирает по `__banked`
|
||||
в ОБЪЯВЛЕНИИ, а не по тому, где функция лежит. Функция без пометки
|
||||
зовётся прямым `call` — верно ровно пока вызывающий и вызываемый в одном
|
||||
банке. Перенесли модуль ради разгрузки банка — и тот же `call` уходит в
|
||||
пустой хвост чужого банка, процессор ползёт по 0xFF до 0x0000, DSS убивает
|
||||
процесс. Ловит `toolchain/check_bank_calls.py`, она встроена в сборку и
|
||||
ВАЛИТ её (проверено намеренной поломкой).
|
||||
|
||||
**2. Указатель на данные своего банка, отданный в чужой.** Обратная
|
||||
сторона: пока обе стороны в одном банке, указатель на литерал или
|
||||
const-таблицу работает; пометили функцию `__banked` — и трамплин на время
|
||||
вызова переключает W3, а указатель показывает уже в чужой банк. Так экран
|
||||
таблицы рекордов залило знаками вопроса: в `pop_text_draw_mapped` (банк 9)
|
||||
уходил литерал `"_"` из банка 10.
|
||||
|
||||
**Правило:** всё, что уходит указателем в другой банк, обязано лежать в
|
||||
W2 — стек, `_DATA` или копия. `const`-таблицы и строковые литералы
|
||||
банкового модуля наружу отдавать нельзя (то же, что уже записано про
|
||||
`pop_tile.c`). Вторая проверка скрипта предупреждает о явных случаях, но
|
||||
она эвристическая и гарантией не является.
|
||||
|
||||
## Порядок слоёв в кадре (важно для окклюзии)
|
||||
|
||||
`sprpop.c` каждый тик рисует в СКРЫТУЮ страницу: `pop_char_heal` (стереть
|
||||
|
||||
@@ -200,10 +200,12 @@ LVL_NUMS := 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14
|
||||
# три секунды. Четыре архива актёров, а не один: имена внутри наборов
|
||||
# совпадают (g0.atl есть и у стража, и у скелета, и у Джафара), а константы
|
||||
# индексов упаковщик делает из имени файла.
|
||||
# README для игрока идёт НА ДИСК рядом с программой. Русский обязан быть в
|
||||
# CP866: DSS и любой местный просмотрщик читают именно её, UTF-8 покажется
|
||||
# кракозябрами. Имена в 8.3: README.TXT и README.RUS.
|
||||
DISK := README.TXT README.RUS \
|
||||
# README для игрока идёт НА ДИСК рядом с программой, В КОРЕНЬ. Русский
|
||||
# обязан быть в CP866: DSS и любой местный просмотрщик читают именно её,
|
||||
# UTF-8 покажется кракозябрами. Имена по 8.3, язык — суффиксом, а не
|
||||
# расширением: расширение .TXT должно остаться, чтобы файл открывался
|
||||
# просмотрщиком как текст.
|
||||
DISK := README_E.TXT README_R.TXT \
|
||||
BG/bg.arc BG/pop_tile.pal BG/pal_tile.pal \
|
||||
KID/kid.arc KID/kid.pal KID/kid.ani \
|
||||
GUARD/guard.arc SKEL/skel.arc VIZIER/vizier.arc SHADOW/shadow.arc \
|
||||
@@ -226,8 +228,15 @@ PACKED_FILES := $(addprefix $(PACKED)/,$(DISK))
|
||||
STAGED := $(addprefix $(BUILD_DIR)/,$(DISK))
|
||||
|
||||
EXTRA_DATA := $(STAGED)
|
||||
# Запись вида КАТАЛОГ:файл кладёт файл в подкаталог образа, голый путь — в
|
||||
# корень. Проверка на слэш ОБЯЗАТЕЛЬНА: без неё `README_E.TXT` превращался
|
||||
# в `README_E.TXT:build/README_E.TXT`, то есть упаковщик заводил каталог с
|
||||
# именем файла и клал файл внутрь него (поймано пользователем на готовом
|
||||
# образе).
|
||||
HDD_PACK_ARGS := $(BUILD_DIR)/$(EXAMPLE).exe \
|
||||
$(foreach f,$(DISK),$(word 1,$(subst /, ,$(f))):$(BUILD_DIR)/$(f))
|
||||
$(foreach f,$(DISK),$(if $(findstring /,$(f)),\
|
||||
$(word 1,$(subst /, ,$(f))):$(BUILD_DIR)/$(f),\
|
||||
$(BUILD_DIR)/$(f)))
|
||||
# Образ живёт в build/, а не в mame/v306/IMG — приложение автономно. Связь с
|
||||
# MAME — одна символьная ссылка, ставится однократно: make mame-link.
|
||||
HDD_IMG := $(CURDIR)/$(BUILD_DIR)/hdd/$(EXAMPLE).chd
|
||||
@@ -417,18 +426,18 @@ $(foreach n,$(LVL_NUMS),$(eval $(call lvl_rule,$(n))))
|
||||
# русского, перекодировка в CP866. iconv БЕЗ -c намеренно: потеря символа
|
||||
# должна ломать сборку, а не молча портить текст (memory
|
||||
# iconv_cp866_workflow; //TRANSLIT на macOS падает).
|
||||
$(PACKED)/README.TXT: dist/README.txt
|
||||
$(PACKED)/README_E.TXT: dist/README.txt
|
||||
@mkdir -p $(dir $@)
|
||||
@sed 's/$$/\r/' $< > $@
|
||||
|
||||
# ПОРЯДОК ВАЖЕН: CRLF ставится ДО перекодировки. BSD sed в UTF-8 локали
|
||||
# отказывается работать с байтами CP866 («RE error: illegal byte sequence»),
|
||||
# а с валидным UTF-8 — работает.
|
||||
$(PACKED)/README.RUS: dist/README.ru.txt
|
||||
$(PACKED)/README_R.TXT: dist/README.ru.txt
|
||||
@mkdir -p $(dir $@)
|
||||
@sed 's/$$/\r/' $< | iconv -f UTF-8 -t CP866 > $@
|
||||
|
||||
resources-readme: $(PACKED)/README.TXT $(PACKED)/README.RUS
|
||||
resources-readme: $(PACKED)/README_E.TXT $(PACKED)/README_R.TXT
|
||||
|
||||
resources-bg: $(PACKED)/BG/bg.arc $(PACKED)/BG/pop_tile.pal $(PACKED)/BG/pal_tile.pal
|
||||
resources-kid: $(PACKED)/KID/kid.arc $(PACKED)/KID/kid.pal $(PACKED)/KID/kid.ani
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
| [T-1](#t-1) | пики перерисовываются безусловно | оптимизация | открыт |
|
||||
| [BUG-SPIKE-1](#bug-spike-1) | пики залипают выдвинутыми рядом с Кидом | **низкий** | маловоспроизводим: ни сценарием, ни попиксельной подгонкой X не поднимается |
|
||||
| [BUG-CHOMP-JUMP-1](#bug-chomp-jump-1) | прыжок с места вплотную к чомперу: кадр с отступом назад | **низкий** | маловоспроизводим: на повторе не поднялся; на пререлиз |
|
||||
| [GRAB-KBD-TIMING](#grab-kbd-timing) | зацеп в падении срабатывает менее стабильно, чем в оригинале | **после всех уровней** | открыт: подозрение на клавиатурный модуль, не на физику |
|
||||
| [GRAB-KBD-TIMING](#grab-kbd-timing) | зацеп в падении срабатывает менее стабильно, чем в оригинале | **после всех уровней** | подозрение на клавиатуру ПОДТВЕРДИЛОСЬ частично: кража байт DSS устранена 2026-08-28, перепроверить |
|
||||
| [GUARD-ENTRY-DEATH](#guard-entry-death) | вход в комнату вплотную к стражу = мгновенная смерть (не портирован `bump_into_opponent`) | порт | **фикс есть, ждёт проверки в MAME** |
|
||||
| [BUG-SHADOW-SET](#bug-shadow-set) | Тень на 12-м уровне дерётся спрайтами СТРАЖА: SHADOW.DAT у нас нет вовсе | порт | открыт, разбор готов |
|
||||
| [PAL-L1-AFTER-INTRO](#pal-l1-after-intro) | вход в игру на уровень 1 после интро — игровая палитра не установлена (чёрный экран) | палитра/fade | открыт: воспроизведение нестабильно, корень не установлен |
|
||||
@@ -32,7 +32,7 @@
|
||||
| [MOB-CLIP-RIGHT](#mob-clip-right) | окна и узор кладки рисуются ПОВЕРХ падающих плит | окклюзия | открыт: корень найден 2026-08-13 |
|
||||
| [MID-OVERLAY-LAYER](#mid-overlay-layer) | оверлей кромки всегда поверх плит и персонажей (у оригинала — midtable с сортировкой) | слои | открыт: нужен разбор архитектуры |
|
||||
| [GUARD-FALLOUT-VICTORY](#guard-fallout-victory) | страж, выпавший из комнаты, не засчитывается убитым | порт | открыт: две строки |
|
||||
| [KBD-STUCK-WAIT](#kbd-stuck-wait) | потерянный break-код клавиши вешает игру НАСМЕРТЬ в межуровневой заставке | клавиатура/клин | открыт: диагноз полный, чинить не сейчас (решение пользователя 2026-08-24) |
|
||||
| [KBD-STUCK-WAIT](#kbd-stuck-wait) | потерянный break-код клавиши вешает игру НАСМЕРТЬ в межуровневой заставке | клавиатура/клин | **корень найден и устранён** 2026-08-28 (байты крал KEYSCAN DSS); ждёт полевой проверки |
|
||||
| [L10-BUTTON-DEBRIS](#l10-button-debris) | ур.10 к.1: плита упала на кнопку (1,8) — щебень есть, а решётки закрыты | кнопки/ворота | открыт: **не воспроизводится**, проверено многое (см. разбор) |
|
||||
| [LOOSE-SEAM-ANIM](#loose-seam-anim) | плита в шве (col −1), уроненная в прошлой комнате, исчезает без анимации падения | **мелкий** | открыт по решению пользователя: «пока пусть будет так» |
|
||||
| [SND-PACE-DEAD](#snd-pace-dead) | пейсинг сцен по насосу CBL не включается: признак «часы идут» вычисляется двумя чтениями подряд | **тайминг/звук** | открыт: диагноз полный, симптом обойдён подгонкой (2026-08-27) |
|
||||
@@ -98,8 +98,67 @@ POS=18`, MAME): Кид стоит на кнопке -> решётка `(2,7)` о
|
||||
<a id="kbd-stuck-wait"></a>
|
||||
## KBD-STUCK-WAIT. Потерянный break-код вешает игру насмерть в межуровневой заставке
|
||||
|
||||
> **Решение пользователя (2026-08-24): не чинить сейчас.** Записан разбор и
|
||||
> варианты; вернуться, когда дойдут руки до клавиатурного слоя.
|
||||
> **КОРЕНЬ НАЙДЕН И УСТРАНЁН 2026-08-28.** Байты у нас крал сам DSS — см.
|
||||
> раздел «Корень» ниже. Правка в `libc/irq/_irq_tramp.c`. Запись остаётся
|
||||
> открытой до полевой проверки и решения по страховке-таймауту.
|
||||
|
||||
## Корень (2026-08-28): скан-коды воровал KEYSCAN DSS
|
||||
|
||||
Пришёл из чтения исходников DSS (`docs/sources/Estex-DSS/DSS/`, подсказка
|
||||
пользователя). Обработчик прерывания DSS живёт в IM1 по `0x0038`
|
||||
(`DSS-MAIN.ASM`), и его `INTx38_Handler` ПЕРВЫМ ДЕЛОМ делает `CALL KEYSCAN`,
|
||||
а `KEYSCAN.RESCAN` (`KEYINTER.ASM`) вычерпывает FIFO SIO досуха.
|
||||
|
||||
Наш трамплин проверял «есть ли клавиатурный байт» ОДИН раз, на входе в
|
||||
прерывание, а хвост кадрового пути уходил в DSS (`jp 0x0038`). Значит
|
||||
скан-код, прилетевший ПОЗЖЕ этой проверки, доставался DSS и уезжал в его
|
||||
буфер — при этом **Rx-overrun не взводится**, потому что байт не потерян
|
||||
железом, он просто прочитан не тем владельцем. Отсюда и загадка
|
||||
исходного диагноза: бит залип, а `_kbdraw_overrun == 0`.
|
||||
|
||||
**Измерено в MAME (сессия 2026-08-28, брейки + `totalcycles`):**
|
||||
|
||||
| Что | Значение |
|
||||
|---|---|
|
||||
| окно от входа трамплина до чтения байта DSS | **738 тактов ≈ 34 мкс**, на КАЖДОМ кадровом прерывании (50/с) |
|
||||
| из них пролог самого DSS (`jp 0x38` → `.Handler` → `call INTx38_Handler` → 11 `push` → `call KEYSCAN`) | 481 такт ≈ 22 мкс |
|
||||
| цепочка кадровых хендлеров у SprPoP | пуста (`_irq_chain_n = 0`) — окно уже минимальное |
|
||||
| расчётная частота на железе при удержании клавиши (typematic 30 Гц = 60 байт/с) | потеря байта раз в ~10 с |
|
||||
|
||||
Последняя строка сходится с полевым наблюдением пользователя: залипание
|
||||
стрелки ловилось на ур. 14, когда Кид долго бежит влево через несколько
|
||||
комнат.
|
||||
|
||||
**Пойманный случай (MAME, старая сборка).** Стоп на `IN A,($18)` внутри
|
||||
`KEYSCAN` при `_kbdraw_active = 1`: DSS прочитал `A = 0x74` — make стрелки
|
||||
«вправо», при том что `_kbdraw_pending = 2` (EXT), то есть префикс `E0` уже
|
||||
разобрали МЫ. Посылка `E0 74` разорвана пополам между двумя владельцами
|
||||
канала. Злейший подвид — кража `E0` у break'а `E0 F0 6B`: тогда мы гасим
|
||||
бит в PLAIN-половине, а взведённый бит в EXT остаётся навсегда, и это ровно
|
||||
залипшая стрелка `0x6B`/`0x72` из исходного разбора.
|
||||
|
||||
**Правка.** Пока raw-канал открыт, кадровый путь трамплина НЕ вызывает
|
||||
обработчик DSS вообще (приватный `RETI` вместо `jp 0x0038`) — окно ровно
|
||||
ноль. Промежуточный вариант «проверить FIFO перед chain'ом» пробовали и
|
||||
отвергли замером: он снимает лишь свою треть окна (738 → 481 такт), кражи
|
||||
продолжались. Цена принятого варианта: пока открыт raw, у DSS замирает
|
||||
кадровое обслуживание — опрос мыши и мигание текстового курсора (PoP не
|
||||
нужно ни то, ни другое; зафиксировано в `<kbd_raw.h>`).
|
||||
|
||||
**Проверка (MAME 2026-08-28).** До правки брейк на входе `KEYSCAN` с
|
||||
условием «страница точно DSS + наш raw открыт» срабатывал мгновенно (50/с);
|
||||
после правки молчит, а положительный контроль — брейк на нашем приватном
|
||||
`RETI` — срабатывает сразу. ВАЖНО про методику: первый брейк без проверки
|
||||
сигнатуры байтов дал ЛОЖНОЕ срабатывание — в W0 периодически лежит
|
||||
драйверная страница DSS, и по адресу `0x0570` там своя команда.
|
||||
|
||||
**Что осталось открытым.** Кража устранена, но потерять break всё ещё
|
||||
может настоящий Rx-overrun SIO (редкий: FIFO вычерпывает `kbd_raw_poll` из
|
||||
главного цикла). Значит вариант A из таблицы ниже — таймаут в циклах
|
||||
«ждём отпускания всех клавиш» — остаётся разумной страховкой от КЛАССА
|
||||
отказа «вечный клин», и его стоит сделать отдельно.
|
||||
|
||||
## Исходный разбор (2026-08-24)
|
||||
|
||||
**Наблюдение (пользователь, 2026-08-24).** Нажат Shift+L (чит «следующий
|
||||
уровень») — игра встала намертво: картинка прежнего уровня, реакции нет.
|
||||
@@ -169,6 +228,13 @@ Rx-overrun не срабатывала. Гашение бита отладчи
|
||||
> **Решение пользователя (2026-08-12): отложено до готовности всех уровней.**
|
||||
> Механика работает, это вопрос ощущения, а не проходимости.
|
||||
|
||||
> **Обновление 2026-08-28.** Гипотеза «дело в клавиатурном модуле, а не в
|
||||
> физике» подтвердилась ЧАСТИЧНО: найдена и устранена кража скан-кодов
|
||||
> обработчиком DSS (разбор — [KBD-STUCK-WAIT](#kbd-stuck-wait)). Украденный
|
||||
> make Shift'а или стрелки — это ровно «нажал, а не сработало», и на
|
||||
> железе при удержании клавиши такое случалось раз в ~10 с. Перепроверить
|
||||
> ощущение зацепа на новой сборке ДО того, как лезть в физику.
|
||||
|
||||
**Наблюдение (пользователь).** Вход на уровень 7: Кид влетает в комнату сверху
|
||||
и, пролетая мимо края пола, МОЖЕТ за него зацепиться — но получается заметно
|
||||
реже, чем в оригинале. «Похоже, это проблемы нашего клавиатурного модуля».
|
||||
|
||||
@@ -647,3 +647,40 @@ Enter/Shift — либо оставить как есть уже осознан
|
||||
отпущено ВСЁ, что игрок держал в момент смерти (иначе зажатая при падении
|
||||
стрелка перезапускает уровень мгновенно), и не раньше `RESPAWN_SETTLE`
|
||||
кадров — труп должен успеть лечь.
|
||||
|
||||
---
|
||||
|
||||
## Таблица рекордов: Esc отменяет запись, а оригинал уйти не даёт
|
||||
|
||||
**Как в оригинале.** `show_hof` вставляет результат в таблицу ДО ввода
|
||||
имени и крутит ввод, пока тот не вернёт положительную длину:
|
||||
|
||||
```c
|
||||
/* SDLPoP seg001.c:624 */
|
||||
while (input_str(&rect, hof[hof_index].name, 24, "", 0, 4, color, bgcolor) <= 0);
|
||||
/* seg009.c input_str: Esc -> return -1; Enter -> return length */
|
||||
```
|
||||
|
||||
То есть **уйти нельзя ни по Esc, ни с пустым именем**: и −1, и 0 просто
|
||||
начинают ввод заново. Запись в таблице остаётся в любом случае.
|
||||
|
||||
**Как у нас.** Enter записывает имя (пустое подставляется как `PLAYER`),
|
||||
**Esc отказывается от записи целиком**: таблица перечитывается с диска
|
||||
(`hof_load`), `hof_save` не вызывается, экран показывается уже без строки
|
||||
ввода.
|
||||
|
||||
**Зачем.** Решение пользователя, 2026-08-27. Оригинальное поведение
|
||||
означает, что случайно добравшийся до финала игрок обязан вписать имя,
|
||||
чтобы вообще уйти с экрана, — на клавиатуре без цифрового блока и с нашим
|
||||
raw-каналом это выглядит как зависание, а не как требование.
|
||||
|
||||
**Чем платим.** Расхождение видно игроку: в оригинале таблица после
|
||||
победы пополняется всегда. Плюс отмена стоит одного лишнего чтения
|
||||
POP.HOF — откатывать сдвиг строк, который сделал `hof_insert_current`,
|
||||
дешевле перечитыванием файла, чем обратным сдвигом (экран холодный,
|
||||
50 мс там никому не мешают).
|
||||
|
||||
**Что проверять при регрессе.** После Esc: таблица на экране прежняя (без
|
||||
новой строки), POP.HOF на диске не изменился, следующий показ таблицы —
|
||||
между credits и attract-demo — даёт тот же список. После Enter с пустым
|
||||
именем строка называется `PLAYER` и сохраняется.
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
* Ctrl+R: Return to intro.
|
||||
* Ctrl+S: Sound on/off.
|
||||
* Ctrl+M: Music on/off.
|
||||
* Ctrl+V: Show version of SprPoP.
|
||||
* Ctrl+Q / F10: Quit game.
|
||||
|
||||
* F6: Quicksave: Save the exact state of the game.
|
||||
|
||||
@@ -665,6 +665,12 @@ void pop_control(void) __banked
|
||||
* «откуда взялся прыжок» останется одним. */
|
||||
#define KBD_HOME (KBD_EXT | 0x6C)
|
||||
#define KBD_PGUP (KBD_EXT | 0x7D)
|
||||
/* Центр нумпада (KP5) — тоже «вниз», как просит раскладка оригинала: там
|
||||
* весь блок 7/8/9-4/6-1/2/3 был управлением, и середина работала как
|
||||
* приседание. Своей навигационной клавиши у KP5 нет, поэтому после
|
||||
* kbd_raw_keypad_as_ext его голый код 0x73 переезжает в EXT-половину как
|
||||
* есть — читаем оттуда и добавляем к стрелке «вниз». */
|
||||
#define KBD_KP5 (KBD_EXT | 0x73)
|
||||
|
||||
static void read_input(void)
|
||||
{
|
||||
@@ -673,7 +679,7 @@ static void read_input(void)
|
||||
uint8_t l = (uint8_t)(kbd_raw_down(KBD_LEFT) || home);
|
||||
uint8_t r = (uint8_t)(kbd_raw_down(KBD_RIGHT) || pgup);
|
||||
uint8_t u = (uint8_t)(kbd_raw_down(KBD_UP) || home || pgup);
|
||||
uint8_t d = kbd_raw_down(KBD_DOWN);
|
||||
uint8_t d = (uint8_t)(kbd_raw_down(KBD_DOWN) || kbd_raw_down(KBD_KP5));
|
||||
control_x = r ? CONTROL_HELD_RIGHT : (l ? CONTROL_HELD_LEFT : CONTROL_RELEASED);
|
||||
control_y = d ? CONTROL_HELD_DOWN : (u ? CONTROL_HELD_UP : CONTROL_RELEASED);
|
||||
control_shift = (kbd_raw_down(KBD_LSHIFT) || kbd_raw_down(KBD_RSHIFT))
|
||||
@@ -781,6 +787,16 @@ int pop_ctrl_init(void) __banked
|
||||
control_forward = control_backward = control_up = control_down = CONTROL_RELEASED;
|
||||
control_shift2 = CONTROL_RELEASED;
|
||||
ctrl1_forward = ctrl1_backward = ctrl1_up = ctrl1_down = ctrl1_shift2 = CONTROL_RELEASED;
|
||||
/* НУМПАД НАМ НЕ НУЖЕН, И ЭТО ЛЕЧИТ ЗАЛИПАНИЕ СТРЕЛОК. Управление и
|
||||
* меню читают только расширенные коды (KBD_LEFT = KBD_EXT|0x6B и т.д.).
|
||||
* А при переполнении FIFO SIO теряется префикс E0, и make стрелки
|
||||
* садится в PLAIN-половину карты как код нумпада — снять его нечем,
|
||||
* потому что break придёт уже с префиксом. Дальше kbd_raw_any_down()
|
||||
* навсегда «да», и всякое ожидание «пока ничего не нажато» виснет
|
||||
* насмерть (BUG KBD-STUCK-WAIT; поймано на 14-м уровне, где почти весь
|
||||
* путь идёт удержанием Left). Гасит биты kbd_raw_sync(), а его
|
||||
* pop_ctrl_tick зовёт каждый кадр. */
|
||||
kbd_raw_keypad_as_ext(1);
|
||||
return kbd_raw_open();
|
||||
}
|
||||
|
||||
|
||||
@@ -343,8 +343,22 @@ static void hof_edit_draw(uint8_t row, const char *name, uint8_t cursor)
|
||||
x += pop_text_width_mapped(POP_TEXT_BIG_DARK, name) + 1;
|
||||
}
|
||||
/* draw_text_cursor(): подчерк той же гарнитуры сразу за текстом,
|
||||
* мигающий сменой цвета на фоновый — у нас проще перерисовать. */
|
||||
if (cursor) pop_text_draw_mapped(POP_TEXT_BIG_DARK, x, baseline, "_");
|
||||
* мигающий сменой цвета на фоновый — у нас проще перерисовать.
|
||||
*
|
||||
* СТРОКА СТРОИТСЯ НА СТЕКЕ, И ЭТО НЕ ПРИДИРКА (поймано в MAME
|
||||
* 2026-08-27). Литерал "_" лёг бы в _BANK10, то есть в окно W3, а
|
||||
* pop_text_draw_mapped живёт в банке 9: трамплин на время вызова
|
||||
* переключает W3, и указатель показывает уже в ЧУЖОЙ банк. Функция
|
||||
* честно читает оттуда байты до первого нуля и рисует их знаками
|
||||
* вопроса — экран заливало мусором на каждое мигание курсора. Стек
|
||||
* лежит в W2, он виден из любого банка. */
|
||||
if (cursor) {
|
||||
char cur[2];
|
||||
|
||||
cur[0] = '_';
|
||||
cur[1] = 0;
|
||||
pop_text_draw_mapped(POP_TEXT_BIG_DARK, x, baseline, cur);
|
||||
}
|
||||
pop_text_unmap();
|
||||
}
|
||||
|
||||
@@ -409,8 +423,8 @@ static const uint8_t hof_key_code[30] = {
|
||||
KBD_SPACE, 0x66, KBD_ENTER, KBD_ESC
|
||||
};
|
||||
|
||||
/* Возвращает 1, когда ввод закончен (Enter/Esc); *changed — надо ли
|
||||
* перерисовать строку. */
|
||||
/* Возвращает 1 — ввод завершён (Enter), 2 — отменён (Esc); 0 — продолжаем.
|
||||
* *changed — надо ли перерисовать строку. */
|
||||
static uint8_t hof_name_tick(hof_entry_t *entry, uint8_t *length,
|
||||
uint8_t *previous, uint8_t *changed)
|
||||
{
|
||||
@@ -432,7 +446,7 @@ static uint8_t hof_name_tick(hof_entry_t *entry, uint8_t *length,
|
||||
*changed = 1;
|
||||
} else if (i == 28 || i == 29) {
|
||||
previous[i] = down;
|
||||
return 1;
|
||||
return (uint8_t)(i == 28 ? 1 : 2); /* Enter : Esc */
|
||||
}
|
||||
}
|
||||
previous[i] = down;
|
||||
@@ -446,7 +460,8 @@ static uint8_t hof_name_tick(hof_entry_t *entry, uint8_t *length,
|
||||
* input_str) — в кадрах луча это HOF_BLINK. */
|
||||
#define HOF_BLINK POP_T60(6)
|
||||
|
||||
static void hof_edit(uint8_t row)
|
||||
/* 1 — игрок отказался вписывать имя (Esc). */
|
||||
static uint8_t hof_edit(uint8_t row)
|
||||
{
|
||||
uint8_t length = 0, previous[30], i, changed = 1;
|
||||
uint8_t cursor = 1, phase = 0;
|
||||
@@ -459,10 +474,14 @@ static void hof_edit(uint8_t row)
|
||||
for (i = 0; i < sizeof(previous); i++)
|
||||
previous[i] = kbd_raw_down(hof_key_code[i]);
|
||||
for (;;) {
|
||||
uint8_t done;
|
||||
|
||||
kbd_raw_sync();
|
||||
if (hof_name_tick(entry, &length, previous, &changed)) {
|
||||
done = hof_name_tick(entry, &length, previous, &changed);
|
||||
if (done == 2) return 1; /* Esc — запись не нужна */
|
||||
if (done) {
|
||||
if (!length) strcpy(entry->name, "PLAYER");
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
if (changed) { cursor = 1; phase = 0; }
|
||||
if (++phase >= HOF_BLINK) {
|
||||
@@ -496,8 +515,13 @@ void pop_hof_show(void) __banked
|
||||
if (hof_page_build(page, row) == 0) {
|
||||
pop_screen_present(page);
|
||||
(void)pop_pal_fade_in_skip(HOF_FADE);
|
||||
hof_edit(row);
|
||||
hof_save();
|
||||
/* ОТКАЗ ПО Esc — РАСХОЖДЕНИЕ С ОРИГИНАЛОМ (docs/impl_diff.md).
|
||||
* SDLPoP крутит `while (input_str(...) <= 0)`, то есть уйти без
|
||||
* имени нельзя вовсе. Мы разрешаем: тогда таблицу перечитываем
|
||||
* с диска (hof_insert_current уже сдвинул строки, и откатывать
|
||||
* их дешевле всего именно так) и НЕ сохраняем. */
|
||||
if (hof_edit(row)) hof_load();
|
||||
else hof_save();
|
||||
/* Имя вписано: строку показываем уже обычным золотым текстом,
|
||||
* без полосы (restore_peel + show_hof_text оригинала). */
|
||||
page = pop_screen_begin();
|
||||
|
||||
@@ -653,53 +653,6 @@ static void menu_erase(uint8_t entry_page)
|
||||
}
|
||||
}
|
||||
|
||||
static void build_info_draw(void)
|
||||
{
|
||||
uint8_t back = pop_screen_begin();
|
||||
int top = UI_CENTER_TOP(ABOUT_H + BUILD_KEYS_GAP);
|
||||
|
||||
pop_screen_fill(BLACK);
|
||||
gfx_set_bank(GFX_BANK_SPRITE);
|
||||
pop_text_map();
|
||||
about_text(top);
|
||||
pop_text_center_mapped(POP_TEXT_SMALL, GFX_WIDTH / 2,
|
||||
top + ABOUT_H + BUILD_KEYS_GAP, UI_KEYS_TEXT);
|
||||
pop_text_unmap();
|
||||
pop_screen_present(back);
|
||||
}
|
||||
|
||||
/* В SprPoP ещё нет автомата TITLE (это FG1/FG6), поэтому build screen
|
||||
* показывается перед первым игровым кадром и после выхода возвращает ровно
|
||||
* ту же сцену. Настройка уже прочитана в pop_boot до initgraph. */
|
||||
void pop_build_info_show(void) __banked
|
||||
{
|
||||
uint8_t entry_page, saved_page, saved_bank, frames;
|
||||
struct fillsettingstype saved_fill;
|
||||
|
||||
if (!pop_ui_ready() || !pop_settings.show_build_info) return;
|
||||
entry_page = gfx_get_visible_page();
|
||||
saved_page = gfx_get_draw_page();
|
||||
saved_bank = gfx_get_bank();
|
||||
getfillsettings(&saved_fill);
|
||||
/* Первый реальный пользователь FG5 fade: старая игровая палитра
|
||||
* запоминается в pop_ui, затем full-screen появляется из чёрного. */
|
||||
pop_pal_fade_out(4);
|
||||
build_info_draw();
|
||||
pop_pal_fade_in(4);
|
||||
for (frames = 0; frames < 150; frames++) { /* около трёх секунд */
|
||||
if (kbd_raw_down(KBD_ESC) || kbd_raw_down(KBD_ENTER) ||
|
||||
kbd_raw_down(KBD_SPACE))
|
||||
break;
|
||||
pop_wait_edge();
|
||||
}
|
||||
ui_wait_release();
|
||||
pop_pal_fade_out(4);
|
||||
menu_erase(entry_page);
|
||||
pop_pal_fade_in(4);
|
||||
setfillstyle(saved_fill.pattern, saved_fill.color);
|
||||
gfx_set_bank(saved_bank);
|
||||
gfx_set_draw_page(saved_page);
|
||||
}
|
||||
|
||||
int pop_menu_init(void) __banked
|
||||
{
|
||||
|
||||
@@ -40,6 +40,5 @@ int8_t pop_menu_process(void) __banked;
|
||||
|
||||
/* Экран Sprinter build, если он включён в POP.CFG. Вызывается после
|
||||
* открытия raw-клавиатуры и до первого игрового кадра. */
|
||||
void pop_build_info_show(void) __banked;
|
||||
|
||||
#endif
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <stdint.h>
|
||||
#include <gfx.h>
|
||||
#include <kbd_raw.h>
|
||||
#include "pop_state.h" /* pop_idle — общий idle-хук */
|
||||
#include "pop_pace.h"
|
||||
|
||||
uint8_t pop_speed_mode; /* crt0 зануляет _DATA → NORMAL */
|
||||
@@ -77,7 +78,11 @@ void pop_wait_edge(void)
|
||||
t = pop_frame_tick;
|
||||
do {
|
||||
pop_beam_sample();
|
||||
kbd_raw_poll(); /* KBD-1: опрос обязан быть плотным */
|
||||
/* ТОТ ЖЕ ХУК, что висит на gfx_wait_vsync, а не голый kbd_raw_poll:
|
||||
* кроме плотного опроса FIFO (KBD-1) он проверяет F10, и без этого
|
||||
* выход из игры в геймплее не работал — пейсинг по лучу через
|
||||
* gfx_wait_vsync не ходит. */
|
||||
pop_idle();
|
||||
} while (pop_frame_tick == t);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,12 @@
|
||||
* отрыв куска, снимает сам обход по факту пустой таблицы. */
|
||||
/* F10 нажата — выйти из программы; см. pop_state.c. */
|
||||
extern uint8_t pop_quit_req;
|
||||
|
||||
/* Общий idle-хук: вычерпать клавиатурный FIFO и проверить F10. Живёт в
|
||||
* sprpop.c (резидент), висит на gfx_wait_vsync и вызывается из цикла
|
||||
* ожидания луча — обе точки ожидания обязаны звать ОДНО И ТО ЖЕ, иначе
|
||||
* выход работает не везде. */
|
||||
void pop_idle(void);
|
||||
extern uint8_t pop_mob_busy;
|
||||
|
||||
/* Кусок loose приземлился (loose_land, seg007:11E8): 0 = нет, иначе
|
||||
|
||||
@@ -71,17 +71,17 @@ uint8_t pop_ui_ready(void)
|
||||
return font_ready;
|
||||
}
|
||||
|
||||
void pop_text_map(void)
|
||||
void pop_text_map(void) __banked
|
||||
{
|
||||
gfx_w0_map(font_at.page);
|
||||
}
|
||||
|
||||
void pop_text_unmap(void)
|
||||
void pop_text_unmap(void) __banked
|
||||
{
|
||||
gfx_w0_unmap();
|
||||
}
|
||||
|
||||
uint8_t pop_text_width_mapped(pop_text_font_t font, const char *s)
|
||||
uint8_t pop_text_width_mapped(pop_text_font_t font, const char *s) __banked
|
||||
{
|
||||
const pop_font_desc_t *desc = &font_desc[font];
|
||||
uint8_t width = 0;
|
||||
@@ -95,7 +95,7 @@ uint8_t pop_text_width_mapped(pop_text_font_t font, const char *s)
|
||||
}
|
||||
|
||||
void pop_text_draw_mapped(pop_text_font_t font, int x, int baseline,
|
||||
const char *s)
|
||||
const char *s) __banked
|
||||
{
|
||||
const pop_font_desc_t *desc = &font_desc[font];
|
||||
|
||||
@@ -115,14 +115,14 @@ void pop_text_draw_mapped(pop_text_font_t font, int x, int baseline,
|
||||
}
|
||||
|
||||
void pop_text_center_mapped(pop_text_font_t font, int center, int baseline,
|
||||
const char *s)
|
||||
const char *s) __banked
|
||||
{
|
||||
pop_text_draw_mapped(font, center - pop_text_width_mapped(font, s) / 2,
|
||||
baseline, s);
|
||||
}
|
||||
|
||||
void pop_text_right_mapped(pop_text_font_t font, int right, int baseline,
|
||||
const char *s)
|
||||
const char *s) __banked
|
||||
{
|
||||
pop_text_draw_mapped(font, right - pop_text_width_mapped(font, s),
|
||||
baseline, s);
|
||||
|
||||
@@ -27,16 +27,25 @@ void pop_ui_free(void) __banked;
|
||||
uint8_t pop_ui_ready(void);
|
||||
|
||||
/* Группа text_*_mapped требует активного pop_text_map() и не вызывает
|
||||
* BIOS/ESTEX. Для прозрачных букв установить GFX_BANK_SPRITE до рисования. */
|
||||
void pop_text_map(void);
|
||||
void pop_text_unmap(void);
|
||||
uint8_t pop_text_width_mapped(pop_text_font_t font, const char *s);
|
||||
* BIOS/ESTEX. Для прозрачных букв установить GFX_BANK_SPRITE до рисования.
|
||||
*
|
||||
* ВСЯ ГРУППА __banked, И ЭТО ОБЯЗАТЕЛЬНО (крэш финала, 2026-08-27). Живёт
|
||||
* она в pop_ui.c, то есть в банке 9, а зовут её и из ДРУГИХ банков — HOF
|
||||
* лежит в банке 10. Трамплин SDCC выбирает по объявлению, а не по тому,
|
||||
* где функция реально лежит (memory sdcc_banked_call_rules): без пометки
|
||||
* получается прямой `call` по адресу банка 9, который в чужом банке
|
||||
* попадает в пустой хвост (0xFF) — процессор ползёт по нему до 0x0000, и
|
||||
* DSS убивает процесс с кодом 0x27. Собиралось это МОЛЧА, поэтому рядом
|
||||
* появилась toolchain/check_bank_calls.py — она такое ловит на сборке. */
|
||||
void pop_text_map(void) __banked;
|
||||
void pop_text_unmap(void) __banked;
|
||||
uint8_t pop_text_width_mapped(pop_text_font_t font, const char *s) __banked;
|
||||
void pop_text_draw_mapped(pop_text_font_t font, int x, int baseline,
|
||||
const char *s);
|
||||
const char *s) __banked;
|
||||
void pop_text_center_mapped(pop_text_font_t font, int center, int baseline,
|
||||
const char *s);
|
||||
const char *s) __banked;
|
||||
void pop_text_right_mapped(pop_text_font_t font, int right, int baseline,
|
||||
const char *s);
|
||||
const char *s) __banked;
|
||||
|
||||
/* Снимок обеих (идентичных по контракту) палитр хранится в свободном хвосте
|
||||
* FONT.ATL. dim_step: 0 — исходная, POP_PAL_DIM_BLACK — чёрная; между ними
|
||||
|
||||
@@ -125,7 +125,10 @@ uint8_t pop_menu_request;
|
||||
* проявление полосами, меню, игра. Одна точка вместо десятка. */
|
||||
#define KBD_KEY_F10 0x09
|
||||
|
||||
static void kbd_idle(void)
|
||||
/* НЕ static: этот же хук зовёт цикл ожидания луча (pop_wait_edge), иначе
|
||||
* в геймплее F10 не работал бы вовсе — пейсинг по лучу не ходит через
|
||||
* gfx_wait_vsync, к которому хук подвешен (поймано 2026-08-27). */
|
||||
void pop_idle(void)
|
||||
{
|
||||
kbd_raw_poll();
|
||||
if (kbd_raw_down(KBD_KEY_F10)) pop_quit_req = 1;
|
||||
@@ -267,14 +270,15 @@ int main(void)
|
||||
puts("kbd_raw open failed");
|
||||
return 1;
|
||||
}
|
||||
gfx_set_idle_hook(kbd_idle); /* вычерпывать FIFO, пока ждём кадр */
|
||||
gfx_set_idle_hook(pop_idle); /* вычерпывать FIFO, пока ждём кадр */
|
||||
if (!pop_app_dispatch(POP_APP_EV_BOOT_READY)) {
|
||||
pop_shutdown();
|
||||
return 1;
|
||||
}
|
||||
/* MS8: CFG уже применён в pop_boot до первого показа. Экран build
|
||||
* показывается только по явной пользовательской настройке. */
|
||||
pop_build_info_show();
|
||||
/* Экран версии при запуске БОЛЬШЕ НЕ ПОКАЗЫВАЕТСЯ (решение пользователя
|
||||
* 2026-08-27): единственная его точка — пункт ABOUT в меню паузы.
|
||||
* Само состояние BUILD_INFO в автомате осталось: через него проходит
|
||||
* маршрут BOOT -> TITLE, и убирать его ради одной пустой фазы незачем. */
|
||||
if (!pop_app_dispatch(POP_APP_EV_BUILD_DONE)) {
|
||||
pop_shutdown();
|
||||
return 1;
|
||||
|
||||
@@ -99,7 +99,7 @@ static void find_start_level_door(uint8_t room)
|
||||
int8_t pop_frame_ui(uint8_t *restart_level, uint8_t *dead_reset) __banked
|
||||
{
|
||||
static uint8_t esc_prev, qs_save_prev, qs_load_prev;
|
||||
static uint8_t restart_prev, version_prev;
|
||||
static uint8_t restart_prev;
|
||||
static uint8_t intro_prev, showtime_prev, timer_prev;
|
||||
uint8_t e = (uint8_t)(kbd_raw_down(KBD_MENU) || kbd_raw_down(KBD_MENU_ALT));
|
||||
uint8_t ctrl = (uint8_t)(kbd_raw_down(KBD_LCTRL) || kbd_raw_down(KBD_RCTRL));
|
||||
@@ -142,14 +142,6 @@ int8_t pop_frame_ui(uint8_t *restart_level, uint8_t *dead_reset) __banked
|
||||
if (r && !restart_prev) *restart_level = 1;
|
||||
restart_prev = r;
|
||||
}
|
||||
/* Ctrl+V — версия сборки. Функция уже есть, до сих пор её показывал
|
||||
* только старт программы. */
|
||||
{
|
||||
uint8_t v = (uint8_t)(ctrl && kbd_raw_down(KBD_VERSION));
|
||||
|
||||
if (v && !version_prev) pop_build_info_show();
|
||||
version_prev = v;
|
||||
}
|
||||
/* Ctrl+R — вернуться в заставку. Тот же переход, что «Restart Game»
|
||||
* пунктом меню: автомат сам разложит title/intro с первого экрана. */
|
||||
{
|
||||
@@ -431,6 +423,13 @@ void pop_start_level(void) __banked
|
||||
* гасит заставка «LEVEL N», которую этот же переход и пропускает. */
|
||||
if (pop_seamless) { pop_seamless = 0; pop_kid_hp_carry(); }
|
||||
else pop_kid_hp_reset();
|
||||
/* ПОЛОСУ HP — НА ОБЕИХ СТРАНИЦАХ. Старт уровня перерисовал фон, то есть
|
||||
* стёр её, а сама она обновляется только при ИЗМЕНЕНИИ чисел. После
|
||||
* Ctrl+A с выросшим за уровень максимумом (большой кувшин, чит Shift+T)
|
||||
* это давало моргание: одна страница показывала старые деления, вторая
|
||||
* новые (поймано пользователем 2026-08-27). invalidate ровно для этого
|
||||
* и заведён — см. его шапку в pop_cdraw.c. */
|
||||
pop_hp_invalidate();
|
||||
}
|
||||
|
||||
/* ---- ОТЛАДКА: обход комнат по номеру -------------------------------- *
|
||||
|
||||
@@ -110,7 +110,6 @@ int8_t pop_level_change(uint8_t back) __banked;
|
||||
* из keys.txt. */
|
||||
#define KBD_QUIT 0x15 /* Q — вместе с Ctrl */
|
||||
#define KBD_RESTART 0x1C /* A — Ctrl+A, рестарт уровня */
|
||||
#define KBD_VERSION 0x2A /* V — Ctrl+V, показать версию */
|
||||
#define KBD_INTRO 0x2D /* R — Ctrl+R, вернуться в заставку */
|
||||
#define KBD_SHOWTIME 0x29 /* Space — сколько осталось (seg000:612) */
|
||||
#define KBD_TIMER 0x2C /* T — постоянный показ таймера. БЕЗ модификаторов:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Эталон размеров _CODE (байт); обновление: python3 toolchain/size_check.py --update
|
||||
accfill 3786
|
||||
accfill 3783
|
||||
accop 7067
|
||||
argv 3431
|
||||
assrtest 3847
|
||||
@@ -9,15 +9,15 @@ banked 1056
|
||||
bankedbg 1067
|
||||
banklocl 4683
|
||||
banktest 3767
|
||||
bgi_img 8680
|
||||
bgitest 3751
|
||||
bgi_img 8677
|
||||
bgitest 3748
|
||||
bios_text 4461
|
||||
blitperf 5860
|
||||
blitw 4939
|
||||
cat 913
|
||||
cblstream 5850
|
||||
cbltest 5996
|
||||
cblwav 6089
|
||||
cblstream 5860
|
||||
cbltest 6006
|
||||
cblwav 6099
|
||||
conio 4605
|
||||
conio2 3929
|
||||
convbench 3508
|
||||
@@ -28,12 +28,12 @@ fdmax 6023
|
||||
filetest 10547
|
||||
fpsdiv 4870
|
||||
gets 509
|
||||
gfx_dbuf 5134
|
||||
gfx_demo 4117
|
||||
gfx_dbuf 5131
|
||||
gfx_demo 4114
|
||||
gfxbanks 6044
|
||||
hello 4167
|
||||
hello2 4244
|
||||
irqtest 6680
|
||||
irqtest 6690
|
||||
kbdpoll 1282
|
||||
kbdraw 4911
|
||||
ls 4835
|
||||
@@ -44,7 +44,7 @@ openenv 6124
|
||||
pageflip 6334
|
||||
palfile 5406
|
||||
ptime 5744
|
||||
rt_test 5187
|
||||
rt_test 5197
|
||||
scroll 6744
|
||||
seek 4188
|
||||
simple 955
|
||||
@@ -59,8 +59,7 @@ strtest 1340
|
||||
text_palette 5024
|
||||
timedir 5454
|
||||
w0page 8831
|
||||
w3bankgfx 5240
|
||||
w3bgfx 5246
|
||||
w3bgfx 5243
|
||||
w3big 3673
|
||||
w3huge 3700
|
||||
w3probe 3556
|
||||
|
||||
|
@@ -23,6 +23,16 @@
|
||||
* cbl_open()/cbl_close() (<cbl.h>) — тот же приватный IM2-хук, не
|
||||
* чейнящийся к DSS.
|
||||
*
|
||||
* КАДРОВОЕ ОБСЛУЖИВАНИЕ DSS НА ЭТО ВРЕМЯ ОСТАНОВЛЕНО. Пока raw открыт,
|
||||
* трамплин НЕ вызывает обработчик прерывания DSS вообще: тот первым делом
|
||||
* зовёт свой KEYSCAN, который вычерпывает FIFO SIO и уводит наши байты
|
||||
* (потерянный make = несработавшее нажатие, потерянный break = залипшая
|
||||
* клавиша; поймано в MAME 2026-08-28). Вместе с KEYSCAN замирают и
|
||||
* остальные его кадровые дела — опрос мыши (Dss.Mouse.GetPackets) и
|
||||
* мигание текстового курсора. ЗНАЧИТ: приложению, которому нужна мышь
|
||||
* через DSS, raw-канал открывать нельзя (мышь встанет); клавиатурные
|
||||
* события всё равно берутся из kbd_raw_down().
|
||||
*
|
||||
* КОДЫ КЛАВИШ: значения ниже — общеизвестный стандарт AT/PS-2 Scan
|
||||
* Code Set 2 (не специфика Sprinter). TODO до использования в PoC:
|
||||
* подтвердить в MAME, что реальный поток байт с SIO-A клавиатуры
|
||||
@@ -46,6 +56,31 @@ void kbd_raw_close(void);
|
||||
* на проводе) прибавить KBD_EXT к базовому коду. */
|
||||
uint8_t kbd_raw_down(uint16_t code);
|
||||
|
||||
/* Считать ГОЛЫЕ (без префикса 0xE0) коды нумпада ТЕМИ ЖЕ клавишами, что и
|
||||
* расширенные: 0x69..0x7D, кроме ESC 0x76 и F11 0x78. По умолчанию
|
||||
* выключено — нумпад остаётся отдельным набором клавиш.
|
||||
*
|
||||
* В PS/2 это и есть одни и те же физические клавиши: навигационный блок
|
||||
* шлёт код с префиксом 0xE0, нумпад — тот же код голым (KP4 = Left,
|
||||
* KP8 = Up, KP7 = Home, KP9 = PgUp и т.д.). Флаг восстанавливает это
|
||||
* равенство: kbd_raw_sync() переносит биты голых кодов в EXT-половину
|
||||
* карты и гасит их в PLAIN.
|
||||
*
|
||||
* ЗАЧЕМ ЭТО НУЖНО (а не только «нумпад тоже работает»). При переполнении
|
||||
* 3-байтового FIFO SIO теряется префикс, и make стрелки садится в PLAIN
|
||||
* как код нумпада — а break придёт уже с префиксом и снимет бит в ДРУГОЙ
|
||||
* половине. Клавиша остаётся зажатой НАВСЕГДА: kbd_raw_any_down()
|
||||
* отвечает «да», и любое ожидание «пока ничего не нажато» виснет насмерть.
|
||||
* После перекладки make и break работают с одним и тем же битом.
|
||||
*
|
||||
* Чего флаг НЕ лечит: потерю префикса у BREAK (`E0 F0 6B` -> `F0 6B`) —
|
||||
* тогда бит стоит уже в EXT-половине и выглядит как реально зажатая
|
||||
* клавиша; снимается перенажатием или typematic-повтором.
|
||||
*
|
||||
* Перекладку делает kbd_raw_sync(), то есть приложение обязано звать его
|
||||
* раз в кадр (оно и так обязано — см. ниже). */
|
||||
void kbd_raw_keypad_as_ext(uint8_t on);
|
||||
|
||||
/* Есть ли хотя бы одна зажатая клавиша (0/1). Полезно для экранов с
|
||||
* семантикой «продолжить любой клавишей»; один вызов читает 64-байтную
|
||||
* bitmap, а не перебирает все 512 scan-кодов через kbd_raw_down(). */
|
||||
|
||||
@@ -30,10 +30,11 @@
|
||||
#define _IRQ_VEC_BUF_SIZE (256 + 257)
|
||||
|
||||
/* Буфер под W2-копию кадрового трамплина (small/huge — _irq_tramp в
|
||||
* W1). С запасом над реальным размером трамплина (267 Б после
|
||||
* упаковки held-карты клавиатуры в биты, 2026-07-30 — проверять
|
||||
* __irq_tramp_end при правках, запас держать >= 60 Б). */
|
||||
#define _IRQ_TRAMP_BUF_SIZE 336
|
||||
* W1). С запасом над реальным размером трамплина (310 Б после отказа
|
||||
* звать обработчик DSS, пока открыт raw-канал, 2026-08-28 — проверять
|
||||
* `grep 'A _CODE' libc/build/fast/irq/_irq_tramp.rel` при правках,
|
||||
* запас держать >= 60 Б). */
|
||||
#define _IRQ_TRAMP_BUF_SIZE 384
|
||||
|
||||
extern uint8_t _irq_vec_buf[_IRQ_VEC_BUF_SIZE];
|
||||
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
* клавиши) в битовую карту `_kbdraw_down[]` (бит на код, 64 Б),
|
||||
* и к DSS НЕ идём
|
||||
* (private RETI — тот же паттерн, что и у CBL ниже). Пока raw
|
||||
* открыт, DSS клавиатуру не видит вообще (см. <kbd_raw.h>).
|
||||
* открыт, DSS клавиатуру не видит вообще (см. <kbd_raw.h>) — и на
|
||||
* кадровом пути тоже: пока raw открыт, обработчик DSS НЕ вызывается
|
||||
* совсем (иначе его KEYSCAN крадёт наши байты — см. tr_frame_exit).
|
||||
* - CBL (бит 7 порта 0xFE) — только при зарегистрированном хуке
|
||||
* _irq_cbl_hook (иначе бит подтянут к 1); личное прерывание, RETI
|
||||
* без чейна к DSS;
|
||||
@@ -312,6 +314,39 @@ void _irq_tramp(void) __naked
|
||||
ld a, (__irq_saved_w1) ; вернуть прерванную W1 перед chain
|
||||
out (#_IRQ_W1_PORT), a
|
||||
tr_frame_exit:
|
||||
;; --- ПОКА RAW ОТКРЫТ, В DSS НЕ ХОДИМ ВООБЩЕ ---
|
||||
;; Хвост кадрового пути уходил в DSS (0x0038), а его обработчик
|
||||
;; INTx38_Handler ПЕРВЫМ ДЕЛОМ зовёт KEYSCAN, и тот вычерпывает FIFO
|
||||
;; SIO досуха (docs/sources/Estex-DSS/DSS/DSS-MAIN.ASM,
|
||||
;; KEYINTER.ASM:KEYSCAN.RESCAN). Наша проверка «есть ли байт» стоит в
|
||||
;; НАЧАЛЕ трамплина, поэтому скан-код, прилетевший позже, доставался
|
||||
;; DSS и уезжал в ЕГО буфер — БЕЗ Rx-overrun. Украденный make =
|
||||
;; несработавшее нажатие, украденный break = залипшая навсегда
|
||||
;; клавиша при `_kbdraw_overrun == 0` (BUGS_OPEN KBD-STUCK-WAIT,
|
||||
;; GRAB-KBD-TIMING). Поймано в MAME 2026-08-28: DSS прочитал 0x74
|
||||
;; (make стрелки «вправо») при `_kbdraw_pending = EXT` — посылка
|
||||
;; `E0 74` разорвана пополам между двумя владельцами канала.
|
||||
;;
|
||||
;; Окно измерено брейками + totalcycles: от входа трамплина до
|
||||
;; чтения DSS — 738 тактов (~34 мкс), и открыто оно на КАЖДОМ
|
||||
;; кадровом прерывании (50/с). Из них 481 такт — пролог самого DSS
|
||||
;; (jp 0x38 → jp .Handler → call INTx38_Handler → 11 push → call
|
||||
;; KEYSCAN), то есть закрыть окно проверкой FIFO перед chain'ом
|
||||
;; нельзя: она снимает лишь треть (проверено там же).
|
||||
;;
|
||||
;; Поэтому пока raw открыт, кадровый путь заканчивается ПРИВАТНЫМ
|
||||
;; RETI и обработчик DSS не вызывается совсем — окно ровно ноль.
|
||||
;; ЦЕНА: на это время замирает кадровое обслуживание DSS —
|
||||
;; Dss.Mouse.GetPackets и мигание текстового курсора. Приложению с
|
||||
;; raw-клавиатурой (игре) не нужно ни то, ни другое; кому нужна мышь
|
||||
;; DSS одновременно с raw — не открывать raw (см. <kbd_raw.h>).
|
||||
ld a, (__kbdraw_active)
|
||||
or a, a
|
||||
jr Z, tr_chain_dss
|
||||
pop af
|
||||
ei
|
||||
reti
|
||||
tr_chain_dss:
|
||||
pop af
|
||||
.db 0xC3 ; jp 0x0038 (литерал — copy-safe)
|
||||
.dw 0x0038
|
||||
|
||||
@@ -57,4 +57,28 @@ extern uint8_t _kbdraw_overrun; /* 1 = ISR поймал Rx-overrun SIO (по
|
||||
* читает kbd_raw_down(). */
|
||||
extern uint8_t _kbdraw_down[64];
|
||||
|
||||
/* ГОЛЫЕ КОДЫ НУМПАДА = ТЕ ЖЕ КЛАВИШИ, ЧТО И РАСШИРЕННЫЕ
|
||||
* (взводит kbd_raw_keypad_as_ext).
|
||||
*
|
||||
* Стрелки приходят как E0 6B / E0 72 / E0 74 / E0 75, то есть в EXT-половину
|
||||
* карты. Если префикс E0 потерян (переполнение 3-байтового FIFO SIO в
|
||||
* длинном DI-окне), make садится в PLAIN-половину как код нумпада, а его
|
||||
* break приходит уже с префиксом и снимает бит в EXT-половине — PLAIN-бит
|
||||
* остаётся зажатым НАВСЕГДА (BUG-KBD: 0x72 2026-08-24, 0x6B 2026-08-27).
|
||||
*
|
||||
* Лечится перекладкой: kbd_raw_sync() переносит биты голых кодов нумпада
|
||||
* в EXT-половину и гасит их в PLAIN. Тогда make без префикса ставит ТОТ ЖЕ
|
||||
* бит, который потом снимет обычный break `E0 F0 xx`, — залипание исчезает,
|
||||
* а нумпад заодно работает как навигационные клавиши (в PS/2 это и есть
|
||||
* одни и те же физические клавиши). Позиции бит совпадают: EXT — те же
|
||||
* байты карты плюс 32.
|
||||
*
|
||||
* Диапазон Set 2 — 0x69..0x7D, но ESC (0x76) и F11 (0x78) лежат внутри и
|
||||
* перекладке НЕ подлежат.
|
||||
*
|
||||
* Чего это не лечит: потерю префикса у BREAK (`E0 F0 6B` -> `F0 6B`). Тогда
|
||||
* бит остаётся стоять уже в EXT-половине, то есть выглядит как реально
|
||||
* зажатая клавиша; снимается перенажатием или typematic-повтором. */
|
||||
extern uint8_t _kbdraw_keypad_ext;
|
||||
|
||||
#endif
|
||||
|
||||
@@ -10,3 +10,6 @@ uint8_t _kbdraw_pending;
|
||||
uint8_t _kbdraw_overrun; /* ISR взвёл при Rx-overrun SIO (потерян
|
||||
* break) — kbd_raw_sync снимет залипшее */
|
||||
uint8_t _kbdraw_down[64]; /* БИТ на скан-код (см. _kbdraw.h) */
|
||||
uint8_t _kbdraw_keypad_ext; /* 1 = голые коды нумпада считать теми же
|
||||
* клавишами, что и расширенные
|
||||
* (kbd_raw_keypad_as_ext; см. _kbdraw.h) */
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* kbd_raw_keypad_as_ext — голые коды нумпада = те же клавиши, что и
|
||||
* расширенные (стрелки и навигация).
|
||||
*
|
||||
* Мотивация и разбор залипания — в шапке _kbdraw.h рядом с
|
||||
* _kbdraw_keypad_ext; публичный контракт — в <kbd_raw.h>. Саму перекладку
|
||||
* делает kbd_raw_sync(): в декодерах ей места нет — клавиатурный блок
|
||||
* трамплина уже длиннее 127 байт, а `jp` внутри него запрещён (копия в W2),
|
||||
* и вставка рвала относительные переходы.
|
||||
*/
|
||||
#include <kbd_raw.h>
|
||||
#include "_kbdraw.h"
|
||||
|
||||
void kbd_raw_keypad_as_ext(uint8_t on)
|
||||
{
|
||||
_kbdraw_keypad_ext = (uint8_t)(on ? 1 : 0);
|
||||
}
|
||||
@@ -47,11 +47,42 @@ static const uint16_t _kbd_mod_codes[6] = {
|
||||
0x12, 0x59, 0x14, 0x11, 0x0114, 0x0111
|
||||
};
|
||||
|
||||
/* Голые коды нумпада (Set 2 0x69..0x7D) — по БИТАМ карты: байт 13 держит
|
||||
* 0x68..0x6F, байт 14 — 0x70..0x77, байт 15 — 0x78..0x7F. Гасим только
|
||||
* клавиши нумпада: ESC (0x76, байт 14 бит 6) и F11 (0x78, байт 15 бит 0)
|
||||
* обязаны проходить, поэтому маски именно такие, а не 0xFF. */
|
||||
#define KP_BYTE0 13
|
||||
#define KP_MASK0 0x1A /* 0x69 KP1, 0x6B KP4, 0x6C KP7 */
|
||||
#define KP_BYTE1 14
|
||||
#define KP_MASK1 0xBF /* 0x70..0x75, 0x77 — всё, кроме ESC 0x76 */
|
||||
#define KP_BYTE2 15
|
||||
#define KP_MASK2 0x3E /* 0x79..0x7D — всё, кроме F11 0x78 */
|
||||
|
||||
void kbd_raw_sync(void)
|
||||
{
|
||||
uint8_t save[6];
|
||||
uint8_t i;
|
||||
|
||||
/* ПЕРЕКЛАДЫВАЕМ ГОЛЫЕ КОДЫ НУМПАДА В EXT-ПОЛОВИНУ, если приложение
|
||||
* попросило. Делается здесь, а не в декодерах: клавиатурный блок
|
||||
* трамплина и так длиннее 127 байт (внутри него `jp` запрещён —
|
||||
* трамплин копируется в W2), и вставка рвала относительные переходы.
|
||||
* Цена — шесть операций раз в кадр вместо проверки на каждый байт.
|
||||
*
|
||||
* Почему это лечит залипание: стрелка приходит как E0 6B, и при потере
|
||||
* префикса make садится в PLAIN-половину как код нумпада, а break
|
||||
* придёт уже с префиксом и снимет бит в EXT — plain-бит остался бы
|
||||
* зажатым навсегда. После перекладки make и break работают с ОДНИМ и
|
||||
* тем же битом. Позиции бит совпадают: EXT — те же байты плюс 32. */
|
||||
if (_kbdraw_keypad_ext) {
|
||||
_kbdraw_down[KP_BYTE0 + 32] |= (uint8_t)(_kbdraw_down[KP_BYTE0] & KP_MASK0);
|
||||
_kbdraw_down[KP_BYTE1 + 32] |= (uint8_t)(_kbdraw_down[KP_BYTE1] & KP_MASK1);
|
||||
_kbdraw_down[KP_BYTE2 + 32] |= (uint8_t)(_kbdraw_down[KP_BYTE2] & KP_MASK2);
|
||||
_kbdraw_down[KP_BYTE0] &= (uint8_t)~KP_MASK0;
|
||||
_kbdraw_down[KP_BYTE1] &= (uint8_t)~KP_MASK1;
|
||||
_kbdraw_down[KP_BYTE2] &= (uint8_t)~KP_MASK2;
|
||||
}
|
||||
|
||||
if (!_kbdraw_overrun) return;
|
||||
_kbdraw_overrun = 0;
|
||||
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
check_bank_calls.py — две проверки банкового кода, которых нет ни у
|
||||
компилятора, ни у линкера.
|
||||
|
||||
ПРОВЕРКА 1 — ПРЯМОЙ вызов в ЧУЖОЙ банк.
|
||||
|
||||
Зачем. Трамплин у SDCC выбирается по `__banked` в ОБЪЯВЛЕНИИ, а не по
|
||||
тому, в каком банке функция реально лежит (memory sdcc_banked_call_rules).
|
||||
Поэтому функция, объявленная без `__banked`, зовётся прямым `call`, и это
|
||||
верно ровно до тех пор, пока вызывающий и вызываемый лежат в ОДНОМ банке.
|
||||
Стоит перенести модуль в другой банк ради разгрузки — и тот же `call`
|
||||
уходит по адресу, где у нового банка пустой хвост (0xFF). Процессор
|
||||
ползёт по нему вверх, переваливает на 0x0000, и DSS убивает процесс.
|
||||
|
||||
Собирается это МОЛЧА: ни компилятор, ни линкер такой вызов не ловят.
|
||||
Поймано вживую 2026-08-27 — `hof_draw_rows` (банк 10) звал
|
||||
`pop_text_map` (банк 9): `call $E503` в пустоту, крэш финала.
|
||||
|
||||
Как проверяем. По .map берём банк каждого глобального символа (старшее
|
||||
слово адреса: 0 = резидент, N = банк N), по сгенерированным bankN_*.asm —
|
||||
все прямые `call`/`jp` по ИМЕНИ символа. Вызов законен, если цель
|
||||
резидентная или лежит в том же банке; иначе это мина.
|
||||
|
||||
ПРОВЕРКА 2 — УКАЗАТЕЛЬ НА ДАННЫЕ СВОЕГО БАНКА, ОТДАННЫЙ В ЧУЖОЙ.
|
||||
|
||||
Обратная сторона той же медали. Пока вызывающий и вызываемый лежали в
|
||||
одном банке, указатель на литерал или const-таблицу банка работал. Стоит
|
||||
пометить вызываемую функцию `__banked` — и трамплин на время вызова
|
||||
переключает W3: тот же указатель показывает уже в чужой банк, и функция
|
||||
читает оттуда мусор. Поймано вживую 2026-08-27: `hof_edit_draw` (банк 10)
|
||||
передавал литерал "_" в `pop_text_draw_mapped` (банк 9), и экран заливало
|
||||
знаками вопроса на каждое мигание курсора.
|
||||
|
||||
Ловим ФОРМУ, а не поток данных: аргументы банкового вызова SDCC кладёт
|
||||
непосредственно перед ним, поэтому идём от каждого `call ___sdcc_bcall_ehl`
|
||||
назад по блоку подготовки и смотрим, не грузился ли там адрес метки из
|
||||
области _BANKn этого же модуля.
|
||||
|
||||
НЕ ПРОВЕРЕНО: режим, в котором const/статики банковых модулей размещаются
|
||||
в РЕЗИДЕНТНОЙ памяти, а не в области банка (SprPoP им не пользуется). По
|
||||
построению там всё должно быть тихо — «данными банка» считаются только
|
||||
метки из `.area _BANKn`, а резидентные в этот список не попадут, и
|
||||
передавать такой указатель в чужой банк законно. Но живьём это не
|
||||
гонялось: если режим включат, проверку надо прогнать и убедиться, что она
|
||||
не начала молчать там, где не должна.
|
||||
|
||||
ЧЕГО ЭТА ПРОВЕРКА НЕ ВИДИТ (и не может): указатель, положенный в
|
||||
переменную и переданный позже; адрес, посчитанный арифметикой; указатель,
|
||||
отданный резидентной функции, которая сохранит его на будущее. Для этого
|
||||
нужен анализ потока данных. Правило остаётся за человеком: всё, что
|
||||
уходит указателем в другой банк, обязано лежать в W2 (стек, _DATA, копия).
|
||||
|
||||
Использование:
|
||||
check_bank_calls.py <build-dir> # каталог с .map и bankN_*.asm
|
||||
Возврат: 0 — чисто, 1 — найдено (печатается списком).
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# `call _foo`, `jp _foo`, с условием и без: `call NZ,_foo` / `jp C,_foo`.
|
||||
CALL_RE = re.compile(
|
||||
r'^\s*(call|jp)\s+(?:[A-Za-z]{1,2}\s*,\s*)?(_[A-Za-z_0-9]+)\s*(?:;.*)?$',
|
||||
re.IGNORECASE)
|
||||
# Строка карты: " 0009E503 _pop_text_map pop_ui"
|
||||
MAP_RE = re.compile(r'^\s+([0-9A-F]{8})\s+(\S+)\s')
|
||||
BANK_ASM_RE = re.compile(r'^bank(\d+)_')
|
||||
|
||||
# Механика самих банковых вызовов — не наша забота.
|
||||
IGNORE = {'___sdcc_bcall_ehl', '___sdcc_bcall'}
|
||||
|
||||
# `ld hl,#_метка` / `ld de,#___str_3` — материализация адреса.
|
||||
LOADADDR_RE = re.compile(
|
||||
r'^\s*ld\s+(hl|de|bc|iy|ix)\s*,\s*#(_{1,3}[A-Za-z_0-9]+)'
|
||||
r'(?:\s*[+-]\s*\w+)?\s*(?:;.*)?$', re.IGNORECASE)
|
||||
PUSH_RE = re.compile(r'^\s*push\s+(hl|de|bc|iy|ix)\s*(?:;.*)?$',
|
||||
re.IGNORECASE)
|
||||
# `ld e,#b_foo` перед трамплином называет банк цели.
|
||||
BANKARG_RE = re.compile(r'^\s*ld\s+e\s*,\s*#(b_[A-Za-z_0-9]+)\s*(?:;.*)?$',
|
||||
re.IGNORECASE)
|
||||
AREA_RE = re.compile(r'^\s*\.area\s+(\S+)')
|
||||
LABEL_RE = re.compile(r'^([A-Za-z_][A-Za-z_0-9]*)::?')
|
||||
# Директивы данных: по ним отличаем метку ДАННЫХ от метки функции. Без
|
||||
# этого проверка принимала бы за указатель саму механику трамплина
|
||||
# (`ld hl,#_функция` перед `call ___sdcc_bcall_ehl`).
|
||||
DATA_DIR_RE = re.compile(r'^\s*\.(ascii|asciz|str|db|byte|dw|word|ds|blkb)\b',
|
||||
re.IGNORECASE)
|
||||
# Сколько строк назад от трамплина считаем «подготовкой аргументов».
|
||||
ARG_WINDOW = 40
|
||||
|
||||
|
||||
def load_symbol_banks(map_path):
|
||||
"""symbol -> номер банка (0 = резидент). Берём ПЕРВОЕ определение:
|
||||
карта повторяет один и тот же список в каждой секции Area."""
|
||||
banks = {}
|
||||
for line in map_path.read_text(errors='replace').splitlines():
|
||||
m = MAP_RE.match(line)
|
||||
if not m:
|
||||
continue
|
||||
value, name = m.group(1), m.group(2)
|
||||
if name.startswith('l__') or name.startswith('s__') or name == '.__.ABS.':
|
||||
continue
|
||||
banks.setdefault(name, int(value[:4], 16))
|
||||
return banks
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) != 2:
|
||||
print(__doc__.strip())
|
||||
return 2
|
||||
build = Path(argv[1])
|
||||
maps = list(build.glob('*.map'))
|
||||
if not maps:
|
||||
print(f'check_bank_calls: нет .map в {build}', file=sys.stderr)
|
||||
return 2
|
||||
banks = load_symbol_banks(maps[0])
|
||||
|
||||
bad = []
|
||||
for asm in sorted(build.glob('bank*_*.asm')):
|
||||
m = BANK_ASM_RE.match(asm.name)
|
||||
if not m:
|
||||
continue
|
||||
self_bank = int(m.group(1))
|
||||
for n, line in enumerate(asm.read_text(errors='replace').splitlines(), 1):
|
||||
c = CALL_RE.match(line)
|
||||
if not c:
|
||||
continue
|
||||
target = c.group(2)
|
||||
if target in IGNORE:
|
||||
continue
|
||||
tb = banks.get(target)
|
||||
if tb is None or tb == 0 or tb == self_bank:
|
||||
continue
|
||||
bad.append((asm.name, n, self_bank, target, tb, line.strip()))
|
||||
|
||||
bad_ptr = check_bank_local_pointers(build, banks)
|
||||
|
||||
if not bad and not bad_ptr:
|
||||
print('check_bank_calls: чисто — ни прямых вызовов в чужой банк, '
|
||||
'ни указателей на данные своего банка наружу')
|
||||
return 0
|
||||
|
||||
if bad:
|
||||
print('check_bank_calls: ПРЯМОЙ ВЫЗОВ В ЧУЖОЙ БАНК '
|
||||
'(объявить цель __banked либо свести модули в один банк):',
|
||||
file=sys.stderr)
|
||||
for name, line_no, sb, target, tb, text in bad:
|
||||
print(f' {name}:{line_no}: банк {sb} -> {target} (банк {tb}): '
|
||||
f'{text}', file=sys.stderr)
|
||||
if bad_ptr:
|
||||
print('check_bank_calls: ВНИМАНИЕ — похоже, указатель на данные '
|
||||
'своего банка уходит в чужой (проверить глазами; лечится '
|
||||
'переносом данных в W2: стек, _DATA или копия):',
|
||||
file=sys.stderr)
|
||||
for name, line_no, sb, label, target, tb in bad_ptr:
|
||||
print(f' {name}:{line_no}: банк {sb}: #{label} -> {target} '
|
||||
f'(банк {tb})', file=sys.stderr)
|
||||
# Сборку валит ТОЛЬКО проверка 1: она доказательна. Проверка 2 —
|
||||
# эвристика по форме кода, её дело предупредить, а не останавливать.
|
||||
return 1 if bad else 0
|
||||
|
||||
|
||||
def bank_local_labels(lines):
|
||||
"""Метки ДАННЫХ, определённые в области _BANKn этого модуля: их адреса
|
||||
видны только пока банк подключён в W3. Метки функций сюда не берём —
|
||||
их адрес законно кладут в HL для самого трамплина."""
|
||||
labels, area = set(), None
|
||||
for i, line in enumerate(lines):
|
||||
a = AREA_RE.match(line)
|
||||
if a:
|
||||
area = a.group(1)
|
||||
continue
|
||||
m = LABEL_RE.match(line)
|
||||
if not (m and area and area.upper().startswith('_BANK')):
|
||||
continue
|
||||
for nxt in lines[i + 1:i + 4]: # пропускаем пустые/метки
|
||||
if not nxt.strip() or LABEL_RE.match(nxt):
|
||||
continue
|
||||
if DATA_DIR_RE.match(nxt):
|
||||
labels.add(m.group(1))
|
||||
break
|
||||
return labels
|
||||
|
||||
|
||||
def check_bank_local_pointers(build, banks):
|
||||
"""Адрес метки своего банка, положенный в аргументы банкового вызова
|
||||
в ЧУЖОЙ банк. Ищем форму, а не поток данных (см. шапку файла)."""
|
||||
bad = []
|
||||
for asm in sorted(build.glob('bank*_*.asm')):
|
||||
m = BANK_ASM_RE.match(asm.name)
|
||||
if not m:
|
||||
continue
|
||||
self_bank = int(m.group(1))
|
||||
lines = asm.read_text(errors='replace').splitlines()
|
||||
local = bank_local_labels(lines)
|
||||
if not local:
|
||||
continue
|
||||
for i, line in enumerate(lines):
|
||||
if '___sdcc_bcall_ehl' not in line or 'call' not in line.lower():
|
||||
continue
|
||||
# Банк цели: ближайший `ld e,#b_...` выше вызова.
|
||||
target, tbank = None, None
|
||||
for j in range(i - 1, max(-1, i - 6), -1):
|
||||
b = BANKARG_RE.match(lines[j])
|
||||
if b:
|
||||
target = b.group(1)[2:] # b_foo -> foo
|
||||
tbank = banks.get('_' + target)
|
||||
break
|
||||
if tbank is None or tbank == self_bank:
|
||||
continue
|
||||
# Назад по блоку подготовки аргументов. Останавливаемся на
|
||||
# первом же `call`: аргументы НАШЕГО вызова не могут готовиться
|
||||
# раньше предыдущего — иначе тот бы их и потребил.
|
||||
#
|
||||
# Признак «указатель ушёл наружу» — адрес метки данных банка,
|
||||
# который затем ПОЛОЖЕН В СТЕК как аргумент. Для банкового
|
||||
# вызова это единственный способ передать указатель: HL занят
|
||||
# самим трамплином. Адрес, загруженный ради индексной
|
||||
# арифметики (`ld de,#tbl` + `add hl,de` + `ld a,(hl)`), в стек
|
||||
# не кладётся — так отсеиваются чтения таблиц своего банка.
|
||||
pushed = {} # регистр -> строка push
|
||||
for j in range(i - 1, max(-1, i - ARG_WINDOW), -1):
|
||||
if LABEL_RE.match(lines[j]): # начало другого блока
|
||||
break
|
||||
if re.match(r'^\s*call\b', lines[j], re.IGNORECASE):
|
||||
break
|
||||
pu = PUSH_RE.match(lines[j])
|
||||
if pu:
|
||||
pushed.setdefault(pu.group(1).lower(), j)
|
||||
continue
|
||||
a = LOADADDR_RE.match(lines[j])
|
||||
if not (a and a.group(2) in local):
|
||||
continue
|
||||
reg = a.group(1).lower()
|
||||
if reg not in pushed:
|
||||
continue
|
||||
# Между загрузкой адреса и push регистр не должен меняться:
|
||||
# иначе в стек уходит не адрес, а посчитанное по нему
|
||||
# значение (индексная арифметика по своей таблице).
|
||||
halves = {'hl': 'hl', 'de': 'de', 'bc': 'bc'}.get(reg, reg)
|
||||
touched = re.compile(
|
||||
r'^\s*(?:ld\s+[' + halves[0] + halves[1] + r']\s*,'
|
||||
r'|ld\s+' + reg + r'\s*,'
|
||||
r'|(?:inc|dec)\s+(?:' + reg + r'|[' + halves[0] +
|
||||
halves[1] + r'])\b'
|
||||
r'|add\s+' + reg + r'\b'
|
||||
r'|ex\s+de\s*,\s*hl)', re.IGNORECASE)
|
||||
if any(touched.match(t) for t in lines[j + 1:pushed[reg]]):
|
||||
break
|
||||
bad.append((asm.name, j + 1, self_bank, a.group(2),
|
||||
target, tbank))
|
||||
break
|
||||
return bad
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main(sys.argv))
|
||||