Advance to engine 7a73e8d26: separate previous-position streams and a depth-read-only motion pass for mod models, with matching alpha cutouts and a debug view. Co-Authored-By: Kimi Code <noreply@moonshot.ai>
26 KiB
Закреплённый движок 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: обновление и запуск
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 — это нормально для зависимости.
Изменение движка
- В
horizon/создать рабочую ветку от закреплённого SHA. Проверить изменение и push в репозиторий движка. - В AirDef выполнить
git add horizon, проверить мод/игру, коммитить обновление gitlink отдельно. - Push AirDef выполняется после движка. Не коммитить Git URL с токенами.
- Для возврата выбрать прежний 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: 7a73e8d26af93f965564ea7da0f110f2d91bafce (API 30; power-of-two staging,
смешанные shadow cascades, dense motion и previous-frame transforms mod-моделей #124). Акустические
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
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.
С 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.
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 с поправкой покрытия.
Актуальный чистый API29-прогон с измерением:
271 шаг, fuel и build identity.
Актуальный прогон с умными UI-приказами #122 и build identity:
flat-world budget.
Проверены реальный 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-сеанс. Данные и воспроизведение.
С профилем воспроизведён и второй класс — остановка в событии 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-сеанса внешняя.
Данные и воспроизведение.