Когда рендер кадра состоит из заранее известных проходов, то управлять состояниями ресурсов можно и вручную.
Первым проходом должно быть обновление данных на GPU.
Все копирования и обновление юниформ должно быть в этом проходе, чтоб избавиться от чередования draw -> transfer -> draw.
Далее идут проходы рисования и вычислений.
Предполагается, что все неизменяемые ресурсы уже находятся в том состоянии, в котором они используются в дескрипторах.
Остаются синхронизации для изменяемых ресурсов (Attachment, StorageImage, StorageBuffer), их легко отслеживать и синхронизировать вручную, для проверки корректности есть способы.
Синхронизации между очередями.
Есть 2 подхода:
- Сделать ресурсы общими для всех очередей (
VK_SHARING_MODE_CONCURRENT
), тогда достаточно сделать синхронизации семафорами, чтобы избежать одновременной записи или чтения и записи (data race), в движке это делается черезCommandBatch::AddInputDependency (CommandBatch &)
. Минус этого подхода - на AMD на общих ресурсах не включается компрессия рендер таргетов (DCC), что снижает производительность.
Пример: Test_RG_AsyncCompute1.cpp - Явно передавать ресурсы между очередями (queue ownership transfer). Внутри рендер таска это сложнее отслеживать, поэтому такие барьеры удобнее вынести в интерфейс
CommandBatch
, так появился методCommandBatch::DeferredBarriers()
иinitial, final
параметры при создании рендер таска. Теперь управление перемещением ресурсов происходит на этапе планирования батчей команд.
Пример: Test_RG_AsyncCompute2.cpp
Работает поверх существующих контекстов.
Добавлено только отслеживание состояний ресурсов в пределах RenderTask
и автоматическое перемещение их в нужное состояние.
Начальное и конечное состояние ресурса это либо дефолтное, либо задается вручную на этапе планирования рендер графа, иногда это добавляет ненужные синхронизации, но потери на них минимальные, если не приводят к декомпресии рендер таргетов.
Точно также создаются батчи, но теперь через builder паттерн, где можно указать какие ресурсы будут использоваться в батче и их начальное/конечное состояние. Если ресурс используется только в одной GPU-очереди (VkQueue), то указывать его не обязательно. Но если ресурс используется в нескольких очередях, то требуется явно добавить его в батч, тогда внутри вставятся все необходимые синхронизации. Для каждого рендер таска также можно указать начальное и конечное состояние ресурса, это позволит оптимизировать синхронизации между тасками.
// Ожидаем когда кадр -1 отправится на GPU и когда кадр -2 завершит выполнение на GPU.
// Пока идет ожидание внутри выполняются задачи из переданого списка очередей.
rg.WaitNextFrame(...);
// начинаем новый кадр
rg.BeginFrame();
// создаем батч, в нем будем использовать 'image' для чтения в фрагментном шейдере
auto batch_gfx = rg.CmdBatch( EQueueType::Graphics, {"graphics batch"} )
.UseResource( image, EResourceState::ShaderSample | EResourceState::FragmentShader )
.Begin();
// создаем второй батч, 'image' используется для чтения/записи в вычислительном шейдере
auto batch_ac = rg.CmdBatch( EQueueType::AsyncCompute, {"compute batch"} )
.UseResource( image, EResourceState::ShaderStorage_RW | EResourceState::ComputeShader )
.Begin();
// при вызове 'UseResource()' неявно устанавливается зависимость 'batch_gfx -> batch_ac'
AsyncTask gfx_task = batch_gfx.Task<GraphicsTask>( Tuple{...}, {"graphics task"} ).SubmitBatch().Run();
AsyncTask comp_task = batch_ac .Task<ComputeTask >( Tuple{...}, {"async compute task"} ).SubmitBatch().Run( Tuple{gfx_task} );
// 'SubmitBatch()' помечает задачу как последнюю, тогда вызов 'RenderTask::Execute(cmdbuf)' также
// добавит батч в очередь на отправку на GPU (submit), иначе отправку батча нужно сделать
// через отдельный таск - 'CommandBatch::SubmitAsTask()'.
AsyncTask end = rg.EndFrame( Tuple{ gfx_task, comp_task });
В рендер графе порядок создания батчей имеет значение, поэтому желательно создать все батчи в одном потоке.
Пример: Test_RG_AsyncCompute3.cpp
Текущая реализация рендер графа не делает перестановку и оптимизацию, только расставляет синхронизации. С помощью профилировщиков можно обнаружить медленные места и вручную расставить синхронизации и переставить команды для лучшего распараллеливания.
Потери производительности на стороне ЦП минимальны, но добавляет редкие кэш-промахи при доступе к состояниям ресурсов.
Исходники: RGCommandBatch.h, RenderGraph.h
Для этого в движке есть логирование команд (проект VulkanSyncLog
), который выдает читаемый лог вызовов Vulkan команд и его результат не меняется в зависимости от запусков, что позволяет следить за изменениями.
Но все синхронизации придется один раз вручную проверить на корректность.
Пример лога
Другой вариант - запустить vkconfig и включить полную валидацию синхронизаций - Synchronization preset.
Guide to Vulkan Synchronization Validation
Либо из кода, при инициализации движка передать флаг EDeviceValidation::SynchronizationPreset
.