Files
airdef/docs/RAID-STABILITY.md
T
chexit d45629ae29
build / mod (push) Failing after 22s
Fix the UI-event fuel trap on a second large raid order
The Windows log for #122 showed the mod stopping in the UI event
(fuel budget 1e9, remaining 0) with known_units <- order_raid <-
launch_wave <- ui_select: order_raid recomputed the planner snapshot
known_units() for every drone, and the snapshot itself rescanned the
whole force and the dedup list per intel entry. A second big Smart
order cost drones x intel-log x force-size inside one event.

Reproduced in actual WASM with measured per-callback fuel through the
exact ui_select(BTN_LAUNCH) handler (diagnostic guest, trigger.patch):
before, a 111-drone second order at revealed=13172/known=144 exhausted
exactly 1e9 and stopped the mod (an 11-drone order at the same state:
247805865); after, the same scenario passes with max command fuel
32946177/1e9, failures=0, and /stats at tick 2602 byte-identical.
A stacked single-point layout dies to one warhead (known=0, cheap
order), so the scenario deploys spread clusters with a moved player.

Fix: one known_units snapshot per order_raid passed to radar_strikes
and choose_target; known_units is a single O(log + force) set pass;
price_grid_on reuses its snapshot. Emitter/network targeting and
carrier choice semantics are unchanged (native equivalence test
against the former predicate; WASM checkpoint counters equal).
Limits unchanged; HD/recording untouched; wave size unrestricted.

Committed regressions: native snapshot-equivalence and
second-smart-order tests; the heavy WASM fixture now orders two Smart
raids through the wave screen's launch function (airdef preset +
airdef raid) with EW and a combat intel log, four accepted orders
asserted; the light contract covers preset and the empty-wave refusal.

Core release 195 + 8 scenarios; adapter 65 + 1 boundary + 5 scenarios;
light WASM 30 steps; heavy WASM 283 steps (tick max 182413686/300M,
command max 6939810/1e9, failures 0). Windows GUI acceptance remains
external and is not claimed.

Co-Authored-By: Kimi Code (Moonshot AI) <noreply@moonshot.cn>
2026-11-06 10:29:36 +03:00

14 KiB
Raw Blame History

Два крупных налёта: проверка стабильности

Пользовательский отчёт: AirDef часто перестаёт работать при двух больших налётах, резкого обвала FPS перед этим не замечено. RTX 3060 + Ryzen 5600X — около 50 FPS во время налёта. Пользователь уточнил: игра продолжает работать, отключается только AirDef; режим эффектов — HD. Точный build и старый лог не предоставлены. Horizon архивирует предыдущие логи в logs/YYYY-MM-DD-N.log.gz, поэтому запись может сохраниться и без ручного копирования. Задача: #122.

Воспроизведены и исправлены две конкретные причины отключения в этом классе нагрузки. Первая — тик и кэш высот (1f28f83, ниже). Вторая — событие UI и второй приказ (order_raid, см. раздел «Второй налёт из UI: событие, не тик»). Без старого лога не утверждается, что Windows-сеанс имел именно их; GPU/FPS отдельно не измерялись.

Второй налёт из UI: событие, не тик

Точный лог Windows: Horizon: mod airdef stopped in UI event (fuel budget 1000000000, remaining Some(0)); backtrace: ew::Sim::known_units ← raid::Sim::order_raid ← airdef::AirDef::launch_wave ← ui_select. Прежний fix 1f28f83 — другой класс: тогда мод останавливался в тике на 300M.

Причина. order_raid для каждого дрона заново вычислял снимок известных комплексов known_units() (choose_target), а сам known_units() на каждую запись журнала разведки пересканировал весь список комплексов и список уже принятых. Журнал держит каждый пуск, каждое раскрытие РЭБ и каждую emitting-РЛС за последние 6000 тиков. Стоимость второго большого приказа росла как дроны × журнал × размер группировки внутри одного UI-события.

Воспроизведение (настоящий WASM, измеренный fuel по callback, диагностический гость — trigger.patch гоняет тот же обработчик ui_select(BTN_LAUNCH) → launch_wave → order_raid с максимальным составом меню; метка callback — command, бюджет тот же 1e9): разнесённая группировка 172 комплекса (40 кластеров × 4 тяжёлых + 12 РЭБ, игрок двигается шагами сценария — стек в одной точке умирает от одной боеголовки и не воспроизводит), первый налёт 111 дронов отбит, второй приказ на тике ~2600 при revealed=13172, known=144 живых. До fix: второй приказ исчерпывает ровно 1e9 и останавливает мод (before-trap.txt); тот же приказ из 11 дронов в том же состоянии — 247805865 (before-scaling.txt). После fix тот же сценарий проходит целиком: max command fuel 32946177/1e9, max tick 236581288/300M, ошибок 0; /stats на тике 2602 совпал побайтово до/после, включая счётчики мира.

Фикс. Один снимок known_units на весь приказ, передаётся в radar_strikes и choose_target; сам known_units — один проход O(журнал + группировка) через множества; price_grid_on переиспользует свой снимок. Семантика планирования не изменилась: нативный тест сверяет новый снимок с прежним предикатом (дубликаты, погибшие, устаревшие, чужие id, порядок «новые первыми»). Лимиты не менялись, HD/запись не отключались, размер волны не ограничивался.

