Компьютерная графика

Vulkan и Modern Graphics API

Почему шейдеры в одних играх компилируются в фоне, а в других - вызывают фризы? Почему мобильные GPU Apple работают иначе десктопных? Vulkan открывает ответы на эти вопросы, давая прямой доступ к GPU без абстракций - ценой явного управления каждым ресурсом и каждым барьером синхронизации.

  • **AAA игры на ПК (Doom Eternal, God of War):** Vulkan как основной API - предсказуемый frame time, низкий CPU overhead, полный контроль над памятью
  • **Shader compilation stutter:** именно создание VkPipeline (SPIR-V -> ISA) вызывает паузы. Игры кешируют VkPipelineCache на диск чтобы не перекомпилировать при следующем запуске
  • **Android GPU-рендеринг:** Vulkan на мобильных - тайловые GPU (Mali, Adreno) выигрывают от правильных loadOp/storeOp в render pass
  • **WebGPU в браузере:** построен поверх Vulkan/Metal/D3D12 - те же концепции descriptor sets и command buffers, но с безопасной моделью памяти

Vulkan: архитектура и инициализация

OpenGL существовал с 1992 года и накопил 30 лет легаси. Драйвер скрывал от разработчика всё: управление памятью, синхронизацию, компиляцию шейдеров - и делал это по-своему на каждом GPU. В 2016 году Khronos выпустил Vulkan: API, который говорит GPU именно то, что написано в коде - никакой магии драйвера.

Vulkan требует явной инициализации каждого объекта. Первые 500 строк кода - до первого треугольника на экране - это не баг, это осознанный дизайн. Взамен: предсказуемая производительность, полный контроль над памятью, многопоточность без гонок.

**Vulkan vs OpenGL в цифрах:** типичный OpenGL драйвер содержит ~1 млн строк кода эвристик и хаков. Vulkan-драйвер - в 10 раз меньше. Всё что OpenGL делал неявно (батчинг draw calls, управление памятью, синхронизация) - теперь явно в коде приложения.

Зачем в Vulkan явно выбирать VkPhysicalDevice и создавать VkDevice отдельно?

Command Buffers и Render Pass

В OpenGL команды отправлялись на GPU немедленно: `glDraw()` - и поехало. GPU ждал, CPU ждал, синхронизация была неявной. Vulkan работает иначе: команды сначала записываются в VkCommandBuffer (как скрипт), а потом весь буфер отправляется на GPU одним вызовом. Это позволяет записывать буферы параллельно на нескольких CPU-потоках.

**Render Pass** описывает структуру рендеринга: какие attachment'ы (color, depth) используются, как они загружаются в начале и сохраняются в конце. GPU-тайловые архитектуры (мобильные GPU: Apple M1, Adreno, Mali) используют эту информацию критически - они держат весь тайл в быстрой on-chip памяти, пока render pass не завершён.

**Secondary command buffers:** в Vulkan можно записывать командные буферы параллельно в нескольких потоках (secondary command buffers), а потом объединять их в primary buffer через vkCmdExecuteCommands. Это ключевая фича для CPU-многопоточного рендеринга без мьютексов.

Почему DONT_CARE в storeOp для depth attachment - правильный выбор в большинстве случаев?

Descriptor Sets и Layouts

Шейдер написал: `layout(set = 0, binding = 0) uniform sampler2D albedoMap`. Но откуда GPU знает, какая именно текстура там находится? В OpenGL был glUniform и glBindTexture - неявные глобальные вызовы. В Vulkan всё явно: **Descriptor Set** - это таблица ссылок на ресурсы (буферы, текстуры, сэмплеры), которую шейдер получает как аргумент.

Descriptor система трёхуровневая: **Layout** описывает структуру (типы binding'ов), **Pool** управляет памятью для descriptor'ов, **Set** - конкретный набор ресурсов. Это разделение позволяет переиспользовать layouts и эффективно аллоцировать наборы для разных объектов сцены.

**Bindless descriptors** (VK_EXT_descriptor_indexing): вместо привязки одной текстуры на draw call - привязываем массив всех текстур сцены сразу, а шейдер выбирает нужную по индексу. Это убирает CPU overhead на vkCmdBindDescriptorSets между draw calls - современный подход в движках (id Tech 7, Unreal 5).

Почему в Vulkan нельзя обновить VkDescriptorSet пока GPU его использует?

Синхронизация: Semaphores и Fences

CPU и GPU работают асинхронно: vkQueueSubmit возвращает управление немедленно, не дожидаясь завершения GPU. Это хорошо для производительности, но создаёт проблему: когда CPU может обновить uniform buffer или начать следующий кадр? Неправильная синхронизация - самый распространённый источник артефактов и вылетов в Vulkan.

Vulkan предоставляет три примитива. **VkFence** - CPU-GPU сигнал: CPU ждёт vkWaitForFences пока GPU не сигнализирует. **VkSemaphore** - GPU-GPU сигнал внутри очереди: один submit ждёт завершения другого. **VkEvent / Pipeline Barrier** - синхронизация внутри command buffer между стадиями pipeline.

**Validation Layers:** в debug-режиме VK_LAYER_KHRONOS_validation ловит ошибки синхронизации - race conditions, неверные layout переходы, использование ресурса без барьера. В продакшне слой отключается, overhead нулевой.

vkQueueSubmit ждёт завершения GPU перед возвратом

vkQueueSubmit возвращает управление немедленно - команды только поставлены в очередь GPU. Для ожидания нужен явный VkFence.

Если бы vkQueueSubmit блокировал CPU, не было бы смысла в асинхронном GPU-рендеринге. CPU и GPU работают параллельно - именно это даёт >60fps. Синхронизация нужна только в конкретных точках (обновление буфера, получение результата).

CPU хочет обновить uniform buffer с матрицами. Какой примитив синхронизации гарантирует, что GPU уже прочитал старые данные?

Vulkan и Modern Graphics API

  • Instance -> PhysicalDevice -> Device -> Queue: явная иерархия без скрытой инициализации
  • Command Buffer записывает команды заранее - vkQueueSubmit отправляет весь пакет асинхронно
  • Render Pass с loadOp/storeOp даёт GPU информацию для оптимизации тайловой архитектуры
  • Descriptor Set - явная таблица ресурсов для шейдера: убирает глобальные bind calls OpenGL
  • VkFence: CPU-GPU синхронизация. VkSemaphore: GPU-GPU. Pipeline Barrier: внутри GPU
  • Double/triple buffering frame resources - ключ к отсутствию stall'ов CPU-GPU

Связанные темы

Vulkan - реализация GPU pipeline. Шейдеры (SPIR-V) и концепции растеризации из предыдущих уроков здесь становятся конкретными объектами API.

  • Шейдеры: GLSL, HLSL, WGSL — SPIR-V шейдеры загружаются через vkCreateShaderModule и входят в VkPipeline
  • GPU Pipeline и растеризация — Весь pipeline (input assembly, rasterization, blending) описывается в VkGraphicsPipelineCreateInfo

Вопросы для размышления

  • Почему тройная буферизация (три frame slots) лучше двойной для производительности, но хуже для input latency?
  • Как bindless descriptors меняют архитектуру рендерера: что упрощается, что усложняется?
  • Если validation layers ловят ошибки синхронизации в debug, почему артефакты могут появляться только в релизной сборке?

Связанные уроки

  • arch-08-memory-hierarchy
Vulkan и Modern Graphics API

0

1

Войти