Skip to content

Latest commit

 

History

History
87 lines (57 loc) · 9.05 KB

RenderGraph-ru.md

File metadata and controls

87 lines (57 loc) · 9.05 KB

Синхронизации без рендер графа

Когда рендер кадра состоит из заранее известных проходов, то управлять состояниями ресурсов можно и вручную.

Первым проходом должно быть обновление данных на GPU.
Все копирования и обновление юниформ должно быть в этом проходе, чтоб избавиться от чередования draw -> transfer -> draw.

Далее идут проходы рисования и вычислений.
Предполагается, что все неизменяемые ресурсы уже находятся в том состоянии, в котором они используются в дескрипторах. Остаются синхронизации для изменяемых ресурсов (Attachment, StorageImage, StorageBuffer), их легко отслеживать и синхронизировать вручную, для проверки корректности есть способы.

Синхронизации между очередями.
Есть 2 подхода:

  1. Сделать ресурсы общими для всех очередей (VK_SHARING_MODE_CONCURRENT), тогда достаточно сделать синхронизации семафорами, чтобы избежать одновременной записи или чтения и записи (data race), в движке это делается через CommandBatch::AddInputDependency (CommandBatch &). Минус этого подхода - на AMD на общих ресурсах не включается компрессия рендер таргетов (DCC), что снижает производительность.
    Пример: Test_RG_AsyncCompute1.cpp
  2. Явно передавать ресурсы между очередями (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.