Регрессии. Нативные: эквивалентность снимка и «второй умный приказ с толстым журналом принят и запущен». Закреплённые WASM: тяжёлый fixture расширен — два умных приказа через функцию кнопки ПУСК (airdef preset 1 + airdef raid — тот же launch_wave, что и у ui_select) с РЭБ и боевым журналом, оба приняты (expect.chat = []), 4 принятых приказа в отчёте; лёгкий контракт проверяет preset и отказ пустой волны. Данные и точный сценарий: summary.json, trigger.patch, до, после. Patch — только для диагностического гостя, в мод не входит.

Приёмка конкретного Windows-сеанса остаётся внешней: нужны build identity и строки latest.log вокруг остановки. Измерение fuel — не прирост FPS.

Первая причина и фикс (тик, 1f28f83)

Два максимальных состава меню (11 видов × 40, два принятых приказа), 64 ПВО, расстояние 300, HD и grid, обслуживаемый плоский мир: tick 1835 исчерпал ровно 300M fuel и мод остановился. Grid, запись и обновление дыма не были необходимы для этого spike: исследовательские абляции не убирали остановку. В производственном исправлении эти функции не отключаются.

Ground::forget при каждом взрыве делал retain по всему кэшу известных высот. Тот же полный обход выполнялся при обработке просроченных blast replies. Когда приходило много взрывов, затраты росли как произведение количества взрывов и размера/ёмкости таблицы высот.

1f28f83 удаляет непосредственно координаты прежнего квадрата: при радиусе 8 это 17×17 ключей, а не просмотр всей таблицы. Вызов world.touched сохранён. Тест сравнивает весь результат со старым predicate, включая отрицательные координаты и границы; правила урона и лимиты не менялись.

Контрольный диагностический гость с полными записью, дымом, HD и grid, без phase markers: 421 шаг, 3602 тика — PASS; max tick fuel 135273690/300M, command 92658271/1B, failures=0. Финальные показатели: 0 в воздухе, 209 сбито, 3435 поражений, 15 столкновений. На тике 1834 до прежней остановки вывод /stats до/после совпал побайтово, включая счётчики мира. Это не утверждение о равенстве каждого скрытого поля симуляции.

Для воспроизведения огромного меню-состава использован временный hook команды mix, вызывающий тот же WaveConfig/order_raid; сам hook не входит в мод. Исходник — 10f24b5, после фикса добавлен только новый forget. Данные, hook и точный сценарий: summary.json, trigger.patch, scenario.toml, до, после. Patch предназначен для отдельного диагностического worktree, а не для обычной игры. Два checkpoint-лога намеренно заканчиваются несовпадением с probe, чтобы получить /stats; это не реальные traps. Успешный полный прогон отдельно в after.txt.

Публичный native-тест two_maximum_menu_raids_with_answered_flat_terrain использует настоящий максимальный RaidOrder без hook, terrain replies и запись. Он также прошёл.

Найденные пробелы старых тестов

В первом wasm_budget.toml два приказа отправлялись без тика между ними. order_raid отклоняет второй во время подготовки первого; ответ не проверялся. Кроме того, TestGame не отвечал на render ground/sections/rays: пуски могли ждать неизвестную землю вместо полноценного полёта. 69 успешных шагов не подтверждали 160 одновременно летящих целей. Старый отчёт сохранён с поправкой.

Что проверено теперь

  • Native: точный состав двух меню-пресетов «Массированный удар», 61 комплекс, плоский мир с ответами геометрии и встроенной записью. Оба приказа приняты, все 72 цели запущены, перекрытие проверено по числу одновременно живых целей.
  • Native: два приказа 40+40 «Орешников» с разделением, многими суббоеприпасами и окончанием записи.
  • Actual WASM: 64 комплекса, два принятых приказа 40+40 «Орешников», плоскость y=64 с ответами ground/sections/rays. В середине включаются shader hd и grid. 271 шаг прошёл; на тике 2102 проверено 184 цели в воздухе. Пик за весь прогон не измеряется.
  • Кадры чередуются с тиками. 2052 вызова frame стенда не означают GPU-рендер: у AirDef нет собственной реализации frame hook, основная работа идёт в tick и событиях.

Отчёт clean 2932364, engine ed7615d36: flat-wasm-raid-budget.json. Memory 64 MiB и fuel init/tick/frame/event 2B/300M/10M/1B не увеличивались. Отчёт фиксирует исходные лимиты, не измеренный расход fuel/peak memory.

python3 tools/airdef.py test-budgets
cargo test --release --manifest-path airdef-core/Cargo.toml --test scenarios overlapping_

Это проверка гостевого кода и контрактов с синтетическим host-миром. Она не воспроизводит RTX-драйвер, реальный рельеф/чанки, звук и стоимость HDR, shadow/light/smoke на GPU пользователя. Включение HD в fixture проверяет путь подготовки модовых данных, а не графическую производительность.

Диагностика следующего воспроизведения

В latest.log после остановки теперь записываются mod ID, callback, заданный и оставшийся fuel, полная цепочка ошибки. Сообщение OutOfFuel больше не объявляет тяжёлую работу бесконечным циклом. Проверка настоящим WASM /spin подтверждает, что при fuel trap останавливается только нарушивший мод. Это не доказывает, что пользовательский случай имеет ту же причину.

Нужны build identity и строки Windows latest.log вокруг остановки, состав двух налётов, HD/обычный профиль, открытые grid/monitor/replay и ответ: закрылась ли игра целиком. До этих данных не подменять диагноз увеличением fuel, отключением записи или обрезанием целей. Измерение FPS вести отдельно от поиска trap: стабильный FPS не исключает лимит WASM, а прошедший WASM-тест не исключает ошибку GPU или native host.