sumskoy.com← Все записи
Bevy · Vulkan · wgpu · Android

Почему на Android пропадал огонь — и что пришлось исправить в wgpu

Андрей Сумской

Делаю игру и движок на Bevy. Часть 1.

Я делаю BoomRivals — игру в духе Bomberman: ставишь бомбы, разбиваешь ящики, стараешься не попасть под собственный взрыв. Для неё нужны огонь, дым и искры. С огнём на Android возникла проблема: на устройстве с Adreno 730 он не рисовался.

Игра работает на Bevy. Вместе с ней я разрабатываю Kedra — движок и инструменты, в которых собираю сцены, настраиваю префабы и запускаю игру. Мне важно проверять всё это на самой игре: отдельный пример с частицами ещё не отвечает на вопрос, будет ли виден взрыв в матче.

Для эффектов я взял Hanabi. Он считает частицы на видеокарте и там же их рисует. В проекте используются Bevy 0.20.0, Hanabi 0.20.0 и wgpu 30.0.1. На Android отрисовка идёт через Vulkan.

Арена BoomRivals с ящиками, каменными стенами и мельницей

Где терялась команда отрисовки

Чтобы понять, куда делся огонь, нужно немного посмотреть на путь от частиц до картинки. В случае Hanabi видеокарта может сама подготовить параметры отрисовки: сколько рисовать, откуда брать данные. Они остаются в GPU-буфере, и следующая команда использует их напрямую. Это indirect draw. Удобно: процессору не приходится читать результат с видеокарты только для того, чтобы отправить его обратно.

В используемом пути Hanabi вызывает draw_indexed_indirect. Но до отрисовки wgpu ещё должен проверить команду. Её параметры подготовлены на GPU, и нельзя просто довериться тому, что они безопасны. Этим занимается отдельный внутренний шейдер: корректную команду он пропускает, некорректную обнуляет.

Разницу удалось увидеть на уровне данных проверки: корректное зависимое чтение на Adreno 730 возвращало ноль, и валидная команда не доходила до отрисовки. Если явно ограничить индексы в шейдере, чтение работало правильно.

Зависимость выглядела так: шейдер читал служебные данные, брал из них смещение и по нему обращался к буферу команды. Именно этот доступ ломался. Для разбора уже важны входные и выходные данные проверки, а не цвет или текстура огня.

В тесте с настоящим шейдером валидатора это закреплено на трёх командах: две корректные должны пройти без изменений, третья, неправильная, — обнулиться. Входные буферы тоже сравниваются с исходными. Получается маленькая задача с проверяемым ответом, без всей сцены, материалов и частиц.

Проверки границ на Qualcomm

wgpu уже умеет полагаться на аппаратную защиту от выхода за границы буфера и не добавлять проверки в шейдер. На проблемном пути Qualcomm понадобилось сохранить и явные проверки.

Я оформил это как отдельный обход — FORCE_SHADER_BUFFER_BOUNDS_CHECKS. Он нужен только определённым драйверам, поэтому включать его для всех устройств не стал.

Код ниже относится к wgpu 30.0.1, ревизия 45de6f7ee4e08c3a59b7f321445db5d1cfd35c09. Начинается всё с выбора драйвера в Vulkan adapter:

rust
workarounds.set(
    super::Workarounds::FORCE_SHADER_BUFFER_BOUNDS_CHECKS,
    phd_capabilities.properties.vendor_id == db::qualcomm::VENDOR
        && phd_capabilities.driver.is_none_or(|driver| {
            driver.driver_id == vk::DriverId::QUALCOMM_PROPRIETARY
        }),
);

Обход включается для закрытого драйвера Qualcomm. is_none_or оставляет его включённым и тогда, когда производитель известен, а драйвер определить не удалось.

Сам флаг ещё ничего не исправляет. Он должен дойти до выбора политики доступа к буферу. В том же adapter условие меняется так — показываю сокращённый diff:

