Почему производительность Vulkan у разных форков эмулятора Yuzu отличается, хотя аппаратное обеспечение и драйверы видеокарты одинаковы?

Я протестировал одну и ту же игру в нескольких форках Yuzu (Eden, Citron и Sudachi) на одном и том же оборудовании, с идентичными драйверами и ОС. Все форки используют Vulkan, но время кадра и уровень микрофризов существенно различаются, даже после полного формирования шейдерного кэша. <br/> <br/> Поскольку эти проекты построены на общей кодовой базе Yuzu, я ожидал близкого поведения Vulkan, однако наблюдаемые результаты противоречат этому ожиданию. Для сравнения я также посмотрел на отличия между Yuzu и Ryujinx: <a href="https://playwithyuzu.com/yuzu-vs-ryujinx/" rel="nofollow">playwithyuzu.com/yuzu-vs-ryujinx/</a>, что не объясняет расхождений между форками Yuzu. <br/> <br/> Вопросы: <br/> 1) Могут ли такие различия объясняться разной реализацией асинхронной компиляции шейдеров в каждом форке? <br/> 2) Насколько сильно на производительность влияет различный подход к синхронизации CPU↔GPU (fences, semaphores и т. п.)? <br/> 3) Какие методы бенчмаркинга и диагностики позволят выяснить, связана ли разница с реализацией Vulkan-бэкенда эмулятора или с графическим драйвером? <br/> <br/> Буду признателен за мнения специалистов, имеющих опыт разработки или анализа Vulkan-бэкендов эмуляторов.
  • 16 июля 2026 г. 12:01
Ответы на вопрос 1
Короткий ответ в начале: да — поведение Vulkan‑бэкенда в форках Yuzu может существенно отличаться даже на одинаковом железе/драйверах. Причины — не только «драйвер», а сочетание деталек реализации (как и когда компилируются шейдеры, как устроены очереди/синхронизация, сколько и как строятся command buffer‑ы, настройка кэшей, режимы сборки форков и т.д.). Ниже — подробный разбор по вашим вопросам и набор практических тестов/инструментов, чтобы локализовать проблему.

1) Могут ли различия объясняться разной реализацией асинхронной компиляции шейдеров?
- Да. Разные форки могут по‑разному:
  - ставить компиляцию шейдеров в отдельный поток(и) или блокировать рендер‑поток;
  - использовать разные политики приоритета/пулирования worker‑потоков (число потоков, очереди задач);
  - решать, компилировать ли всю PSO/PIPELINE сразу или лениво по мере first‑use;
  - заранее прогревать pipeline cache или полагаться на JIT в драйвере.
- Все это влияет как на пиковые задержки (микрофризы при первой встрече шейдера), так и на средний fps: если один форк умеет строить/параллелить компиляцию и «списать» работу на фоновые потоки без блокировок, а другой постоянно блокирует submit/wait — получится заметная разница.
- Ещё важный момент: «шейдерный кэш» у эмулятора (список high‑level шейдеров) и «pipeline cache» драйвера/устройства — разные вещи. Форки могут по‑разному сериализовать/валидировать кэши, поэтому даже при «заполненном» эмуляторным кешем драйвер всё равно может на месте JIT‑ить бинарные GPU‑шифты.

2) Насколько сильно влияет подход к синхронизации CPU↔GPU (fences, semaphores и т. п.)?
- Очень сильно. Неправильная/неэффективная синхронизация — частая причина плохой производительности:
  - Частые vkQueueWaitIdle/vkDeviceWaitIdle или ожидание fence на каждом кадре превращают GPU в последовательный ресурс и убивают параллелизм CPU↔GPU.
  - Неправильное использование очередей/семафоров (слишком тонкая синхронизация между потоками/сабмиссиями) увеличивает количество контекстных переключений и синхронных ожиданий.
  - Малые батчи submit (каждый draw — отдельный submit) создают накладные расходы на драйвер; наоборот, агрегация работы в fewer submits обычно эффективней.
  - Количество in‑flight кадров (стратегия triple buffering и т.п.) напрямую влияет на загрузку CPU и GPU: слишком мало — GPU ждёт CPU; слишком много — растёт latency и память.
  - Разные типы семафоров (binary vs timeline) и их использование тоже меняют поведение (timeline упрощает, но требует поддержки и изменяет логику).
- Итог: даже при одинаковом наборе API‑вызовов, различия в порядке/частоте ожиданий и submit'ов у разных форков дают разницу в fps/микрофризах.

3) Какие методы бенчмаркинга и диагностики помогут понять, где именно разница — в реализации эмулятора или в драйвере?
Ниже — практический чеклист и инструменты, что собирать и на что смотреть.

A) Инструменты (рекомендую использовать несколько):
- RenderDoc — захват кадра, анализ drawcalls, state. Полезен для проверки различий в rebind/descriptor, pipeline state, push constants.
- Nsight Systems (NVIDIA) / Nsight Graphics — system timeline: видно CPU‑потоки, API‑вызовы, ожидания, точки, где драйвер компилирует шейдеры.
- AMD Radeon GPU Profiler (RGP) / Radeon GPU Analyzer — аналогично для AMD.
- gfxreconstruct / vktrace / apitrace — записать весь Vulkan‑стрим и воспроизвести в контролируемой среде.
- CPU‑профайлеры: perf / VTune / Windows ETW / Visual Studio Profiler — посмотреть где тратится CPU‑время (сборка command buffer, блокировки, компиляция шейдеров).
- vulkaninfo, vkconfig, включение validation layers (для отлова ошибок, но они тормозят).
- Логирование/инструменты внутри самих форков (если есть) — вывести времена на ключевых этапах: build-cmdbufs, submit, wait, shader compile times.

