Compare commits

...

6 Commits

Author SHA1 Message Date
snark13 f4b4852d51 QuickSave F6/F9 в roomtest; bank_load_file/bank_save_file/gfx_w0_page_prepare; sprinter-cc: авто n_banks
roomtest:
- QuickSave/QuickLoad (F6/F9): снапшот 'POPQ' v3 в POP.SAV/POP.BAK на HDD,
  транзакционная запись (POP.NEW -> rename, откат при ошибке), XOR-контрольная
  сумма payload'а; сериализация всех игровых переменных через W0-примитивы
  pop_qs_*; pop_qsave_process() на границе кадра вне Char-окон
- pop_qsave_restore_room(): полная перезагрузка комнаты после загрузки
  (карта/края/швы, сброс bake-кэша, перерисовка обеих страниц, инвалидация
  кэшей спрайтов и HP)
- сериализаторы в pop_map/pop_loose_mob/pop_trob/pop_guard_ai
  (+ восстановление инвариантов: mobs_live, trob_drawn, redraw)
- immortal-чит 2 уровня: уровень 2 поглощает только малый урон Kid'а

libc/libbgi:
- bank_load_file()/bank_save_file() — резидентное файловое I/O в банк,
  без правила W3 (путь читается до переключения страницы)
- gfx_w0_page_prepare(page) — подготовка W0-окна (IRQ/NMI-стабы) одной
  функцией; atlas_load.c и roomtest переведены на новые примитивы;
  ручные ISR-стабы удалены

sprinter-cc / сборка:
- --bank N=FILE.c: автогенерация n_banks (_n_banks_auto.c), ручные
  const n_banks удалены из тестов
- roomtest/app.mk: ресурсы через stamp-файлы (.resource-stamps/) — один
  запуск упаковщика на группу вместо N под -B; HDD_PACK_ARGS
2026-08-22 11:53:10 +03:00
snark13 b6699b3aef Разрез pop_tile: холодная половина в банк 5 (−1788 Б резидента)
pop_tile.c был крупнейшим жильцом резидента (5972 Б кода).  Целиком он не
уедет — его const-таблицы читают банки 2, 3, 7 и 8, а таблица в чужом банке
не видна.  Поэтому разрез, а не перенос.

Отбор ЗАМЕРОМ, а не по смыслу: каждая функция посчитана брейкпоинтом-
счётчиком в MAME — сколько вызовов в кадре покоя и сколько в кадре полной
перерисовки комнаты (форсируется читом +/-).  Порог — пик не больше 3.
Проверено на ДВУХ тайлсетах, подземелье и дворец: pop_mem_b рисует
композитный кусок и мог оказаться дворцовым, но и там 0 вызовов.

Уехало: pop_mem_b, pop_cd_hit (+hit_rect), pop_cd_hit_slot, pop_cd_init,
pop_cd_clear, pop_t_win_set/clear, pop_bar_black, pop_heal_off,
pop_potion_flask, pop_room_set_above/below.
Осталось: pop_blit_b со статиками (184 вызова на редрав), pop_cd_touch
(198), pop_tile_code (296), pop_wall_modifier (101), pop_env_b (73),
pop_tile_mod (70), все таблицы.  pop_fore_set_clip оставлен намеренно —
88 Б не стоят отказа от прямого вызова из банка 4.

Цена трамплина замерена: 252 такта пролог + 84 эпилог + ~50 у вызывающего
= ~410.  Это ~1000 тактов на кадр покоя (0,2 % работы) и ~3700 на редрав.

blit_b_clip перестал быть static и объявлен в _pop_tile.h: вызов
банк -> резидент прямой, трамплин не нужен, поэтому статик горячей половины
переносить следом не пришлось.

Итог: _CODE 23716 -> 21928, свободно 747 -> 2535 Б (с 129 Б до всех работ).
Проверено в MAME на уровнях 1 и 4, с переходами комнат.

Метод замера, таблица частот и ловушка с данными банка — docs/resident_budget.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 22:26:05 +03:00
snark13 536c60d14c AGENTS.md + .codex/config.toml — конфигурация агента
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 21:59:28 +03:00
snark13 e2730f78e0 gfx_scroll_v: колоночный accel-проход без буфера-посредника + тест scroll
Вертикальный скролл перестал ходить через строку-буфер на стеке: колонку
читаем с Port_Y=ys, пишем с Port_Y=yd, а STOP между read- и write-триггером
делает промежуточный OUT Port_Y безопасным.  Один проход вместо
grab→blit-через-буфер.

Справочник libc приведён в соответствие (там же строка про новую точку
входа cbl_open_silence).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 21:59:28 +03:00
snark13 3ea4546656 cbltest: пропущенный вызывающий cbl_open + обновление размерного эталона
tests/cbltest не попал в правку API (мой греп обрезался на артефактах
сборки, полный make его и поймал).

Эталон размеров: cblstream −614, cbltest −617, cblwav −592 — это ушедший
malloc.  bgi_img +229 к моей правке отношения не имеет (CBL он не линкует
вовсе): рост пришёл с b3e754a, реверта «каталог атласа читается из W0».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 21:59:28 +03:00
snark13 8458ec65f4 libc/cbl: две точки входа вместо underrun_mode — malloc больше не в резиденте
Линкер тянет .rel целиком, поэтому malloc/free, стоявшие в мёртвой ветке
CBL_UNDERRUN_SILENCE внутри cbl_open, приезжали в резидент КАЖДОМУ
приложению — включая те, что льют тишину сами и кучей не пользуются.

Разведено:
  cbl_open(freq, fmt, pump, fill)          — ничего не аллоцирует;
  cbl_open_silence(freq, fmt, pump, fill)  — аллоцирует буфер тишины;
  _cbl_open_raw(...)                       — общее тело.
Параметр underrun_mode из публичного API убран: режим задаёт выбор функции.

cbl_close больше не зовёт free — иначе malloc возвращался бы тем же путём.
Буфер тишины живёт до выхода из программы и переиспользуется; его размер
запоминается, иначе открытие 16-бит после 8-бит писало бы memset'ом мимо
выделенного куска.  _cbl_open_raw указатель на буфер не трогает вовсе —
иначе cbl_open после cbl_open_silence терял бы уже выделенную память.

В дереве режим SILENCE не использовал никто: все три вызова (cblwav,
cblstream, PoP) передавали CBL_UNDERRUN_APP.