diff
- buffer: if self.private_caps.robust_buffer_access2 {
+ buffer: if self.private_caps.robust_buffer_access2
+     && !self
+         .workarounds
+         .contains(super::Workarounds::FORCE_SHADER_BUFFER_BOUNDS_CHECKS)
+ {
      naga::proc::BoundsCheckPolicy::Unchecked
  } else {
      naga::proc::BoundsCheckPolicy::Restrict
  },

Смысл правки в добавленном условии: при включённом обходе наличие аппаратной защиты больше не отправляет буфер в Unchecked. Остаётся Restrict — сгенерированный шейдер сам ограничивает индекс допустимыми границами. Аппаратную защиту я при этом не отключаю.

Внутренний шейдер мог отключить проверки снова

На этом правку легко было бы закончить, но она была бы неполной. У внутренних шейдеров wgpu есть настройка runtime_checks.bounds_checks. Если она выключена, код компиляции заменяет политики доступа на Unchecked. И именно такой режим запрашивает проверка indirect-команд, из-за которой пропадал огонь.

Получается, настройку из adapter могут просто перезаписать дальше. Для буферов нужно сохранить Restrict и в этом месте. В коде компиляции это выглядит так:

rust
if !runtime_checks.bounds_checks {
    temp_options.bounds_check_policies = naga::proc::BoundsCheckPolicies {
        index: naga::proc::BoundsCheckPolicy::Unchecked,
        buffer: if self
            .shared
            .workarounds
            .contains(super::Workarounds::FORCE_SHADER_BUFFER_BOUNDS_CHECKS)
        {
            naga::proc::BoundsCheckPolicy::Restrict
        } else {
            naga::proc::BoundsCheckPolicy::Unchecked
        },
        image_load: naga::proc::BoundsCheckPolicy::Unchecked,
        binding_array: naga::proc::BoundsCheckPolicy::Unchecked,
    };
}

Исключение касается только buffer: внутренний шейдер больше не отменяет обход Qualcomm. Остальные политики остаются прежними.

Такая же ветка добавлена во второй путь компиляции. Объявление флага и остальной контекст есть в полном патче.

Почему закрытый драйвер выдаёт ноль при таком чтении, я точно не знаю. Но связь с проверками границ воспроизводится: с явным ограничением индексов команда проходит корректно. На этом и построен обход.

Огонь и взрывы бомб в матче BoomRivals, запись Linux/Vulkan

Огонь вернулся

На Adreno 730 исправление вернуло корректную indirect-отрисовку, а вместе с ней — огонь в игру. Симуляция и отрисовка частиц остались на Hanabi.

В патче сохранены обе стороны проверки: корректная команда проходит, неправильная обнуляется, а indirect draw действительно рисует пиксели. Здесь это важно: вычислительный тест может работать, пока внутренний путь отрисовки всё ещё выбирает другие настройки компиляции.

Как забрать исправление

Полный фикс с тестами есть в коммите форка wgpu. Рядом со статьёй лежит патч относительно использованной upstream-базы. В полном патче для wgpu 30.0.1 есть и независимая правка доступа к матрицам в Naga — она понадобилась для анимации skinned mesh. К пропавшему огню относится обход Qualcomm, разобранный выше.

Получить тот же diff из репозитория можно так:

sh
git clone https://github.com/kedra-engine/wgpu.git
cd wgpu
git show 45de6f7ee4e08c3a59b7f321445db5d1cfd35c09
git diff 40f4a34ebaf56f9a046231f54125ad046239d3f3 \
  45de6f7ee4e08c3a59b7f321445db5d1cfd35c09 > qualcomm-buffer-fix.patch

В приложении я подключаю форк через [patch] в корневом Cargo manifest. Там закреплена одна ревизия для связанных пакетов wgpu и Naga: Cargo не передаёт overrides из библиотеки её потребителю.


Модели в сцене — KayKit / Kay Lousberg, CC0. Текстуры частиц — Kenney Particle Pack, CC0.

← Все записи