Почему на 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.

Где терялась команда отрисовки
Чтобы понять, куда делся огонь, нужно немного посмотреть на путь от частиц до картинки. В случае 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:
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:
- 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 и в этом месте. В коде компиляции это выглядит так:
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. Остальные политики остаются прежними.
Такая же ветка добавлена во второй путь компиляции. Объявление флага и остальной контекст есть в полном патче.
Почему закрытый драйвер выдаёт ноль при таком чтении, я точно не знаю. Но связь с проверками границ воспроизводится: с явным ограничением индексов команда проходит корректно. На этом и построен обход.

Огонь вернулся
На Adreno 730 исправление вернуло корректную indirect-отрисовку, а вместе с ней — огонь в игру. Симуляция и отрисовка частиц остались на Hanabi.
В патче сохранены обе стороны проверки: корректная команда проходит, неправильная обнуляется, а indirect draw действительно рисует пиксели. Здесь это важно: вычислительный тест может работать, пока внутренний путь отрисовки всё ещё выбирает другие настройки компиляции.
Как забрать исправление
Полный фикс с тестами есть в коммите форка wgpu. Рядом со статьёй лежит патч относительно использованной upstream-базы. В полном патче для wgpu 30.0.1 есть и независимая правка доступа к матрицам в Naga — она понадобилась для анимации skinned mesh. К пропавшему огню относится обход Qualcomm, разобранный выше.
Получить тот же diff из репозитория можно так:
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.