Итог для PoP: _CODE 24329 -> 23716 (−613 Б), свободно в резиденте 129 ->
747 Б.  make size-check чистый (67 программ), звук в MAME проверен —
насос отработал 696 запросов, pop_snd_ok/want = 1/1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 21:53:08 +03:00
79 changed files with 2553 additions and 633 deletions
+11
View File
@@ -0,0 +1,11 @@
[mcp_servers.mame-z80]
command = "/Users/alex/.local/bin/uv"
args = [
"run",
"--python",
"3.12",
"--no-project",
"--with",
"mcp<2",
"/Volumes/SAM8/Projects/DIY/Z80/Sprinter/C-Compiler/mame/sources/MAME/src/mame_mcp.py",
]
+1
View File
@@ -8,6 +8,7 @@ build/
# sprinter-cc per-example intermediate directory # sprinter-cc per-example intermediate directory
.sprinter-cc-*/ .sprinter-cc-*/
.resource-stamps/
# Per-program final/intermediate outputs landing alongside the source # Per-program final/intermediate outputs landing alongside the source
# (real apps under examples/ and libc feature tests under tests/). # (real apps under examples/ and libc feature tests under tests/).
+83
View File
@@ -0,0 +1,83 @@
# Sprinter C-Compiler — правила проекта
Target-слой SDCC 4.5 (z80) для компьютера Sprinter Sp2000: crt0,
линковка, libc, mkexe. Общение и комментарии — на русском.
## Сборка и проверка
```
make # tools + lib + libbgi + все тесты (45) + examples
make -C libc # только libc → lib/sprinter.lib (fast) + sprinter_safe.lib
make -C libbgi # только BGI → lib/bgi256.lib (fast) + bgi256_safe.lib
make floppy # упаковать все .exe в mame/v306/IMG/mc.img
make size-check # размерный регресс: _CODE vs docs/size_baseline.tsv
make size-baseline # принять текущие размеры эталоном
```
Обе библиотеки собираются в двух вариантах: fast (дефолт; `-D*_NOCHECK`
параметр-валидации вырезаны) и safe (линкуется по `sprinter-cc --safe`).
Критичные гарды (напр. _fd_guard — 9-й OPEN вешает DSS) — в ОБОИХ.
Графика (BGI) — отдельная библиотека libbgi/ (см. ниже). Программа,
использующая graphics.h, собирается с `--gfx 256` (или `--gfx 16` в
Фазе 2): sprinter-cc подлинкует lib/bgi256.lib и добавит -I libbgi/include.
Одиночный тест: `cd tests/<имя> && make run` (пакует exe + EXTRA_DATA на
дискету и запускает MAME автоматически через `toolchain/mame_interactive.py`,
снимает скриншоты, выводит пути). Для сложных сценариев (диалог, несколько
шагов ввода) — прямой вызов:
`python3 toolchain/mame_interactive.py tests/<имя>/<имя>.exe --snap T1,T2 --timeout T`.
Скриншоты лежат в `mame/v306/snap_auto/sprinter/` (читаются инструментом Read).
Подробности: `docs/mame-autotest.md`.
После правок libc/libbgi: пересборка от чистого листа (`make -C libc clean`
/ `make -C libbgi clean`) не обязательна — stale .rel чистятся
автоматически; `make size-check` обязателен (рост _CODE без причины —
регрессия).
## Правила libc
- **1 публичная функция = 1 .c-модуль** (линкер тянет .rel целиком —
гранулярность файлов = гранулярность DCE). Никакой группировки
«используются вместе». Internal-хелперы — тоже по одному на модуль
(`_`-префикс); общие статики — в отдельные data-модули
(`_xxx_state.c`); internal-заголовки (`_file.h`, `_gfx.h`, …) —
рядом с исходниками, НЕ в libc/include.
- Имя файла = имя функции. libc/Makefile собирает wildcard'ом —
ничего регистрировать не надо.
- Комментарии — на русском; шапка модуля объясняет что/зачем + ABI.
- File-scope переменные НЕ инициализировать `= 0` (crt0 зануляет
_DATA; см. memory/sdcc_static_storage_gotcha).
- asm-связки между модулями: `call/jp _global` — ок; `jr/djnz` через
границу и fall-through — НЕЛЬЗЯ (docs/libc-split-asm-cases.md).
- Заголовки: сначала пробовать include_next-паттерн; полная замена
SDCC-заголовка обязана дублировать его контракт
(docs/libc-headers.md).
- Справочник API — docs/libc-reference.md (обновлять при добавлении
функций).
## ABI и платформа (кратко; детали в memory/)
- SDCC `__sdcccall(1)`: arg1 → HL (8-бит → A), arg2 → DE, остальные
на стеке (callee-pops в __naked); **возврат int/ptr в DE**, uint8 в A.
IX callee-saved (в __naked с IX — push/pop обязательны).
- ESTEX (rst #0x10): CF=1 — ошибка, код в A → `call __errno_set`;
все регистры клобберятся (IX сохранять); стек обязан быть в W2.
- BIOS (rst #0x08): строки/буферы в #4000-#BFFF.
- Квирки: ESTEX WRITE возвращает DE=0 на успехе (судить по CF/A);
лимит 8 файловых манипуляторов, 9-й OPEN ВЕШАЕТ DSS (_fd_guard);
ENV $46: A=0 = NOT FOUND.
- Перед обвинением компилятора/железа — подтвердить артефактом
(сгенерированный .asm в libc/build/ или libbgi/build/, дамп, репро) — см.
memory/defer_unexplained_quirks.
## Структура
- `libc/<area>/*.c` — модули libc (ядро, БЕЗ графики); `libc/include/` — публичные заголовки libc
- `libbgi/` — графика BGI (отдельная библиотека): `common/` — mode-agnostic (один исходник, .rel в обеих driver-библиотеках), `bgi256/` + `bgi16/` — mode-specific leaf'ы (реальные реализации, без обёрток); `include/` — graphics.h + gfx.h; `_bgi.h` — внутренний заголовок. Собирает `lib/bgi256.lib``bgi16.lib` в Фазе 2). Выбор режима линковкой: `--gfx 256` / `--gfx 16`.
- `runtime/` — crt0-семейство, heap, bank (bank.s собирается per-build)
- `bin/sprinter-cc` — обёртка компилятора; `toolchain/mkexe` — упаковщик
- `tests/` — по одному API/фиче; `examples/` — реальные приложения
- `docs/` — дизайн-доки; `docs/TODO.md` — roadmap
- `third_party/solid-c/` — нативный Sprinter C (референс, CP866;
их ABI несовместим — только как образец)
+2 -1
View File
@@ -144,7 +144,8 @@ sprinter-cc -o foo.exe foo.c [more.c ...] [options]
--memory-manual SPEC explicit placement (CODE=W1|W2,DATA=W1|W2|SAME,BANKED=W1|W3) --memory-manual SPEC explicit placement (CODE=W1|W2,DATA=W1|W2|SAME,BANKED=W1|W3)
--stack-size N bytes reserved for the stack (default ~1278) --stack-size N bytes reserved for the stack (default ~1278)
--crt0=TYPE default | minimal | banked | small --crt0=TYPE default | minimal | banked | small
--bank N=FILE.c compile FILE.c into bank N (repeatable, max 15) --bank N=FILE.c compile FILE.c into bank N (repeatable, consecutive 1..15;
crt0 bank count is generated automatically)
--debug enable runtime diagnostics (defines DEBUG_RT) --debug enable runtime diagnostics (defines DEBUG_RT)
-I PATH extra include path -I PATH extra include path
-L 0xADDR / -E / -S override load / entry / stack addresses -L 0xADDR / -E / -S override load / entry / stack addresses
+4 -1
View File
@@ -44,6 +44,9 @@ RUN_MAME := $(MAME_DIR)/run_mame.sh
# Optional knobs — see top of file. # Optional knobs — see top of file.
MEMORY ?= tiny MEMORY ?= tiny
SOURCES := $(EXAMPLE).c $(EXTRA_SRCS) SOURCES := $(EXAMPLE).c $(EXTRA_SRCS)
# Аргументы упаковщика HDD. Обычно это exe и EXTRA_DATA; приложение со
# своей раскладкой каталогов может переопределить переменную до include.
HDD_PACK_ARGS ?= $(EXAMPLE).exe $(EXTRA_DATA)
CC_FLAGS := --memory $(MEMORY) CC_FLAGS := --memory $(MEMORY)
ifneq ($(STACK_SIZE),) ifneq ($(STACK_SIZE),)
@@ -92,7 +95,7 @@ run: floppy
# используется MCP-мостом к MAME (run_bridge.sh). После пересборки образа # используется MCP-мостом к MAME (run_bridge.sh). После пересборки образа
# MAME ОБЯЗАН полный рестарт (chdman -f = новый inode; см. memory). # MAME ОБЯЗАН полный рестарт (chdman -f = новый inode; см. memory).
hdd: $(EXAMPLE).exe hdd: $(EXAMPLE).exe
$(MAKE_HDD) $(HDD_IMG) $(EXAMPLE).exe $(EXTRA_DATA) $(MAKE_HDD) $(HDD_IMG) $(HDD_PACK_ARGS)
@echo @echo
@echo "HDD (D:) ready: $(HDD_IMG) (with $(EXAMPLE).exe$(if $(EXTRA_DATA), + $(EXTRA_DATA)))" @echo "HDD (D:) ready: $(HDD_IMG) (with $(EXAMPLE).exe$(if $(EXTRA_DATA), + $(EXTRA_DATA)))"
@echo "ВНИМАНИЕ: перезапусти MAME (run_bridge.sh) — образ пересобран." @echo "ВНИМАНИЕ: перезапусти MAME (run_bridge.sh) — образ пересобран."
+2
View File
@@ -14,6 +14,8 @@
| [`perf_cyan_phase.md`](perf_cyan_phase.md) | **ЦИАН фаза (персонажи + передний слой)**: раскладка тактов, способы ускорения (C1..C7), журнал правок — рабочий документ между сессиями. 2026-08-17 | | [`perf_cyan_phase.md`](perf_cyan_phase.md) | **ЦИАН фаза (персонажи + передний слой)**: раскладка тактов, способы ускорения (C1..C7), журнал правок — рабочий документ между сессиями. 2026-08-17 |
| [`perf_backlog.md`](perf_backlog.md) | Отложенная оптимизация отрисовки с замерами 2026-08-10 + **как мерить** (wait-state'ы, границы кадра). Позиции 1–7 переехали в фазовые документы выше | | [`perf_backlog.md`](perf_backlog.md) | Отложенная оптимизация отрисовки с замерами 2026-08-10 + **как мерить** (wait-state'ы, границы кадра). Позиции 1–7 переехали в фазовые документы выше |
| [`quicksave_plan.md`](quicksave_plan.md) | **QuickSave/QuickLoad**: разбор (это enhancement SDLPoP, в оригинале 1989 его НЕТ), инвентаризация нашего состояния, формат снимка, шаги QS1..QS6. План, код не начат. 2026-08-17 | | [`quicksave_plan.md`](quicksave_plan.md) | **QuickSave/QuickLoad**: разбор (это enhancement SDLPoP, в оригинале 1989 его НЕТ), инвентаризация нашего состояния, формат снимка, шаги QS1..QS6. План, код не начат. 2026-08-17 |
| [`full_game_plan.md`](full_game_plan.md) | **Полноценная игра**: app state machine, title/intro, demo level 0, таймер, cutscenes, уровни 1..14, ending и Hall of Fame. Уровень 15 исключён. 2026-08-21 |
| [`menu_settings_plan.md`](menu_settings_plan.md) | **Pause menu и Settings**: QuickSave/QuickLoad в основном menu, `POP.CFG`, один `POP.SAV` + `POP.BAK`, текущий VANILLA и задел под ENHANCED. 2026-08-21 |
| [`levels_plan.md`](levels_plan.md) | Следующий этап: уровни 2+, второй тайлсет, читы SDLPoP | | [`levels_plan.md`](levels_plan.md) | Следующий этап: уровни 2+, второй тайлсет, читы SDLPoP |
| [`levels_12_15_plan.md`](levels_12_15_plan.md) | **Уровни 12/13** (тень, Джафар, падающие плиты) + что такое 14/15 и 0. 2026-08-13 | | [`levels_12_15_plan.md`](levels_12_15_plan.md) | **Уровни 12/13** (тень, Джафар, падающие плиты) + что такое 14/15 и 0. 2026-08-13 |
| [`midtable_analysis.md`](midtable_analysis.md) | **Слои отрисовки**: как устроены back/mid/fore и objtable в оригинале, чего стоит порт, развилки. 2026-08-13 | | [`midtable_analysis.md`](midtable_analysis.md) | **Слои отрисовки**: как устроены back/mid/fore и objtable в оригинале, чего стоит порт, развилки. 2026-08-13 |
+349
View File
@@ -0,0 +1,349 @@
# От roomtest к полноценной игре — сценарий и оболочка
Статус: **согласованный план, код не начат** (2026-08-21).
Этот документ описывает превращение текущего игрового цикла
`roomtest` в законченную игру: заставка, интро, демонстрационный уровень,
сцены между уровнями, таймер, финал и Hall of Fame. План меню и постоянных
настроек вынесен в [`menu_settings_plan.md`](menu_settings_plan.md),
детальный план QuickSave — в [`quicksave_plan.md`](quicksave_plan.md).
## 1. Зафиксированный scope
- Целевая последовательность — оригинальная SDLPoP/DOS PoP с уровнями
**1..14**. Уровень 14 — скрытая финальная часть после Джаффара: его номер
игроку не показывается, победа наступает в комнате 5.
- Уровень **15 удаляется полностью**: не пакуется на HDD, не загружается,
отсутствует в переходах, читах и UI; специальная логика potions/copy
protection level удаляется.
- Уровень **0** остаётся только демонстрационным (attract mode), а не частью
новой игры.
- Title sequence повторяет SDLPoP. Перед ней допускается отдельный
пропускаемый экран с информацией о Sprinter-сборке.
- Первая версия использует текущий профиль поведения `VANILLA`. Сейчас это
означает **существующую реализацию roomtest**, включая уже встроенные
исправления. Аудит и разведение `VANILLA/ENHANCED` — будущая задача.
- Программа работает **только с HDD**. Варианты без сохранения для floppy не
проектируются.
- Моды и выбор levelset в этот план не входят.
## 2. Что делает SDLPoP
Источники истины в локальном SDLPoP:
- `src/seg000.c`: `start_game()`, `show_title()`, demo mode, общий кадр,
проверка финала;
- `src/seg003.c`: `init_game()`, `play_level()`, `play_level_2()`;
- `src/seg001.c`: cutscene engine, `pv_scene()`, сцены 2/4/6/8/9/12,
`time_expired()`, `end_sequence()` и Hall of Fame;
- `src/data.h`: `tbl_cutscenes`, параметры уровня 0, win level/room;
- `data/TITLE`, `data/PV`, `data/LEVELS/res2000.bin`: ресурсы оболочки.
Штатный маршрут:
```text
boot
-> title / story screens
-> Princess + Jaffar intro
-> credits / Hall of Fame
-> demo level 0
-> title или новая игра
-> levels 1..14
before 2 -> princess cutscene
before 4 -> princess cutscene
before 6 -> princess cutscene
before 8 -> princess + mouse
before 9 -> princess + mouse
before 12 -> scene selected by remaining time
-> level 14, room 5
-> embrace + mouse
-> ending text/music
-> Hall of Fame
-> title
```
Кроме уровней, здесь есть глобальный 60-минутный таймер, сцена истечения
времени, пропуск сцен клавишей, fade/flash, ожидание музыки и возврат в
attract loop после демо или финала.
## 3. Текущее состояние roomtest
Уже реализованы игровой кадр, комнаты, уровни, тайлсеты, Kid/Guard/Shadow,
Джаффар, специальные события, checkpoint, переходы уровней, бесшовный
выход 12-го уровня, перенос максимального HP и звуковые эффекты.
Отсутствуют:
- верхнеуровневая оболочка приложения;
- title/story sequence и текстовые экраны;
- уровень 0 и записанное управление демо;
- общий cutscene engine и PV-ресурсы;
- сцены между уровнями;
- глобальный таймер и time-expired sequence;
- корректный переход из уровня 14 в ending;
- финальная сцена и Hall of Fame;
- возврат к title без перезапуска программы.
## 4. Архитектура: автомат состояний приложения
Нельзя наращивать все режимы условиями внутри кадрового цикла. Текущий
цикл должен стать реализацией одного состояния `PLAYING`:
```text
BOOT -> BUILD_INFO -> TITLE -> INTRO -> DEMO
| |
+---- NEW_GAME <-+
NEW_GAME -> LEVEL_LOAD -> PLAYING <-> PAUSE_MENU
|
+-> CUTSCENE -> LEVEL_LOAD
+-> TIME_EXPIRED -> TITLE
+-> ENDING -> HALL_OF_FAME -> TITLE
```
Минимальный контекст оболочки:
```c
typedef enum {
POP_APP_BOOT,
POP_APP_BUILD_INFO,
POP_APP_TITLE,
POP_APP_INTRO,
POP_APP_DEMO,
POP_APP_LEVEL_LOAD,
POP_APP_PLAYING,
POP_APP_PAUSE_MENU,
POP_APP_CUTSCENE,
POP_APP_TIME_EXPIRED,
POP_APP_ENDING,
POP_APP_HALL_OF_FAME,
POP_APP_QUIT
} pop_app_state_t;
```
Переходы задаются результатом состояния, а не прямыми рекурсивными
вызовами наподобие SDLPoP `start_game()`/`longjmp()`. На Z80 это проще для
стека и позволяет освобождать ресурсы каждого режима в одном месте.
## 5. Ресурсная модель
Title и cutscene-ресурсы нельзя постоянно держать рядом с игровыми
атласами. Для каждого состояния нужен явный lifecycle:
```text
enter: pause sound -> unload incompatible set -> load set -> apply palette
run: process input/timer/animation
leave: stop sound -> release EMM pages -> clear transient state
```
Новые группы HDD:
```text
TITLE\ title/story images, palette, optional build-screen assets
PV\ princess room, Princess/Jaffar/mouse frames, palettes
MUSIC\ intro, cutscene and ending tracks/samples
LEVELS\ res2000..res2014.bin
```
Конкретный формат атласов выбирает упаковщик. Runtime не должен разбирать
PNG/DAT: как и игровые спрайты, он получает подготовленные `.atl`/`.bin`.
## 6. Экран Sprinter build
Отдельное состояние перед оригинальной заставкой:
```text
PRINCE OF PERSIA
SPRINTER SP2000 BUILD
version / date / build id
```
Требования:
- пропускается любой клавишей;
- выключается в Settings;
- не запускает музыку оригинального title и не меняет её тайминги;
- данные версии генерируются сборкой, а не правятся вручную в C;
- отсутствие экрана приводит прямо к `TITLE`.
## 7. Title и текстовая подсистема
Порядок переносится из `show_title()`:
1. основной титульный экран;
2. Presents;
3. название игры и Jordan Mechner;
4. story frame / “In the absence…”;
5. intro Princess + Jaffar;
6. story “Marry Jaffar…”;
7. credits;
8. Hall of Fame, если таблица непуста;
9. demo level 0.
Нужны общие примитивы: загрузить full-screen image, вывести строку,
показать экран заданное время, transition left-to-right, fade in/out,
прервать ожидание клавишей. Текст и меню должны использовать один renderer.
Критерий: последовательность и музыкальные точки совпадают с SDLPoP;
Sprinter build screen не сдвигает оригинальный soundtrack.
## 8. Demo level 0
- Добавить на HDD `res2000.bin`.
- Загружать уровень обычным loader, но выставлять demo HP и demo mode.
- Воспроизводить `demo_moves` как синтетический источник `control_*`.
- Пользовательский ввод прерывает демо и начинает новую игру.
- Достижение demo end room (у SDLPoP — 24), смерть или конец скрипта
возвращают в `TITLE`.
- Pause menu, QuickSave и cheats в demo недоступны.
- RNG демо и начальное состояние должны быть детерминированы.
Критерий: без ввода attract loop не требует перезапуска процесса;
title -> demo -> title повторяется неограниченно.
## 9. Глобальный таймер
Состояние: минуты, тики и флаг показа. Таймер создаётся при New Game,
переносится между уровнями и входит в QuickSave.
Правила `VANILLA`:
- на pause menu, загрузке HDD, QuickSave/QuickLoad время не идёт;
- игровые тики следуют темпу логического кадра, а не частоте render loop;
- поведение во время level-end sound и cutscenes сверяется буквально с
SDLPoP;
- после Джаффара/на финальном уровне время не должно вызвать поражение;
- ноль времени переводит приложение в `TIME_EXPIRED`.
Критерий: одинаковый игровой отрезок в NORMAL даёт то же уменьшение времени,
что SDLPoP; сохранение/загрузка не добавляет и не отнимает тики.
## 10. Cutscene engine
Сцены SDLPoP состоят из небольшого набора повторяемых команд. Вместо набора
крупных C-функций нужен компактный интерпретатор:
```text
SET_ACTOR actor
SET_POS x,y,dir
START_SEQ seq
WAIT_FRAMES n
PLAY_SOUND id
WAIT_SOUND
SET_HOURGLASS frame
SET_SAND state
FLASH color,frames
FADE_IN / FADE_OUT
CLEAR_ACTOR actor
END
```
Скрипты — `const` в холодном банке или подготовленный бинарный ресурс.
Interpreter обязан:
- исполнять один шаг/кадр без блокирующих длинных циклов;
- поддерживать пропуск сцены;
- при пропуске выполнять cleanup и выходить в заранее заданное состояние;
- освобождать PV-ресурсы перед загрузкой игрового тайлсета;
- не разрешать pause menu/QuickSave внутри сцены.
Порядок переноса: intro, 2/6, 4, 8, 9, 12, time expired, ending. Сцена 12
выбирает короткий или обычный вариант по остатку времени.
## 11. Переходы между уровнями
Таблица сценария должна быть отдельна от таблиц механики уровня:
```c
typedef struct {
uint8_t level;
uint8_t pre_cutscene;
uint8_t show_level_number;
uint8_t ending_rule;
} pop_level_flow_t;
```
Особые правила:
- New Game начинает уровень 1;
- перед 2/4/6/8/9/12 запускается сцена;
- 12 -> 13 остаётся бесшовным;
- после победы над Джаффаром переход идёт в 14;
- номер 14 не показывается;
- вход в комнату 5 уровня 14 переводит в `ENDING`;
- значения больше 14 недопустимы и дают диагностическую ошибку, а не
попытку открыть файл.
## 12. Ending и Hall of Fame
Ending:
1. загрузить PV-набор;
2. встреча Kid и Princess;
3. объятие;
4. появление мыши;
5. ending music;
6. финальные story/title экраны;
7. переход в Hall of Fame.
Hall of Fame хранится на HDD в отдельном версионированном `POP.HOF`.
Сохраняются имя и результат; ввод имени использует тот же текстовый/UI слой.
Повреждённый или неизвестный формат означает пустую таблицу, но не мешает
запуску игры. После показа — возврат в `TITLE`.
## 13. Удаление уровня 15
Отдельный ранний этап, чтобы новый flow не наследовал лишний маршрут:
- убрать `res2015.bin` из `LVL_NUMS` и HDD image;
- заменить последний игровой уровень на 14;
- остановить Shift+L и прочую навигацию на 14;
- удалить `POP_POTIONS_LEVEL` и специальный половинный урон синих зелий;
- исключить copy protection из конфигурации и меню;
- добавить тест: после уровня 14 приложение входит в ending и никогда не
запрашивает `res2015.bin`.
## 14. Этапы реализации
| этап | результат | критерий приёмки |
|---|---|---|
| **FG0** | удалить уровень 15, формализовать 1..14 | HDD не содержит res2015; переход выше 14 невозможен |
| **FG1** | автомат состояний, текущая игра = PLAYING | старт/рестарт/выход проходят без рекурсии и утечки EMM |
| **FG2** | QuickSave/QuickLoad | критерии `quicksave_plan.md`, включая POP.BAK |
| **FG3** | минимальный pause menu | Resume/Save/Load/Restart/Settings/Quit работают |
| **FG4** | глобальный таймер | совпадение с SDLPoP и корректный save/load |
| **FG5** | text/full-screen/fade primitives | тестовые экраны и переходы на Sprinter |
| **FG6** | build info + оригинальный title | точный порядок, корректный пропуск |
| **FG7** | demo level 0 | бесконечный attract loop, ввод начинает игру |
| **FG8** | cutscene interpreter + intro | интро проходит и пропускается без утечек |
| **FG9** | сцены 2/4/6/8/9/12 | правильный вызов один раз перед уровнем |
| **FG10** | time expired | отдельная сцена и возврат на title |
| **FG11** | level 14 -> ending | полный маршрут после Джаффара |
| **FG12** | Hall of Fame | запись HDD, ввод имени, возврат к attract loop |
QuickSave допускается реализовать до остальных частей оболочки: FG2 — одна
из ближайших самостоятельных задач.
## 15. Проверки
- Host-тест автомата: все допустимые переходы и отсутствие уровня 15.
- Host-тест cutscene interpreter на синтетическом скрипте и skip в каждой
ожидающей команде.
- Host-тест demo input: одинаковый seed даёт одинаковый поток управления.
- MAME: cold boot -> build info -> title -> demo -> title.
- MAME: новая игра -> принудительный переход по всем pre-level scenes.
- MAME: time expired и пропуск сцены.
- MAME: 13 -> 14 -> room 5 -> ending -> HOF -> title.
- Проверка EMM/FD до и после каждого состояния: число страниц и открытых
файлов возвращается к базовому.
- `make size-check`; крупный cold-код размещать в банках и отдельно следить
за лимитом 16 КБ каждого банка.
## 16. Не входит в план
- уровень 15 и copy protection;
- моды и выбор levelset;
- replay/recording;
- точная эмуляция SDL video/controller options;
- профиль ENHANCED и индивидуальные switches fixes.
+319
View File
@@ -0,0 +1,319 @@
# Pause menu и Settings для Sprinter PoP
Статус: **согласованный план, код не начат** (2026-08-21).
Связанные документы:
- [`full_game_plan.md`](full_game_plan.md) — автомат состояний, title,
demo, cutscenes и ending;
- [`quicksave_plan.md`](quicksave_plan.md) — состав и восстановление снимка.
## 1. Решения
- Программа работает только с HDD; настройки, QuickSave и Hall of Fame
всегда могут быть постоянными файлами.
- Основной pause menu обязательно содержит QuickSave и QuickLoad.
- QuickSave имеет один слот `POP.SAV`; предыдущая корректная запись хранится
как `POP.BAK`.
- Первая версия имеет один профиль `VANILLA`. Под этим именем пока понимается
**текущее поведение roomtest**, включая уже встроенные исправления.
- Дизайн файла и API предусматривает будущий `ENHANCED`, но аудит и
переключение fixes сейчас не выполняются.
- Уровень 15/copy protection отсутствует.
- Моды, levelsets и меню Mods отложены.
## 2. Что есть в SDLPoP
`src/menu.c` содержит:
- Resume, QuickSave, QuickLoad, Restart Level, Settings, Restart Game, Quit;
- General, Gameplay, Visuals, Mods, Controls;
- toggle/number/key controls, пояснения, scroll и confirmation dialogs;
- большой список fixes/enhancements и custom level options.
На Sprinter не переносятся SDL-специфичные параметры: fullscreen, hardware
acceleration, scaling, aspect ratio, rumble. UI берёт структуру SDLPoP, но
набор настроек соответствует платформе.
## 3. Pause menu первой версии
```text
RESUME
QUICKSAVE
QUICKLOAD
RESTART LEVEL
SETTINGS
RESTART GAME
QUIT
```
Поведение:
- `Esc` в `PLAYING` открывает меню; повторный Esc или Resume возвращает игру;
- игра, логический таймер и звуковой насос ставятся на паузу согласованно;
- QuickSave/QuickLoad только взводят запрос, фактическая операция идёт на
безопасной границе кадра;
- QuickLoad disabled/показывает `NO QUICKLOAD`, если нет валидных SAV/BAK;
- Restart Level и Restart Game требуют подтверждения;
- Quit требует подтверждения и закрывает файлы/каналы штатным путём;
- меню недоступно в demo, cutscene, time-expired и ending;
- отдельная debug-комбинация немедленного выхода может остаться только в
отладочной сборке.
## 4. Settings первой версии
```text
GENERAL
Sound ON / OFF
Show Sprinter screen ON / OFF
Restore defaults...
GAMEPLAY
Speed NORMAL / FAST / FASTEST
Gameplay profile VANILLA
Cheats ON / OFF
CONTROLS
Show key bindings
BACK
```
`Gameplay profile: VANILLA` показывается read-only: место в модели уже есть,
но пользователь не может выбрать ещё не реализованный ENHANCED.
Отладочные параметры `ROOMNAV`, border profiling, stop-frame и переключение
double buffering не являются пользовательскими Settings. Они остаются
compile-time/debug функциями и скрываются из release UI.
## 5. Модель настроек
Игровой код не должен читать UI-структуры. Единственный runtime-контракт:
```c
typedef enum {
POP_PROFILE_VANILLA = 0,
POP_PROFILE_ENHANCED = 1
} pop_gameplay_profile_t;
typedef struct {
uint8_t sound_enabled;
uint8_t speed_mode;
uint8_t gameplay_profile;
uint8_t cheats_enabled;
uint8_t show_build_info;
uint16_t enhancement_flags;
} pop_settings_t;
```
В первой версии загрузчик принимает только `POP_PROFILE_VANILLA`. Значение
ENHANCED из более нового/ручного файла заменяется на VANILLA с диагностикой,
а не включает частично реализованный режим.
Будущий профиль задаёт маску возможностей централизованно:
```text
VANILLA -> текущий согласованный набор
ENHANCED -> будущий рекомендуемый набор fixes
CUSTOM -> только если позже действительно понадобится
```
До отдельного аудита существующие `fix_exit_door`, feather guard behavior,
jump grab и sound priorities не переключаются и считаются частью текущего
VANILLA.
## 6. Файл POP.CFG
Бинарный, компактный, версионированный формат:
```text
+0 "PCFG" magic, 4 Б
+4 format_version 1 Б
+5 payload_size 2 Б
+7 payload фиксированные поля little-endian
.. checksum 2 Б
```
Требования:
- путь рядом с exe/в выделенном каталоге игры на HDD;
- неизвестная версия, неверная длина или checksum -> defaults;
- неизвестные будущие хвостовые поля можно пропустить по `payload_size`;
- запись только после Apply/выхода из Settings, не на каждый шаг курсора;
- ошибка записи не завершает игру: показать сообщение и оставить runtime
значения;
- Restore defaults меняет RAM только после подтверждения и затем сохраняет.
CFG не содержит состояние уровня, QuickSave или Hall of Fame.
## 7. QuickSave / QuickLoad в меню
Детальный состав снимка и порядок восстановления — в
[`quicksave_plan.md`](quicksave_plan.md). Здесь фиксируется UI и файловая
транзакция.
### Один слот и backup
Файлы:
```text
POP.SAV текущий слот
POP.BAK предыдущий валидный слот
POP.NEW временный файл во время записи
```
Безопасная запись:
1. записать полный снимок в `POP.NEW`;
2. закрыть файл;
3. повторно открыть/прочитать заголовок и checksum;
4. старый валидный `POP.SAV` перенести/скопировать в `POP.BAK`;
5. `POP.NEW` сделать новым `POP.SAV`;
6. при любой ошибке сохранить прежний `POP.SAV`.
Точную последовательность rename/copy выбрать после характеризации DSS.
Если атомарный rename не гарантирован, использовать copy + fsync/close и
никогда не удалять единственную валидную копию до проверки новой.
### Загрузка
1. проверить `POP.SAV`;
2. если он отсутствует/повреждён/несовместим — проверить `POP.BAK`;
3. при валидном BAK показать `LOAD BACKUP?`;
4. несовместимая версия — `INCOMPATIBLE SAVE`, без частичной загрузки;
5. после успеха закрыть menu, перерисовать обе страницы, перезапустить звук.
### Сообщения
Минимальный набор:
```text
QUICKSAVED
QUICKLOADED
NO QUICKLOAD
SAVE ERROR
INCOMPATIBLE SAVE
LOAD BACKUP?
```
Сообщение показывается UI-слоем, но операция завершается до возврата в
игровой кадр.
## 8. Restart Level / Restart Game
Restart Level:
- использует существующий штатный reset текущего уровня;
- не перечитывает CFG;
- не меняет `POP.SAV`;
- сбрасывает состояние, которое сбрасывает текущая реализация roomtest.
Restart Game:
- подтверждение;
- завершить текущий gameplay session;
- освободить level/atlas/temporary EMM;
- создать новую игру с уровня 1 и новым глобальным таймером;
- настройки оставить;
- QuickSave не удалять.
## 9. Controls
Первая версия только показывает активную раскладку. Переназначение клавиш
откладывается: raw PS/2 канал имеет особенности Shift и расширенных кодов,
поэтому generic key-binding UI требует отдельного проекта.
Экран должен перечислить минимум:
- движение и Shift/action;
- Esc/menu;
- F6/F9 QuickSave/QuickLoad;
- Ctrl+S sound;
- P speed;
- доступные cheats, только если они включены.
## 10. UI renderer и ввод
Меню использует общую с title текстовую подсистему:
- фиксированный bitmap font без `_gfx_font_buf` на 2 КБ в W2;
- фон/рамка и выделенная строка;
- вертикальная навигация, left/right для значения, Enter, Esc;
- edge-triggered клавиши поверх существующего `kbd_raw`;
- двойная буферизация либо один заранее сохранённый фон menu;
- строки и холодный код — в отдельном банке, постоянное состояние — в W2.
Первый UI может быть визуально простым. Критично отсутствие потери клавиш,
предсказуемая пауза и отсутствие повреждения игрового back buffer.
## 11. Диалоги
Общий диалог подтверждения:
```text
RESTART LEVEL?
RESTART GAME?
QUIT GAME?
RESTORE DEFAULTS?
LOAD BACKUP?
YES / NO
```
Диалог не выполняет действие напрямую: он возвращает решение автомату меню,
который формирует команду приложению. Так UI не зависит от gameplay-модулей.
## 12. Будущий ENHANCED
Не реализуется сейчас, но дизайн обязан позволять:
- добавить второй профиль без смены всего UI;
- хранить `enhancement_flags` в CFG;
- отличать технические исправления порта (всегда включены) от изменений
оригинальной механики;
- провести аудит уже встроенных исправлений;
- покрыть каждый переключаемый fix host/MAME тестом;
- при необходимости добавить Advanced screen, не раздувая основной menu.
До этого момента нельзя рассыпать проверки `if (enhanced)` по горячему коду.
Сначала составляется реестр и выбирается минимальная битовая модель.
## 13. Этапы реализации
| этап | результат | критерий приёмки |
|---|---|---|
| **MS0** | определить команды app/menu и структуру settings | UI не вызывает gameplay internals напрямую |
| **MS1** | проверить запись/rename/copy на HDD DSS | crash/power-loss сценарий не теряет обе копии save |
| **MS2** | `POP.CFG`: defaults, load, validate, save | повреждённый CFG безопасно даёт defaults |
| **MS3** | QuickSave hotkeys + POP.SAV/BAK | полный критерий `quicksave_plan.md` |
| **MS4** | минимальный pause menu | все семь пунктов доступны и корректно паузят игру |
| **MS5** | General/Gameplay Settings | значения применяются и переживают рестарт |
| **MS6** | dialogs + backup recovery | подтверждения и fallback на POP.BAK |
| **MS7** | Controls help | полная актуальная раскладка на экране |
| **MS8** | интеграция с title/build info | CFG применяется до первого экрана |
QuickSave (`MS1/MS3`) можно реализовать раньше визуального menu: сначала
F6/F9 и сообщения, затем подключить те же команды к пунктам UI.
## 14. Тесты
- Host: CFG round-trip, defaults, bad magic/version/size/checksum.
- Host: menu navigation, disabled items, confirmations, команды приложению.
- Host: SAV invalid -> BAK valid; оба invalid -> NO QUICKLOAD.
- MAME: F6, изменение сцены, F9; затем рестарт программы и повторный F9.
- MAME: прервать запись/испортить SAV — BAK остаётся загружаемым.
- MAME: pause на бое/падении, Resume не меняет состояние и таймер.
- MAME: Settings сохраняются после полного выхода и запуска с HDD.
- Проверка лимита 8 DSS handles на каждом error path.
- `make size-check`; menu/text строки не должны съесть резидентный бюджет.
## 15. Не входит в план
- Mods и выбор levelset;
- уровень 15/copy protection;
- несколько save slots;
- replay/recording;
- key rebinding;
- SDL visual/controller options;
- фактическая реализация ENHANCED и individual fix switches.
+15 -13
View File
@@ -180,12 +180,14 @@ SDLPoP, сохраняются.
--- ---
## 4. Куда писать снимок: файл, а не EMM-страница ## 4. Куда писать снимок: HDD-файл, а не EMM-страница
Рекомендация: **основной путь — файл `QUICKSAVE.SAV`; EMM-страница — Решение: **один основной слот `POP.SAV` на HDD; предыдущая
необязательный второй слот.** валидная запись хранится в `POP.BAK`.** EMM-слота нет: программа
работает только с HDD, а главный сценарий QuickSave обязан переживать
перезапуск игры.
> Пересмотрено 2026-08-17 по вопросу пользователя «почему EMM, а не файл». > Пересмотрено 2026-08-21 по вопросу пользователя «почему EMM, а не файл».
> Первая редакция плана рекомендовала EMM — это была ошибка: она взвешивала > Первая редакция плана рекомендовала EMM — это была ошибка: она взвешивала
> скорость и недооценивала главный сценарий использования. Разбор оставлен > скорость и недооценивала главный сценарий использования. Разбор оставлен
> целиком, потому что довод переносится и на другие «положить в память > целиком, потому что довод переносится и на другие «положить в память
@@ -214,10 +216,8 @@ MAME, обязательный после каждой пересборки об
`_fd_guard` в libc и так стоит), а «не нужен путь и права» — экономия одной `_fd_guard` в libc и так стоит), а «не нужен путь и права» — экономия одной
строки. строки.
Что остаётся за EMM: мгновенный слот для «переиграть» без обращения к диску. Обход состояния всё равно писать с абстракцией чтения/записи, как у SDLPoP
Делается тем же сериализатором и добавляется, если понадобится. Поэтому через `process_func`, но второй EMM-слот в scope не входит.
обход состояния писать сразу так, чтобы «куда» было параметром — как у
SDLPoP через `process_func`.
**Проверить ДО кодинга:** пишется ли `test_hdd.chd` из-под MAME. Если образ **Проверить ДО кодинга:** пишется ли `test_hdd.chd` из-под MAME. Если образ
только на чтение, файловый путь упрётся в это на первом же шаге и порядок только на чтение, файловый путь упрётся в это на первом же шаге и порядок
@@ -234,6 +234,7 @@ roomtest.
+5 pop_current_level 1 Б +5 pop_current_level 1 Б
+6 длина полезной части 2 Б (контроль, что обход совпал) +6 длина полезной части 2 Б (контроль, что обход совпал)
+8 ... поля встык, ОДИН порядок на запись и на чтение ... +8 ... поля встык, ОДИН порядок на запись и на чтение ...
.. checksum 2 Б (заголовок + payload)
``` ```
Версия проверяется первой; несовпадение — отказ, как в SDLPoP. Никаких Версия проверяется первой; несовпадение — отказ, как в SDLPoP. Никаких
@@ -282,13 +283,12 @@ static void qs_walk(qs_io_t io) /* io = запись или чтение */
| шаг | что | критерий готовности | | шаг | что | критерий готовности |
|---|---|---| |---|---|---|
| **QS1** | Аксессоры/сериализаторы для `static`-состояния банковых модулей: `pop_trob.c` (`room_modif`, `room_seen`, `trobs`, `trob_seed`), `pop_room.c` (`mobs_live`), страница уровня (чтение `fg`) | хост-тест `tests-host/t_qsave.c`: обход туда-обратно на синтетическом состоянии даёт байт-в-байт исходное |
| **QS0** | Проверить, что `D:` пишется из-под MAME (пробный файл из roomtest) | файл создался и читается обратно после рестарта программы | | **QS0** | Проверить, что `D:` пишется из-под MAME (пробный файл из roomtest) | файл создался и читается обратно после рестарта программы |
| **QS2** | Ядро: `qs_walk` + запись/чтение файла `QUICKSAVE.SAV`, магия и версия, отказ при несовпадении | сохранение и загрузка **в той же комнате, без движения** — картинка и состояние не изменились | | **QS1** | Аксессоры/сериализаторы для `static`-состояния банковых модулей: `pop_trob.c` (`room_modif`, `room_seen`, `trobs`, `trob_seed`), `pop_room.c` (`mobs_live`), страница уровня (чтение `fg`) | хост-тест `tests-host/t_qsave.c`: обход туда-обратно на синтетическом состоянии даёт байт-в-байт исходное |
| **QS2** | Ядро: `qs_walk` + `POP.SAV`, магия/версия/checksum, безопасная замена с предыдущей валидной копией в `POP.BAK` | сохранение и загрузка **в той же комнате, без движения**; порча SAV не портит BAK |
| **QS3** | Восстановление отрисовки (§6), включая обе страницы дабл-буфера | загрузка после перехода в другую комнату; нет мерцания через кадр | | **QS3** | Восстановление отрисовки (§6), включая обе страницы дабл-буфера | загрузка после перехода в другую комнату; нет мерцания через кадр |
| **QS4** | Клавиши **F6/F9** (или свободные из `pop_cheat.h`) через `<kbd_raw.h>`, флаги `need_quick_save/load`, обработка **между кадрами** | загрузка посреди боя/падения не ломает `play_seq` | | **QS4** | Клавиши **F6/F9** (или свободные из `pop_cheat.h`) через `<kbd_raw.h>`, флаги `need_quick_save/load`, обработка **между кадрами** | загрузка посреди боя/падения не ломает `play_seq` |
| **QS5** | Загрузка с **другого уровня** (перезагрузка уровня и атласов) | сохранить на ур. 2, уйти на ур. 12, загрузить — тайлсет и стражи верные | | **QS5** | Загрузка с **другого уровня** (перезагрузка уровня и атласов) | сохранить на ур. 2, уйти на ур. 12, загрузить — тайлсет и стражи верные |
| **QS6** | Опционально: второй слот в EMM-странице тем же сериализатором | мгновенное «переиграть» без обращения к диску |
Порядок не переставлять: QS0 первым (он может изменить весь план), QS3 без Порядок не переставлять: QS0 первым (он может изменить весь план), QS3 без
QS2 нечего проверять, а QS5 обязан идти после QS3 — иначе смена тайлсета QS2 нечего проверять, а QS5 обязан идти после QS3 — иначе смена тайлсета
@@ -320,5 +320,7 @@ QS2 нечего проверять, а QS5 обязан идти после QS3
5. **Дабл-буфер.** Самый вероятный источник «почти работает»: забыть вторую 5. **Дабл-буфер.** Самый вероятный источник «почти работает»: забыть вторую
страницу. Симптом — мерцание через кадр страницу. Симптом — мерцание через кадр
(см. `roomtest/CLAUDE.md`, раздел про дабл-буфер). (см. `roomtest/CLAUDE.md`, раздел про дабл-буфер).
6. **Открытый вопрос:** нужен ли снимок в файле вообще, или EMM-страницы 6. **Транзакция SAV/BAK.** До кодинга проверить на DSS семантику
достаточно. Решать после QS3, по факту использования. rename/replace. Если атомарная замена не гарантирована, писать через
`POP.NEW`, проверять его после close и не удалять единственную валидную копию
до завершения новой.
+111
View File
@@ -0,0 +1,111 @@
# Бюджет резидента W1/W2: как мерить и как освобождать
Резидент huge-режима — окно `0x4100..0xBB00` (стек с 0xBB00): код в W1,
данные в W2, между концом данных и стеком остаётся куча. Всё, что туда не
влезло, живёт в банках.
## Как СМОТРЕТЬ, а не гадать
**Карта линкера врёт.** File-static SDCC в неё не попадает, и «дырка» между
двумя именованными символами приписывается предыдущему целиком. По карте
выходило, что у `pop_bg` 1529 Б данных (на деле 289), а у `pop_t_win_clear`
1282 Б кода — при том, что это однострочник, а 1282 Б это два статических
помощника соседнего `pop_blit_b`.
Точный источник — объектные файлы: строки `A <area> size <n> flags <f>` в
`.rel` дают ровный размер каждой области модуля, а `S <sym> Def/Ref` — кто
символ определяет и кто на него ссылается.
**Частоту вызовов мерить в MAME счётчиком**, а не оценивать по смыслу:
```
bpset <frame_probe>,1,{printf "F %d ...",temp0,...; temp0=0;...; g}
bpset <func_addr>,1,{temp0=temp0+1; g}
```
Обязательна **канарейка** — счётчик заведомо горячей функции в том же
прогоне. Дважды спасала: один раз показала, что перехода комнаты в окне
замера не было (все нули), другой — что зонды вообще не встали (в zsh
`set -- $pair` НЕ разбивает строку на слова, и адрес уезжал в мусор).
Полную перерисовку комнаты форсировать читом `+`/`-`, ходьбой ненадёжно.
## Сделано
### 1. malloc вон из резидента (−613 Б)
`cbl_open` держал `malloc`/`free` в мёртвой ветке `CBL_UNDERRUN_SILENCE`, а
линкер тянет `.rel` целиком — и куча приезжала каждому приложению. Разведены
две публичные точки входа (`cbl_open` / `cbl_open_silence`) поверх общего
`_cbl_open_raw`; `cbl_close` больше не зовёт `free`.
### 2. Разрез pop_tile: холодная половина в банк 5 (−1788 Б)
`pop_tile.c` был крупнейшим жильцом резидента (5 972 Б кода). Целиком он не
уедет: его const-таблицы (`POP_TILE_DIV/MOD`, `pop_tile_table`, таблицы
кадров) читают банки 2, 3, 7 и 8, а таблица в чужом банке не видна.
Отбирали ЗАМЕРОМ, на двух тайлсетах (подземелье ур. 1 и дворец ур. 4 —
`pop_mem_b` рисует композитный кусок и мог оказаться дворцовым). Порог —
пик не больше 3 вызовов на кадр.
| уехало в банк 5 | пик/кадр | | осталось в резиденте | пик/кадр |
|---|---:|---|---|---:|
| `pop_mem_b` | 0 | | `pop_tile_code` | 296 |
| `pop_cd_hit` (+`hit_rect`) | 0 | | `pop_cd_touch` | 198 |
| `pop_t_win_set/clear` | 0..1 | | `pop_blit_b` (+2 статика) | 184 |
| `pop_heal_off` | 0..1 | | `pop_wall_modifier` | 101 |
| `pop_potion_flask` | 0..1 | | `pop_env_b` | 73 |
| `pop_room_set_above/below` | 1 | | `pop_tile_mod` | 70 |
| `pop_cd_init/clear` | 1 | | `pop_cd_batch_end` | 40 |
| `pop_bar_black` | 3 | | `pop_fore_set_clip` | 2 |
| `pop_cd_hit_slot` | 2..3 | | все const-таблицы | — |
`pop_fore_set_clip` (88 Б) оставлен намеренно: не стоит отказа от прямого
вызова из банка 4, ради которого он и заводился.
**Цена трамплина замерена**: 252 такта пролог + 84 эпилог + ~50 на стороне
вызывающего = **~410 тактов** на вызов. Итого ~1 000 тактов на кадр покоя
(0,2 % работы) и ~3 700 на кадр редрава (0,009 растра).
**Ключ, почему это безопасно:** вызов банк → резидент ПРЯМОЙ, трамплин не
нужен (W1/W2 замаплены всегда). Поэтому `blit_b_clip` просто перестал быть
`static` и объявлен в `_pop_tile.h`, а не переехал следом за `pop_mem_b`.
### Итог
| | было | стало |
|---|---:|---:|
| `_CODE` резидента | 24 329 | **21 928** |
| свободно до стека | **129 Б** | **2 535 Б** |
| BANK5 | 2 080 (13 %) | 3 954 (24 %) |
Проверено в MAME: уровень 1 (подземелье) и уровень 4 (дворец), переходы
комнат читом `+`, ходьба — фон, факелы, решётки, гобелены, колонны без
искажений.
## ЛОВУШКА: данные банка в его страницу — НЕ ДЕЛАТЬ без разбора
Отдельная попытка (`--bank-data=SRC`, коммиты 3545826/9025573) **откачена**:
перенос писучих данных банкового модуля в его 16-КБ страницу давал цветной
мусор блоками и ронял DSS.
У `pop_trob` причина найдена: `pop_trob_modif()` ВОЗВРАЩАЕТ УКАЗАТЕЛЬ на
`room_modif[24][30]`, а зовут её из банков 2, 3, 7 и резидента — после
переноса они пишут по 0xC000+ в СВОЮ страницу, поверх чужого кода.
Def/Ref-анализ такого не видит: снаружи ссылки на символ нет, есть ссылка на
функцию, отдающую его адрес. Но и `pop_room`, у которого утечки указателя
найти не удалось, ломался так же — механизм понят не до конца.
Нулевая инициализация при этом ни при чём: `mkexe -p 0` был проверен по
образу (прогон нулей 14 304 Б, самый длинный прогон 0xFF — 14).
**Перенос КОДА в банк — штатный путь, на нём стоят все наши банки. Ломался
именно перенос ДАННЫХ.**
## Что осталось
- `roomtest.c` 2 508 Б и `pop_kid.c` 2 418 Б — следующие по величине, но оба
горячие (главный цикл и `play_seq`).
- `pop_level.c` 956 Б кода + 660 Б данных (из них `pop_dl1`/`pop_dl2` по
256 Б — таблицы дверных связей).
- BANK7 на 77 %: если понадобится место в нём — выносить `pop_redraw.c`.
+116 -17
View File
@@ -35,7 +35,32 @@ MEMORY ?= huge # small-раскладка + банки кода в W
# ВАЖНО: любое сравнение занятости банков имеет смысл только при ОДНОМ и том # ВАЖНО: любое сравнение занятости банков имеет смысл только при ОДНОМ и том
# же ALLOCS — иначе сравниваются не правки, а уровни оптимизации. # же ALLOCS — иначе сравниваются не правки, а уровни оптимизации.
ALLOCS ?= 3000 ALLOCS ?= 3000
EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/../poc/res/bg --max-allocs $(ALLOCS) --bank 2=pop_bg.c --bank 7=pop_room.c --bank 4=pop_cdraw.c --bank 4=pop_kdraw.c --bank 3=pop_map.c --bank 1=guards.c --bank 5=pop_ctrl.c --bank 6=pop_trob.c --bank 7=pop_redraw.c --bank 8=roomtest_cold.c --bank 8=pop_level_cold.c --bank 8=pop_kboot.c --bank 8=pop_guard_cold.c --bank 8=pop_shadow.c --bank 8=pop_sfx_cold.c $(PROF_FLAGS) # Единственный список банковых исходников: из него строятся И ключи
# sprinter-cc, И зависимости roomtest.exe. Раньше эти списки жили отдельно,
# поэтому добавленные pop_shadow.c/pop_sfx_cold.c компилировались, но make не
# замечал их последующие изменения и оставлял устаревший exe.
BANK1_SRCS := guards.c
BANK2_SRCS := pop_bg.c
BANK3_SRCS := pop_map.c
BANK4_SRCS := pop_cdraw.c pop_kdraw.c
BANK5_SRCS := pop_ctrl.c pop_tile_cold.c
BANK6_SRCS := pop_trob.c
BANK7_SRCS := pop_room.c pop_redraw.c
BANK8_SRCS := roomtest_cold.c pop_level_cold.c pop_kboot.c pop_guard_cold.c \
pop_shadow.c pop_sfx_cold.c pop_qsave.c pop_qsave_coldio.c \
pop_level_qsave.c
BANKED_SRCS := $(BANK1_SRCS) $(BANK2_SRCS) $(BANK3_SRCS) $(BANK4_SRCS) \
$(BANK5_SRCS) $(BANK6_SRCS) $(BANK7_SRCS) $(BANK8_SRCS)
BANK_FLAGS := $(foreach f,$(BANK1_SRCS),--bank 1=$(f)) \
$(foreach f,$(BANK2_SRCS),--bank 2=$(f)) \
$(foreach f,$(BANK3_SRCS),--bank 3=$(f)) \
$(foreach f,$(BANK4_SRCS),--bank 4=$(f)) \
$(foreach f,$(BANK5_SRCS),--bank 5=$(f)) \
$(foreach f,$(BANK6_SRCS),--bank 6=$(f)) \
$(foreach f,$(BANK7_SRCS),--bank 7=$(f)) \
$(foreach f,$(BANK8_SRCS),--bank 8=$(f))
EXTRA_FLAGS ?= --gfx 256 -I $(CURDIR)/../poc/res/bg --max-allocs $(ALLOCS) \
$(BANK_FLAGS) $(PROF_FLAGS)
# Профилирование полосами бордюра (см. PROF() в roomtest.c) — ВКЛЮЧЕНО по # Профилирование полосами бордюра (см. PROF() в roomtest.c) — ВКЛЮЧЕНО по
# умолчанию, пока идёт работа с производительностью; make PROF=0 выключает. # умолчанию, пока идёт работа с производительностью; make PROF=0 выключает.
# Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте # Каждая фаза кадра красит бордюр в свой цвет, высота полосы на скриншоте
@@ -57,7 +82,7 @@ POS ?= 2
ifneq ($(strip $(ROOM)),) ifneq ($(strip $(ROOM)),)
PROF_FLAGS += -DDBG_START_ROOM=$(ROOM) -DDBG_START_POS=$(POS) PROF_FLAGS += -DDBG_START_ROOM=$(ROOM) -DDBG_START_POS=$(POS)
endif endif
EXTRA_SRCS := pop_vflip.c pop_pace.c pop_sfx.c pop_state.c pop_draw.c pop_tile.c pop_kid.c pop_level.c pop_geom.c pop_guard.c EXTRA_SRCS := pop_vflip.c pop_pace.c pop_sfx.c pop_state.c pop_draw.c pop_tile.c pop_kid.c pop_level.c pop_geom.c pop_guard.c pop_qsave_io.c
BG_DIR := $(CURDIR)/../poc/res/bg BG_DIR := $(CURDIR)/../poc/res/bg
KID_DIR := $(CURDIR)/../poc/res/kid KID_DIR := $(CURDIR)/../poc/res/kid
@@ -127,17 +152,13 @@ SND_DATA := $(foreach n,0 1 2 3 4 5 6 7,$(SND_DIR)/s$(n).bin)
EXTRA_DATA := $(BG_DATA) $(KID_DATA) $(KID_BIN) $(GUARD_DATA) $(SKEL_DATA) \ EXTRA_DATA := $(BG_DATA) $(KID_DATA) $(KID_BIN) $(GUARD_DATA) $(SKEL_DATA) \
$(VIZIER_DATA) $(SHADOW_DATA) $(SND_DATA) $(LVL_DATA) $(VIZIER_DATA) $(SHADOW_DATA) $(SND_DATA) $(LVL_DATA)
include $(PROJ_ROOT)/app.mk
# Образ HDD для MAME: ресурсы раскладываются ПО КАТАЛОГАМ (8.3, как в DSS), # Образ HDD для MAME: ресурсы раскладываются ПО КАТАЛОГАМ (8.3, как в DSS),
# чтобы корень диска не зарастал десятками .atl. # чтобы корень диска не зарастал десятками .atl.
# BG\ — фон (env/wall/fore/pot) # BG\ — фон (env/wall/fore/pot)
# KID\ — персонаж (kid0..27, палитра, меч, таблицы анимации) # KID\ — персонаж (kid0..27, палитра, меч, таблицы анимации)
# GUARD\ — стражи (появятся здесь же) # GUARD\ — стражи (появятся здесь же)
# LEVELS\ — уровни # LEVELS\ — уровни
hdd: $(EXAMPLE).exe HDD_PACK_ARGS := $(EXAMPLE).exe \
$(PROJ_ROOT)/toolchain/make_hdd.sh $(PROJ_ROOT)/mame/v306/IMG/test_hdd.chd \
$(EXAMPLE).exe \
$(foreach f,$(BG_DATA),BG:$(f)) \ $(foreach f,$(BG_DATA),BG:$(f)) \
$(foreach f,$(KID_DATA) $(KID_BIN),KID:$(f)) \ $(foreach f,$(KID_DATA) $(KID_BIN),KID:$(f)) \
$(foreach f,$(GUARD_DATA),GUARD:$(f)) \ $(foreach f,$(GUARD_DATA),GUARD:$(f)) \
@@ -147,27 +168,105 @@ hdd: $(EXAMPLE).exe
$(foreach f,$(SND_DATA),SND:$(f)) \ $(foreach f,$(SND_DATA),SND:$(f)) \
$(foreach f,$(LVL_DATA),LEVELS:$(f)) $(foreach f,$(LVL_DATA),LEVELS:$(f))
include $(PROJ_ROOT)/app.mk
TC := $(PROJ_ROOT)/applications/PoP/toolchain TC := $(PROJ_ROOT)/applications/PoP/toolchain
$(BG_DATA): $(TC)/pop_pack_bg.py $(TC)/render_room.py
# Упаковщики создают сразу ГРУППУ файлов. Обычное multi-output правило
# заставляло GNU make при `-B` запускать один и тот же упаковщик отдельно для
# каждого .atl/.bin. Stamp-файл представляет один запуск генератора; все
# созданные им файлы зависят от этого stamp без собственных рецептов.
RESOURCE_STAMP_DIR := .resource-stamps
BG_STAMP := $(RESOURCE_STAMP_DIR)/bg
GUARD_STAMP := $(RESOURCE_STAMP_DIR)/guard
SKEL_STAMP := $(RESOURCE_STAMP_DIR)/skel
VIZIER_STAMP := $(RESOURCE_STAMP_DIR)/vizier
SHADOW_STAMP := $(RESOURCE_STAMP_DIR)/shadow
SOUND_STAMP := $(RESOURCE_STAMP_DIR)/sound
KID_STAMP := $(RESOURCE_STAMP_DIR)/kid
KID_BIN_STAMP := $(RESOURCE_STAMP_DIR)/kid-bin
$(RESOURCE_STAMP_DIR):
mkdir -p $@
$(BG_STAMP): $(TC)/pop_pack_bg.py $(TC)/render_room.py | $(RESOURCE_STAMP_DIR)
cd $(TC) && python3 pop_pack_bg.py dungeon && python3 pop_pack_bg.py palace cd $(TC) && python3 pop_pack_bg.py dungeon && python3 pop_pack_bg.py palace
$(GUARD_DATA): $(TC)/pop_pack_guard.py touch $@
$(BG_DATA): $(BG_STAMP)
@test -f $@ || { $(MAKE) -B $(BG_STAMP); test -f $@; }
$(GUARD_STAMP): $(TC)/pop_pack_guard.py | $(RESOURCE_STAMP_DIR)
cd $(TC) && python3 pop_pack_guard.py cd $(TC) && python3 pop_pack_guard.py
touch $@
$(GUARD_DATA): $(GUARD_STAMP)
@test -f $@ || { $(MAKE) -B $(GUARD_STAMP); test -f $@; }
$(SKEL_DATA): $(TC)/pop_pack_guard.py $(SKEL_STAMP): $(TC)/pop_pack_guard.py | $(RESOURCE_STAMP_DIR)
cd $(TC) && python3 pop_pack_guard.py SKEL cd $(TC) && python3 pop_pack_guard.py SKEL
touch $@
$(SKEL_DATA): $(SKEL_STAMP)
@test -f $@ || { $(MAKE) -B $(SKEL_STAMP); test -f $@; }
$(VIZIER_DATA): $(TC)/pop_pack_guard.py $(VIZIER_STAMP): $(TC)/pop_pack_guard.py | $(RESOURCE_STAMP_DIR)
cd $(TC) && python3 pop_pack_guard.py VIZIER cd $(TC) && python3 pop_pack_guard.py VIZIER
touch $@
$(VIZIER_DATA): $(VIZIER_STAMP)
@test -f $@ || { $(MAKE) -B $(VIZIER_STAMP); test -f $@; }
$(SHADOW_DATA) pop_shadow_atlas.h: $(TC)/pop_pack_shadow.py $(TC)/pop_pack_bg.py $(TC)/pop_pack_kid.py $(SHADOW_STAMP): $(TC)/pop_pack_shadow.py $(TC)/pop_pack_bg.py $(TC)/pop_pack_kid.py | $(RESOURCE_STAMP_DIR)
cd $(TC) && python3 pop_pack_shadow.py cd $(TC) && python3 pop_pack_shadow.py
touch $@
$(SHADOW_DATA) pop_shadow_atlas.h: $(SHADOW_STAMP)
@test -f $@ || { $(MAKE) -B $(SHADOW_STAMP); test -f $@; }
$(SND_DATA) pop_sound_tbl.h: $(TC)/pop_pack_sound.py $(SOUND_STAMP): $(TC)/pop_pack_sound.py | $(RESOURCE_STAMP_DIR)
cd $(TC) && python3 pop_pack_sound.py cd $(TC) && python3 pop_pack_sound.py
touch $@
$(SND_DATA) pop_sound_tbl.h: $(SOUND_STAMP)
@test -f $@ || { $(MAKE) -B $(SOUND_STAMP); test -f $@; }
$(KID_DATA): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py $(KID_STAMP): $(TC)/pop_pack_kid.py $(TC)/pop_pack_bg.py | $(RESOURCE_STAMP_DIR)
cd $(TC) && python3 pop_pack_kid.py cd $(TC) && python3 pop_pack_kid.py
kid_data.h $(KID_BIN): $(TC)/pop_extract_kid_data.py touch $@
cd $(TC) && python3 pop_extract_kid_data.py $(KID_DATA): $(KID_STAMP)
@test -f $@ || { $(MAKE) -B $(KID_STAMP); test -f $@; }
$(EXAMPLE).exe: roomtest_cold.c roomtest_cold.h guards.c pop_guard.c pop_guard_cold.c pop_guard.h pop_cdraw.c pop_cdraw.h pop_draw.c _pop_draw.h pop_tile.c pop_tile.h pop_state.c pop_state.h pop_cheat.h pop_bg.c pop_bg.h pop_room.c _pop_bg.h pop_geom.c pop_geom.h pop_redraw.c pop_redraw.h pop_vflip.c pop_vflip.h pop_kid.c pop_kdraw.c pop_kboot.c _pop_kid.h _pop_kdraw.h pop_kid.h pop_ctrl.c pop_ctrl.h pop_map.c pop_map.h pop_level.c pop_level_cold.c _pop_level.h pop_level.h pop_trob.c pop_trob.h kid_data.h $(BG_DATA) $(KID_DATA) $(KID_BIN) $(LVL_DATA) $(KID_BIN_STAMP): $(TC)/pop_extract_kid_data.py | $(RESOURCE_STAMP_DIR)
cd $(TC) && python3 pop_extract_kid_data.py
touch $@
kid_data.h $(KID_BIN): $(KID_BIN_STAMP)
@test -f $@ || { $(MAKE) -B $(KID_BIN_STAMP); test -f $@; }
# Явные цели позволяют собирать ресурсы без `make -B`, который по смыслу
# принудительно пересобирает также libc, libbgi и само приложение.
resources-bg: $(BG_DATA)
resources-kid: $(KID_DATA) $(KID_BIN)
resources-actors: $(GUARD_DATA) $(SKEL_DATA) $(VIZIER_DATA) $(SHADOW_DATA)
resources-sound: $(SND_DATA)
resources-levels: $(LVL_DATA)
resources: resources-bg resources-kid resources-actors resources-sound resources-levels
resources-rebuild:
$(MAKE) -B resources
# Принудительная перелинковка только roomtest, без глобального `-B` и без
# принудительной пересборки библиотек/ресурсов.
relink: resources
$(MAKE) -W Makefile $(EXAMPLE).exe
# HDD_PACK_ARGS содержит все эти файлы; make обязан подготовить их до запуска
# упаковщика и при чистой сборке, а не полагаться на старое содержимое дерева.
hdd: resources
# sprinter-cc собирает все модули одним вызовом и depfile пока не выдаёт.
# Банковые .c берём из того же списка, что формирует --bank; любой локальный
# заголовок также обязан перелинковать приложение. wildcard намеренно
# широкий: цена лишней полной сборки ниже риска получить stale-бинарник.
LOCAL_HEADERS := $(wildcard *.h)
$(EXAMPLE).exe: Makefile $(PROJ_ROOT)/app.mk $(BANKED_SRCS) $(LOCAL_HEADERS) \
$(BG_DATA) $(KID_DATA) $(KID_BIN) $(LVL_DATA)
clean-resources:
rm -rf $(RESOURCE_STAMP_DIR)
.PHONY: resources resources-rebuild resources-bg resources-kid resources-actors relink \
resources-sound resources-levels clean-resources
+2 -5
View File
@@ -20,10 +20,7 @@ extern uint8_t kdat_blk; /* EMM-блок страницы kid_data.bin */
extern uint8_t kdat_page; /* её номер (маппится в W0 на время play_seq) */ extern uint8_t kdat_page; /* её номер (маппится в W0 на время play_seq) */
extern uint8_t kdat_ok; /* данные загружены */ extern uint8_t kdat_ok; /* данные загружены */
/* ПРАВИЛО ОКНА W3: банковый модуль исполняется из W3 и переключать это окно /* Файловый обмен с W3 делает общий резидентный bank_load_file(), поэтому
* не может (следующая инструкция придёт уже из чужой страницы). Поэтому * оркестратор загрузки kid_data.bin целиком живёт в холодном банке 8. */
* ЧТЕНИЕ kid_data.bin — pop_kid_data_load — осталось в резиденте, а в банк
* уехало только то, что читает через W0 или вообще не трогает окна.
* То же правило и в _pop_level.h. */
#endif /* _POP_KID_H */ #endif /* _POP_KID_H */
+3 -7
View File
@@ -69,13 +69,9 @@ static const uint8_t *room_bg_ptr(uint8_t r)
return (const uint8_t *)(LVL_DATA_OFF + BP_BG) + (uint16_t)(r - 1) * ROOM_TILES; return (const uint8_t *)(LVL_DATA_OFF + BP_BG) + (uint16_t)(r - 1) * ROOM_TILES;
} }
/* ПРАВИЛО ОКНА W3. Банковый модуль исполняется ИЗ W3, поэтому сам он /* ПРАВИЛО ОКНА W3. Банковый модуль исполняется ИЗ W3 и сам менять окно
* менять это окно НЕ МОЖЕТ: после sprinter_page_w3(другая_страница) * не может. Файлы в EMM читает резидентный bank_load_file(), после его
* следующая инструкция читается уже из чужой страницы. Всё чтение файлов * возврата холодный код продолжает подготовку страницы через W0. */
* (оно грузит данные по 0xC000) обязано жить в резиденте — отсюда
* pop_level_read_file здесь, а не в pop_level_cold.c. Через W0
* (gfx_w0_map) банкам работать можно: это другое окно. */
int pop_level_read_file(const char *path);
/* Живая копия стражей из данных уровня (загрузка/рестарт) — в холодной. */ /* Живая копия стражей из данных уровня (загрузка/рестарт) — в холодной. */
void pop_gstate_init(void) __banked; void pop_gstate_init(void) __banked;
-4
View File
@@ -19,8 +19,4 @@ extern uint8_t pop_snd_ok;
* cbl_open() холодной половиной. */ * cbl_open() холодной половиной. */
int pop_sfx_fill(uint16_t n); int pop_sfx_fill(uint16_t n);
/* Прочитать файл целиком в ФИЗИЧЕСКУЮ страницу (через W3). Резидентный:
* банковый код, переключив W3, вырезал бы сам себя. */
int pop_page_read(const char *path, uint8_t phys, uint16_t want);
#endif #endif
+18
View File
@@ -0,0 +1,18 @@
/*
* _pop_tile.h — внутренний контракт между половинами общих листьев слоя
* фона (НЕ публичный, публичный — pop_tile.h). Горячая половина живёт в
* резиденте (pop_tile.c), холодная — в банке 5 (pop_tile_cold.c);
* критерий разреза и замеры — в шапке pop_tile_cold.c.
*/
#ifndef _POP_TILE_INTERNAL_H
#define _POP_TILE_INTERNAL_H
#include <stdint.h>
/* Клипованный путь блита — статик горячей половины, ставший общим: его
* зовёт pop_mem_b из холодной. Переносить его следом НЕЛЬЗЯ (его основной
* вызывающий pop_blit_b делает 184 вызова на кадр редрава), а вызов
* банк -> резидент и так прямой: W1/W2 замаплены всегда. */
void blit_b_clip(const uint8_t *img, int x, int top, uint8_t w, uint8_t h);
#endif
+6
View File
@@ -265,6 +265,12 @@ void pop_check_mouse(void) __banked
static uint8_t demo_time; /* кадров с начала записи (0xFE = стоп) */ static uint8_t demo_time; /* кадров с начала записи (0xFE = стоп) */
static uint8_t demo_index; /* текущая запись */ static uint8_t demo_index; /* текущая запись */
void pop_guard_ai_qsave(pop_qs_io_t *io) __banked
{
pop_qs_u8(io, &demo_time);
pop_qs_u8(io, &demo_index);
}
#define SHADOW_SKILL 3 #define SHADOW_SKILL 3
#define SHADOW_HP 4 #define SHADOW_HP 4
#define SEQ_2_STAND 2 #define SEQ_2_STAND 2
+10
View File
@@ -0,0 +1,10 @@
Уровень 4, комната 22 - страж идет навстречу Киду и попадает сам в челюсти - должен быть инстинк самосохранения
Перед решеткой Кид делает лишний мелкий шаг.
Уровень 5, комната 2 - с разбега чуть раньше - иногда попадаем в стену (некорректное отображение падения)
Уровень 7, комната 9 - падает плита -1,1 - звук потери HP у Кида (причем раз даже когда Кид просто бъет по плите стоя на 0,0)
Уровень 10, комната 1 - когда Кид стоя на кнопке 1,8 роняет плиту 0,8 на кнопку 1,8 - кнопка ломается (превращается в щебень)
но двери которые она должна открыть остаются закрытыми
+4 -2
View File
@@ -13,6 +13,7 @@
#include <stdint.h> #include <stdint.h>
#include "pop_char.h" /* pop_char_t — общий проход отрисовки поверх Char */ #include "pop_char.h" /* pop_char_t — общий проход отрисовки поверх Char */
#include "pop_qsave.h"
/* ---- Вертикальный layout (PoP-экран 320×200 в нашем 320×256) --------- * /* ---- Вертикальный layout (PoP-экран 320×200 в нашем 320×256) --------- *
* Игровой экран 200px центрируем по высоте: сдвиг всего рисунка вниз на * Игровой экран 200px центрируем по высоте: сдвиг всего рисунка вниз на
@@ -64,12 +65,12 @@ void pop_room_draw(uint8_t room_num,
* [10] — тайл (0,9) комнаты СНИЗУ-СЛЕВА (сосед угла (2,0); см. * [10] — тайл (0,9) комнаты СНИЗУ-СЛЕВА (сосед угла (2,0); см.
* load_rowbelow, seg008:368). 0 — нет комнаты снизу. */ * load_rowbelow, seg008:368). 0 — нет комнаты снизу. */
/* НЕ __banked: тело в резиденте (pop_tile.c) — из банка зовётся прямым call. */ /* НЕ __banked: тело в резиденте (pop_tile.c) — из банка зовётся прямым call. */
void pop_room_set_below(const uint8_t *below_row0_fg); void pop_room_set_below(const uint8_t *below_row0_fg) __banked;
/* Задать ряд 2 (fg-коды + модификаторы, 10 шт.) комнаты СВЕРХУ — для полосы /* Задать ряд 2 (fg-коды + модификаторы, 10 шт.) комнаты СВЕРХУ — для полосы
* кладки у потолка (seg008 draw_room доп. верхний ряд из room_A). 0 — нет * кладки у потолка (seg008 draw_room доп. верхний ряд из room_A). 0 — нет
* комнаты сверху (кромка уровня → пол). */ * комнаты сверху (кромка уровня → пол). */
void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod); void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod) __banked;
/* Перерисовать передний слой (fore) и оверлеи тайлов ПОВЕРХ персонажа — /* Перерисовать передний слой (fore) и оверлеи тайлов ПОВЕРХ персонажа —
* ОДИН проход на всех Char (Кид, страж, скелет, дальше тень/визирь): в * ОДИН проход на всех Char (Кид, страж, скелет, дальше тень/визирь): в
@@ -178,6 +179,7 @@ void pop_loose_mob_spawn(int row, int col) __banked;
void pop_loose_mob_spawn_at(uint8_t room, int row, int col) __banked; void pop_loose_mob_spawn_at(uint8_t room, int row, int col) __banked;
void pop_loose_mob_tick(void) __banked; void pop_loose_mob_tick(void) __banked;
void pop_loose_mob_reset(void) __banked; /* убрать все куски (СТАРТ УРОВНЯ, mobs_count=0) */ void pop_loose_mob_reset(void) __banked; /* убрать все куски (СТАРТ УРОВНЯ, mobs_count=0) */
void pop_loose_mob_qsave(pop_qs_io_t *io) __banked;
/* Забыть, на какой странице сделана первая запечка тайлов (см. bake_copy в /* Забыть, на какой странице сделана первая запечка тайлов (см. bake_copy в
* pop_room.c). Звать при смене комнаты/уровня: номера тайлов повторяются, и * pop_room.c). Звать при смене комнаты/уровня: номера тайлов повторяются, и
+4 -4
View File
@@ -128,12 +128,12 @@ extern uint8_t pop_cd_dirty; /* биты страниц: ме
extern uint8_t pop_cd_ymin[2][10], pop_cd_ymax[2][10]; extern uint8_t pop_cd_ymin[2][10], pop_cd_ymax[2][10];
/* Задели ли прямоугольник (ЭКРАННЫЕ координаты, x1/y1 включительно) то, /* Задели ли прямоугольник (ЭКРАННЫЕ координаты, x1/y1 включительно) то,
* что трогали на странице p. Резидент (pop_tile.c). */ * что трогали на странице p. Резидент (pop_tile.c). */
uint8_t pop_cd_hit(uint8_t p, int x0, int y0, int x1, int y1); uint8_t pop_cd_hit(uint8_t p, int x0, int y0, int x1, int y1) __banked;
/* То же для СЛОТА персонажа: спрайт и накладной, координаты изнутри pop_cd /* То же для СЛОТА персонажа: спрайт и накладной, координаты изнутри pop_cd
* (см. тело — там про цену пяти аргументов). */ * (см. тело — там про цену пяти аргументов). */
uint8_t pop_cd_hit_slot(uint8_t who, uint8_t p); uint8_t pop_cd_hit_slot(uint8_t who, uint8_t p) __banked;
void pop_cd_clear(uint8_t p); /* снять метку страницы целиком */ void pop_cd_clear(uint8_t p) __banked; /* снять метку страницы целиком */
void pop_cd_init(void); /* пустые диапазоны на обеих страницах */ void pop_cd_init(void) __banked; /* пустые диапазоны на обеих страницах */
void pop_cd_touch(int x, int y, int w, int h); void pop_cd_touch(int x, int y, int w, int h);
#define POP_CD_TOUCH_ALL() pop_cd_touch(0, 0, 320, 256) #define POP_CD_TOUCH_ALL() pop_cd_touch(0, 0, 320, 256)
+2
View File
@@ -21,6 +21,7 @@
#include "kid_data.h" /* kframe */ #include "kid_data.h" /* kframe */
#include "pop_char.h" #include "pop_char.h"
#include "pop_geom.h" /* pop_rnd_t — сид бросков боёвки */ #include "pop_geom.h" /* pop_rnd_t — сид бросков боёвки */
#include "pop_qsave.h"
/* seqids (types.h:1052+) — последовательности боя и стойки. */ /* seqids (types.h:1052+) — последовательности боя и стойки. */
#define SEQ_55_DRAW_SWORD 55 #define SEQ_55_DRAW_SWORD 55
@@ -303,6 +304,7 @@ void pop_check_sword_hurt(void) __banked;
/* Текущий кадр стража (порт cur_frame для Char): заполняет логика в W1/W2, /* Текущий кадр стража (порт cur_frame для Char): заполняет логика в W1/W2,
* читает отрисовка в резиденте W3. image == 255 — рисовать нечего. */ * читает отрисовка в резиденте W3. image == 255 — рисовать нечего. */
extern kframe pop_gframe; extern kframe pop_gframe;
void pop_guard_ai_qsave(pop_qs_io_t *io) __banked;
void pop_guard_load_frame(void); void pop_guard_load_frame(void);
/* check_guard_fallout (seg002:0241): убрать персонажа слота Guard, если он /* check_guard_fallout (seg002:0241): убрать персонажа слота Guard, если он
+15 -8
View File
@@ -17,11 +17,6 @@
#include "pop_vflip.h" #include "pop_vflip.h"
#include "pop_guard.h" /* pop_guard_vflip_load — зеркала соперника */ #include "pop_guard.h" /* pop_guard_vflip_load — зеркала соперника */
/* ISR-стаб W0-страницы (тот же приём, что atlas_load): при прерывании с
* замапленной в W0 страницей CPU прыгает на 0x0038 → там обязан быть JP на
* _gfx_w0_isr (восстановит окно). */
extern void _gfx_w0_isr(void);
#define KID_MAXPAGES 28 #define KID_MAXPAGES 28
int pop_kid_load(uint8_t npages) __banked int pop_kid_load(uint8_t npages) __banked
@@ -50,9 +45,21 @@ void pop_kid_free(void) __banked
kid_npages = 0; kid_npages = 0;
} }
/* ЗАГРУЗКА kid_data.bin (pop_kid_data_load) осталась в РЕЗИДЕНТЕ: она int pop_kid_data_load(const char *path) __banked
* маппит страницу в W3 на время чтения, а этот модуль сам в W3 — см. {
* то же правило в _pop_kid.h. */ uint8_t blk, page;
int n;
blk=mem_alloc_pages(1);
if (!blk) return -1;
page=mem_get_page(blk,0);
n=bank_load_file(page,KD_DATA_OFF,path,16384-KD_DATA_OFF);
if (n < (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) {
mem_free_block(blk); return -1;
}
gfx_w0_page_prepare(page);
kdat_blk=blk; kdat_page=page; kdat_ok=1;
return 0;
}
void pop_kid_data_free(void) __banked void pop_kid_data_free(void) __banked
{ {
-34
View File
@@ -61,8 +61,6 @@ static const uint16_t kid_seq_off[KID_NSEQ] = KID_SEQ_OFF_INIT;
* Это ровно устройство оригинала: load_frame (seg006) заполняет cur_frame, * Это ровно устройство оригинала: load_frame (seg006) заполняет cur_frame,
* и коллизия/отрисовка работают с ним, а не с таблицей. * и коллизия/отрисовка работают с ним, а не с таблицей.
* Страница адресуется как у pop_level: данные с 0x100, ниже — ISR-стаб. */ * Страница адресуется как у pop_level: данные с 0x100, ниже — ISR-стаб. */
extern void _gfx_w0_isr(void);
/* Адрес кадра i в замапленной странице. Считаем В uint16_t и кастуем ОДИН /* Адрес кадра i в замапленной странице. Считаем В uint16_t и кастуем ОДИН
* раз: запись `(const uint8_t *)CONST + (uint16_t)i * 5u` SDCC 4.5 собирает * раз: запись `(const uint8_t *)CONST + (uint16_t)i * 5u` SDCC 4.5 собирает
* НЕВЕРНО — умножение делает в 16 битах (add hl,hl / add hl,bc), а потом * НЕВЕРНО — умножение делает в 16 битах (add hl,hl / add hl,bc), а потом
@@ -449,35 +447,3 @@ void kid_tick(void)
* W1/W2 — самый дефицитный ресурс. Теперь они лежат в отдельной EMM- * W1/W2 — самый дефицитный ресурс. Теперь они лежат в отдельной EMM-
* странице, которая маппится в W0 РОВНО на время play_seq (раз за тик). * странице, которая маппится в W0 РОВНО на время play_seq (раз за тик).
* Страница адресуется как у pop_level: данные с 0x100, ниже — ISR-стаб. */ * Страница адресуется как у pop_level: данные с 0x100, ниже — ISR-стаб. */
/* ЖИВЁТ В РЕЗИДЕНТЕ, хотя зовётся раз за игру: маппит страницу в W3 на
* время чтения, а банковый код сам лежит в W3 (см. _pop_kid.h). */
int pop_kid_data_load(const char *path)
{
uint8_t *pg = (uint8_t *)0xC000; /* страница мапится в W3 на время чтения */
uint8_t saved_w3, blk;
uint16_t stub;
int fd, n;
fd = open(path, O_RDONLY);
if (fd < 0) return -1;
blk = mem_alloc_pages(1);
if (!blk) { close(fd); return -1; }
kdat_blk = blk;
kdat_page = mem_get_page(blk, 0);
saved_w3 = _io_page_w3;
sprinter_page_w3(kdat_page);
n = read(fd, pg + KD_DATA_OFF, 16384 - KD_DATA_OFF);
if (n >= (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) {
stub = (uint16_t)&_gfx_w0_isr; /* ISR-стаб W0-страницы (как pop_level) */
pg[0x38] = 0xC3;
pg[0x39] = (uint8_t)(stub & 0xFF);
pg[0x3A] = (uint8_t)(stub >> 8);
pg[0x66] = 0xED; pg[0x67] = 0x45; /* RETN */
}
sprinter_page_w3(saved_w3);
close(fd);
if (n < (int)(KID_BIN_SEQTBL_OFF + KID_SEQTBL_LEN)) { mem_free_block(blk); return -1; }
kdat_ok = 1;
return 0;
}
+1 -1
View File
@@ -35,7 +35,7 @@ int pop_kid_load(uint8_t npages) __banked;
* (kid_data.bin): в _CODE окна W1/W2 они занимали 3.5 КБ. Грузить ДО * (kid_data.bin): в _CODE окна W1/W2 они занимали 3.5 КБ. Грузить ДО
* первого kid_init/play_seq. 0 — OK, -1 — ошибка (без них Kid не оживёт). * первого kid_init/play_seq. 0 — OK, -1 — ошибка (без них Kid не оживёт).
* Страница маппится в W0 внутри play_seq, раз за тик. */ * Страница маппится в W0 внутри play_seq, раз за тик. */
int pop_kid_data_load(const char *path); int pop_kid_data_load(const char *path) __banked;
/* Прочитать кадр из таблицы страницы данных: tbl_off — KID_BIN_FRAMES_OFF /* Прочитать кадр из таблицы страницы данных: tbl_off — KID_BIN_FRAMES_OFF
* (Kid) или KID_BIN_GFRAMES_OFF (страж, своя таблица frame_tbl_guard). * (Kid) или KID_BIN_GFRAMES_OFF (страж, своя таблица frame_tbl_guard).
-54
View File
@@ -17,10 +17,6 @@
#include <sprinter_mem.h> #include <sprinter_mem.h>
#include <sprite.h> /* gfx_w0_map / gfx_w0_unmap */ #include <sprite.h> /* gfx_w0_map / gfx_w0_unmap */
/* ISR-стаб W0-страницы (тот же приём, что atlas_load): при прерывании с
* замапленной в W0 страницей CPU прыгает на 0x0038 → там обязан быть JP на
* _gfx_w0_isr (восстановит окно). */
extern void _gfx_w0_isr(void);
#include "pop_level.h" #include "pop_level.h"
#include "_pop_level.h" #include "_pop_level.h"
@@ -138,53 +134,3 @@ uint8_t pop_guard_state_x(uint8_t room)
if (room < 1 || room > 24) return 0; if (room < 1 || room > 24) return 0;
return pop_gstate[(uint16_t)(room - 1) * 6 + GS_X]; return pop_gstate[(uint16_t)(room - 1) * 6 + GS_X];
} }
/* Чтение файла уровня в свежую EMM-страницу. ЖИВЁТ В РЕЗИДЕНТЕ, хотя
* зовётся раз на уровень: функция маппит страницу в W3 на время read(), а
* весь холодный код лежит В ЭТОМ ЖЕ окне — переключив его оттуда, банк
* вырезает из-под себя собственные инструкции (первый же вызов улетал в
* halt по мусору, поймано в MAME 2026-08-12). Правило — в _pop_level.h.
* Наружу не публичная в pop_level.h: уровень грузят по НОМЕРУ. */
int pop_level_read_file(const char *path)
{
uint8_t *pg = (uint8_t *)0xC000; /* страница мапится в W3 на время чтения */
uint8_t saved_w3, blk;
uint16_t stub;
int fd, n;
fd = open(path, O_RDONLY);
if (fd < 0) return -1;
blk = mem_alloc_pages(1);
if (!blk) { close(fd); return -1; }
pop_lvl_blk = blk;
pop_lvl_page = mem_get_page(blk, 0);
saved_w3 = _io_page_w3;
sprinter_page_w3(pop_lvl_page);
/* Файл целиком в offset 0x100 (ESTEX READ пишет в W3 — как atlas_load). */
n = read(fd, pg + LVL_DATA_OFF, 16384 - LVL_DATA_OFF);
if (n >= (int)MIN_SIZE) { /* пропатчить ISR-стаб */
uint16_t i;
stub = (uint16_t)&_gfx_w0_isr;
pg[0x38] = 0xC3; /* JP _gfx_w0_isr */
pg[0x39] = (uint8_t)(stub & 0xFF);
pg[0x3A] = (uint8_t)(stub >> 8);
pg[0x66] = 0xED; pg[0x67] = 0x45; /* RETN (NMI-хвост) */
/* Скопировать таблицы дверных связей в W2 (страница ещё в W3). */
for (i = 0; i < 256; i++) {
pop_dl1[i] = pg[LVL_DATA_OFF + BP_LINKLOC + i];
pop_dl2[i] = pg[LVL_DATA_OFF + BP_LINKMAP + i];
}
/* Эталон foretable — для рестарта уровня (см. LVL_PRISTINE_OFF). */
for (i = 0; i < 24u * ROOM_TILES; i++)
pg[LVL_PRISTINE_OFF + i] = pg[LVL_DATA_OFF + BP_FG + i];
}
sprinter_page_w3(saved_w3);
close(fd);
if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; }
pop_lvl_ok = 1;
pop_gstate_init(); /* живая копия состояния стражей — из уровня */
return 0;
}
+3
View File
@@ -18,6 +18,8 @@
#ifndef POP_LEVEL_H #ifndef POP_LEVEL_H
#define POP_LEVEL_H #define POP_LEVEL_H
#include "pop_qsave.h"
#include <stdint.h> #include <stdint.h>
/* Загрузить уровень из файла в EMM-страницу. 0 — OK, -1 — ошибка /* Загрузить уровень из файла в EMM-страницу. 0 — OK, -1 — ошибка
@@ -32,6 +34,7 @@ void pop_level_free(void) __banked;
* level). Уровень 1..15; 0 — демо-уровень, в скоуп не входит. Читают * level). Уровень 1..15; 0 — демо-уровень, в скоуп не входит. Читают
* потабличные функции ниже, стражи (HP/тип) и стартовая логика. */ * потабличные функции ниже, стражи (HP/тип) и стартовая логика. */
extern uint8_t pop_current_level; extern uint8_t pop_current_level;
void pop_level_qsave(pop_qs_io_t *io);
/* Загрузить уровень НОМЕРОМ: освободить страницу прошлого, взять /* Загрузить уровень НОМЕРОМ: освободить страницу прошлого, взять
* `LEVELS\res20NN.bin` (fallback `a:\res20NN.bin`) и выставить * `LEVELS\res20NN.bin` (fallback `a:\res20NN.bin`) и выставить
+29 -12
View File
@@ -18,16 +18,8 @@
#include "_pop_level.h" #include "_pop_level.h"
#include "pop_geom.h" /* pop_x_bump / FIRST_ONSCREEN_COLUMN — pos_guards */ #include "pop_geom.h" /* pop_x_bump / FIRST_ONSCREEN_COLUMN — pos_guards */
/* ISR-стаб W0-страницы (тот же приём, что atlas_load): при прерывании с /* Низкоуровневое чтение через W3 выполняет резидентный bank_load_file();
* замапленной в W0 страницей CPU прыгает на 0x0038 → там обязан быть JP на * остальная одноразовая подготовка уровня безопасно остаётся в банке 8. */
* _gfx_w0_isr (восстановит окно). Символ внутренний для libbgi, но
* стабильный — линкер тянет его из bgi256.lib (графика уже подключена). */
extern void _gfx_w0_isr(void);
/* ЧТЕНИЕ ФАЙЛА уровня живёт в РЕЗИДЕНТЕ (pop_level.c, pop_level_read_file):
* оно маппит страницу в W3 на время read(), а этот модуль сам лежит в W3 —
* переключив окно, он вырезал бы из-под себя собственный код (проверено:
* первый же вызов улетал в halt по мусору). Правило записано в _pop_level.h. */
void pop_level_free(void) __banked void pop_level_free(void) __banked
{ {
@@ -58,6 +50,31 @@ int8_t pop_level_guard_type(uint8_t n) __banked { return n < 16 ? tbl_guard_typ
uint8_t pop_level_tileset(uint8_t n) __banked { return n < 16 ? tbl_level_type[n] : 0; } uint8_t pop_level_tileset(uint8_t n) __banked { return n < 16 ? tbl_level_type[n] : 0; }
uint8_t pop_level_type(void) __banked { return tbl_level_type[pop_current_level & 15]; } uint8_t pop_level_type(void) __banked { return tbl_level_type[pop_current_level & 15]; }
static int level_read_file(const char *path)
{
uint8_t blk, page;
uint16_t i;
int n;
blk=mem_alloc_pages(1);
if (!blk) return -1;
page=mem_get_page(blk,0);
n=bank_load_file(page,LVL_DATA_OFF,path,16384-LVL_DATA_OFF);
if (n < (int)MIN_SIZE) { mem_free_block(blk); return -1; }
gfx_w0_page_prepare(page);
gfx_w0_map(page);
for (i=0; i<256; i++) {
pop_dl1[i]=((const uint8_t *)LVL_DATA_OFF)[BP_LINKLOC+i];
pop_dl2[i]=((const uint8_t *)LVL_DATA_OFF)[BP_LINKMAP+i];
}
for (i=0; i<24u*ROOM_TILES; i++)
((uint8_t *)LVL_PRISTINE_OFF)[i]=
((const uint8_t *)(LVL_DATA_OFF+BP_FG))[i];
gfx_w0_unmap();
pop_lvl_blk=blk; pop_lvl_page=page; pop_lvl_ok=1;
pop_gstate_init();
return 0;
}
int pop_level_load_num(uint8_t n) __banked int pop_level_load_num(uint8_t n) __banked
{ {
/* Имена патчим на месте, а не через sprintf: printf-семейство тянет в /* Имена патчим на месте, а не через sprintf: printf-семейство тянет в
@@ -71,10 +88,10 @@ int pop_level_load_num(uint8_t n) __banked
pri[13] = alt[9] = (char)('0' + n % 10); pri[13] = alt[9] = (char)('0' + n % 10);
/* Старую страницу отпускаем ТОЛЬКО после успешной загрузки новой: /* Старую страницу отпускаем ТОЛЬКО после успешной загрузки новой:
* pop_level_read_file сам аллоцирует страницу, и если файла нет — остаёмся * level_read_file сам аллоцирует страницу, и если файла нет — остаёмся
* на текущем уровне вместо падения в пустой уровень. */ * на текущем уровне вместо падения в пустой уровень. */
pop_lvl_ok = 0; /* чтобы level_load_path не мешался */ pop_lvl_ok = 0; /* чтобы level_load_path не мешался */
if (pop_level_read_file(pri) != 0 && pop_level_read_file(alt) != 0) { if (level_read_file(pri) != 0 && level_read_file(alt) != 0) {
pop_lvl_blk = saved_blk; pop_lvl_page = saved_page; pop_lvl_ok = saved_ok; pop_lvl_blk = saved_blk; pop_lvl_page = saved_page; pop_lvl_ok = saved_ok;
return -1; return -1;
} }
@@ -0,0 +1,38 @@
/*
* Сериализация уровня для QuickSave (банк 8).
*
* Код выполняется только по F6/F9 и вынесен из горячего резидентного
* pop_level.c. Данные уровня и снимка используют одно окно W0, поэтому
* foretable переносится небольшими блоками через стековый буфер W2.
*/
#include <sprite.h>
#include "pop_level.h"
#include "_pop_level.h"
void pop_level_qsave(pop_qs_io_t *io)
{
uint8_t tmp[30];
uint16_t off;
pop_qs_bytes(io, pop_dl1, sizeof(pop_dl1));
pop_qs_bytes(io, pop_dl2, sizeof(pop_dl2));
pop_qs_bytes(io, pop_gstate, sizeof(pop_gstate));
if (!pop_lvl_ok) { io->failed = 1; return; }
/* W0 одновременно не может держать страницу уровня и страницу снимка. */
for (off = 0; off < 24u * ROOM_TILES; off += sizeof(tmp)) {
uint8_t i;
if (!io->reading || io->apply) {
gfx_w0_map(pop_lvl_page);
if (!io->reading)
for (i=0; i<sizeof(tmp); i++)
tmp[i]=((uint8_t *)(LVL_DATA_OFF+BP_FG))[off+i];
gfx_w0_unmap();
}
pop_qs_bytes(io,tmp,sizeof(tmp));
if (io->reading && io->apply && !io->failed) {
gfx_w0_map(pop_lvl_page);
for (i=0; i<sizeof(tmp); i++)
((uint8_t *)(LVL_DATA_OFF+BP_FG))[off+i]=tmp[i];
gfx_w0_unmap();
}
}
}
+13
View File
@@ -3394,3 +3394,16 @@ void pop_check_mirror(void) __banked
/* Char остался ОТРАЖЁННЫМ — как в оригинале: обратно он не сохраняется, /* Char остался ОТРАЖЁННЫМ — как в оригинале: обратно он не сохраняется,
* а следующий тик перезагружает окно из Kid. */ * а следующий тик перезагружает окно из Kid. */
} }
/* Закрытое состояние физики, которое влияет на следующие кадры. Таблицы
* коллизии и кэши соседей намеренно не входят: их пересчитает вход в комнату. */
void pop_map_qsave(pop_qs_io_t *io) __banked
{
pop_qs_u8(io, &is_screaming);
pop_qs_u8(io, &grab_timer);
pop_qs_i8(io, &pickup_obj_type);
pop_qs_u16(io, &loose_seed.lo);
pop_qs_u16(io, &loose_seed.hi);
pop_qs_u8(io, &last_loose_snd);
if (io->reading && io->apply) pop_coll_invalidate();
}
+2
View File
@@ -13,6 +13,7 @@
#include <stdint.h> #include <stdint.h>
#include "pop_char.h" /* pop_char_t — окклюзия ворот спрашивает про персонажа */ #include "pop_char.h" /* pop_char_t — окклюзия ворот спрашивает про персонажа */
#include "pop_qsave.h"
/* Задать карту текущей комнаты: fg[30] = коды тайлов (row*10+col, 3×10). /* Задать карту текущей комнаты: fg[30] = коды тайлов (row*10+col, 3×10).
* col<0/>9 или row<0/>2 трактуются как стена (край уровня). Массив * col<0/>9 или row<0/>2 трактуются как стена (край уровня). Массив
@@ -197,6 +198,7 @@ void pop_check_knock(void) __banked;
* links.down — сменить комнату (pop_room_load+перерисовка) и продолжить; * links.down — сменить комнату (pop_room_load+перерисовка) и продолжить;
* иначе — выпал из уровня (респавн). Снять флаг после обработки. */ * иначе — выпал из уровня (респавн). Снять флаг после обработки. */
extern uint8_t pop_fell_out; extern uint8_t pop_fell_out;
void pop_map_qsave(pop_qs_io_t *io) __banked;
/* Пер-кадровая физика ПОСЛЕ kid_tick(play_seq): fall_accel + fall_speed + /* Пер-кадровая физика ПОСЛЕ kid_tick(play_seq): fall_accel + fall_speed +
* determine_col + check_action (триггер падения / do_fall / приземление). * determine_col + check_action (триггер падения / do_fall / приземление).
+207
View File
@@ -0,0 +1,207 @@
/*
* pop_qsave.c — файловая транзакция и схема игрового снимка.
* Живёт в холодном банке: в обычном кадре исполняется только проверка
* резидентного флага запроса, вся тяжёлая работа бывает по F6/F9.
*/
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h> /* rename */
#include <sprinter_mem.h>
#include <sprite.h>
#include "pop_qsave.h"
#include "pop_kid.h"
#include "pop_guard.h"
#include "pop_map.h"
#include "pop_level.h"
#include "pop_trob.h"
#include "pop_bg.h"
#include "pop_state.h"
#include "pop_tile.h"
#include "pop_sfx.h"
#include "pop_pace.h"
#include "roomtest_cold.h"
#define QS_VERSION 3
#define QS_SAVE 1
#define QS_LOAD 2
#define QS_LIMIT 4096
#define QS_BASE 0x100
extern uint8_t pop_qsave_request;
static void qs_char(pop_qs_io_t *io, pop_char_t *c)
{
pop_qs_u8(io, &c->frame); pop_qs_u8(io, &c->x); pop_qs_u8(io, &c->y);
pop_qs_i8(io, &c->direction); pop_qs_i8(io, &c->curr_col);
pop_qs_i8(io, &c->curr_row); pop_qs_u8(io, &c->action);
pop_qs_i8(io, &c->fall_x); pop_qs_i8(io, &c->fall_y);
pop_qs_u8(io, &c->room); pop_qs_u8(io, &c->repeat);
pop_qs_u8(io, &c->charid); pop_qs_u8(io, &c->sword);
pop_qs_i8(io, &c->alive); pop_qs_u16(io, &c->curr_seq);
}
static void qs_payload(pop_qs_io_t *io)
{
uint8_t speed=pop_speed_mode, immortal=pop_immortal, sound=pop_snd_want;
/* Пользовательские параметры входят в снимок, а не остаются свойством
* текущего запуска. Читаем сначала во временные байты: повреждённые
* значения не должны частично применить остальное игровое состояние. */
pop_qs_u8(io, &speed); pop_qs_u8(io, &immortal); pop_qs_u8(io, &sound);
if (io->reading && io->apply) {
if (speed >= POP_SPEED_MODES || immortal > 2 || sound > 1) {
io->failed=1; return;
}
pop_speed_mode=speed; pop_immortal=immortal; pop_snd_want=sound;
}
qs_char(io, &Kid); qs_char(io, &Guard);
pop_qs_u8(io, &cur_room); pop_qs_u8(io, &kid_room);
pop_qs_u8(io, &pop_checkpoint); pop_qs_u8(io, &pop_next_level);
pop_qs_i8(io, &knock);
pop_qs_u8(io, &hitp_curr); pop_qs_u8(io, &hitp_max);
pop_qs_u8(io, &hitp_beg_lev); pop_qs_i8(io, &hitp_delta);
pop_qs_u8(io, &guardhp_curr); pop_qs_u8(io, &guardhp_max);
pop_qs_i8(io, &guardhp_delta); pop_qs_u8(io, &guard_skill);
pop_qs_i8(io, &guard_refrac); pop_qs_i8(io, &justblocked);
pop_qs_i8(io, &kid_sword_strike); pop_qs_i8(io, &offguard);
pop_qs_i8(io, &holding_sword); pop_qs_u8(io, &pop_guard_hurt);
pop_qs_i8(io, &can_guard_see_kid); pop_qs_i8(io, &is_guard_notice);
pop_qs_i8(io, &pop_united_shadow); pop_qs_u8(io, &pop_shadow_init);
pop_qs_i8(io, &pop_guard_notice_timer);
pop_qs_u16(io, &pop_fight_seed.lo); pop_qs_u16(io, &pop_fight_seed.hi);
pop_qs_u8(io, &pop_kid_dead); pop_qs_u8(io, &pop_kid_hurt);
pop_qs_u8(io, &pop_have_sword); pop_qs_u8(io, &pop_item_taken);
pop_qs_u8(io, &pop_flash_time); pop_qs_u8(io, &pop_flash_color);
pop_qs_u8(io, &pop_feather); pop_qs_u8(io, &pop_upside);
pop_qs_u8(io, &pop_upside_want); pop_qs_u8(io, &pop_upside_dirty);
pop_qs_u8(io, &pop_leave_dir); pop_qs_u8(io, &pop_leave_timer);
pop_qs_u8(io, &pop_loose_fell); pop_qs_u8(io, &pop_ceil_fell);
pop_qs_u8(io, &pop_debris_at); pop_qs_u8(io, &pop_fell_out);
pop_qs_bytes(io, pop_loose_modif, sizeof(pop_loose_modif));
pop_qs_bytes(io, pop_ceil_modif, sizeof(pop_ceil_modif));
pop_qs_u8(io, &pop_loose_landed); pop_qs_u8(io, &pop_loose_exit);
pop_qs_u8(io, &pop_loose_exit_room); pop_qs_u8(io, &pop_droppedout);
pop_qs_u16(io, &pop_leveldoor_open); pop_qs_i8(io, &pop_jumped_mirror);
pop_qs_u8(io, &pop_seamless); pop_qs_u8(io, &pop_nav_hold);
pop_level_qsave(io);
pop_trob_qsave(io);
pop_loose_mob_qsave(io);
pop_map_qsave(io);
pop_guard_ai_qsave(io);
}
static void qs_init(pop_qs_io_t *io, uint8_t page, uint16_t limit,
uint8_t reading, uint8_t apply)
{
io->page=page; io->pos=QS_BASE; io->limit=(uint16_t)(QS_BASE+limit);
io->checksum=0;
io->count=0; io->reading=reading; io->apply=apply; io->failed=0;
}
static uint8_t qs_header_read(pop_qs_io_t *io, uint8_t *level)
{
uint8_t h[7];
if (!pop_qs_raw_get(io,h,sizeof(h))) return 0;
if (h[0]!='P' || h[1]!='O' || h[2]!='P' || h[3]!='Q' ||
h[4] != QS_VERSION || h[5] < 1 || h[5] > 15 || h[6] != 0) return 0;
*level = h[5]; return 1;
}
static uint8_t qs_validate_page(uint8_t page, uint16_t size,
uint8_t *level, uint16_t *payload_size)
{
pop_qs_io_t io;
uint8_t tail[4];
uint16_t n, sum;
if (size < 11) return 0;
qs_init(&io,page,size,1,0);
if (!qs_header_read(&io,level)) return 0;
io.pos=(uint16_t)(QS_BASE+size-4);
if (!pop_qs_raw_get(&io,tail,4)) return 0;
n = (uint16_t)(tail[0] | ((uint16_t)tail[1] << 8));
sum = (uint16_t)(tail[2] | ((uint16_t)tail[3] << 8));
if (n != (uint16_t)(size-11) ||
sum != pop_qs_checksum_page(page,QS_BASE+7,n)) return 0;
*payload_size=n; return 1;
}
static uint8_t qs_apply(uint8_t page, uint16_t size, uint16_t payload_size,
uint8_t *level)
{
pop_qs_io_t io;
qs_init(&io,page,size,1,1);
if (!qs_header_read(&io,level)) return 0;
qs_payload(&io);
return (uint8_t)(!io.failed && io.count == payload_size &&
io.pos == (uint16_t)(QS_BASE+7+payload_size));
}
static int8_t qs_save(uint8_t page)
{
pop_qs_io_t io;
uint8_t tail[4];
const uint8_t h[7]={'P','O','P','Q',QS_VERSION,pop_current_level,0};
uint16_t size;
qs_init(&io,page,QS_LIMIT,0,0);
pop_qs_raw_put(&io,h,sizeof(h));
qs_payload(&io);
tail[0]=(uint8_t)io.count; tail[1]=(uint8_t)(io.count >> 8);
io.checksum=pop_qs_checksum_page(page,QS_BASE+7,io.count);
tail[2]=(uint8_t)io.checksum; tail[3]=(uint8_t)(io.checksum >> 8);
pop_qs_raw_put(&io,tail,4); size=(uint16_t)(io.pos-QS_BASE);
if (io.failed) return -1;
unlink("POP.NEW");
if (bank_save_file(page,QS_BASE,"POP.NEW",size) != size) {
unlink("POP.NEW"); return -1;
}
unlink("POP.BAK");
(void)rename("POP.SAV", "POP.BAK");
if (rename("POP.NEW", "POP.SAV") != 0) {
(void)rename("POP.BAK", "POP.SAV"); return -1;
}
return 1;
}
static int8_t qs_load_one(const char *name, uint8_t page)
{
uint8_t level;
uint16_t payload_size;
int size=bank_load_file(page,QS_BASE,name,QS_LIMIT);
if (size <= 0 || !qs_validate_page(page,(uint16_t)size,&level,
&payload_size)) return -1;
if (level != pop_current_level) {
pop_next_level = level;
if (pop_level_switch() != 0 || pop_current_level != level) return -1;
}
if (!qs_apply(page,(uint16_t)size,payload_size,&level)) return -1;
Char = Kid; Opp = Guard;
pop_qsave_restore_room(cur_room);
return 1;
}
int8_t pop_qsave_process(void) __banked
{
uint8_t request = pop_qsave_request;
uint8_t blk,page;
int8_t rc=-1;
pop_qsave_request = 0;
if (!request) return 0;
blk=mem_alloc_pages(1);
if (!blk) return -1;
page=mem_get_page(blk,0);
gfx_w0_page_prepare(page);
pop_sfx_pause();
if (request == QS_SAVE) rc = qs_save(page);
else {
rc = qs_load_one("POP.SAV",page);
if (rc < 0) rc = qs_load_one("POP.BAK",page);
}
pop_sfx_start();
mem_free_block(blk);
return rc;
}
+40
View File
@@ -0,0 +1,40 @@
/*
* pop_qsave.h — потоковый QuickSave/QuickLoad одного игрового слота.
*
* На диске используются POP.NEW, POP.SAV и POP.BAK. Формат не содержит
* указателей, файловых дескрипторов и графических кэшей; многобайтные поля
* записываются явно little-endian. Внутренние функции pop_qs_* нужны
* модулям-владельцам закрытого состояния.
*/
#ifndef POP_QSAVE_H
#define POP_QSAVE_H
#include <stdint.h>
typedef struct {
uint8_t page;
uint16_t pos;
uint16_t limit;
uint16_t checksum;
uint16_t count;
uint8_t reading;
uint8_t apply;
uint8_t failed;
} pop_qs_io_t;
void pop_qs_bytes(pop_qs_io_t *io, void *data, uint16_t size);
void pop_qs_u8(pop_qs_io_t *io, uint8_t *value);
void pop_qs_i8(pop_qs_io_t *io, int8_t *value);
void pop_qs_u16(pop_qs_io_t *io, uint16_t *value);
void pop_qs_i16(pop_qs_io_t *io, int *value);
void pop_qs_raw_put(pop_qs_io_t *io, const uint8_t *data, uint8_t size);
uint8_t pop_qs_raw_get(pop_qs_io_t *io, uint8_t *data, uint8_t size);
uint16_t pop_qs_checksum_page(uint8_t page, uint16_t pos, uint16_t size);
/* Вызывается в безопасной границе кадра. Возвращает: 0 — запроса нет,
* 1 — сохранено/загружено, -1 — ошибка. */
int8_t pop_qsave_process(void) __banked;
void pop_qsave_request_save(void);
void pop_qsave_request_load(void);
#endif
@@ -0,0 +1,38 @@
/*
* Холодная часть ввода-вывода QuickSave (банк 8).
*
* Эти функции вызываются только банковым оркестратором pop_qsave.c, поэтому
* им не место в дефицитном резидентном _CODE. Здесь только доступ через W0:
* переключающие W3 DSS-операции обязаны оставаться в pop_qsave_io.c.
*/
#include "pop_qsave.h"
#include <sprite.h>
void pop_qs_raw_put(pop_qs_io_t *io, const uint8_t *data, uint8_t size)
{
uint8_t *q, i;
if (io->pos + size > io->limit) { io->failed=1; return; }
gfx_w0_map(io->page); q=(uint8_t *)io->pos;
for (i=0; i<size; i++) q[i]=data[i];
gfx_w0_unmap(); io->pos += size;
}
uint8_t pop_qs_raw_get(pop_qs_io_t *io, uint8_t *data, uint8_t size)
{
uint8_t *q, i;
if (io->pos + size > io->limit) return 0;
gfx_w0_map(io->page); q=(uint8_t *)io->pos;
for (i=0; i<size; i++) data[i]=q[i];
gfx_w0_unmap(); io->pos += size; return 1;
}
/* XOR8 достаточно для обнаружения случайной порчи транзакционного файла;
* размер payload проверяется отдельно, старший байт содержит инверсию XOR. */
uint16_t pop_qs_checksum_page(uint8_t page, uint16_t pos, uint16_t size)
{
uint8_t *p, sum=0;
gfx_w0_map(page); p=(uint8_t *)pos;
while(size--) sum ^= *p++;
gfx_w0_unmap();
return (uint16_t)(sum | ((uint16_t)(uint8_t)~sum << 8));
}
+33
View File
@@ -0,0 +1,33 @@
/* Резидентная часть QuickSave: часто вызываемые межбанковые примитивы
* потока и флаг запроса. Файловый обмен с W3 теперь общий в libc. */
#include "pop_qsave.h"
#include <sprite.h>
#include <string.h>
uint8_t pop_qsave_request;
void pop_qsave_request_save(void) { pop_qsave_request=1; }
void pop_qsave_request_load(void) { pop_qsave_request=2; }
void pop_qs_bytes(pop_qs_io_t *io, void *data, uint16_t size)
{
uint8_t *p=(uint8_t *)data, *q;
if (io->failed || io->pos + size > io->limit) { io->failed=1; return; }
gfx_w0_map(io->page); q=(uint8_t *)io->pos;
if (!io->reading) memcpy(q,p,size);
else if (io->apply) memcpy(p,q,size);
gfx_w0_unmap(); io->pos += size; io->count += size;
}
void pop_qs_u8(pop_qs_io_t *io, uint8_t *v) { pop_qs_bytes(io,v,1); }
void pop_qs_i8(pop_qs_io_t *io, int8_t *v) { pop_qs_bytes(io,v,1); }
void pop_qs_u16(pop_qs_io_t *io, uint16_t *value)
{
uint8_t b[2]; b[0]=(uint8_t)*value; b[1]=(uint8_t)(*value >> 8);
pop_qs_bytes(io,b,2);
if (io->reading && io->apply && !io->failed)
*value=(uint16_t)(b[0] | ((uint16_t)b[1] << 8));
}
void pop_qs_i16(pop_qs_io_t *io, int *value)
{
uint16_t v=(uint16_t)*value; pop_qs_u16(io,&v);
if (io->reading && io->apply && !io->failed) *value=(int)v;
}
+22
View File
@@ -1350,6 +1350,28 @@ void pop_loose_mob_reset(void) __banked
* мапит и размапливает W0. Инвалидируется сменой комнаты. */ * мапит и размапливает W0. Инвалидируется сменой комнаты. */
static uint8_t mob_link_room, mob_link_up, mob_link_dn; static uint8_t mob_link_room, mob_link_up, mob_link_dn;
void pop_loose_mob_qsave(pop_qs_io_t *io) __banked
{
uint8_t i, live = 0;
for (i = 0; i < MOB_MAX; i++) {
mob_t *m = &mobs[i];
pop_qs_u8(io, &m->active);
pop_qs_i16(io, &m->x); pop_qs_i16(io, &m->y);
pop_qs_i8(io, &m->speed); pop_qs_i8(io, &m->row);
pop_qs_i8(io, &m->col); pop_qs_u8(io, &m->room);
if (io->reading && io->apply) {
m->clean = m->defer = 0;
m->draw_y = m->prev_y[0] = m->prev_y[1] = MOB_Y_NONE;
if (m->active) live++;
}
}
if (io->reading && io->apply) {
mobs_live = live;
pop_mob_busy = (uint8_t)(live != 0);
mob_link_room = 0;
}
}
static void mob_links(void) static void mob_links(void)
{ {
if (mob_link_room == pop_t_room) return; if (mob_link_room == pop_t_room) return;
+1 -21
View File
@@ -9,9 +9,7 @@
* ставит базовую страницу W1, значит хендлер обязан быть в базе; * ставит базовую страницу W1, значит хендлер обязан быть в базе;
* - pop_sfx_play дёргается из горячих мест (play_seq на каждый шаг * - pop_sfx_play дёргается из горячих мест (play_seq на каждый шаг
* Кида, физика, боёвка) — трамплин там был бы чистой потерей; * Кида, физика, боёвка) — трамплин там был бы чистой потерей;
* - pop_page_read маппит W3 на время чтения, а банковый код сам живёт * Файловое чтение страниц выполняет общий резидентный bank_load_file().
* в W3 и вырезал бы себя из-под ног (та же причина, по которой
* резидентен pop_level_read_file, см. _pop_level.h).
* Холодная половина (аллокация страниц, cbl_open) — в pop_sfx_cold.c. * Холодная половина (аллокация страниц, cbl_open) — в pop_sfx_cold.c.
*/ */
@@ -198,21 +196,3 @@ void pop_sfx_stop(void)
sfx_left = 0; sfx_left = 0;
IRQ_ENABLE(); IRQ_ENABLE();
} }
/* ---- чтение файла в ФИЗИЧЕСКУЮ страницу ------------------------------ *
* Резидентно по причине из шапки: маппим W3, читаем, возвращаем окно.
* Ровно то же делает atlas_load, поэтому вызов из банка безопасен.
* 0 — прочитано ожидаемое число байт, -1 — нет. */
int pop_page_read(const char *path, uint8_t phys, uint16_t want)
{
uint8_t saved_w3;
int fd, n;
fd = open(path, O_RDONLY);
if (fd < 0) return -1;
saved_w3 = _io_page_w3;
sprinter_page_w3(phys);
n = read(fd, (uint8_t *)0xC000, want);
_io_page_w3 = saved_w3;
close(fd);
return (n == (int)want) ? 0 : -1;
}
+6 -5
View File
@@ -26,7 +26,7 @@ int pop_sfx_init(void) __banked
for (i = 0; i < POP_SND_PAGES; i++) { for (i = 0; i < POP_SND_PAGES; i++) {
pop_snd_page[i] = mem_get_page(blk, i); pop_snd_page[i] = mem_get_page(blk, i);
if (!pop_snd_page[i] || if (!pop_snd_page[i] ||
pop_page_read(pop_snd_files[i], pop_snd_page[i], 16384) != 0) { bank_load_file(pop_snd_page[i],0,pop_snd_files[i],16384) != 16384) {
mem_free_block(blk); mem_free_block(blk);
return -1; return -1;
} }
@@ -48,16 +48,17 @@ int pop_sfx_init(void) __banked
* *
* Параметры: 10 937,5 Гц, 8 бит моно — формат данных один в один * Параметры: 10 937,5 Гц, 8 бит моно — формат данных один в один
* (sound_plan.md). OTIR, а не акселератор: тот нужен только для 16 бит. * (sound_plan.md). OTIR, а не акселератор: тот нужен только для 16 бит.
* UNDERRUN_APP, а не SILENCE: тишину насос льёт СВОЮ (первый блок * cbl_open, а не cbl_open_silence: тишину насос льёт СВОЮ (первый блок
* набора), и просить у libc буфер через malloc не надо — куча в * набора), и просить у libc буфер через malloc не надо — куча в резиденте
* резиденте W2 тесная, и её отказ ронял бы весь звук. */ * W2 тесная, и её отказ ронял бы весь звук. Заодно так malloc вообще не
* приезжает в резидент (275 Б, см. libc/cbl/_cbl_open_raw.c). */
int pop_sfx_start(void) __banked int pop_sfx_start(void) __banked
{ {
if (pop_snd_ok) return 0; if (pop_snd_ok) return 0;
if (!pop_snd_want) return 0; /* выключено пользователем (Ctrl+S) */ if (!pop_snd_want) return 0; /* выключено пользователем (Ctrl+S) */
if (!snd_blk) return -1; /* набор не загружен */ if (!snd_blk) return -1; /* набор не загружен */
if (cbl_open(CBL_FREQ_10K9, CBL_FMT_MONO8, CBL_PUMP_OTIR, if (cbl_open(CBL_FREQ_10K9, CBL_FMT_MONO8, CBL_PUMP_OTIR,
CBL_UNDERRUN_APP, pop_sfx_fill) != 0) pop_sfx_fill) != 0)
return -1; return -1;
pop_snd_ok = 1; pop_snd_ok = 1;
return 0; return 0;
+2 -181
View File
@@ -17,6 +17,7 @@
#include "pop_pace.h" /* pop_beam_sample — счёт кадров по лучу */ #include "pop_pace.h" /* pop_beam_sample — счёт кадров по лучу */
#include "pop_level.h" /* pop_doorlink2 — подстановка нажатой кнопки */ #include "pop_level.h" /* pop_doorlink2 — подстановка нажатой кнопки */
#include "_pop_draw.h" /* pop_heal_fast */ #include "_pop_draw.h" /* pop_heal_fast */
#include "_pop_tile.h" /* контракт с холодной половиной (pop_tile_cold.c) */
/* ---- Состояние (писучее, _DATA/W2 — видно из любого банка) ---------- */ /* ---- Состояние (писучее, _DATA/W2 — видно из любого банка) ---------- */
atlas_t pop_env[POP_ENV_PAGES], pop_wall_at, pop_fore_at, pop_pot_at; atlas_t pop_env[POP_ENV_PAGES], pop_wall_at, pop_fore_at, pop_pot_at;
@@ -307,89 +308,6 @@ void pop_cd_touch(int x, int y, int w, int h)
/* Задел ли прямоугольник (ЭКРАННЫЕ координаты, x1/y1 ВКЛЮЧИТЕЛЬНО) то, что /* Задел ли прямоугольник (ЭКРАННЫЕ координаты, x1/y1 ВКЛЮЧИТЕЛЬНО) то, что
* трогали на странице p. Резидент: зовёт pop_cdraw.c из банка 4 прямым * трогали на странице p. Резидент: зовёт pop_cdraw.c из банка 4 прямым
* вызовом, без трамплина. */ * вызовом, без трамплина. */
/* Прямоугольник слота против метки — БЕЗ передачи пяти аргументов.
*
* pop_cd_hit принимает (p, x0, y0, x1, y1): три последних идут стеком, и
* функция целиком уезжает в IX-фрейм — 45 % её тактов уходит на `-n(ix)`
* (замер asm 2026-08-19). А зовут её из cd_quiet дважды на слот, то есть
* до восьми раз за кадр. Здесь координаты берутся прямо из pop_cd, и
* аргументов остаётся два.
*
* Спрайт и накладной проверяются РАЗДЕЛЬНО — объединённый bbox ловит
* касания углом, которых нет (разбор в cd_quiet, pop_cdraw.c). */
static int hit_x, hit_y; /* аргументы hit_rect — file-scope, не стек */
static uint16_t hit_w, hit_h;
static uint8_t hit_p;
static uint8_t hit_rect(void)
{
int8_t c0, c1, c;
uint8_t ya, yb;
int x1 = hit_x + (int)hit_w - 1;
int y1 = hit_y + (int)hit_h - 1;
if (!hit_w || !hit_h) return 0;
if (x1 < 0 || hit_x > 319) return 0;
if (y1 < 0 || hit_y > 255) return 0;
ya = (uint8_t)(hit_y < 0 ? 0 : hit_y);
yb = (uint8_t)(y1 > 255 ? 255 : y1);
c0 = (int8_t)((hit_x < 0 ? 0 : hit_x) >> 5);
c1 = (int8_t)((x1 > 319 ? 319 : x1) >> 5);
for (c = c0; c <= c1; c++)
if (ya <= pop_cd_ymax[hit_p][c] && yb >= pop_cd_ymin[hit_p][c]) return 1;
return 0;
}
uint8_t pop_cd_hit_slot(uint8_t who, uint8_t p)
{
const pop_cdraw_t *s = &pop_cd[who];
if (!(pop_cd_dirty & (1 << p))) return 0;
hit_p = p;
hit_x = s->x[p]; hit_y = s->y[p] + POP_YOFF;
hit_w = s->w[p]; hit_h = s->h[p];
if (hit_rect()) return 1;
if (!s->ovalid[p]) return 0;
hit_x = s->ox[p]; hit_y = s->oy[p] + POP_YOFF;
hit_w = s->ow[p]; hit_h = s->oh[p];
return hit_rect();
}
uint8_t pop_cd_hit(uint8_t p, int x0, int y0, int x1, int y1)
{
int8_t c0, c1, c;
uint8_t ya, yb;
if (!(pop_cd_dirty & (1 << p))) return 0;
if (x1 < 0 || x0 > 319) return 0;
if (x0 < 0) x0 = 0;
if (x1 > 319) x1 = 319;
if (y1 < 0 || y0 > 255) return 0;
ya = (uint8_t)(y0 < 0 ? 0 : y0);
yb = (uint8_t)(y1 > 255 ? 255 : y1);
c0 = (int8_t)(x0 >> 5);
c1 = (int8_t)(x1 >> 5);
/* Пересечение отрезков [ya,yb] и [ymin,ymax] хотя бы в одной колонке.
* Пустая колонка держит ymin = 255, ymax = 0 — условие ниже её отсеет
* само, отдельной проверки «пусто» не нужно. */
for (c = c0; c <= c1; c++)
if (ya <= pop_cd_ymax[p][c] && yb >= pop_cd_ymin[p][c]) return 1;
return 0;
}
/* Страница приведена в порядок — снять с неё метку целиком. */
/* Начальное состояние обеих страниц — «ничего не трогали». ЯВНО, а не
* расчётом на обнуление _DATA: пустая колонка обозначается ymin = 255,
* ymax = 0, а нули от crt0 читались бы как «затронута строка 0». */
void pop_cd_init(void)
{
pop_cd_clear(0);
pop_cd_clear(1);
}
void pop_cd_clear(uint8_t p)
{
uint8_t c;
for (c = 0; c < 10; c++) { pop_cd_ymin[p][c] = 255; pop_cd_ymax[p][c] = 0; }
pop_cd_dirty = (uint8_t)(pop_cd_dirty & ~(1 << p));
}
/* ---- Блит куска атласа ---------------------------------------------- */ /* ---- Блит куска атласа ---------------------------------------------- */
/* Клипованный путь — ОТДЕЛЬНОЙ функцией. У него девять 16-битных локалей, /* Клипованный путь — ОТДЕЛЬНОЙ функцией. У него девять 16-битных локалей,
@@ -399,27 +317,6 @@ void pop_cd_clear(uint8_t p)
* перерисовка тайлов — идёт с pop_t_fclip_on == 0. */ * перерисовка тайлов — идёт с pop_t_fclip_on == 0. */
#define FLIP_TOP(top, h) POP_FLIP_TOP(top, h) /* формула — в pop_bg.h */ #define FLIP_TOP(top, h) POP_FLIP_TOP(top, h) /* формула — в pop_bg.h */
/* Окно под точечную перерисовку/запечку — контракт в шапке pop_tile.h.
* Логическая пара нужна для отсева тайлов (он считается из ряда комнаты),
* экранная — для клипа самого блита; при перевороте они разные. */
void pop_t_win_set(int x, int ytop, int w, int h)
{
pop_t_fclip_x0 = x;
pop_t_fclip_x1 = x + w;
pop_t_fclip_ly0 = ytop;
pop_t_fclip_ly1 = ytop + h;
if (pop_upside) {
int t = FLIP_TOP(ytop, h);
pop_t_fclip_y0 = t;
pop_t_fclip_y1 = t + h;
} else {
pop_t_fclip_y0 = ytop;
pop_t_fclip_y1 = ytop + h;
}
pop_t_fclip_on = 1;
}
void pop_t_win_clear(void) { pop_t_fclip_on = 0; }
/* Клипованный путь блита. Габарит и смещения внутри куска — БАЙТЫ, а вся /* Клипованный путь блита. Габарит и смещения внутри куска — БАЙТЫ, а вся
* рабочая переменная — file-scope, НЕ локали. * рабочая переменная — file-scope, НЕ локали.
@@ -447,7 +344,7 @@ static int bc_dx, bc_dy;
static int16_t bc_d; static int16_t bc_d;
static uint8_t bc_w, bc_h, bc_dw, bc_dh, bc_sx, bc_sy; static uint8_t bc_w, bc_h, bc_dw, bc_dh, bc_sx, bc_sy;
static void blit_b_clip(const uint8_t *img, int x, int top, uint8_t w, uint8_t h) void blit_b_clip(const uint8_t *img, int x, int top, uint8_t w, uint8_t h)
{ {
bc_img = img; bc_w = w; bc_h = h; bc_img = img; bc_w = w; bc_h = h;
bc_dx = x; bc_dy = top; bc_dx = x; bc_dy = top;
@@ -653,59 +550,12 @@ void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom)
gfx_w0_unmap(); gfx_w0_unmap();
} }
/* Блит getimage-блоба из ОБЫЧНОЙ памяти (W2), а не из EMM-страницы атласа:
* те же клип полосы у потолка и окно перерисовки, что у pop_blit_b, но без
* atlas_image и без gfx_w0_map/unmap (это ещё ~1 350 такта). Заведено под
* композитный спрайт падающего куска (pop_room.c: три части сложены в один
* блоб, один вызов блита вместо трёх). */
void pop_mem_b(const uint8_t *img, int x, int ybottom)
{
uint8_t w = img[0], h = img[2];
int top;
if (pop_t_fclip_on &&
(x >= pop_t_fclip_x1 || x + 64 <= pop_t_fclip_x0 ||
ybottom + POP_YOFF + 1 <= pop_t_fclip_ly0 ||
ybottom + POP_YOFF - 64 >= pop_t_fclip_ly1))
return;
if (!w || !h) return;
top = ybottom - (int)h + 1 + POP_YOFF;
if (pop_upside) top = FLIP_TOP(top, h);
if (!pop_t_fclip_on && !pop_t_clip_top &&
!(pop_upside && (top < POP_YOFF || top + (int)h > POP_CLIP_BOTTOM)) &&
x >= 0 && top >= 0 && x + (int)w <= 320 && top + (int)h <= 256) {
if (pop_upside) gfx_blit_noclip_vflip(x, top, img);
else gfx_blit_noclip(x, top, img);
pop_cd_touch(x, top, (int)w, (int)h);
} else {
blit_b_clip(img, x, top, w, h);
}
}
void pop_env_b(uint8_t id, int x, int yb) { pop_blit_b(&pop_env[id >> POP_ENV_SHIFT], (uint8_t)(id & POP_ENV_MASK), x, yb); } void pop_env_b(uint8_t id, int x, int yb) { pop_blit_b(&pop_env[id >> POP_ENV_SHIFT], (uint8_t)(id & POP_ENV_MASK), x, yb); }
void pop_wall_b(uint8_t id, int x, int yb) { pop_blit_b(&pop_wall_at, id, x, yb); } void pop_wall_b(uint8_t id, int x, int yb) { pop_blit_b(&pop_wall_at, id, x, yb); }
void pop_fore_b(uint8_t id, int x, int yb) { pop_blit_b(&pop_fore_at, id, x, yb); } void pop_fore_b(uint8_t id, int x, int yb) { pop_blit_b(&pop_fore_at, id, x, yb); }
void pop_pot_b(uint8_t id, int x, int yb) { pop_blit_b(&pop_pot_at, id, x, yb); } void pop_pot_b(uint8_t id, int x, int yb) { pop_blit_b(&pop_pot_at, id, x, yb); }
/* Heal по КОМНАТНОЙ координате (её и знают точечные перерисовки: чомпер,
* пики, loose, ворота, полоса потолка). При перевороте прямоугольник
* обязан отразиться ровно так же, как сам тайл, — иначе heal чистит одно
* место, а рисует в другое: именно так «ломалась анимация чомперов»
* (найдено пользователем на первом же прогоне зеркального фона). */
void pop_bar_black(int x, int top, int w, int h)
{
if (pop_upside) top = FLIP_TOP(top, h);
gfx_set_bank(GFX_BANK_NORMAL);
setfillstyle(SOLID_FILL, BLACK);
bar(x, top, x + w - 1, top + h - 1);
pop_cd_touch(x, top, w, h); /* bar идёт мимо pop_blit_b (DRAW-COST) */
}
void pop_heal_off(int x, int y, int w, int h)
{
int top = y + POP_YOFF;
if (pop_upside) top = FLIP_TOP(top, h);
pop_heal_fast(x, top, w, h);
}
/* ---- Чтение тайла отрисованной комнаты ------------------------------ */ /* ---- Чтение тайла отрисованной комнаты ------------------------------ */
uint8_t pop_tile_code(int row, int col) uint8_t pop_tile_code(int row, int col)
@@ -757,25 +607,6 @@ uint8_t pop_wall_modifier(int row, int col)
return 0; /* SWS */ return 0; /* SWS */
} }
/* ---- Зелье: склянка (tiles_10_potion, seg008 draw_tile_fore) --------- *
* Тип = (modif & 0xF8) >> 3: 0 = пусто (ничего не рисуем), 2..4 — «большая»
* склянка (id 13), остальные — маленькая (id 12). Спрайты из chtab_1 (атлас
* pop_pot), поэтому НЕ из env: раньше рисовали chtab_6 id 12 (res212),
* которого в VDUNGEON нет — оттуда и «мусорный объект». Позиция оригинала:
* xh = fore_x + draw_xh, xl = 6, ybottom = fore_y + draw_main_y.
*
* Здесь, а не в банке: склянку рисует и полная отрисовка тайла (холодная
* половина), и fore-проход поверх персонажа (горячая). */
void pop_potion_flask(int row, int col, uint8_t modif)
{
uint8_t type = (uint8_t)((modif & 0xF8) >> 3);
uint8_t id = 12;
if (type == 0) return;
if (type >= 2 && type < 5) id = 13; /* большая склянка */
if (pop_palace) id += 2; /* seg008:747: во дворце склянки другие */
pop_pot_b(id, (POT_FORE_X + POP_COL_XH[col]) * 8 + 6,
POT_FORE_Y + 63 * row + 62);
}
/* Окно fore-клипа: ставит тот, кто нарисовал персонажа (pop_cdraw, банк 4), /* Окно fore-клипа: ставит тот, кто нарисовал персонажа (pop_cdraw, банк 4),
* читает blit_b. Резидент — чтобы вызов из банка шёл без трамплина. */ * читает blit_b. Резидент — чтобы вызов из банка шёл без трамплина. */
@@ -791,13 +622,3 @@ void pop_fore_set_clip(int x, int y, int w, int h)
} }
} }
/* Ряд 0 комнаты СНИЗУ (fg-коды) — нужен draw_tile_topright/floorright, когда
* тайл нижнего ряда стал видимым (empty после падения loose): «стена вниз» =
* топрайт тайла room_B(0, col-1). 0 — нет комнаты снизу (кромка = стена). */
void pop_room_set_below(const uint8_t *below_row0_fg) { pop_t_below = below_row0_fg; }
/* Ряд 2 (fg-коды + модификаторы) комнаты СВЕРХУ — для полосы кладки у
* потолка (seg008 draw_room: доп. верхний ряд из room_A, draw_tile_aboveroom
* при draw_main_y=-1). 0 — нет комнаты сверху (кромка уровня → пол). */
void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod)
{ pop_t_above = above_row2_fg; pop_t_abovemod = above_row2_mod; }
+6 -6
View File
@@ -93,8 +93,8 @@ extern uint8_t pop_t_fclip_on;
* ВАЖНО: пока окно стоит, pop_blit_b НЕ ставит пометку «фон трогали» (признак * ВАЖНО: пока окно стоит, pop_blit_b НЕ ставит пометку «фон трогали» (признак
* fore-прохода) вызывающий обязан пометить прямоугольник САМ (pop_cd_touch * fore-прохода) вызывающий обязан пометить прямоугольник САМ (pop_cd_touch
* или pop_bar_black, который метит сам). */ * или pop_bar_black, который метит сам). */
void pop_t_win_set(int x, int ytop, int w, int h); void pop_t_win_set(int x, int ytop, int w, int h) __banked;
void pop_t_win_clear(void); void pop_t_win_clear(void) __banked;
/* ---- Таблица тайлов (piece, seg008.c:27) ---------------------------- */ /* ---- Таблица тайлов (piece, seg008.c:27) ---------------------------- */
typedef struct { typedef struct {
@@ -154,7 +154,7 @@ void pop_blit_b(atlas_t *a, uint8_t idx, int x, int ybottom);
/* Блит getimage-блоба из обычной памяти (W2) — без atlas_image и без /* Блит getimage-блоба из обычной памяти (W2) — без atlas_image и без
* маппинга страницы; клип полосы у потолка и окно перерисовки те же, что у * маппинга страницы; клип полосы у потолка и окно перерисовки те же, что у
* pop_blit_b. Для композитных спрайтов, собранных в рантайме. */ * pop_blit_b. Для композитных спрайтов, собранных в рантайме. */
void pop_mem_b(const uint8_t *img, int x, int ybottom); void pop_mem_b(const uint8_t *img, int x, int ybottom) __banked;
void pop_env_b(uint8_t id, int x, int yb); void pop_env_b(uint8_t id, int x, int yb);
void pop_wall_b(uint8_t id, int x, int yb); void pop_wall_b(uint8_t id, int x, int yb);
@@ -162,14 +162,14 @@ void pop_fore_b(uint8_t id, int x, int yb);
void pop_pot_b(uint8_t id, int x, int yb); void pop_pot_b(uint8_t id, int x, int yb);
/* heal в room-space (со сдвигом POP_YOFF) — все heal слоя фона/loose. */ /* heal в room-space (со сдвигом POP_YOFF) — все heal слоя фона/loose. */
void pop_heal_off(int x, int y, int w, int h); void pop_heal_off(int x, int y, int w, int h) __banked;
/* Чёрная плита по ЭКРАННЫМ координатам, с учётом переворота (POP_FLIP_TOP). /* Чёрная плита по ЭКРАННЫМ координатам, с учётом переворота (POP_FLIP_TOP).
* Пишет банком 0x50 и в видео-ОЗУ, и в ОЗУ-копию, иначе heal вернёт * Пишет банком 0x50 и в видео-ОЗУ, и в ОЗУ-копию, иначе heal вернёт
* стёртое. ВСЕ стирания слоя фона обязаны идти через неё, а не через голый * стёртое. ВСЕ стирания слоя фона обязаны идти через неё, а не через голый
* bar: иначе при перевороте плита ложится в неотражённое место (так * bar: иначе при перевороте плита ложится в неотражённое место (так
* ломались решётка и плита-потолок). */ * ломались решётка и плита-потолок). */
void pop_bar_black(int x, int top, int w, int h); void pop_bar_black(int x, int top, int w, int h) __banked;
/* ---- Чтение тайла отрисованной комнаты ------------------------------ */ /* ---- Чтение тайла отрисованной комнаты ------------------------------ */
uint8_t pop_tile_code(int row, int col); uint8_t pop_tile_code(int row, int col);
@@ -183,6 +183,6 @@ uint8_t pop_wall_modifier(int row, int col);
/* Склянка зелья (draw_tile_fore, chtab_1): нужна и полной отрисовке тайла, и /* Склянка зелья (draw_tile_fore, chtab_1): нужна и полной отрисовке тайла, и
* fore-проходу поверх персонажа. */ * fore-проходу поверх персонажа. */
void pop_potion_flask(int row, int col, uint8_t modif); void pop_potion_flask(int row, int col, uint8_t modif) __banked;
#endif #endif
+240
View File
@@ -0,0 +1,240 @@
/*
* pop_tile_cold.c ХОЛОДНАЯ половина общих листьев слоя фона (банк 5).
*
* ЗАЧЕМ РАЗРЕЗ. pop_tile.c был самым крупным жильцом резидента (5 972 Б
* кода), а резидент huge-режима это всё окно 0x4100..0xBB00, и место в нём
* нужно под музыку, заставки и UI. Но целиком pop_tile не уедет: его
* const-таблицы (POP_TILE_DIV/MOD, pop_tile_table, таблицы кадров) читают
* банки 2, 3, 7 и 8, а таблица в чужом банке не видна.
*
* КРИТЕРИЙ ОТБОРА ЗАМЕР, а не догадка. Каждая функция pop_tile была
* посчитана брейкпоинтом-счётчиком в MAME: сколько вызовов в кадре покоя и
* сколько в кадре ПОЛНОЙ перерисовки комнаты (её форсирует чит +/-). Сюда
* переехало только то, у чего пик <= 3 вызовов на кадр и проверено это
* на ДВУХ тайлсетах: подземелье (уровень 1) и дворец (уровень 4), потому
* что pop_mem_b рисует композитный кусок и мог оказаться дворцовым.
* Замер: pop_mem_b 0, pop_cd_hit 0, pop_t_win_set 0..1, pop_heal_off 0..1,
* pop_room_set_above 1, pop_bar_black 3, pop_potion_flask 0..1,
* pop_cd_hit_slot 2..3 при канарейке pop_blit_b 184..214 в том же кадре.
*
* ЦЕНА. Трамплин замерен: 252 такта пролог + 84 эпилог + ~50 на стороне
* вызывающего = ~410 тактов на вызов. На кадр покоя выходит ~1 000 тактов
* (0,2 % работы кадра), на кадр редрава ~3 700 (0,009 растра).
*
* ЧТО ОСТАЛОСЬ В РЕЗИДЕНТЕ и почему: pop_blit_b (184 вызова на редрав) со
* своими статиками blit_b_clip/blit_b_oversize, pop_cd_touch (198),
* pop_tile_code (296), pop_wall_modifier (101), pop_env_b (73),
* pop_tile_mod (70), pop_cd_batch_end (40) и все const-таблицы.
* pop_fore_set_clip оставлен намеренно: 88 Б не стоят отказа от прямого
* вызова из банка 4, ради которого он и заводился.
*
* ВЫЗОВЫ ОТСЮДА В РЕЗИДЕНТ прямые, без трамплина: W1/W2 замаплены всегда.
* Поэтому blit_b_clip (статик горячей половины) просто перестал быть static
* и объявлен в _pop_tile.h, а не переехал следом.
*/
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#include "pop_bg.h" /* POP_YOFF, POP_PLAYFIELD_H */
#include "pop_map.h" /* pop_upside — переворот (зелье инверсии) */
#include "pop_cdraw.h" /* pop_cd, pop_cd_dirty, pop_cd_ymin/ymax */
#include "pop_tile.h"
#include "_pop_draw.h" /* pop_heal_fast */
#include "_pop_tile.h" /* blit_b_clip — из горячей половины */
#define FLIP_TOP(top, h) POP_FLIP_TOP(top, h) /* формула — в pop_bg.h */
/* Смещение склянки внутри тайла (seg008 draw_tile_fore): те же числа, что в
* tile_table горячей половины дублируем, потому что там они локальные
* define'ы, а тащить их в публичный заголовок ради одной функции незачем. */
#define POT_FORE_X 2
#define POT_FORE_Y (-3)
/* Прямоугольник слота против метки — БЕЗ передачи пяти аргументов.
*
* pop_cd_hit принимает (p, x0, y0, x1, y1): три последних идут стеком, и
* функция целиком уезжает в IX-фрейм 45 % её тактов уходит на `-n(ix)`
* (замер asm 2026-08-19). А зовут её из cd_quiet дважды на слот, то есть
* до восьми раз за кадр. Здесь координаты берутся прямо из pop_cd, и
* аргументов остаётся два.
*
* Спрайт и накладной проверяются РАЗДЕЛЬНО объединённый bbox ловит
* касания углом, которых нет (разбор в cd_quiet, pop_cdraw.c). */
static int hit_x, hit_y; /* аргументы hit_rect — file-scope, не стек */
static uint16_t hit_w, hit_h;
static uint8_t hit_p;
static uint8_t hit_rect(void)
{
int8_t c0, c1, c;
uint8_t ya, yb;
int x1 = hit_x + (int)hit_w - 1;
int y1 = hit_y + (int)hit_h - 1;
if (!hit_w || !hit_h) return 0;
if (x1 < 0 || hit_x > 319) return 0;
if (y1 < 0 || hit_y > 255) return 0;
ya = (uint8_t)(hit_y < 0 ? 0 : hit_y);
yb = (uint8_t)(y1 > 255 ? 255 : y1);
c0 = (int8_t)((hit_x < 0 ? 0 : hit_x) >> 5);
c1 = (int8_t)((x1 > 319 ? 319 : x1) >> 5);
for (c = c0; c <= c1; c++)
if (ya <= pop_cd_ymax[hit_p][c] && yb >= pop_cd_ymin[hit_p][c]) return 1;
return 0;
}
uint8_t pop_cd_hit_slot(uint8_t who, uint8_t p) __banked
{
const pop_cdraw_t *s = &pop_cd[who];
if (!(pop_cd_dirty & (1 << p))) return 0;
hit_p = p;
hit_x = s->x[p]; hit_y = s->y[p] + POP_YOFF;
hit_w = s->w[p]; hit_h = s->h[p];
if (hit_rect()) return 1;
if (!s->ovalid[p]) return 0;
hit_x = s->ox[p]; hit_y = s->oy[p] + POP_YOFF;
hit_w = s->ow[p]; hit_h = s->oh[p];
return hit_rect();
}
uint8_t pop_cd_hit(uint8_t p, int x0, int y0, int x1, int y1) __banked
{
int8_t c0, c1, c;
uint8_t ya, yb;
if (!(pop_cd_dirty & (1 << p))) return 0;
if (x1 < 0 || x0 > 319) return 0;
if (x0 < 0) x0 = 0;
if (x1 > 319) x1 = 319;
if (y1 < 0 || y0 > 255) return 0;
ya = (uint8_t)(y0 < 0 ? 0 : y0);
yb = (uint8_t)(y1 > 255 ? 255 : y1);
c0 = (int8_t)(x0 >> 5);
c1 = (int8_t)(x1 >> 5);
/* Пересечение отрезков [ya,yb] и [ymin,ymax] хотя бы в одной колонке.
* Пустая колонка держит ymin = 255, ymax = 0 условие ниже её отсеет
* само, отдельной проверки «пусто» не нужно. */
for (c = c0; c <= c1; c++)
if (ya <= pop_cd_ymax[p][c] && yb >= pop_cd_ymin[p][c]) return 1;
return 0;
}
/* Страница приведена в порядок — снять с неё метку целиком. */
/* Начальное состояние обеих страниц — «ничего не трогали». ЯВНО, а не
* расчётом на обнуление _DATA: пустая колонка обозначается ymin = 255,
* ymax = 0, а нули от crt0 читались бы как «затронута строка 0». */
void pop_cd_init(void) __banked
{
pop_cd_clear(0);
pop_cd_clear(1);
}
void pop_cd_clear(uint8_t p) __banked
{
uint8_t c;
for (c = 0; c < 10; c++) { pop_cd_ymin[p][c] = 255; pop_cd_ymax[p][c] = 0; }
pop_cd_dirty = (uint8_t)(pop_cd_dirty & ~(1 << p));
}
/* Окно под точечную перерисовку/запечку — контракт в шапке pop_tile.h.
* Логическая пара нужна для отсева тайлов (он считается из ряда комнаты),
* экранная для клипа самого блита; при перевороте они разные. */
void pop_t_win_set(int x, int ytop, int w, int h) __banked
{
pop_t_fclip_x0 = x;
pop_t_fclip_x1 = x + w;
pop_t_fclip_ly0 = ytop;
pop_t_fclip_ly1 = ytop + h;
if (pop_upside) {
int t = FLIP_TOP(ytop, h);
pop_t_fclip_y0 = t;
pop_t_fclip_y1 = t + h;
} else {
pop_t_fclip_y0 = ytop;
pop_t_fclip_y1 = ytop + h;
}
pop_t_fclip_on = 1;
}
void pop_t_win_clear(void) __banked
{ pop_t_fclip_on = 0; }
/* Блит getimage-блоба из ОБЫЧНОЙ памяти (W2), а не из EMM-страницы атласа:
* те же клип полосы у потолка и окно перерисовки, что у pop_blit_b, но без
* atlas_image и без gfx_w0_map/unmap (это ещё ~1 350 такта). Заведено под
* композитный спрайт падающего куска (pop_room.c: три части сложены в один
* блоб, один вызов блита вместо трёх). */
void pop_mem_b(const uint8_t *img, int x, int ybottom) __banked
{
uint8_t w = img[0], h = img[2];
int top;
if (pop_t_fclip_on &&
(x >= pop_t_fclip_x1 || x + 64 <= pop_t_fclip_x0 ||
ybottom + POP_YOFF + 1 <= pop_t_fclip_ly0 ||
ybottom + POP_YOFF - 64 >= pop_t_fclip_ly1))
return;
if (!w || !h) return;
top = ybottom - (int)h + 1 + POP_YOFF;
if (pop_upside) top = FLIP_TOP(top, h);
if (!pop_t_fclip_on && !pop_t_clip_top &&
!(pop_upside && (top < POP_YOFF || top + (int)h > POP_CLIP_BOTTOM)) &&
x >= 0 && top >= 0 && x + (int)w <= 320 && top + (int)h <= 256) {
if (pop_upside) gfx_blit_noclip_vflip(x, top, img);
else gfx_blit_noclip(x, top, img);
pop_cd_touch(x, top, (int)w, (int)h);
} else {
blit_b_clip(img, x, top, w, h);
}
}
/* Heal по КОМНАТНОЙ координате (её и знают точечные перерисовки: чомпер,
* пики, loose, ворота, полоса потолка). При перевороте прямоугольник
* обязан отразиться ровно так же, как сам тайл, иначе heal чистит одно
* место, а рисует в другое: именно так «ломалась анимация чомперов»
* (найдено пользователем на первом же прогоне зеркального фона). */
void pop_bar_black(int x, int top, int w, int h) __banked
{
if (pop_upside) top = FLIP_TOP(top, h);
gfx_set_bank(GFX_BANK_NORMAL);
setfillstyle(SOLID_FILL, BLACK);
bar(x, top, x + w - 1, top + h - 1);
pop_cd_touch(x, top, w, h); /* bar идёт мимо pop_blit_b (DRAW-COST) */
}
void pop_heal_off(int x, int y, int w, int h) __banked
{
int top = y + POP_YOFF;
if (pop_upside) top = FLIP_TOP(top, h);
pop_heal_fast(x, top, w, h);
}
/* ---- Зелье: склянка (tiles_10_potion, seg008 draw_tile_fore) --------- *
* Тип = (modif & 0xF8) >> 3: 0 = пусто (ничего не рисуем), 2..4 «большая»
* склянка (id 13), остальные маленькая (id 12). Спрайты из chtab_1 (атлас
* pop_pot), поэтому НЕ из env: раньше рисовали chtab_6 id 12 (res212),
* которого в VDUNGEON нет оттуда и «мусорный объект». Позиция оригинала:
* xh = fore_x + draw_xh, xl = 6, ybottom = fore_y + draw_main_y.
*
* Здесь, а не в банке: склянку рисует и полная отрисовка тайла (холодная
* половина), и fore-проход поверх персонажа (горячая). */
void pop_potion_flask(int row, int col, uint8_t modif) __banked
{
uint8_t type = (uint8_t)((modif & 0xF8) >> 3);
uint8_t id = 12;
if (type == 0) return;
if (type >= 2 && type < 5) id = 13; /* большая склянка */
if (pop_palace) id += 2; /* seg008:747: во дворце склянки другие */
pop_pot_b(id, (POT_FORE_X + POP_COL_XH[col]) * 8 + 6,
POT_FORE_Y + 63 * row + 62);
}
/* Ряд 0 комнаты СНИЗУ (fg-коды) — нужен draw_tile_topright/floorright, когда
* тайл нижнего ряда стал видимым (empty после падения loose): «стена вниз» =
* топрайт тайла room_B(0, col-1). 0 нет комнаты снизу (кромка = стена). */
void pop_room_set_below(const uint8_t *below_row0_fg) __banked
{ pop_t_below = below_row0_fg; }
/* Ряд 2 (fg-коды + модификаторы) комнаты СВЕРХУ — для полосы кладки у
* потолка (seg008 draw_room: доп. верхний ряд из room_A, draw_tile_aboveroom
* при draw_main_y=-1). 0 нет комнаты сверху (кромка уровня пол). */
void pop_room_set_above(const uint8_t *above_row2_fg, const uint8_t *above_row2_mod) __banked
{ pop_t_above = above_row2_fg; pop_t_abovemod = above_row2_mod; }
+22 -1
View File
@@ -651,13 +651,34 @@ uint8_t pop_trob_room_active(uint8_t room) __banked
* анимацию или нет). */ * анимацию или нет). */
#define TORCH_ANIM_DIV 1 #define TORCH_ANIM_DIV 1
#if TORCH_ANIM_DIV > 1
static uint8_t torch_tick; static uint8_t torch_tick;
#if TORCH_ANIM_DIV > 1
#define TORCH_ANIM_NOW ((uint8_t)(torch_tick % TORCH_ANIM_DIV) == 0) #define TORCH_ANIM_NOW ((uint8_t)(torch_tick % TORCH_ANIM_DIV) == 0)
#else #else
#define TORCH_ANIM_NOW 1 #define TORCH_ANIM_NOW 1
#endif #endif
void pop_trob_qsave(pop_qs_io_t *io) __banked
{
uint8_t i;
pop_qs_bytes(io, room_modif, sizeof(room_modif));
pop_qs_bytes(io, room_seen, sizeof(room_seen));
pop_qs_u8(io, &trobs_count);
for (i = 0; i < TROBS_MAX; i++) {
pop_qs_u8(io, &trobs[i].room);
pop_qs_u8(io, &trobs[i].tilepos);
pop_qs_i8(io, &trobs[i].type);
}
pop_qs_u16(io, &trob_seed.lo);
pop_qs_u16(io, &trob_seed.hi);
pop_qs_u8(io, &torch_tick);
if (io->reading && io->apply) {
if (trobs_count > TROBS_MAX) { io->failed = 1; return; }
for (i = 0; i < POP_ROOMTILES; i++) trob_drawn[i] = 0;
pop_redraw_reset();
}
}
void pop_process_trobs(uint8_t cur_room) __banked void pop_process_trobs(uint8_t cur_room) __banked
{ {
uint8_t i, w = 0; uint8_t i, w = 0;
+3
View File
@@ -18,6 +18,8 @@
#ifndef POP_TROB_H #ifndef POP_TROB_H
#define POP_TROB_H #define POP_TROB_H
#include "pop_qsave.h"
#include <stdint.h> #include <stdint.h>
#define POP_NROOMS 24 #define POP_NROOMS 24
@@ -26,6 +28,7 @@
/* Сброс всего состояния (новый уровень / рестарт): забыть все комнаты и /* Сброс всего состояния (новый уровень / рестарт): забыть все комнаты и
* очистить список trob. */ * очистить список trob. */
void pop_trob_reset(void) __banked; void pop_trob_reset(void) __banked;
void pop_trob_qsave(pop_qs_io_t *io) __banked;
/* Указатель на persistent modifier-массив комнаты room (1..24), 30 байт. /* Указатель на persistent modifier-массив комнаты room (1..24), 30 байт.
* При ПЕРВОМ обращении лениво инициализируется из backtable уровня * При ПЕРВОМ обращении лениво инициализируется из backtable уровня
+21 -10
View File
@@ -26,6 +26,7 @@
#include "pop_vflip.h" /* зеркальный набор персонажей (зелье инверсии) */ #include "pop_vflip.h" /* зеркальный набор персонажей (зелье инверсии) */
#include "pop_pace.h" /* фиксированный логический кадр по лучу */ #include "pop_pace.h" /* фиксированный логический кадр по лучу */
#include "roomtest_cold.h" /* холодная половина цикла — в банке */ #include "roomtest_cold.h" /* холодная половина цикла — в банке */
#include "pop_qsave.h"
__sfr __at (0xFE) io_border; __sfr __at (0xFE) io_border;
@@ -62,8 +63,8 @@ __sfr __at (0xFE) io_border;
#define PROF(n) ((void)0) #define PROF(n) ((void)0)
#endif #endif
/* Сколько банков грузит crt0_banked (читает его стартовый код). /* Раскладка банков; их число crt0_banked получает автоматически из
* 1 = guards (ИИ/боёвка), 2 = pop_bg (ГОРЯЧАЯ половина фона: fore-проход, * --bank флагов: 1 = guards (ИИ/боёвка), 2 = pop_bg (ГОРЯЧАЯ половина фона: fore-проход,
* оверлеи, кладка), 3 = pop_map (коллизия/физика), 4 = pop_cdraw * оверлеи, кладка), 3 = pop_map (коллизия/физика), 4 = pop_cdraw
* (отрисовка персонажей + полоса HP), 5 = pop_ctrl (диспетчер управления), * (отрисовка персонажей + полоса HP), 5 = pop_ctrl (диспетчер управления),
* 6 = pop_trob (ловушки/анимация тайлов), 7 = pop_room (ХОЛОДНАЯ половина * 6 = pop_trob (ловушки/анимация тайлов), 7 = pop_room (ХОЛОДНАЯ половина
@@ -72,14 +73,7 @@ __sfr __at (0xFE) io_border;
* отладочная навигация см. roomtest_cold.h). * отладочная навигация см. roomtest_cold.h).
* Отрисовка персонажей и холодная половина фона вынесены из банка 2 * Отрисовка персонажей и холодная половина фона вынесены из банка 2
* отдельно: тот дважды подходил к 16 КБ вплотную, а расти ему ещё есть * отдельно: тот дважды подходил к 16 КБ вплотную, а расти ему ещё есть
* куда тайлы поздних уровней, чомперы, зеркало. * куда тайлы поздних уровней, чомперы, зеркало. */
*
* ДЕРЖАТЬ В СОГЛАСИИ с числом --bank в Makefile. Забыть это не ошибка
* сборки, а зависание: crt0 выделит меньше страниц, _bank_pages[N] так и
* останется нулём, и трамплин прыгнет по 0xC000 в мусор (см. шапку
* runtime/bank.s). Проверено 2026-08-05: с n_banks=4 и пятым банком игра
* доходила до конца загрузки ресурсов и вставала намертво в DSS. */
const uint8_t n_banks = 8;
/* Режим читов (pop_cheat.h): на время разработки включаем в main. */ /* Режим читов (pop_cheat.h): на время разработки включаем в main. */
uint8_t pop_cheats; uint8_t pop_cheats;
@@ -115,6 +109,8 @@ static void kbd_idle(void)
#define SEQ_STAND 2 #define SEQ_STAND 2
#define ROOM_1 1 #define ROOM_1 1
#define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */ #define LINK_DOWN 3 /* сторона links: 0=L,1=R,2=U,3=D */
#define KBD_QUICKSAVE 0x0B /* F6, PS/2 set 2 */
#define KBD_QUICKLOAD 0x01 /* F9, PS/2 set 2 */
#define RESPAWN_DELAY 400 /* авто-респавн (фолбэк), если не нажали ↑ */ #define RESPAWN_DELAY 400 /* авто-респавн (фолбэк), если не нажали ↑ */
#define RESPAWN_SETTLE 20 /* тиков до приёма ↑ (труп успел лечь) */ #define RESPAWN_SETTLE 20 /* тиков до приёма ↑ (труп успел лечь) */
@@ -250,6 +246,8 @@ int main(void)
* железо), pop_pace_* молча откатываются на прежние gfx_wait_vsync. */ * железо), pop_pace_* молча откатываются на прежние gfx_wait_vsync. */
pop_pace_arm(); pop_pace_arm();
{
uint8_t qs_save_prev = 0, qs_load_prev = 0;
while (!kbd_raw_down(KBD_ESC)) { while (!kbd_raw_down(KBD_ESC)) {
/* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один /* SPACE — тумблер дабл-буфера (edge, чтоб одно нажатие = один
* переключ). Выключение возвращает показ на страницу 0. */ * переключ). Выключение возвращает показ на страницу 0. */
@@ -286,6 +284,18 @@ int main(void)
/* Читы — В БАНКЕ (roomtest_cold.c): они дёргаются раз в кадр, а места /* Читы — В БАНКЕ (roomtest_cold.c): они дёргаются раз в кадр, а места
* в резиденте W1/W2 нет (см. TASKS_OPEN, MEM). */ * в резиденте W1/W2 нет (см. TASKS_OPEN, MEM). */
pop_cheat_tick(); pop_cheat_tick();
/* F6/F9 только ставят запрос; файловая операция выполняется здесь,
* на границе кадров вне окон Char и до heal/tick/draw. */
{
uint8_t s = kbd_raw_down(KBD_QUICKSAVE);
uint8_t l = kbd_raw_down(KBD_QUICKLOAD);
int8_t qrc;
if (s && !qs_save_prev) pop_qsave_request_save();
if (l && !qs_load_prev) pop_qsave_request_load();
qs_save_prev = s; qs_load_prev = l;
qrc = pop_qsave_process();
if (qrc > 0) dead_frames = 0;
}
if (frozen) { if (frozen) {
PROF(0); /* чёрный: ждём vsync */ PROF(0); /* чёрный: ждём vsync */
/* Заморозка — тоже полноценный логический кадр: иначе якорь /* Заморозка — тоже полноценный логический кадр: иначе якорь
@@ -733,6 +743,7 @@ int main(void)
front = back; front = back;
} }
} }
}
pop_shutdown(); /* снять хук, закрыть ввод/графику, отдать ресурсы */ pop_shutdown(); /* снять хук, закрыть ввод/графику, отдать ресурсы */
return 0; return 0;
+63
View File
@@ -602,6 +602,69 @@ void enter_room_side(uint8_t room, uint8_t side) __banked
* Страж такой смены не «видит» он остаётся в своей комнате. */ * Страж такой смены не «видит» он остаётся в своей комнате. */
void enter_room(uint8_t room) __banked { enter_room_side(room, 0xFF); } void enter_room(uint8_t room) __banked { enter_room_side(room, 0xFF); }
void pop_qsave_restore_room(uint8_t room) __banked
{
uint8_t i, pg, *room_mod;
gfx_rect_t full;
uint8_t lft, rgt, up;
pop_room_load(room, room_fg, room_bg, lcol_fg, lcol_bg,
rcol_fg, rcol_bg, below_fg);
pop_map_set(room_fg);
lft = pop_room_link(room, 0); rgt = pop_room_link(room, 1);
up = pop_room_link(room, 2);
pop_map_set_edges(lft, rgt, up, pop_room_link(room, 3));
pop_room_set_below(below_fg);
if (up) {
uint8_t *um = pop_trob_modif(up);
pop_level_access_begin();
for (i = 0; i < 10; i++)
above_fg[i] = pop_level_tile_raw(up, (uint8_t)(20 + i));
pop_level_access_end();
for (i = 0; i < 10; i++) above_mod[i] = um[20 + i];
pop_room_set_above(above_fg, above_mod);
pop_map_set_above(above_fg);
} else {
pop_room_set_above(0, 0); pop_map_set_above(0);
}
pop_map_set_room(room);
pop_loose_reset();
pop_trob_room_changed();
pop_bake_forget();
pop_loose_mob_room_changed();
room_mod = pop_trob_modif(room);
if (lft) {
uint8_t *m = pop_trob_modif(lft);
for (i = 0; i < 3; i++) {
lcol_bg[i] = m[9 + i * 10];
seam_sig[i] = seam_row_sig(m, lcol_fg, i);
}
}
if (rgt) {
uint8_t *m = pop_trob_modif(rgt);
rcol_bg[0] = m[0]; rcol_bg[1] = m[10]; rcol_bg[2] = m[20];
}
seam_redraw = seam_rows = 0;
pg = (uint8_t)(gfx_get_visible_page() ^ 1);
gfx_set_draw_page(pg);
setfillstyle(SOLID_FILL, BLACK); bar(0, 0, 319, 255);
pop_room_draw(room, room_fg, room_mod, lcol_fg, lcol_bg);
gfx_set_visible_page(pg);
gfx_set_draw_page((uint8_t)(pg ^ 1));
full.x = 0; full.y = 0; full.w = 320; full.h = 256;
gfx_copy_page(&full, GFX_COPY_DIRECT);
cur_room = room; kid_room = Kid.room;
/* Обе страницы теперь содержат только свежий фон. Старые габариты
* персонажей относятся к кадру до QuickLoad и не должны запускать heal
* поверх восстановленной комнаты. */
pop_cd[POP_CH_KID].valid[0] = pop_cd[POP_CH_KID].valid[1] = 0;
pop_cd[POP_CH_KID].ovalid[0] = pop_cd[POP_CH_KID].ovalid[1] = 0;
pop_cd[POP_CH_OPP].valid[0] = pop_cd[POP_CH_OPP].valid[1] = 0;
pop_cd[POP_CH_OPP].ovalid[0] = pop_cd[POP_CH_OPP].ovalid[1] = 0;
pop_cd_init();
pop_hp_invalidate();
}
/* start_level (seg003:0055) + do_startpos (seg003:01A3) + set_start_pos /* start_level (seg003:0055) + do_startpos (seg003:01A3) + set_start_pos
* (seg003:028A) поставить Кида в стартовую позицию УРОВНЯ. Один вход и * (seg003:028A) поставить Кида в стартовую позицию УРОВНЯ. Один вход и
* для запуска, и для рестарта после смерти / выпадения из уровня: в * для запуска, и для рестарта после смерти / выпадения из уровня: в
@@ -47,6 +47,9 @@ extern uint8_t pop_checkpoint; /* checkpoint (seg000:270) — ставит у
* Обе в банке 8: зовутся раз на комнату, а кода там на полтора килобайта. */ * Обе в банке 8: зовутся раз на комнату, а кода там на полтора килобайта. */
void enter_room_side(uint8_t room, uint8_t side) __banked; void enter_room_side(uint8_t room, uint8_t side) __banked;
void enter_room(uint8_t room) __banked; void enter_room(uint8_t room) __banked;
/* Пересобрать только производные кэши и обе страницы после QuickLoad.
* Игровые таймеры, trobs, mobs и персонажей не изменяет. */
void pop_qsave_restore_room(uint8_t room) __banked;
/* ---- холодная половина ----------------------------------------------- */ /* ---- холодная половина ----------------------------------------------- */
+30 -2
View File
@@ -27,6 +27,17 @@
#include <sprite.h> #include <sprite.h>
#include <sprinter.h> #include <sprinter.h>
#include <sprinter_mem.h> #include <sprinter_mem.h>
#include "../pop_qsave.h"
/* Сериализаторы приватного состояния входят в те же крупные модули, что и
* физика/ИИ. Сами QuickSave-тесты работают на target с EMM; обычным
* ucsim-сценам нужны только символы, и вызывать их они не должны. */
void pop_qs_bytes(pop_qs_io_t *io, void *p, uint16_t n)
{ (void)io; (void)p; (void)n; }
void pop_qs_u8(pop_qs_io_t *io, uint8_t *p) { (void)io; (void)p; }
void pop_qs_i8(pop_qs_io_t *io, int8_t *p) { (void)io; (void)p; }
void pop_qs_u16(pop_qs_io_t *io, uint16_t *p) { (void)io; (void)p; }
void pop_qs_i16(pop_qs_io_t *io, int *p) { (void)io; (void)p; }
#include "pop_kid.h" #include "pop_kid.h"
#include "pop_guard.h" #include "pop_guard.h"
@@ -35,6 +46,8 @@
#include "pop_trob.h" #include "pop_trob.h"
#include "pop_level.h" #include "pop_level.h"
#include "pop_redraw.h" #include "pop_redraw.h"
#include "pop_pace.h"
#include "pop_sfx.h"
#include "_pop_draw.h" #include "_pop_draw.h"
#include "pop_cdraw.h" #include "pop_cdraw.h"
#include "stubs.h" #include "stubs.h"
@@ -49,6 +62,7 @@ void _gfx_w0_isr(void) { }
void gfx_w0_map(uint8_t page) { (void)page; } void gfx_w0_map(uint8_t page) { (void)page; }
void gfx_w0_unmap(void) { } void gfx_w0_unmap(void) { }
void gfx_w0_page_prepare(uint8_t page) { (void)page; }
void gfx_blit_cols(int x, int y, const void *img, uint8_t flip) void gfx_blit_cols(int x, int y, const void *img, uint8_t flip)
{ (void)x; (void)y; (void)img; (void)flip; } { (void)x; (void)y; (void)img; (void)flip; }
@@ -95,6 +109,12 @@ int read(int fd, void *b, int n)
int close(int fd) { (void)fd; return 0; } int close(int fd) { (void)fd; return 0; }
int bank_load_file(uint8_t page, uint16_t off, const char *path, uint16_t max)
{
(void)page; (void)off; (void)path;
return kid_blob_size <= max ? (int)kid_blob_size : (int)max;
}
/* ---- клавиатура: управляемая тестом --------------------------------- */ /* ---- клавиатура: управляемая тестом --------------------------------- */
uint16_t tk_keys[TK_KEYS_MAX]; uint16_t tk_keys[TK_KEYS_MAX];
@@ -124,6 +144,13 @@ void tk_key_down(uint16_t code)
if (tk_keys[i] == 0) { tk_keys[i] = code; return; } if (tk_keys[i] == 0) { tk_keys[i] = code; return; }
} }
/* Темп кадра и звук относятся к окружению реальной машины. Физика и
* управление только оставляют выборки луча и заявки эффектов; host-тесты
* проверяют их игровую логику, поэтому здесь достаточно no-op контрактов. */
void pop_beam_sample(void) __preserves_regs(b, c, d, e) { }
void pop_sfx_play(uint8_t id) { (void)id; }
void pop_sfx_stop(void) { }
/* ---- 2. соседние модули движка -------------------------------------- */ /* ---- 2. соседние модули движка -------------------------------------- */
/* room_modif текущей комнаты: пики (счётчик фазы), ворота (высота), /* room_modif текущей комнаты: пики (счётчик фазы), ворота (высота),
@@ -167,8 +194,9 @@ void pop_start_chompers(uint8_t room, int8_t row) __banked
{ (void)room; (void)row; } { (void)room; (void)row; }
void pop_trigger_button(uint8_t room, uint8_t tilepos, void pop_trigger_button(uint8_t room, uint8_t tilepos,
uint8_t button_type, uint8_t modifier) __banked uint8_t button_type, uint8_t modifier,
{ (void)modifier; log_put(TK_BUTTON, room, tilepos, button_type); } uint8_t snd) __banked
{ (void)modifier; (void)snd; log_put(TK_BUTTON, room, tilepos, button_type); }
void pop_set_redraw(uint8_t tilepos, uint8_t kind, uint8_t pages) __banked void pop_set_redraw(uint8_t tilepos, uint8_t kind, uint8_t pages) __banked
{ log_put(TK_REDRAW, tilepos, kind, pages); } { log_put(TK_REDRAW, tilepos, kind, pages); }
@@ -84,19 +84,21 @@ TC_TEST(char_take_hp_goes_to_active)
pop_saveshad(); pop_saveshad();
} }
/* Чит бессмертия — только Киду: страж обязан гибнуть и с ним (иначе /* Уровень 2 чита гасит только мелкий урон Киду. Мгновенная смерть
* отладочный режим ломает боёвку). */ * (count=100) проходит всегда, а на стража чит не действует вовсе. */
TC_TEST(char_immortal_covers_kid_only) TC_TEST(char_immortal_level2_covers_kid_only)
{ {
sc_room(room_flat, 1); sc_room(room_flat, 1);
sc_kid_at(3, 1, -1); sc_kid_at(3, 1, -1);
sc_guard_at(CHARID_2_GUARD, 6, 1, -1, 3); sc_guard_at(CHARID_2_GUARD, 6, 1, -1, 3);
pop_immortal = 1; pop_immortal = 2;
pop_loadkid(); pop_loadkid();
hitp_delta = 0; hitp_delta = 0;
tc_check(pop_take_hp(100), 0, "kid: бессмертен", __LINE__); tc_check(pop_take_hp(1), 0, "kid: мелкий урон погашен", __LINE__);
tc_check(hitp_delta, 0, "kid: HP не тронуто", __LINE__); tc_check(hitp_delta, 0, "kid: HP не тронуто", __LINE__);
tc_check(pop_take_hp(100), 1, "kid: мгновенная смерть проходит", __LINE__);
tc_check(hitp_delta, -3, "kid: смертельная дельта", __LINE__);
pop_savekid(); pop_savekid();
pop_loadshad(); pop_loadshad();
@@ -497,7 +499,7 @@ void main(void)
{ {
sc_init(); sc_init();
TC_RUN(char_take_hp_goes_to_active); TC_RUN(char_take_hp_goes_to_active);
TC_RUN(char_immortal_covers_kid_only); TC_RUN(char_immortal_level2_covers_kid_only);
TC_RUN(char_delta_hp_applies_per_character); TC_RUN(char_delta_hp_applies_per_character);
TC_RUN(char_kid_survives_two_row_fall); TC_RUN(char_kid_survives_two_row_fall);
TC_RUN(char_guard_dies_from_two_row_fall); TC_RUN(char_guard_dies_from_two_row_fall);
+37 -10
View File
@@ -163,6 +163,34 @@ done
[[ -z "$OUT" ]] && { echo "sprinter-cc: -o NAME is required" >&2; exit 1; } [[ -z "$OUT" ]] && { echo "sprinter-cc: -o NAME is required" >&2; exit 1; }
[[ ${#SOURCES[@]} -eq 0 ]] && { echo "sprinter-cc: no input files" >&2; exit 1; } [[ ${#SOURCES[@]} -eq 0 ]] && { echo "sprinter-cc: no input files" >&2; exit 1; }
# ------- Validate bank layout and derive n_banks ----------------------------
# crt0_banked loads 16 KB images consecutively and fills _bank_pages[1..N],
# therefore N is the highest bank number and the numbering must have no gaps.
# Several source modules may share one bank, so counting --bank options would
# be incorrect.
BANK_MAX=0
BANK_SEEN=()
for spec in "${BANK_SPECS[@]}"; do
if [[ ! "$spec" =~ ^([0-9]+)=(.+)$ ]]; then
echo "sprinter-cc: bad --bank specification: '$spec' (expected N=FILE.c)" >&2
exit 1
fi
bank_n="${BASH_REMATCH[1]}"
bank_n=$((10#$bank_n))
if (( bank_n < 1 || bank_n > 15 )); then
echo "sprinter-cc: bank number must be in range 1..15: '$spec'" >&2
exit 1
fi
BANK_SEEN[$bank_n]=1
(( bank_n > BANK_MAX )) && BANK_MAX=$bank_n
done
for ((bank_n = 1; bank_n <= BANK_MAX; bank_n++)); do
if [[ -z "${BANK_SEEN[$bank_n]:-}" ]]; then
echo "sprinter-cc: bank $bank_n is missing; banks must be consecutive from 1 to $BANK_MAX" >&2
exit 1
fi
done
# ------- --w3: резидентный код окна W3 (подход A) ---------------------------- # ------- --w3: резидентный код окна W3 (подход A) ----------------------------
# W3-модули едут в область W3CODE @0xC000 и вызываются напрямую (без # W3-модули едут в область W3CODE @0xC000 и вызываются напрямую (без
# трамплинов). Работает поверх small-лейаута: main-код с 0x4100 (W1/W2), # трамплинов). Работает поверх small-лейаута: main-код с 0x4100 (W1/W2),
@@ -399,8 +427,8 @@ fi
# 3. bank infrastructure — required whenever crt0_banked is in play. # 3. bank infrastructure — required whenever crt0_banked is in play.
# Always assemble bank.s for _bank_pages + the bcall/bjump trampolines. # Always assemble bank.s for _bank_pages + the bcall/bjump trampolines.
# If the user did not pass any --bank, generate a tiny stub providing # Generate n_banks from the validated --bank layout. crt0_banked.s imports
# n_banks = 0 (crt0_banked.s imports this symbol unconditionally). # this symbol unconditionally, including banked memory modes with zero banks.
# In BIG mode banks live in W1 at low16 = 0x4000; in HUGE / default # In BIG mode banks live in W1 at low16 = 0x4000; in HUGE / default
# mode they live in W3 at low16 = 0xC000. # mode they live in W3 at low16 = 0xC000.
BANK_RELS=() BANK_RELS=()
@@ -416,14 +444,13 @@ if [[ "$CRT0_TYPE" == "banked" ]]; then
asm_runtime "$RUNTIME/bank.s" "$BANK_TRAMP_REL" asm_runtime "$RUNTIME/bank.s" "$BANK_TRAMP_REL"
BANK_RELS+=("$BANK_TRAMP_REL") BANK_RELS+=("$BANK_TRAMP_REL")
if [[ ${#BANK_SPECS[@]} -eq 0 ]]; then BANK_COUNT_C="$WORK/_n_banks_auto.c"
# User has no banks — supply n_banks=0 so crt0_banked links. BANK_COUNT_REL="$WORK/_n_banks_auto.rel"
STUB_C="$WORK/_n_banks_stub.c" echo "const unsigned char n_banks = $BANK_MAX;" > "$BANK_COUNT_C"
STUB_REL="$WORK/_n_banks_stub.rel" run "$SDCC" "${CC_FLAGS[@]}" -c -o "$BANK_COUNT_REL" "$BANK_COUNT_C"
echo "const unsigned char n_banks = 0;" > "$STUB_C" BANK_RELS+=("$BANK_COUNT_REL")
run "$SDCC" "${CC_FLAGS[@]}" -c -o "$STUB_REL" "$STUB_C"
BANK_RELS+=("$STUB_REL") if [[ ${#BANK_SPECS[@]} -gt 0 ]]; then
else
for spec in "${BANK_SPECS[@]}"; do for spec in "${BANK_SPECS[@]}"; do
bank_n="${spec%%=*}" bank_n="${spec%%=*}"
bank_src="${spec#*=}" bank_src="${spec#*=}"
+18 -20
View File
@@ -5,6 +5,17 @@
## Ближайшее ## Ближайшее
- [ ] **Перевести новые EMM/W0-функции libc/libbgi на Z80 asm:**
`bank_load_file()`, `bank_save_file()` и `gfx_w0_page_prepare()`.
Сейчас универсальная C-обвязка (четыре аргумента, IX-frame, проверки
диапазона и сохранение временных значений) съедает часть выигрыша от
унификации загрузчиков PoP. Сделать компактные `__naked`-реализации
по образцу остальных низкоуровневых функций библиотек, строго сохранив
`__sdcccall(1)`, IX, W3 до возврата и семантику `-1 + errno`; fast-
вариант может вырезать проверки диапазона, safe обязан их оставить.
После правки: `make -C libc`, `make -C libbgi`, `make size-check` и
MAME-проверка Atlas/Kid/Level/Sound + QuickSave/QuickLoad.
- [ ] **Модульные тесты libc и libbgi под ucsim_z80** — план: - [ ] **Модульные тесты libc и libbgi под ucsim_z80** — план:
`docs/host-tests-plan.md`. Обвязка уже готова и обкатана `docs/host-tests-plan.md`. Обвязка уже готова и обкатана
(`testkit/`, первый потребитель — PoP/roomtest). Не срочно, но (`testkit/`, первый потребитель — PoP/roomtest). Не срочно, но
@@ -61,12 +72,10 @@
## Auto-banking (memory/banking_roadmap.md) ## Auto-banking (memory/banking_roadmap.md)
- [ ] **Сверять `n_banks` с реальным числом банков — ошибкой сборки.** - [x] **Генерировать `n_banks` из реальной раскладки банков.**
Сейчас число банков задаётся ДВАЖДЫ и вручную: флагами `--bank N=…` `sprinter-cc` вычисляет максимальный N из `--bank N=…`, проверяет диапазон
у `sprinter-cc` и константой `const uint8_t n_banks = N;` в 1..15 и отсутствие дыр, затем линкует сгенерированный `_n_banks`.
приложении (её читает `crt0_banked` при старте, чтобы выделить Раньше число задавалось ещё и вручную в приложении; рассогласование давало зависание в
EMM-страницы и загрузить в них образы банков). Рассогласование не
даёт ни ошибки компиляции, ни ошибки линковки — только зависание в
рантайме: `_bank_pages[N]` остаётся нулём, трамплин отображает в рантайме: `_bank_pages[N]` остаётся нулём, трамплин отображает в
окно страницу 0 и прыгает по 0xC000 в мусор (предупреждение об этом окно страницу 0 и прыгает по 0xC000 в мусор (предупреждение об этом
уже есть в шапке `runtime/bank.s`). уже есть в шапке `runtime/bank.s`).
@@ -75,20 +84,9 @@
загрузки ресурсов и вставала намертво в дисковом коде DSS. Диагноз загрузки ресурсов и вставала намертво в дисковом коде DSS. Диагноз
занял заметно больше, чем сама правка: симптом (зависание в чужом занял заметно больше, чем сама правка: симптом (зависание в чужом
коде) никак не указывает на причину. коде) никак не указывает на причину.
Сверять надо не количество, а **максимальный индекс** банка: `--bank` Берётся не число опций (в одном банке может быть несколько модулей), а
нумеруются с 1 и в принципе могут идти с дырами, а `crt0_banked` **максимальный индекс**. Дыры запрещены, поскольку `crt0_banked` и `mkexe`
заполняет `_bank_pages[1..n_banks]` подряд. обрабатывают `_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`) — когда Phase 1 — file-level bin-packing (`toolchain/auto_bank.py`) — когда
проект перерастёт ~30 KB кода: парсинг размеров из .rel/.map, проект перерастёт ~30 KB кода: парсинг размеров из .rel/.map,
+12 -5
View File
@@ -262,11 +262,14 @@ tests/bgi_img.
Скролл региона из НЕактивной страницы в активную (<sprite.h>): Скролл региона из НЕактивной страницы в активную (<sprite.h>):
`gfx_scroll_h(area, dx, dirty)` — горизонтальный (dx>0 = вправо; быстрый `gfx_scroll_h(area, dx, dirty)` — горизонтальный (dx>0 = вправо; быстрый
построчный accel-скролл без страйдов, DI-банды по 16 строк), построчный accel-скролл без страйдов, DI-банды по 16 строк),
`gfx_scroll_v(area, dy, dirty)` — вертикальный (dy>0 = вниз; через `gfx_scroll_v(area, dy, dirty)` — вертикальный (dy>0 = вниз; прямой
строку-буфер на стеке, ПОКА не оптимален — Port_Y один на burst, разный колоночный accel-проход: STOP между чтением и записью позволяет безопасно
read/write Y невозможен без ломающего accel OUT). Копия = скролл + heal сменить Port_Y, промежуточный RAM-буфер не нужен). Копия = скролл + heal
цели (банк 0x50). Открывшуюся полосу |d| не заполняют — возвращают в цели (банк 0x50). Открывшуюся полосу |d| не заполняют — возвращают в
*dirty (NULL = не нужно). Пример: examples/scroll. *dirty (NULL = не нужно). Пример: examples/scroll.
Для произвольных EMM-данных, которые затем маппятся в W0,
`gfx_w0_page_prepare(page)` устанавливает IRQ/NMI-стабы в `0x38/0x66` и
запоминает штатную DSS-страницу. `atlas_load()` вызывает её автоматически.
Стиль линий (Ф2d): `setlinestyle(style,upattern,thick)`/`getlinesettings` Стиль линий (Ф2d): `setlinestyle(style,upattern,thick)`/`getlinesettings`
(SOLID/DOTTED/CENTER/DASHED/USERBIT + NORM/THICK) — на line/rectangle/ (SOLID/DOTTED/CENTER/DASHED/USERBIT + NORM/THICK) — на line/rectangle/
drawpoly. drawpoly.
@@ -395,7 +398,8 @@ applications/PoP/docs/PORT_PLAN.md §2 — пользователь подтве
| Сигнатура | Описание | | Сигнатура | Описание |
|---|---| |---|---|
| `int cbl_open(freq_code, fmt, pump_mode, underrun_mode, fill)` | включить CBL (`0x90\|fmt\|freq`), зарегистрировать callback; 0 / -1+errno (EBUSY повтор, EINVAL — freq/fmt/pump_mode/underrun_mode плохие или OTIR+16-бит, ENOMEM — буфер тишины) | | `int cbl_open(freq_code, fmt, pump_mode, fill)` | включить CBL (`0x90\|fmt\|freq`), зарегистрировать callback; недолив — забота приложения; **malloc не тянет**; 0 / -1+errno (EBUSY повтор, EINVAL — freq/fmt/pump_mode плохие или OTIR+16-бит) |
| `int cbl_open_silence(freq_code, fmt, pump_mode, fill)` | то же, но тишину при недоливе шлёт libc из своего буфера; ТОЛЬКО этот вариант тянет malloc (+ENOMEM). Буфер не освобождается — живёт до выхода |
| `void cbl_close(void)` | выключить CBL, снять хук, освободить буфер тишины (если был); висит на atexit | | `void cbl_close(void)` | выключить CBL, снять хук, освободить буфер тишины (если был); висит на atexit |
| `void cbl_push_otir(const void *src, uint16_t n)` | пропихнуть n байт через `otir` в порт 0x4F; звать ИЗ fill() | | `void cbl_push_otir(const void *src, uint16_t n)` | пропихнуть n байт через `otir` в порт 0x4F; звать ИЗ fill() |
| `void cbl_push_accel(const void *src, uint16_t n)` | то же акселератором (страница EMM 0xFD@0xC000); единственный путь для 16-бит | | `void cbl_push_accel(const void *src, uint16_t n)` | то же акселератором (страница EMM 0xFD@0xC000); единственный путь для 16-бит |
@@ -475,7 +479,10 @@ inline `sprinter_page_w0..w3(page)`; ENV: `getenv putenv sysenv`.
mem_info(&total,&free)` (реализации _bios/_estex; макро-выбор mem_info(&total,&free)` (реализации _bios/_estex; макро-выбор
MEM_MANAGE_MODE_*). HOME-резидентный доступ к чужим страницам: MEM_MANAGE_MODE_*). HOME-резидентный доступ к чужим страницам:
`bank_load_byte/store_byte/read/write` (своп W3 внутри; *_w1 — `bank_load_byte/store_byte/read/write` (своп W3 внутри; *_w1 —
вариант через окно W1). вариант через окно W1). `bank_load_file(page,off,path,max_size)` читает
файл непосредственно в диапазон EMM-страницы, `bank_save_file` записывает
диапазон одним WRITE; обе функции возвращают фактический размер либо -1.
Они восстанавливают W3 до возврата и безопасны для вызова из huge-банка.
## <sprinter_exit.h> ## <sprinter_exit.h>
+5 -4
View File
@@ -9,15 +9,15 @@ banked 1056
bankedbg 1067 bankedbg 1067
banklocl 4683 banklocl 4683
banktest 3767 banktest 3767
bgi_img 8451 bgi_img 8680
bgitest 3751 bgitest 3751
bios_text 4461 bios_text 4461
blitperf 5860 blitperf 5860
blitw 4939 blitw 4939
cat 913 cat 913
cblstream 6464 cblstream 5850
cbltest 6613 cbltest 5996
cblwav 6681 cblwav 6089
conio 4605 conio 4605
conio2 3929 conio2 3929
convbench 3508 convbench 3508
@@ -45,6 +45,7 @@ pageflip 6334
palfile 5406 palfile 5406
ptime 5744 ptime 5744
rt_test 5187 rt_test 5187
scroll 6744
seek 4188 seek 4188
simple 955 simple 955
solidt 11531 solidt 11531
1 # Эталон размеров _CODE (байт); обновление: python3 toolchain/size_check.py --update
9 bankedbg
10 banklocl
11 banktest
12 bgi_img
13 bgitest
14 bios_text
15 blitperf
16 blitw
17 cat
18 cblstream
19 cbltest
20 cblwav
21 conio
22 conio2
23 convbench
45 palfile
46 ptime
47 rt_test
48 scroll
49 seek
50 simple
51 solidt
+11 -35
View File
@@ -2,58 +2,37 @@
* atlas_load загрузить .atl-файл в свежую EMM-страницу (§3.1/§9в * atlas_load загрузить .atl-файл в свежую EMM-страницу (§3.1/§9в
* дизайна, вариант II: файл-офсет == офсет страницы == W0-адрес). * дизайна, вариант II: файл-офсет == офсет страницы == W0-адрес).
* *
* Порядок: выделить страницу (mem_alloc_pages) замапить в W3 * Порядок: выделить страницу bank_load_file() проверить магию 'SPA1'
* read() файла ЦЕЛИКОМ в 0xC000 (ESTEX READ пишет в W3 проверено * gfx_w0_page_prepare(). Никакого пересчёта офсетов: каталог и ленты
* tests/w0page P1) проверить магию 'SPA1' пропатчить ISR-стаб * уже лежат по своим W0-адресам.
* (0x38: JP _gfx_w0_isr; 0x66: RETN место зарезервировано в
* заголовке файла) вернуть W3. Никакого пересчёта офсетов: каталог
* и ленты уже лежат по своим W0-адресам.
* *
* Вызывать ВНЕ рендера (W3 на время загрузки занята страницей атласа). * Вызывать ВНЕ рендера (W3 на время загрузки занята страницей атласа).
* Ошибки: -1 + errno (open/read/alloc; EINVAL не .atl). * Ошибки: -1 + errno (open/read/alloc; EINVAL не .atl).
*/ */
#include <fcntl.h>
#include <unistd.h>
#include <errno.h> #include <errno.h>
#include <sprinter.h>
#include <sprinter_mem.h> #include <sprinter_mem.h>
#include <sprite.h> #include <sprite.h>
#include "../_bgi.h" #include "../_bgi.h"
int atlas_load(atlas_t *a, const char *path) int atlas_load(atlas_t *a, const char *path)
{ {
uint8_t *pg = (uint8_t *)0xC000; uint8_t blk, ok, count, h[5];
uint8_t saved_w3, blk, ok, count; int n;
uint16_t stub;
int fd, n;
fd = open(path, O_RDONLY);
if (fd < 0) return -1;
blk = mem_alloc_pages(1); blk = mem_alloc_pages(1);
if (!blk) { close(fd); return -1; } if (!blk) return -1;
if (!_gfx_w0_dss)
_gfx_w0_dss = _io_page_w0; /* снять страницу ядра DSS */
a->blk = blk; a->blk = blk;
a->page = mem_get_page(blk, 0); a->page = mem_get_page(blk, 0);
saved_w3 = _io_page_w3; n = bank_load_file(a->page, 0, path, 16384);
sprinter_page_w3(a->page); if (n >= 5) bank_read(a->page, 0, h, sizeof(h));
n = read(fd, pg, 16384);
ok = (n >= (int)_ATL_DATA_OFF && ok = (n >= (int)_ATL_DATA_OFF &&
pg[0] == 'S' && pg[1] == 'P' && pg[2] == 'A' && pg[3] == '1'); h[0] == 'S' && h[1] == 'P' && h[2] == 'A' && h[3] == '1');
count = 0; count = 0;
if (ok) { if (ok) {
count = pg[4]; count = h[4];
stub = (uint16_t)&_gfx_w0_isr; gfx_w0_page_prepare(a->page);
pg[0x38] = 0xC3; /* JP _gfx_w0_isr */
pg[0x39] = (uint8_t)(stub & 0xFF);
pg[0x3A] = (uint8_t)(stub >> 8);
pg[0x66] = 0xED; /* RETN (NMI) */
pg[0x67] = 0x45;
} }
/* Восстановить W3 ДО записи в *a: если atlas_t лежит в банковской /* Восстановить W3 ДО записи в *a: если atlas_t лежит в банковской
@@ -63,9 +42,6 @@ int atlas_load(atlas_t *a, const char *path)
* a->count оставался мусором, room_draw читал пиксели не с того * a->count оставался мусором, room_draw читал пиксели не с того
* оффсета). a->blk/a->page выше пишутся ДО ремапа их это не * оффсета). a->blk/a->page выше пишутся ДО ремапа их это не
* касалось, только a->count. */ * касалось, только a->count. */
sprinter_page_w3(saved_w3);
close(fd);
if (!ok) { if (!ok) {
mem_free_block(blk); mem_free_block(blk);
a->page = 0; a->page = 0;
+69 -9
View File
@@ -27,18 +27,73 @@
#include "../_bgi.h" #include "../_bgi.h"
void gfx_scroll_h(const gfx_rect_t *area, int16_t dx, gfx_rect_t *dirty) { void gfx_scroll_h(const gfx_rect_t *area, int16_t dx, gfx_rect_t *dirty) {
#ifdef GFX_NOCHECK
uint8_t saved = gfx_get_bank(); uint8_t saved = gfx_get_bank();
uint16_t adx = (dx < 0) ? (uint16_t)(-dx) : (uint16_t)dx; uint16_t adx = (dx < 0) ? (uint16_t)(-dx) : (uint16_t)dx;
/* src-строка стартует правее на |dx| при сдвиге влево, dst — правее
* на dx при сдвиге вправо; так оба края остаются внутри area. */
uint16_t src_x = area->x + ((dx < 0) ? adx : 0); uint16_t src_x = area->x + ((dx < 0) ? adx : 0);
uint16_t dst_x = area->x + ((dx > 0) ? adx : 0); uint16_t dst_x = area->x + ((dx > 0) ? adx : 0);
uint16_t w = area->w - adx; /* байт (пикселей) в строке */ uint16_t w = area->w - adx;
uint8_t h = (uint8_t)area->h; /* строк (256 => 0 = 256 у leaf) */ uint8_t h = (uint8_t)area->h;
uint16_t src = _gfx_addr_shadow_base + src_x; uint16_t src = _gfx_addr_shadow_base + src_x;
uint16_t dst = _gfx_addr_base + dst_x; uint16_t dst = _gfx_addr_base + dst_x;
gfx_set_bank(GFX_BANK_NORMAL);
_bgi_begin();
while (w) {
uint8_t cw = (w > 255) ? 255 : (uint8_t)w;
_bgi_scroll_rows_raw(src, dst, cw, h, (uint8_t)area->y);
src += cw; dst += cw; w -= cw;
}
_bgi_end();
gfx_set_bank(saved);
if (dirty) {
dirty->x = (dx >= 0) ? area->x : (int16_t)(area->x + area->w - adx);
dirty->y = area->y; dirty->w = adx; dirty->h = area->h;
}
#else
int16_t x, y;
uint16_t aw, ah, cut, adx;
uint8_t saved, h;
uint16_t src_x, dst_x, w, src, dst;
if (!area) return;
x = area->x; y = area->y; aw = area->w; ah = area->h;
if (!aw || !ah) goto empty;
/* Raw-leaf не клипит. Нормализуем область до экрана и не допускаем
* 16-битного underflow в area->w-|dx|. */
if (x < 0) {
cut = (uint16_t)(-x);
if (cut >= aw) goto empty;
aw -= cut; x = 0;
}
if (y < 0) {
cut = (uint16_t)(-y);
if (cut >= ah) goto empty;
ah -= cut; y = 0;
}
if (x >= GFX_WIDTH || y >= GFX_HEIGHT) goto empty;
if (aw > (uint16_t)(GFX_WIDTH - x)) aw = (uint16_t)(GFX_WIDTH - x);
if (ah > (uint16_t)(GFX_HEIGHT - y)) ah = (uint16_t)(GFX_HEIGHT - y);
/* Сначала сравнение со знаком: так не вычисляем -INT16_MIN. */
if (dx >= (int16_t)aw || dx <= -(int16_t)aw) {
if (dirty) { dirty->x=x; dirty->y=y; dirty->w=aw; dirty->h=ah; }
return;
}
adx = (dx < 0) ? (uint16_t)(-dx) : (uint16_t)dx;
saved = gfx_get_bank();
/* src-строка стартует правее на |dx| при сдвиге влево, dst — правее
* на dx при сдвиге вправо; так оба края остаются внутри area. */
src_x = (uint16_t)x + ((dx < 0) ? adx : 0);
dst_x = (uint16_t)x + ((dx > 0) ? adx : 0);
w = aw - adx;
h = (uint8_t)ah; /* 256 => 0 у leaf */
src = _gfx_addr_shadow_base + src_x;
dst = _gfx_addr_base + dst_x;
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
_bgi_begin(); _bgi_begin();
@@ -54,7 +109,7 @@ void gfx_scroll_h(const gfx_rect_t *area, int16_t dx, gfx_rect_t *dirty) {
// src += 256; dst += 256; // src += 256; dst += 256;
// w -= (w > 256) ? 256 : w; // w -= (w > 256) ? 256 : w;
uint8_t cw = (w > 255) ? 255 : (uint8_t)w; /* 0 => 256 */ uint8_t cw = (w > 255) ? 255 : (uint8_t)w; /* 0 => 256 */
_bgi_scroll_rows_raw(src, dst, cw, h, (uint8_t)area->y); _bgi_scroll_rows_raw(src, dst, cw, h, (uint8_t)y);
src += cw; dst += cw; w -= cw; src += cw; dst += cw; w -= cw;
} }
@@ -62,9 +117,14 @@ void gfx_scroll_h(const gfx_rect_t *area, int16_t dx, gfx_rect_t *dirty) {
gfx_set_bank(saved); gfx_set_bank(saved);
if (dirty) { if (dirty) {
dirty->x = (dx >= 0) ? area->x : (int16_t)(area->x + area->w - adx); dirty->x = (dx >= 0) ? x : (int16_t)(x + aw - adx);
dirty->y = area->y; dirty->y = y;
dirty->w = adx; dirty->w = adx;
dirty->h = area->h; dirty->h = ah;
} }
return;
empty:
if (dirty) { dirty->x=0; dirty->y=0; dirty->w=0; dirty->h=0; }
#endif
} }
+66 -9
View File
@@ -24,20 +24,72 @@
#include "../_bgi.h" #include "../_bgi.h"
void gfx_scroll_v(const gfx_rect_t *area, int16_t dy, gfx_rect_t *dirty) { void gfx_scroll_v(const gfx_rect_t *area, int16_t dy, gfx_rect_t *dirty) {
#ifdef GFX_NOCHECK
uint8_t saved = gfx_get_bank(); uint8_t saved = gfx_get_bank();
uint16_t ady = (dy < 0) ? (uint16_t)(-dy) : (uint16_t)dy; uint16_t ady = (dy < 0) ? (uint16_t)(-dy) : (uint16_t)dy;
/* Строку чтения (ys) сдвигаем вниз на |dy| при сдвиге вверх, строку
* записи (yd) вниз на dy при сдвиге вниз; края в пределах area. */
uint8_t ys = (uint8_t)(area->y + ((dy < 0) ? ady : 0)); uint8_t ys = (uint8_t)(area->y + ((dy < 0) ? ady : 0));
uint8_t yd = (uint8_t)(area->y + ((dy > 0) ? ady : 0)); uint8_t yd = (uint8_t)(area->y + ((dy > 0) ? ady : 0));
uint16_t hb = area->h - ady; /* строк к переносу (≤256) */ uint16_t hb = area->h - ady;
if (hb) { /* 0 = переносить нечего */ if (hb) {
uint8_t hblk = (uint8_t)hb; /* размер accel-блока: 256 => 0 */ uint8_t hblk = (uint8_t)hb;
uint16_t w = area->w; uint16_t w = area->w;
uint16_t src = _gfx_addr_shadow_base + area->x; uint16_t src = _gfx_addr_shadow_base + area->x;
uint16_t dst = _gfx_addr_base + area->x; uint16_t dst = _gfx_addr_base + area->x;
gfx_set_bank(GFX_BANK_NORMAL);
_bgi_begin();
while (w) {
uint8_t cw = (w > 255) ? 255 : (uint8_t)w;
_bgi_scroll_cols_raw(src, dst, cw, hblk, ys, yd);
src += cw; dst += cw; w -= cw;
}
_bgi_end();
gfx_set_bank(saved);
}
if (dirty) {
dirty->x = area->x;
dirty->y = (dy >= 0) ? area->y : (int16_t)(area->y + area->h - ady);
dirty->w = area->w; dirty->h = ady;
}
#else
int16_t x, y;
uint16_t aw, ah, cut, ady, hb;
uint8_t saved, ys, yd;
if (!area) return;
x = area->x; y = area->y; aw = area->w; ah = area->h;
if (!aw || !ah) goto empty;
if (x < 0) {
cut = (uint16_t)(-x);
if (cut >= aw) goto empty;
aw -= cut; x = 0;
}
if (y < 0) {
cut = (uint16_t)(-y);
if (cut >= ah) goto empty;
ah -= cut; y = 0;
}
if (x >= GFX_WIDTH || y >= GFX_HEIGHT) goto empty;
if (aw > (uint16_t)(GFX_WIDTH - x)) aw = (uint16_t)(GFX_WIDTH - x);
if (ah > (uint16_t)(GFX_HEIGHT - y)) ah = (uint16_t)(GFX_HEIGHT - y);
if (dy >= (int16_t)ah || dy <= -(int16_t)ah) {
if (dirty) { dirty->x=x; dirty->y=y; dirty->w=aw; dirty->h=ah; }
return;
}
ady = (dy < 0) ? (uint16_t)(-dy) : (uint16_t)dy;
saved = gfx_get_bank();
/* Строку чтения (ys) сдвигаем вниз на |dy| при сдвиге вверх, строку
* записи (yd) вниз на dy при сдвиге вниз; края в пределах area. */
ys = (uint8_t)(y + ((dy < 0) ? ady : 0));
yd = (uint8_t)(y + ((dy > 0) ? ady : 0));
hb = ah - ady;
if (hb) { /* 0 = переносить нечего */
uint8_t hblk = (uint8_t)hb; /* размер accel-блока: 256 => 0 */
uint16_t w = aw;
uint16_t src = _gfx_addr_shadow_base + (uint16_t)x;
uint16_t dst = _gfx_addr_base + (uint16_t)x;
gfx_set_bank(GFX_BANK_NORMAL); gfx_set_bank(GFX_BANK_NORMAL);
_bgi_begin(); _bgi_begin();
@@ -55,9 +107,14 @@ void gfx_scroll_v(const gfx_rect_t *area, int16_t dy, gfx_rect_t *dirty) {
} }
if (dirty) { if (dirty) {
dirty->x = area->x; dirty->x = x;
dirty->y = (dy >= 0) ? area->y : (int16_t)(area->y + area->h - ady); dirty->y = (dy >= 0) ? y : (int16_t)(y + ah - ady);
dirty->w = area->w; dirty->w = aw;
dirty->h = ady; dirty->h = ady;
} }
return;
empty:
if (dirty) { dirty->x=0; dirty->y=0; dirty->w=0; dirty->h=0; }
#endif
} }
+21
View File
@@ -0,0 +1,21 @@
/*
* gfx_w0_page_prepare сделать произвольную EMM-страницу безопасной в W0.
*
* При первом вызове запоминает штатную страницу DSS, затем ставит в целевой
* странице переход на общий IRQ-восстановитель и RETN для NMI. Функция
* резидентна: она сама временно переключает W3.
*/
#include <sprinter.h>
#include <sprite.h>
#include "../_bgi.h"
void gfx_w0_page_prepare(uint8_t page)
{
uint8_t saved=_io_page_w3, *p=(uint8_t *)0xC000;
uint16_t stub=(uint16_t)&_gfx_w0_isr;
if (!_gfx_w0_dss) _gfx_w0_dss=_io_page_w0;
sprinter_page_w3(page);
p[0x38]=0xC3; p[0x39]=(uint8_t)stub; p[0x3A]=(uint8_t)(stub >> 8);
p[0x66]=0xED; p[0x67]=0x45;
sprinter_page_w3(saved);
}
+4
View File
@@ -290,6 +290,10 @@ typedef struct {
* 0 OK; -1 + errno (файл/память/формат). */ * 0 OK; -1 + errno (файл/память/формат). */
int atlas_load(atlas_t *a, const char *path); int atlas_load(atlas_t *a, const char *path);
/* Подготовить уже выделенную EMM-страницу для безопасного маппинга в W0:
* установить IRQ/NMI-стабы и запомнить штатную страницу DSS. */
void gfx_w0_page_prepare(uint8_t page);
/* Освободить EMM-страницу атласа (все sprite_t с его лентами должны /* Освободить EMM-страницу атласа (все sprite_t с его лентами должны
* быть скрыты/переинициализированы ДО вызова). */ * быть скрыты/переинициализированы ДО вызова). */
void atlas_free(atlas_t *a); void atlas_free(atlas_t *a);
+6
View File
@@ -55,6 +55,12 @@ extern uint8_t _cbl_port_refs; /* держатели bit7 помимо
/* Запись в 16-битный порт управления 0x004E. */ /* Запись в 16-битный порт управления 0x004E. */
void _cbl_ctrl(uint8_t v); void _cbl_ctrl(uint8_t v);
/* Общее тело обеих публичных точек входа (cbl_open / cbl_open_silence).
* Буфер тишины не трогает им владеет cbl_open_silence.c; так malloc не
* приезжает в резидент приложениям, которые льют тишину сами. */
int _cbl_open_raw(uint8_t freq_code, uint8_t fmt, uint8_t pump_mode,
uint8_t underrun_mode, cbl_fill_fn fill);
/* Залить весь аппаратный буфер (256 слотов) байтом тишины сразу после /* Залить весь аппаратный буфер (256 слотов) байтом тишины сразу после
* включения иначе первые 23 мс играет то, что лежало в буфере раньше. * включения иначе первые 23 мс играет то, что лежало в буфере раньше.
* Подробный разбор в шапке _cbl_prime.c. */ * Подробный разбор в шапке _cbl_prime.c. */
+78
View File
@@ -0,0 +1,78 @@
/*
* _cbl_open_raw общее тело обеих точек входа CBL (cbl_open и
* cbl_open_silence). Проверяет параметры, вешает насос и включает железо.
*
* ПОЧЕМУ ТОЧЕК ВХОДА ДВЕ, а не один параметр underrun_mode. Линкер тянет
* .rel ЦЕЛИКОМ, поэтому malloc/free, стоящие в ветке CBL_UNDERRUN_SILENCE,
* приезжали в резидент КАЖДОМУ приложению даже тому, которое льёт тишину
* само и кучей не пользуется вовсе. Это 275 Б кода плюс вся область кучи;
* в huge-режиме, где под резидент отведено 0x4100..0xBB00, такая мелочь
* заметна (у PoP оставалось 129 Б свободных). Разведя аллокацию в отдельный
* публичный модуль, мы отдаём выбор линкеру: зовёшь cbl_open malloc не
* приезжает.
*
* Указатель на буфер тишины (_cbl_silence) здесь НЕ ТРОГАЕТСЯ: им владеет
* cbl_open_silence.c, который его и аллоцирует. В режиме APP насос на него
* не смотрит вовсе (условие в _cbl_pump_*), а обнуляй мы его тут cbl_open
* после cbl_open_silence терял бы уже выделенный буфер, и следующая
* silence-сессия аллоцировала бы второй.
*/
#include <errno.h>
#include <sprinter_exit.h>
#include "_cbl.h"
#include "../irq/_irq.h"
int _cbl_open_raw(uint8_t freq_code, uint8_t fmt, uint8_t pump_mode,
uint8_t underrun_mode, cbl_fill_fn fill)
{
if (_irq_cbl_hook) { errno = EBUSY; return -1; }
if (freq_code < CBL_FREQ_7K8 || freq_code > CBL_FREQ_109K) {
errno = EINVAL;
return -1;
}
if (fmt & (uint8_t)~(CBL_FMT_MONO16 | CBL_FMT_STEREO8)) {
errno = EINVAL; /* биты вне 5/6 — неизвестный формат */
return -1;
}
if (pump_mode != CBL_PUMP_OTIR && pump_mode != CBL_PUMP_ACCEL) {
errno = EINVAL;
return -1;
}
/* ЗАПРЕТ CBL_PUMP_OTIR + 16-бит (подтверждено чтением исходника MAME
* sprinter.cpp): порт данных 0x4F ВСЕГДА кладёт байт как есть в один
* слот, не собирая пару байт в 16-бит значение и не сверяясь с
* cbl_mode16() вообще это умеет только акселераторный путь (спец-
* страница 0xFD, do_accel_block). Комбинация принципиально не может
* звучать правильно, поэтому запрещена. */
if (pump_mode == CBL_PUMP_OTIR && (fmt & CBL_FMT_MONO16)) {
errno = EINVAL;
return -1;
}
if (_irq_table_ref() != 0) return -1;
_cbl_block = (fmt & CBL_FMT_MONO16) ? 256u : 128u;
_cbl_reqs = 0;
_cbl_undr = 0;
_cbl_fill = fill;
_cbl_underrun_mode = underrun_mode;
static uint8_t atexit_armed;
if (!atexit_armed) {
atexit_armed = 1;
atexit(cbl_close); /* CBL обязан умолкнуть до шелла */
}
IRQ_DISABLE();
_irq_cbl_hook = (pump_mode == CBL_PUMP_ACCEL) ? _cbl_pump_accel : _cbl_pump_otir;
_cbl_ctrl((uint8_t)(0x90 | fmt | freq_code)); /* on + int + формат + частота */
/* ...и НЕМЕДЛЕННО забить буфер тишиной: железо пускает воспроизведение
* с нулевого слота той же записью в порт управления, а буфер хранит
* старое содержимое без этого первые 23 мс уходит мусор. Заливка
* обязана быть внутри той же DI-скобки и сразу за включением (разбор
* _cbl_prime.c). */
_cbl_prime((uint8_t)((fmt & CBL_FMT_MONO16) ? 0x00 : 0x80));
IRQ_ENABLE();
return 0;
}
+7 -6
View File
@@ -1,7 +1,11 @@
/* /*
* cbl_close выключить CBL, снять хук насоса, освободить буфер * cbl_close выключить CBL, снять хук насоса и отпустить IM2-таблицу.
* тишины (если был аллоцирован под CBL_UNDERRUN_SILENCE) и отпустить * Идемпотентно; вызывается и из atexit.
* IM2-таблицу. Идемпотентно; вызывается и из atexit. *
* БУФЕР ТИШИНЫ НЕ ОСВОБОЖДАЕТСЯ. Раньше здесь стоял free(_cbl_silence), и
* он один тянул malloc/free в резидент КАЖДОМУ приложению включая те, что
* cbl_open_silence() не зовут вовсе. Буфер живёт до выхода из программы и
* переиспользуется при повторном открытии (см. cbl_open_silence.c).
* *
* Порт 0x004E не гасится напрямую вместо этого _cbl_port_sync() * Порт 0x004E не гасится напрямую вместо этого _cbl_port_sync()
* пересчитывает нужное значение: если gfx_wait_vsync() ещё держит * пересчитывает нужное значение: если gfx_wait_vsync() ещё держит
@@ -9,7 +13,6 @@
* синхронизации; иначе порт гасится полностью. * синхронизации; иначе порт гасится полностью.
*/ */
#include <stdlib.h>
#include "_cbl.h" #include "_cbl.h"
#include "../irq/_irq.h" #include "../irq/_irq.h"
@@ -20,7 +23,5 @@ void cbl_close(void)
_irq_cbl_hook = 0; _irq_cbl_hook = 0;
IRQ_ENABLE(); IRQ_ENABLE();
_cbl_port_sync(); _cbl_port_sync();
free(_cbl_silence);
_cbl_silence = 0;
_irq_table_unref(); _irq_table_unref();
} }
+6 -80
View File
@@ -1,89 +1,15 @@
/* /*
* cbl_open включить CBL, зарегистрировать fill()-callback и один * cbl_open включить CBL, когда тишину при недоливе льёт САМО ПРИЛОЖЕНИЕ
* из ISR-насосов. * (режим CBL_UNDERRUN_APP). Буфера не просит, поэтому НЕ ТЯНЕТ malloc
* ради этого точки входа и разведены (см. шапку _cbl_open_raw.c).
* *
* Управляющее слово: bit7 (CBL on) + bit4 (int enable) + fmt (bit6 * Если тишина нужна от libc cbl_open_silence() в соседнем модуле.
* stereo, bit5 16-бит) + freq_code. Размер блока/буфер тишины
* зависят от fmt и underrun_mode (см. cbl.h). cbl_close вешается на
* atexit.
*
* ЗАПРЕТ CBL_PUMP_OTIR + 16-бит (подтверждено чтением исходника MAME
* sprinter.cpp): порт данных 0x4F ВСЕГДА кладёт байт как есть в один
* слот, не собирая пару байт в 16-бит значение и не сверяясь с
* cbl_mode16() вообще это умеет только акселераторный путь (спец-
* страница 0xFD, do_accel_block). Комбинация принципиально не может
* звучать правильно, поэтому запрещена (EINVAL). Для 16-бит
* CBL_PUMP_ACCEL.
*/ */
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sprinter_exit.h>
#include "_cbl.h" #include "_cbl.h"
#include "../irq/_irq.h"
int cbl_open(uint8_t freq_code, uint8_t fmt, uint8_t pump_mode, int cbl_open(uint8_t freq_code, uint8_t fmt, uint8_t pump_mode,
uint8_t underrun_mode, cbl_fill_fn fill) cbl_fill_fn fill)
{ {
if (_irq_cbl_hook) { errno = EBUSY; return -1; } return _cbl_open_raw(freq_code, fmt, pump_mode, CBL_UNDERRUN_APP, fill);
if (freq_code < CBL_FREQ_7K8 || freq_code > CBL_FREQ_109K) {
errno = EINVAL;
return -1;
}
if (fmt & (uint8_t)~(CBL_FMT_MONO16 | CBL_FMT_STEREO8)) {
errno = EINVAL; /* биты вне 5/6 — неизвестный формат */
return -1;
}
if (pump_mode != CBL_PUMP_OTIR && pump_mode != CBL_PUMP_ACCEL) {
errno = EINVAL;
return -1;
}
if (pump_mode == CBL_PUMP_OTIR && (fmt & CBL_FMT_MONO16)) {
errno = EINVAL; /* OTIR не может собрать 16-бит сэмпл — только ACCEL */
return -1;
}
if (underrun_mode != CBL_UNDERRUN_APP && underrun_mode != CBL_UNDERRUN_SILENCE) {
errno = EINVAL;
return -1;
}
uint16_t block = (fmt & CBL_FMT_MONO16) ? 256u : 128u;
uint8_t *silence = 0;
if (underrun_mode == CBL_UNDERRUN_SILENCE) {
silence = malloc(block);
if (!silence) { errno = ENOMEM; return -1; }
memset(silence, (fmt & CBL_FMT_MONO16) ? 0x00 : 0x80, block);
}
if (_irq_table_ref() != 0) {
free(silence);
return -1;
}
_cbl_block = block;
_cbl_reqs = 0;
_cbl_undr = 0;
_cbl_fill = fill;
_cbl_underrun_mode = underrun_mode;
_cbl_silence = silence;
static uint8_t atexit_armed;
if (!atexit_armed) {
atexit_armed = 1;
atexit(cbl_close); /* CBL обязан умолкнуть до шелла */
}
IRQ_DISABLE();
_irq_cbl_hook = (pump_mode == CBL_PUMP_ACCEL) ? _cbl_pump_accel : _cbl_pump_otir;
_cbl_ctrl((uint8_t)(0x90 | fmt | freq_code)); /* on + int + формат + частота */
/* ...и НЕМЕДЛЕННО забить буфер тишиной: железо пускает воспроизведение
* с нулевого слота той же записью в порт управления, а буфер хранит
* старое содержимое без этого первые 23 мс уходит мусор. Заливка
* обязана быть внутри той же DI-скобки и сразу за включением (разбор
* _cbl_prime.c). */
_cbl_prime((uint8_t)((fmt & CBL_FMT_MONO16) ? 0x00 : 0x80));
IRQ_ENABLE();
return 0;
} }
+37
View File
@@ -0,0 +1,37 @@
/*
* cbl_open_silence то же, что cbl_open, но при недоливе тишину шлёт САМА
* libc: буфер под неё аллоцируется здесь. Именно этот модуль тянет malloc,
* поэтому приложения на cbl_open() за него не платят.
*
* БУФЕР НЕ ОСВОБОЖДАЕТСЯ. cbl_close() его не трогает намеренно: free()
* в cbl_close вернул бы malloc в резидент всем подряд ровно то, от чего
* уходили. Потеря невелика (128 или 256 байт), а при повторном открытии
* буфер переиспользуется; окончательно память возвращает выход из
* программы.
*/
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include "_cbl.h"
/* Сколько байт под буфером сейчас лежит. Помнить обязательно: у 8-бит
* формата блок 128, у 16-бит 256, и открытие 16-бит ПОСЛЕ 8-бит писало бы
* memset'ом мимо выделенного куска. */
static uint16_t silence_cap;
int cbl_open_silence(uint8_t freq_code, uint8_t fmt, uint8_t pump_mode,
cbl_fill_fn fill)
{
uint16_t block = (fmt & CBL_FMT_MONO16) ? 256u : 128u;
uint8_t quiet = (fmt & CBL_FMT_MONO16) ? 0x00 : 0x80;
if (silence_cap < block) {
uint8_t *p = malloc(block);
if (!p) { errno = ENOMEM; return -1; }
_cbl_silence = p; /* прежний кусок не освобождаем — см. шапку */
silence_cap = block;
}
memset(_cbl_silence, quiet, block);
return _cbl_open_raw(freq_code, fmt, pump_mode, CBL_UNDERRUN_SILENCE, fill);
}
+29 -13
View File
@@ -44,13 +44,14 @@
* кладёт байт как есть в один слот, не собирая пару байт в 16-бит * кладёт байт как есть в один слот, не собирая пару байт в 16-бит
* значение это умеет только акселераторный путь (страница 0xFD). * значение это умеет только акселераторный путь (страница 0xFD).
* *
* Поведение при недоливе CBL_UNDERRUN_* (4-й параметр cbl_open): * Поведение при недоливе задаётся ВЫБОРОМ ТОЧКИ ВХОДА (cbl_open /
* cbl_open_silence), а не параметром:
* CBL_UNDERRUN_APP (по умолчанию, 0) недолив не наша забота; * CBL_UNDERRUN_APP (по умолчанию, 0) недолив не наша забота;
* в CBL доигрывает то, что уже лежит в его буфере (никакого * в CBL доигрывает то, что уже лежит в его буфере (никакого
* буфера тишины не аллоцируется). * буфера тишины не аллоцируется).
* CBL_UNDERRUN_SILENCE (1) насос сам пропихивает тишину при * CBL_UNDERRUN_SILENCE (1) насос сам пропихивает тишину при
* недоливе; буфер тишины (128/256 Б по формату) аллоцируется * недоливе; буфер тишины (128/256 Б по формату) аллоцируется
* malloc'ом ВНУТРИ cbl_open(), только в этом режиме. * malloc'ом внутри cbl_open_silence(), только в этом режиме.
* cbl_underruns() считает недоливы в ОБОИХ режимах это диагностика, * cbl_underruns() считает недоливы в ОБОИХ режимах это диагностика,
* поведение не меняет. * поведение не меняет.
* *
@@ -89,8 +90,10 @@
#define CBL_PUMP_ACCEL 1 /* cbl_push_accel() — единственный путь для 16-бит */ #define CBL_PUMP_ACCEL 1 /* cbl_push_accel() — единственный путь для 16-бит */
/* Поведение при недоливе (fill() вернул 0 или не задан). */ /* Поведение при недоливе (fill() вернул 0 или не задан). */
#define CBL_UNDERRUN_APP 0 /* по умолчанию: не наша забота, буфер тишины не аллоцируется */ /* Внутренние коды режима недолива: приложение их не передаёт (режим задаёт
#define CBL_UNDERRUN_SILENCE 1 /* насос сам шлёт тишину; буфер аллоцируется в cbl_open() */ * выбор cbl_open / cbl_open_silence), но насос по ним и различает поведение. */
#define CBL_UNDERRUN_APP 0 /* не наша забота, буфер тишины не аллоцируется */
#define CBL_UNDERRUN_SILENCE 1 /* насос сам шлёт тишину из буфера libc */
/* fill(n): пропихнуть n байт (через cbl_push_otir/accel) прямо из /* fill(n): пропихнуть n байт (через cbl_push_otir/accel) прямо из
* данных приложения. Вызывается ИЗ ISR обязан быть быстрым (без * данных приложения. Вызывается ИЗ ISR обязан быть быстрым (без
@@ -98,17 +101,30 @@
* данных сейчас (недолив). */ * данных сейчас (недолив). */
typedef int (*cbl_fill_fn)(uint16_t n); typedef int (*cbl_fill_fn)(uint16_t n);
/* Включить CBL с частотой CBL_FREQ_*, форматом CBL_FMT_*, способом /* Включить CBL с частотой CBL_FREQ_*, форматом CBL_FMT_* и способом выдачи
* выдачи CBL_PUMP_* и поведением при недоливе CBL_UNDERRUN_*; fill * CBL_PUMP_*; fill callback, зовущийся из ISR за новым блоком (может быть
* callback, зовущийся из ISR за новым блоком (может быть NULL, тогда * NULL, тогда недолив на каждом запросе). Недолив НЕ наша забота
* недолив на каждом запросе). 0 или -1 + errno (EBUSY уже * (CBL_UNDERRUN_APP): железо доиграет по кругу неактивную половину, и если
* открыт; EINVAL плохой код частоты/формат/pump/underrun_mode, или * это слышно приложение обязано отдавать тишину само.
* не-W2 режим; ENOMEM не хватило памяти под буфер тишины). */ *
* 0 или -1 + errno (EBUSY уже открыт; EINVAL плохой код частоты, формат,
* pump, или не-W2 режим).
*
* ТОЧКИ ВХОДА ДВЕ, и это не косметика: cbl_open() не аллоцирует ничего и
* потому НЕ ТЯНЕТ malloc в резидент. За тишиной от libc cbl_open_silence()
* ниже, и malloc приезжает только тому, кто её позвал. */
int cbl_open(uint8_t freq_code, uint8_t fmt, uint8_t pump_mode, int cbl_open(uint8_t freq_code, uint8_t fmt, uint8_t pump_mode,
uint8_t underrun_mode, cbl_fill_fn fill); cbl_fill_fn fill);
/* Выключить CBL, отпустить IM2-таблицу, освободить буфер тишины (если /* То же, но при недоливе тишину шлёт САМА libc (CBL_UNDERRUN_SILENCE):
* был). Идемпотентно. */ * буфер под неё аллоцируется здесь. Добавляет ENOMEM к кодам ошибок.
* Буфер живёт до конца программы и переиспользуется при повторном открытии
* cbl_close() его не освобождает намеренно (иначе free вернул бы malloc
* в резидент всем подряд). */
int cbl_open_silence(uint8_t freq_code, uint8_t fmt, uint8_t pump_mode,
cbl_fill_fn fill);
/* Выключить CBL и отпустить IM2-таблицу. Идемпотентно. */
void cbl_close(void); void cbl_close(void);
/* Пропихнуть n байт из src прямо в CBL (звать ИЗ fill()). */ /* Пропихнуть n байт из src прямо в CBL (звать ИЗ fill()). */
+9
View File
@@ -110,6 +110,15 @@ void bank_read(uint8_t phys_page, uint16_t off, void *dst, uint16_t n);
/* Copy `n` bytes from near buffer `src` into phys_page[off..off+n-1]. */ /* Copy `n` bytes from near buffer `src` into phys_page[off..off+n-1]. */
void bank_write(uint8_t phys_page, uint16_t off, const void *src, uint16_t n); void bank_write(uint8_t phys_page, uint16_t off, const void *src, uint16_t n);
/* Файловый обмен непосредственно с диапазоном EMM-страницы через W3.
* В отличие от bank_read/bank_write безопасен для вызова из huge-банка:
* path используется до переключения W3, а окно восстановлено до возврата.
* Возвращает фактический размер либо -1 + errno. */
int bank_load_file(uint8_t phys_page, uint16_t off, const char *path,
uint16_t max_size);
int bank_save_file(uint8_t phys_page, uint16_t off, const char *path,
uint16_t size);
/* =================================================================== /* ===================================================================
* Far-page accessors via window 1 (base 0x4000, port 0xA2) * Far-page accessors via window 1 (base 0x4000, port 0xA2)
* *
+33
View File
@@ -0,0 +1,33 @@
/*
* bank_load_file прочитать файл непосредственно в физическую EMM-страницу.
*
* ESTEX принимает файловый буфер только в W1..W3; используем W3, сохраняя и
* восстанавливая его раскладку. Функция резидентна, поэтому её безопасно
* вызывать из банкового кода huge: после open() указатель path больше не
* читается, пока W3 временно занят целевой страницей.
*
* Возврат: фактически прочитанный размер либо -1 + errno.
*/
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sprinter.h>
#include <sprinter_mem.h>
int bank_load_file(uint8_t phys_page, uint16_t off, const char *path,
uint16_t max_size)
{
uint8_t saved;
int fd, n;
if (off > 0x3FFFu || max_size > (uint16_t)(0x4000u - off)) {
errno=EINVAL; return -1;
}
fd = open(path, O_RDONLY);
if (fd < 0) return -1;
saved = _io_page_w3;
sprinter_page_w3(phys_page);
n = read(fd, (void *)(0xC000u + off), max_size);
sprinter_page_w3(saved);
close(fd);
return n;
}
+30
View File
@@ -0,0 +1,30 @@
/*
* bank_save_file записать диапазон физической EMM-страницы в файл.
*
* Файл создаётся заново (O_CREAT|O_TRUNC), данные передаются одним WRITE.
* W3 восстанавливается до close(), поэтому функцию можно вызывать из
* банкового кода huge. Возврат: записанный размер либо -1 + errno.
*/
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sprinter.h>
#include <sprinter_mem.h>
int bank_save_file(uint8_t phys_page, uint16_t off, const char *path,
uint16_t size)
{
uint8_t saved;
int fd, n;
if (off > 0x3FFFu || size > (uint16_t)(0x4000u - off)) {
errno=EINVAL; return -1;
}
fd = open(path, O_WRONLY | O_CREAT | O_TRUNC);
if (fd < 0) return -1;
saved = _io_page_w3;
sprinter_page_w3(phys_page);
n = write(fd, (const void *)(0xC000u + off), size);
sprinter_page_w3(saved);
if (close(fd) != 0) return -1;
return n;
}
+1 -1
View File
@@ -23,7 +23,7 @@ sprinter-cc -o OUT.exe SRC.c [more.c ...] [options]
| Flag | Description | | Flag | Description |
|---|---| |---|---|
| `--bank N=FILE.c` | Compile FILE.c into bank N (1..15). Repeatable. Functions in banked files need the `__banked` qualifier. | | `--bank N=FILE.c` | Compile FILE.c into bank N (1..15). Numbers must be consecutive; crt0's bank count is generated automatically. Repeatable; functions need `__banked`. |
| `--crt0=TYPE` | Override startup file: `default` / `minimal` / `banked` / `small`. Normally chosen automatically by the memory mode. | | `--crt0=TYPE` | Override startup file: `default` / `minimal` / `banked` / `small`. Normally chosen automatically by the memory mode. |
### Diagnostics ### Diagnostics
+1 -1
View File
@@ -23,7 +23,7 @@ sprinter-cc -o OUT.exe SRC.c [more.c ...] [options]
| Флаг | Описание | | Флаг | Описание |
|---|---| |---|---|
| `--bank N=FILE.c` | Компилировать FILE.c в банк N (1..15). Повторяемый. Функции в banked-файлах нужны с квалификатором `__banked`. | | `--bank N=FILE.c` | Компилировать FILE.c в банк N (1..15). Номера без пропусков; число банков crt0 получает автоматически. Повторяемый; функции нужны с `__banked`. |
| `--crt0=TYPE` | Переопределение startup-файла: `default` / `minimal` / `banked` / `small`. Обычно выбирается автоматически по memory mode. | | `--crt0=TYPE` | Переопределение startup-файла: `default` / `minimal` / `banked` / `small`. Обычно выбирается автоматически по memory mode. |
### Диагностика ### Диагностика
+3 -5
View File
@@ -20,11 +20,9 @@
;; 6. CLOSE file ;; 6. CLOSE file
;; 7. Standard gsinit + call _main + ESTEX EXIT ;; 7. Standard gsinit + call _main + ESTEX EXIT
;; ;;
;; _n_banks MUST be defined as `const uint8_t n_banks = N;` — we read it ;; _n_banks is generated by sprinter-cc from the highest --bank number.
;; before gsinit, so a plain initialized `uint8_t` would still hold 0 ;; It is emitted as const because we read it before gsinit; a normal
;; (the initializer hasn't been copied yet) and bank loading would be ;; initialized variable would still contain 0 at this point.
;; silently skipped, leaving window 3 mapped to a garbage page.
;; For zero banks, link crt0.s instead.
;; ---------------------------------------------------------------------- ;; ----------------------------------------------------------------------
.module crt0_banked .module crt0_banked
-3
View File
@@ -2,9 +2,6 @@
#include <stdint.h> #include <stdint.h>
#include <sprinter.h> #include <sprinter.h>
/* crt0_banked.s reads this constant to know how many banks to load. */
const uint8_t n_banks = 2;
void bank1_func(int x) __banked; void bank1_func(int x) __banked;
void bank2_func(int x) __banked; void bank2_func(int x) __banked;
-3
View File
@@ -13,9 +13,6 @@
#include <stdint.h> #include <stdint.h>
#include <sprinter.h> #include <sprinter.h>
/* crt0_banked.s reads this constant to know how many banks to load. */
const uint8_t n_banks = 2;
void bank1_func(int x) __banked; void bank1_func(int x) __banked;
void bank2_func(int x) __banked; void bank2_func(int x) __banked;
-5
View File
@@ -18,11 +18,6 @@ extern void bank1_map_print(void) __banked;
extern uint8_t *bank1_alloc_and_fill(int n, uint8_t pattern) __banked; extern uint8_t *bank1_alloc_and_fill(int n, uint8_t pattern) __banked;
extern void bank1_free (void *p) __banked; extern void bank1_free (void *p) __banked;
/* MUST be `const`: crt0_banked reads _n_banks BEFORE gsinit, so
* non-const initialized variables haven't been copied yet and would
* read 0 bank loading is skipped garbage in window 3. */
const uint8_t n_banks = 1;
uint8_t local0_buf[16]; uint8_t local0_buf[16];
-3
View File
@@ -2,9 +2,6 @@
#include <stdint.h> #include <stdint.h>
#include <sprinter.h> #include <sprinter.h>
/* crt0_banked.s reads this constant to know how many banks to load. */
const uint8_t n_banks = 2;
void bank1_func(void) __banked; void bank1_func(void) __banked;
void bank2_func(void) __banked; void bank2_func(void) __banked;
void bank1_func2(uint16_t) __banked; void bank1_func2(uint16_t) __banked;
+1 -1
View File
@@ -100,7 +100,7 @@ int main(void)
} }
if (cbl_open(CBL_FREQ_21K9, CBL_FMT_MONO8, CBL_PUMP_OTIR, if (cbl_open(CBL_FREQ_21K9, CBL_FMT_MONO8, CBL_PUMP_OTIR,
CBL_UNDERRUN_APP, fill_stream) != 0) { fill_stream) != 0) {
printf("cbl_open failed: errno=%d\n", errno); printf("cbl_open failed: errno=%d\n", errno);
close(fd); close(fd);
return 1; return 1;
+2 -2
View File
@@ -82,7 +82,7 @@ static void play_combo(uint16_t idx, uint16_t total,
gen_chunk(bits, stereo, want_l, want_r); gen_chunk(bits, stereo, want_l, want_r);
current_pump = pump_mode; current_pump = pump_mode;
if (cbl_open(freq_code, fmt, pump_mode, CBL_UNDERRUN_APP, fill_tone) != 0) { if (cbl_open(freq_code, fmt, pump_mode, fill_tone) != 0) {
printf("[%2u/%u] %s %s %s: cbl_open failed errno=%d\n", printf("[%2u/%u] %s %s %s: cbl_open failed errno=%d\n",
idx, total, pump_name, fmt_name, freq_name, errno); idx, total, pump_name, fmt_name, freq_name, errno);
return; return;
@@ -136,7 +136,7 @@ int main(void)
for (uint8_t q = 0; q < 4; q++) { for (uint8_t q = 0; q < 4; q++) {
idx++; idx++;
/* OTIR принципиально не может собрать 16-бит сэмпл /* OTIR принципиально не может собрать 16-бит сэмпл
* (см. cbl_open.c) cbl_open вернёт EINVAL; не тратим * (см. _cbl_open_raw.c) cbl_open вернёт EINVAL; не тратим
* время на заведомо запрещённую комбинацию. */ * время на заведомо запрещённую комбинацию. */
if (pumps[p] == CBL_PUMP_OTIR && fmt_bits[f] == 16) { if (pumps[p] == CBL_PUMP_OTIR && fmt_bits[f] == 16) {
printf("[%2u/%u] %s %s %s : skip (OTIR can't do 16-bit)\n", printf("[%2u/%u] %s %s %s : skip (OTIR can't do 16-bit)\n",
+1 -1
View File
@@ -113,7 +113,7 @@ int main(void)
done = 0; done = 0;
if (cbl_open(CBL_FREQ_21K9, CBL_FMT_MONO8, CBL_PUMP_OTIR, if (cbl_open(CBL_FREQ_21K9, CBL_FMT_MONO8, CBL_PUMP_OTIR,
CBL_UNDERRUN_APP, fill_speech) != 0) { fill_speech) != 0) {
printf("cbl_open failed: errno=%d\n", errno); printf("cbl_open failed: errno=%d\n", errno);
mem_free_block(blk); mem_free_block(blk);
return 1; return 1;
+4
View File
@@ -0,0 +1,4 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := scroll
EXTRA_FLAGS ?= --gfx 256 --safe
include $(PROJ_ROOT)/app.mk
+139
View File
@@ -0,0 +1,139 @@
/* Побайтовый регресс gfx_scroll_h/gfx_scroll_v.
* Источник неактивная page 1, приёмник draw page 0. */
#include <graphics.h>
#include <gfx.h>
#include <sprite.h>
#define AX 10
#define AY 24
#define AW 300 /* проверяет разбиение ширины 255+45 */
#define AH 160
#define DIRT 0x31
static uint8_t pattern(int x, int y)
{
return (uint8_t)(0x40 + ((x * 3 + y * 5) & 0x3f));
}
static void draw_source(void)
{
int x, y;
for (y = AY; y < AY + AH; y++)
for (x = AX; x < AX + AW; x++)
putpixel(x, y, pattern(x, y));
}
static void clear_target(void)
{
setfillstyle(SOLID_FILL, DIRT);
bar(0, 0, 319, 255);
}
static uint8_t rect_eq(const gfx_rect_t *r, int x, int y, int w, int h)
{
return r->x == x && r->y == y && r->w == w && r->h == h;
}
static uint8_t check_h(int dx)
{
gfx_rect_t a = { AX, AY, AW, AH }, d;
int x, y, sx;
clear_target();
gfx_scroll_h(&a, dx, &d);
if (dx > 0) {
if (!rect_eq(&d, AX, AY, dx, AH)) return 0;
} else if (dx < 0) {
if (!rect_eq(&d, AX + AW + dx, AY, -dx, AH)) return 0;
} else if (!rect_eq(&d, AX, AY, 0, AH)) return 0;
for (y = AY; y < AY + AH; y++)
for (x = AX; x < AX + AW; x++) {
sx = x - dx;
if (sx >= AX && sx < AX + AW) {
if ((uint8_t)getpixel(x, y) != pattern(sx, y)) return 0;
} else if ((uint8_t)getpixel(x, y) != DIRT) return 0;
}
return 1;
}
static uint8_t check_v(int dy)
{
gfx_rect_t a = { AX, AY, AW, AH }, d;
int x, y, sy;
clear_target();
gfx_scroll_v(&a, dy, &d);
if (dy > 0) {
if (!rect_eq(&d, AX, AY, AW, dy)) return 0;
} else if (!rect_eq(&d, AX, AY + AH + dy, AW, -dy)) return 0;
for (y = AY; y < AY + AH; y++)
for (x = AX; x < AX + AW; x++) {
sy = y - dy;
if (sy >= AY && sy < AY + AH) {
if ((uint8_t)getpixel(x, y) != pattern(x, sy)) return 0;
} else if ((uint8_t)getpixel(x, y) != DIRT) return 0;
}
return 1;
}
static uint8_t check_limits(void)
{
gfx_rect_t a = { -5, -7, 25, 27 }, d;
gfx_scroll_h(&a, 40, &d);
if (!rect_eq(&d, 0, 0, 20, 20)) return 0;
gfx_scroll_v(&a, -40, &d);
return rect_eq(&d, 0, 0, 20, 20);
}
static uint8_t check_source(void)
{
int x, y;
gfx_set_draw_page(1);
for (y = AY; y < AY + AH; y++)
for (x = AX; x < AX + AW; x++)
if ((uint8_t)getpixel(x, y) != pattern(x, y)) {
gfx_set_draw_page(0);
return 0;
}
gfx_set_draw_page(0);
return 1;
}
static void say(int y, const char *name, uint8_t ok)
{
setcolor(WHITE); outtextxy(8, y, (char *)name);
setcolor(ok ? GREEN : RED); outtextxy(232, y, ok ? (char *)"PASS" : (char *)"FAIL");
}
void main(void)
{
uint8_t t[7], i;
initgraph();
gfx_set_bank(GFX_BANK_NORMAL);
gfx_set_draw_page(1); draw_source();
gfx_set_draw_page(0);
t[0] = check_h(8);
t[1] = check_h(-13);
t[2] = check_v(9);
t[3] = check_v(-11);
t[4] = check_limits();
t[5] = check_h(0);
t[6] = check_source();
clear_target();
say(40, "H +8, wide", t[0]);
say(56, "H -13, wide", t[1]);
say(72, "V +9, wide", t[2]);
say(88, "V -11, wide", t[3]);
say(104, "clip/full dirty", t[4]);
say(120, "H zero/full copy", t[5]);
say(136, "source intact", t[6]);
for (i = 0; i < 7 && t[i]; i++) { }
setcolor(WHITE);
outtextxy(8, 160, i == 7 ? (char *)"ALL PASS" : (char *)"SOME FAILED");
for (;;) { }
}
-2
View File
@@ -15,8 +15,6 @@
#include <stdint.h> #include <stdint.h>
#include <sprinter.h> #include <sprinter.h>
const uint8_t n_banks = 1; /* читает crt0_banked */
extern uint8_t res_page_seen; extern uint8_t res_page_seen;
uint8_t bank_page_before, bank_page_after, bank_page_after_home, bank_alive; uint8_t bank_page_before, bank_page_after, bank_page_after_home, bank_alive;
extern uint8_t bank_pages[]; extern uint8_t bank_pages[];
-3
View File
@@ -7,9 +7,6 @@
#include <stdint.h> #include <stdint.h>
#include <sprinter.h> #include <sprinter.h>
/* crt0_banked читает это ДО gsinit — только `const`. */
const uint8_t n_banks = 1;
void bigbank_func(void) __banked; /* W1-банк, трамплин */ void bigbank_func(void) __banked; /* W1-банк, трамплин */
extern uint8_t w3_probe(uint8_t x); /* W3-резидент, прямо */ extern uint8_t w3_probe(uint8_t x); /* W3-резидент, прямо */
extern void w3_show(void); extern void w3_show(void);
-2
View File
@@ -12,8 +12,6 @@
#include <stdint.h> #include <stdint.h>
#include <sprinter.h> #include <sprinter.h>
const uint8_t n_banks = 1; /* один свапаемый W3-банк */
void hugebank_func(void) __banked; /* W3-банк, трамплин */ void hugebank_func(void) __banked; /* W3-банк, трамплин */
extern uint8_t w3_probe(uint8_t x); /* W3-резидент, прямой вызов */ extern uint8_t w3_probe(uint8_t x); /* W3-резидент, прямой вызов */
extern void w3_show(void); /* W3-резидент, прямой вызов */ extern void w3_show(void); /* W3-резидент, прямой вызов */