3db1b73d64
Advance the engine to 94b572cd2, backporting upstream f7b77fbc8 with its GPU growth/readback regression. Keep API30 unchanged. Co-Authored-By: Kimi Code <noreply@moonshot.ai>
257 lines
26 KiB
Markdown
257 lines
26 KiB
Markdown
# Закреплённый движок AirDef
|
||
|
||
Движок: https://gitea.maoniao.team/chexit/horizon-airdef, ветка `airdef`.
|
||
AirDef хранит точный SHA как gitlink `horizon/`; новый push движка не меняет старые сборки мода.
|
||
История upstream и всех 44 прежних патчей сохранена в форке. Старые patch-файлы и HORIZON_COMMIT
|
||
доступны в истории AirDef и больше не являются вторым источником движка. Baseline описан в AIRDEF-FORK.md.
|
||
|
||
## Windows: обновление и запуск
|
||
|
||
```powershell
|
||
git fetch origin
|
||
git switch kimi-work
|
||
git pull --ff-only
|
||
python tools/airdef.py all
|
||
python tools/airdef.py play
|
||
```
|
||
|
||
Первое клонирование: `git clone --recurse-submodules https://gitea.maoniao.team/chexit/airdef.git`.
|
||
Обычный clone тоже поддерживается: `python tools/airdef.py fork` или команда submodule выше.
|
||
`all` инициализирует зависимость сам, включая переход со старой папки `horizon/`.
|
||
Для чистого checkout можно отдельно выполнить `git submodule update --init --recursive`. Нужны Git, Python и Rust согласно INSTALL.md.
|
||
Сборка клиента и мод устанавливаются в обычное место; ресурсы Minecraft готовятся отдельно по INSTALL.md.
|
||
|
||
При первом переходе со старого отдельного clone `horizon/` скрипт сохраняет его Git-историю и ветки,
|
||
переводит в сабмодуль и получает закреплённый SHA. Если там есть изменения при другом SHA,
|
||
обновление останавливается с объяснением: сначала закоммитьте/сохраните свои изменения.
|
||
Скрипт не выполняет reset --hard или clean. Чистый checkout может остаться detached HEAD — это нормально для зависимости.
|
||
|
||
## Изменение движка
|
||
|
||
1. В `horizon/` создать рабочую ветку от закреплённого SHA. Проверить изменение и push в репозиторий движка.
|
||
2. В AirDef выполнить `git add horizon`, проверить мод/игру, коммитить обновление gitlink отдельно.
|
||
3. Push AirDef выполняется после движка. Не коммитить Git URL с токенами.
|
||
4. Для возврата выбрать прежний commit AirDef и выполнить `git submodule update --init --recursive`.
|
||
|
||
Упаковщик собирается repo-local в `.horizon-cache/packer/<engine-identity>/`, глобальный
|
||
`cargo-horizon-mod` не используется. Изменения tracked-исходников движка входят в identity.
|
||
Рядом с airdef.hmod записывается build-identity.json: SHA AirDef, pin/identity движка,
|
||
SHA256 WIT и артефакта. API-диагностика остаётся в `python tools/airdef.py check`.
|
||
Нативные тесты не заменяют отдельную игровую проверку графики, звука и экспорта.
|
||
|
||
## Проверка настоящего WASM
|
||
|
||
`python tools/airdef.py test-wasm` собирает `.hmod` и запускает `tests/*.toml` в настоящем
|
||
runtime закреплённого движка. Используется временное storage; игровые миры не нужны.
|
||
Сценарий проверяет загрузку/регистрацию команды, два комплекса, налёт, tick/frame,
|
||
выход из мира и перезагрузку мода. Действуют лимиты runtime: 64 MiB linear memory,
|
||
300 млн fuel на tick, 10 млн на frame, 1 млрд на event, 2 млрд на init.
|
||
Это проверка ABI и fallback при отсутствии render replies; она не измеряет GPU,
|
||
не подтверждает акустику/MP4 и не заменяет максимальный боевой полигон.
|
||
|
||
В CI проверка assets входит в лёгкий build. Wasmtime tier вынесен в ручной workflow
|
||
`wasm-contract`, чтобы не компилировать тяжёлый runtime на каждом push слабого общего runner.
|
||
|
||
## Готовность объёмного дыма (API 26)
|
||
|
||
`render.smoke-status` разделяет доступность API, включённый режим, принятую отправку этого мода
|
||
и готовность smoke/fog пути последнего подготовленного основного кадра. Возвращаются поколение мира
|
||
и причина (`awaiting-frame`, `disabled`, `path-unavailable`, `ready`). AirDef убирает обычный дым
|
||
только когда выполнены все четыре условия; после смены мира старые сегменты и часы очищаются.
|
||
Это подтверждение выбранного пути рендера, а не завершения GPU или видимости каждого сегмента.
|
||
Сам `smoke-status` не подтверждает акустику и capture; их отдельные статусы добавлены в API 27 ниже.
|
||
|
||
## Акустика и владение захватом (API 27)
|
||
|
||
Текущий pin: `94b572cd24e2a7e0960ab6f801ddbe223eef17a7` (API 30; поверх API29 — нормали моделей,
|
||
очистка неудачного захвата и power-of-two oversized staging allocations из #130). Акустические
|
||
`render.acoustics-status` и `render.capture-owner-status` возвращают поколение,
|
||
`available`, `enabled`, `request-accepted`, `prepared` и причину. В игре всё это показывает
|
||
`/airdef capabilities`. Отсутствие запроса, выключение, ожидание backend и ошибка различаются.
|
||
|
||
Для акустики `prepared` означает выполненный шаг обработки с геометрией при доступном live/offline
|
||
микшере. Шаг может поставить worker job или принять результаты; это **не** подтверждение результата
|
||
для каждого источника, завершения worker, слышимого звука или точного отражения. В режиме no-audio
|
||
без offline-микшера готовность не наступает. AirDef подтверждает текущее глобальное предпочтение,
|
||
не меняя его, и сохраняет собственные дальние варианты звука до подтверждённой подготовки.
|
||
|
||
Для capture запрос принадлежит конкретному запуску мода и поколению мира. `prepared` соответствует
|
||
его стадиям Recording/Encoding/Done; состояние файла и ошибка по-прежнему читаются из `capture-status`.
|
||
AirDef не принимает чужие Recording/Done за свой экспорт. Ожидание первоначальной подготовки
|
||
ограничено 600 тиками. Остановка проверяет владельца; смена мира отменяет модовый захват и
|
||
сбрасывает готовность. Уже переданный writer файл на Encoding может всё же завершиться:
|
||
это запрос отмены, не гарантия удаления файла.
|
||
|
||
Проверено на реальном экспорте 4 ноября (pin `57f8b2853`; очистка ошибки в `73b9d3070`):
|
||
|
||
- Смена мира доходит до движка только через снимок мода (`set_snapshot`). На экране заголовка тики
|
||
не вызываются, поэтому мод без хука `frame` не сообщает о закрытии мира, и захват продолжает
|
||
писать заголовок. AirDef теперь просит `frame` и завершает показ по `game.world-id = none`.
|
||
- При ошибке энкодера временные файлы этого захвата (`<имя>.video.mp4`, `<имя>.audio.raw`,
|
||
`<имя>.ffmpeg.log`) удаляются, а последние строки лога переносятся в текст ошибки: ссылки на
|
||
уже удалённый лог больше нет. Кодировщик всегда пишет `<имя>.video.mp4`, а `<имя>.mp4`
|
||
появляется только после успешного mux или переименования, поэтому неудачный, отменённый или
|
||
беззвучный захват не перезаписывает и не удаляет готовый фильм прежнего экспорта с тем же
|
||
именем. Исправлено в `73b9d307048fc44d7f21fa3c829fc5a855d144a6` (`game/film.rs`), проверено
|
||
реальной ошибкой ffmpeg и контрольным MP4.
|
||
|
||
## Отдельный тяжёлый WASM tier
|
||
|
||
```sh
|
||
python3 tools/airdef.py test-budgets
|
||
python3 tools/test_budget_report.py
|
||
python3 tools/test_airdef_build.py
|
||
```
|
||
|
||
Сценарий `airdef-mod/tests/budgets/wasm_flat_budget.toml` запускается в изолированном временном `tests/`,
|
||
поскольку CLI читает только непосредственные `tests/*.toml`. Обычный `test-wasm` остаётся лёгким.
|
||
Ручной workflow `.gitea/workflows/wasm-budget.yml` не добавляет нагрузку на каждый push.
|
||
|
||
Исходный профиль запрашивал 64 комплекса и две волны по 80 целей на 4000 блоках,
|
||
602 вызова tick и 300 frame, 69 шагов. Позднее обнаружено: второй заказ во время подготовки
|
||
первого отклонялся, а fixture не проверял ответ. Поэтому тот прогон **не доказывает перекрытие
|
||
160 целей**; сохранённый JSON помечен `coverage_correction`. Новый сценарий проверяет принятие
|
||
обоих заказов и отличает requested counts от неизвестного пика в воздухе.
|
||
Это не полный лимит расстановки (2000) и не нагрузочный
|
||
тест GPU, mixer voices или всех физических world replies: sandbox не выполняет настоящий рендер/звук.
|
||
|
||
Текущий default заменён на flat render-world: ground/sections/rays отвечают для плоскости y=64,
|
||
GPU не имитируется. Проверяются два принятых налёта по 40 «Орешников» и 64 ПВО; в середине
|
||
включаются HD и grid. На тике 2102 assertion подтверждает **184 цели в воздухе**, а не пик за всё время.
|
||
Прогон `2932364` прошёл: 271 шаг, 2102 tick и 2052 вызова frame стенда. У AirDef нет собственного
|
||
frame hook; эти вызовы не являются нагрузкой на GPU или измерением 10M frame fuel мода.
|
||
Точный отчёт: [flat-world budget](measurements/flat-wasm-raid-budget.json).
|
||
|
||
С 4 ноября (#122) fixture расширен хвостом умных приказов через UI-путь: 4 РЭБ, `airdef preset 1`
|
||
(та же `apply()`, что у кнопки готового состава) и два `airdef raid` — это та же функция
|
||
`launch_wave`, что у кнопки ПУСК (`ui_select`), с боевым журналом разведки и EW; оба приказа
|
||
приняты (`expect.chat = []`), итого 4 принятых приказа, финальный `/stats` закреплён строкой.
|
||
Прогон исправления #122: 283 шага, 3302 tick; max fuel tick 182413686/300M, command 6939810/1e9,
|
||
ошибок 0. Отчёт: [flat-world budget](measurements/flat-wasm-raid-budget.json).
|
||
|
||
Fixture копируется из тех же байтов, чей SHA записан до сборки. Проверенные 2 принятия и точка
|
||
184/2102 заносятся только после success и совпадения `BUDGET_ASSERTED_SHA256` с точной версией файла.
|
||
После изменения fixture нужно повторно проверить её и обновить SHA/метаданные; простой список
|
||
запрошенных целей не заменяет assertions. Пик остаётся null, если он не измерен.
|
||
Отчёт `.horizon-cache/wasm-budget-report.json` содержит SHA/dirty/diff AirDef, pin/identity движка,
|
||
API/WIT/артефакт, проверку build identity, сценарий, Wasmtime и исходные memory/fuel limits.
|
||
`AIRDEF_BUDGET_REPORT` меняет путь отчёта. Stale identity запрещает запуск, ошибки сборки/тестов
|
||
записываются как failed и дают ненулевой exit code.
|
||
|
||
Лимиты берутся из исходников: memory 64 MiB; fuel init/tick/frame/event — 2B/300M/10M/1B.
|
||
Начиная с продолжения 4 ноября `test-budgets` включает `--fuel-profile` и сохраняет
|
||
`measured_fuel.callbacks`: число вызовов, максимум guest fuel, бюджет и ошибки отдельно по callback.
|
||
Отсутствие запрошенных счётчиков делает проверку неуспешной, даже если сценарий завершился.
|
||
Это не время host/GPU и не peak memory; невызывавшиеся callbacks, включая init до включения профиля,
|
||
не получают выдуманного нуля. Первый прогон: tick 2102 вызова, max 106132646/300000000;
|
||
command 69 вызовов, max 1013370/1000000000; ошибок нет. Прогон исправления #122 (283 шага,
|
||
включая два умных UI-приказа): tick 3302 вызова, max 182413686/300M; command 79 вызовов,
|
||
max 6939810/1e9; ошибок нет.
|
||
Исторический отчёт без измерения fuel:
|
||
[API27 budget report с поправкой покрытия](measurements/api27-wasm-budget.json).
|
||
Актуальный чистый API29-прогон с измерением:
|
||
[271 шаг, fuel и build identity](measurements/api29-wasm-budget.json).
|
||
Актуальный прогон с умными UI-приказами #122 и build identity:
|
||
[flat-world budget](measurements/flat-wasm-raid-budget.json).
|
||
|
||
Проверены реальный WASM guest с переходами состояний/владельца/мира, сборка клиента и два коротких
|
||
offscreen-профиля Metal. При `HORIZON_ACOUSTICS=1` + `HORIZON_NO_AUDIO=1` в игре получено
|
||
enabled/request=true, prepared=false, awaiting-backend; HD smoke переходит в ready после подготовки
|
||
основного кадра. Это не замер производительности и не прослушивание.
|
||
|
||
API28/29 добавляют отдельные `point-lights-status` и `sound-sources-status`: свет подтверждается
|
||
после cache/main-view HD обработки, звук — после source-manager apply с live/offline output.
|
||
Заявки привязаны к запуску мода и поколению мира; звук также к source epoch. Clear/reload/выход
|
||
снимают подтверждение. Это не GPU fence, загрузка сэмпла, наличие голоса или слышимость.
|
||
Камеры и их targets изолированы по `(run_serial, local_id)` вместо глобального ID.
|
||
|
||
API30 добавляет `camera-status` для камер `set-views` вызывающего запуска: доступность API,
|
||
включение пути картинок в последнем отмеченном кадре, принятие заявки поколения и поштучная
|
||
готовность каждой камеры. `prepared` означает, что рендерер закодировал кадр камеры в её
|
||
texture target, либо кадр мира отрендерен с неё как main-view **этого** запуска: если одинаковый
|
||
local ID выбрали два мода, экран использует выбор первого, и milestone получает только он; чужая
|
||
картинка с тем же ID чужую камеру не готовит. `enabled` — последний отмеченный кадр был кадром
|
||
мира (не заголовок/загрузка). Это не регистрация target и не GPU fence: видимый пиксель,
|
||
представление кадра и обход ротации per-frame бюджета не гарантируются. Пустая `set-views`
|
||
снимает заявку и готовность; смена мира очищает заявку, выбор main-view и готовность (камеры
|
||
другого мира не рисуются), перезагрузка мода начинает заново, а повторная отправка и камера,
|
||
отданная в main-view, ждут нового кадра (у такой камеры нет своего picture target).
|
||
Проверено двумя настоящими Wasmtime-гостями с одинаковым local ID, пустой заявкой, сменой мира
|
||
и reload. `/airdef capabilities` показывает строку камер.
|
||
|
||
### GPU-приёмка картинки камер (Metal/offscreen)
|
||
|
||
Итоговый engine `278f356aa` с AirDef API30 проверен в запущенной игре, а не только в песочнице:
|
||
macOS, Apple M5 (Metal, IntegratedGpu), `--offscreen`, `HORIZON_NO_AUDIO=1`, чистый
|
||
`HORIZON_HOME=/tmp/airdef-camera-accept` и копии тестовых миров из scratch-папки прежних прогонов
|
||
(`AirDefenseLab`, `Polygon`), ресурсы лаунчера v0.2.91. `.hmod` — из этого дерева
|
||
(`python3 tools/airdef.py mod`, API30), не из основного дерева.
|
||
|
||
```
|
||
tools/airdef.py install # HORIZON_HOME=<scratch>
|
||
horizon-ctl --socket $H/ctl.sock launch --game horizon/target/fast/horizon-client \
|
||
--world AirDefenseLab --creative --offscreen --wait --resources <26.3>
|
||
# мод-команды клиентские: ctl command/chat уходят в консоль сервера, поэтому — через поле чата
|
||
horizon-ctl key t tap; horizon-ctl type "/airdef capabilities"; horizon-ctl key enter tap
|
||
horizon-ctl screenshot <png>
|
||
```
|
||
|
||
Наблюдения (строки `/airdef capabilities`, `поколение` = generation):
|
||
|
||
| Когда | Камеры |
|
||
| --- | --- |
|
||
| камер нет | `API=true, включено=true, запрос=false, подготовлено=false, поколение=1, not-requested, main=нет, []` |
|
||
| `/airdef deploy s400` + `/airdef monitor` | `... запрос=true, подготовлено=true, поколение=1, ready, main=нет, [32:ready]` |
|
||
| `/airdef monitor` (пустая заявка) | `... запрос=false, ... not-requested, main=нет, []` |
|
||
| `/airdef replay film best` | `... main=1996488705, [1996488705:ready]`, затем `main=1996488707` на следующем плане |
|
||
| другой мир (AirDefenseLab → Polygon) | `поколение=2, запрос=false, ... not-requested, main=нет, []` |
|
||
| там же deploy+monitor | `поколение=2, запрос=true, подготовлено=true, ready, [32:ready]` |
|
||
|
||
Снимки подтверждают, что `prepared` — нарисованная картинка, а не регистрация (число различных
|
||
цветов, выборка каждого 6-го пикселя): при взгляде в небо (кадр однородный) включение монитора
|
||
добавляло панель и picture-область — 477→4867 цветов на кадре; в том же мире A/B
|
||
«монитор выключен/включён» в области панели: 199→2080 цветов,
|
||
доля тёмной панели 1.2%→48.7%, а в области картинки 110→1771 цветов с доминирующим небом (рисуется
|
||
сцена с камеры С-400, а не плоская заглушка «НЕТ СВЯЗИ»). В film-режиме экран показывал карточку
|
||
«Готовлю кадры…» (фильм готовит планы), при этом main-view milestone был `ready` — это API-отметка
|
||
и отрисовка кадров рендерером, не проверка пикселей фильма.
|
||
|
||
`title → тот же мир` не меняет поколение: заявка и milestone сохраняются (смена поколения — это
|
||
смена мира), а `enabled` сбрасывается кадром заголовка (note с `world=false`), поэтому `ready`
|
||
требует нового кадра мира после возврата; он и наступает (наблюдалось). 8 из 8 запросов при
|
||
работающем мониторе дали `ready`. `set_views`/`set_main_view` вызываются модом каждый tick, то есть каждый
|
||
tick начинает ожидание нового кадра: выборка между tick и note может честно показать
|
||
`awaiting-frame` по порядку вызовов — это не flicker, а порядок tick → render → note.
|
||
|
||
Остаток этой приёмки: визуальная оценка человеком, Windows/другие GPU, два запуска с одинаковым
|
||
local ID на GPU (покрыто Wasmtime-гостями), reload мода на GPU (покрыт host-тестом), fence/представление
|
||
кадров не проверялись.
|
||
|
||
Остаток #102: полные query/voice/peak budgets,
|
||
GPU/готовый MP4 и платформенная приёмка. Эти результаты текущими тестами не объявляются выполненными.
|
||
|
||
## Измеренный fuel и массовые взрывы
|
||
|
||
В runtime добавлен выключенный по умолчанию профиль вызовов. У тестового CLI флаг
|
||
`test --fuel-profile` выводит mod/callback, число вызовов, максимальный израсходованный guest fuel,
|
||
бюджет и число ошибок; результат печатается и при неуспехе сценария. Init до включения профиля
|
||
и предыдущие поколения после reload в эти значения не входят. Это гостевые инструкции,
|
||
не время CPU, не heap peak и не работа GPU/host. Обычный `test-budgets` без этого флага по-прежнему
|
||
не выдаёт свои исходные caps за измеренный расход.
|
||
|
||
С профилем воспроизведён остановившийся tick на двух максимальных составах меню и HD:
|
||
полный обход height-cache при каждом взрыве доводил его до 300M. `1f28f83` заменяет обход
|
||
удалением только затронутого квадрата. В контрольном диагностическом WASM-прогоне после исправления
|
||
3602 тика прошли, max tick fuel = 135273690/300M, failures=0. Лимиты не менялись.
|
||
Диагностический гость содержит только временный способ запуска точного меню-состава;
|
||
это не исходный пользовательский Windows-сеанс. [Данные и воспроизведение](RAID-STABILITY.md).
|
||
|
||
С профилем воспроизведён и второй класс — остановка в **событии UI** (не тике) на втором
|
||
крупном приказе: переснимок `known_units` на каждом дроне приказа давал `дроны × журнал ×
|
||
группировка` за одно событие, 1e9 исчерпаны ровно (max command fuel 1000000000/1e9,
|
||
failures=1; состояние: 13172 записей журнала, 144 известных живых комплекса). Исправление
|
||
(#122, одним снимком на приказ и O(журнал+группировка) за проход) доводит тот же сценарий
|
||
до max command fuel 32946177/1e9, failures=0, `/stats` на тике 2602 совпал побайтово.
|
||
Это гостевые инструкции, не прирост FPS; приёмка Windows-сеанса внешняя.
|
||
[Данные и воспроизведение](RAID-STABILITY.md).
|