B) Что измерять и сравнивать (конкретные тесты):
1. Снимите timeline (Nsight Systems или аналог) для одного и того же сцена/кадра в каждом форке. Сравните:
   - Где CPU ждёт GPU (функции wait, время блокировки).
   - Есть ли spikes, соответствующие JIT компиляции в драйвере (наблюдается как длинная «driver» активность/стопка).
   - Частота vkQueueSubmit и vkQueueWaitIdle.
2. Захватите кадр в RenderDoc и сравните:
   - Набор pipeline/descriptor‑bindings/семплеров — отличаются ли янычки;
   - Есть ли лишняя перезапись descriptor set или частые vkUpdateDescriptorSet.
3. Проверьте распределение frametime: percentiles (1%, 0.1%), не только средний FPS. Микрофризы видны в хвостах.
4. Попробуйте изменить параметры в форках:
   - Отключить/включить async shader compile (если такая опция есть).
   - Изменить количество in‑flight кадров.
   - Пробовать разное количество worker‑потоков для компиляции (если опции есть).
   - Собрать форки в одинаковом режиме (Release, одинаковые флаги компиляции) — иногда разница в собранных бинарях даёт разницу.
5. Проверка pipeline cache:
   - Очистите и заново разогрейте кэш для каждого форка; сравните, остаются ли микрофризы.
   - Сравните файлы pipeline cache: некоторые форки могут включать в них хэши/версию, несовместимые между сборками — тогда кэш бесполезен.
6. GPU‑профилировщики и логи драйвера:
   - На NVIDIA/AMD можно включить детальное логирование компилятора шейдеров/появления «pipeline compilation» — это покажет, откуда берутся паузы.
   - Посмотрите на загрузку GPU: если GPU постоянно загружен, а кадры «подскакивают», причина на CPU/синхронизации; если GPU простаивает во время spike — скорее сборка/компиляция.
7. Контролируемый replay:
   - Запишите Vulkan API stream в одном форке (gfxreconstruct), воспроизведите на другом или в тестовой среде; если поведение меняется — причина в runtime (драйвер), если идентично — причина в эмуляторе.
8. Сравните API‑взаимодействие: difference in number of draws, descriptors updates, memory barriers, vkCmdPipelineBarrier — большие различия укажут на разные стратегии.

C) Частые «ловушки», которые тоже проверить:
- Один форк может собираться в Debug/RelWithDebInfo и иметь дополнительные asserts/locks.
- Различия в версий сторонних библиотек (SPIRV‑tools, shader translators) — это влияет на генерируемый SPIR‑V и поведение драйвера.
- Различия в порядке bind/transition: лишние pipeline barriers и layout transitions дорого обходятся.
- Разное использование secondary command buffers и multithreaded recording.
- Неправильное использование памяти (частые vkMap/unmap, нестыковки с host coherent), что вызывает дополнительные flush/invalidate.

Рекомендованная диагностическая процедура (пошагово)
1. Убедитесь, что все форки запущены в одинаковой конфигурации сборки (Release, те же опции компилятора). Если возможно пересоберите сами.
2. Включите в каждом форке подробное логирование времени (логировать время на: сборку command buffers, submit, ожидание fence, время работы worker‑threads компиляции шейдеров).
3. Запишите timeline (Nsight Systems) на 30–60 секунд в каждом форке, при тех же условиях. Сравните периоды ожидания и spikes.
4. Сделайте capture одной проблемной сценки в RenderDoc и сравните drawlist и pipeline states между форками.
5. Попробуйте запустить форки с отключённым асинхронным компилем (если есть) — если различия исчезают, виновата именно асинхронная логика.
6. Включите driver‑level логирование компиляции шейдеров (см. документацию поставщика драйвера) и посмотрите, появляются ли вызовы компиляции/линковки во время микрофризов.
7. Если возможно, сделайте трассировку API (gfxreconstruct) и воспроизведите её в «чистом» окружении/инструменте — это поможет отделить поведение клиента (эмулятора) от поведения драйвера.

Краткие рекомендации/выводы
- Различия вполне объяснимы разными реализациями асинхронной компиляции и синхронизации — обе подсистемы сильно влияют на latency и стабильность frametime.
- Чтобы локализовать виновника, нужны timeline‑записи и трассировки API; они однозначно покажут: где CPU ждёт/блокируется, и когда драйвер выполняет длительные операции (JIT шейдеров).
- Начните с Nsight Systems/RenderDoc + логов из самих форков, затем углубляйтесь driver‑side профайлингом.

Если хотите, могу:
- подсказать конкретные команды/настройки для Nsight/RenderDoc на вашей платформе (укажите GPU/ОС), или
- разобрать выводы одной такой timeline‑сессии, если прикрепите её (скриншоты/трейсы).
Похожие вопросы