четверг, 30 августа 2018 г.

FrameGraph. часть 1

  Здесь будет рассказано о функциях фреймграфа: управление памятью, управление ресурсами, оптимизация кадра и тд.

Стримминг.
  В vulkan для наилучшей производительности все ресурсы должны создаваться на device local memory, поэтому для доступа к ним нужно делать промежуточные буферы, которые доступны для изменения на хосте (host visible). Пользователь фреймграфа ничего не должен задумываться об этом, просто сказал скопировать данные и фреймграф копирует. Из device local memory обратно в host visible memory будет немного сложнее - нужно дождаться когда команда копирования выполнится (обычно задержка в 2-3 кадра) и только потом дергается ивент, что данные загрузились и можно их использовать.


RenderPass.
  Весьма полезная вещь, но создает некоторые сложности - конфигурировать рендер пассы и сабпассы в ручную очень неудобно, особенно когда рисование может меняться и некоторые сабпассы пропускаются. Фреймграф обладая подробной информацией о кадре может сам сформировать наилучший рендер пасс. Чтобы постоянно не пересоздавать пайпланы (так как они зависят от конкретного рендер пасса), фреймграф должен подбирать наиболее общий тип рендер пасса, пусть в нем и будут пустые сабпассы. Для оптимизации неплохо бы сохранять кэш от предыдущих запусков и использовать его для ускорения последующих запусков.


Pipeline.
  Рендер пассы уже скрыты от пользователя, следовательно и сами пайплайны должны быть скрыты, они автоматически создаются перед рисованием (чаще всего берутся из кэша). дополнительная фича - возможность подменять шейдеры для наилучшего соответствия рендер пассам, например: несколько рендер пассов объединились в один с сабпассами, соответственно чтение из рендер таргета с предыдущего сабпасса должно быть input attachment. Добиться такого можно например при компиляции шейдера в SPIRV, на вход подавать шейдер, где чтение из всех возможных рендер таргетов сделано через input attachment, а при компиляции генерировать различные варианты кода, где input attachment будут заменяться на texelFetch(..., ivec2(gl_FragCoord.xy), 0).
  Создание пайплайнов во время рисования кадра может сильно снизить производительность. Для оптимизации vulkan дает возможность сохранять кэш пайплайнов для ускорения следующих запусков программы, кроме этого фреймграф может возвращать свои комбинации наиболее часто используемых пайплайнов и рендер пассов, которые желательно создавать при старте программы. Дополнительную оптимизацию может дать наследование пайплайнов, но это зависит от вендора, например ARM Mali это не поддерживает.


Descriptor Set.
  В работе с descriptor set есть свои особенности - обновить их можно до первого использования в command buffer, а затем надо дождаться когда command buffer выполнится и будет удален или переиспользован. Но пользователю не нужно задумываться об этом, фреймграф сам записывает command buffer'ы и знает в какой момент обновить descriptor set, а когда создать новый, например потому что пользователь забиндил другой uniform buffer и использовал тот же descriptor set в другой команде рисования. Управление памятью descriptor pool'а также входит в задачи фреймграфа.
  Для оптимизации тут есть одна особенность - динамические смещения для uniform и storage buffer, где смещения задаются при бинде descriptor set'ов, а не при их обновлении. Это позволит уменьшить количество самих descriptor sets.
  Желательно чтобы компилятор шейдеров в SPIRV мог оптимизировать descriptor set layout'ы, это позволит использовать одинаковые descriptor set'ы для разных пайплайнов.


Pipeline Barrier.
  Фреймграф имеет полную информацию о кадре, соответственно ничего не мешает ему выставлять зависимости между ресурсами. Но тут появляется нагрузка на процессор - обойти тысячи команд рисования, обойти их descriptor sets, чтобы получить все ожидаемые состояния ресурсов и при необходимости выставить барьеры, тут нужна оптимизация: только малая часть ресурсов меняется за кадр, поэтому если ресурс впоследствии не будет меняться, то ему выставляется immutable флаг, записывается барьер и ресурс больше не проверяется.
  Для отладки можно заменять конкретные барьеры по ресурсам на execution barrier или memory barrier, что немного (или много) снизит производительность, но позволит исключить проблемы с барьерами из списка потенциальных багов, что удобно.
  Есть и дополнительные возможности по анализу кадра, например по барьерам можно увидить повторную запись в память, которая отбросит предыдущий результат, значит работа была выполнена впустую и, возможно, это ошибка.
  Также барьерами можно заменять subpass dependency в рендер пассах, что минимизирует количество рендер пассов и соответственно пайплайнов, но может немного снизить производительность.


Queue.
  Рекомендуют держать несколько очередей:
Graphics - основная очередь рисования.
Async compute - предполагается, что есть часть команд на compute шейдере (например постпроцессинг), которые лучше выполнять в отдельной очереди, чтобы не вызывать частые переключения graphics -> compute -> graphics, это дает небольшой прирост производительности. Но Graphics и AsyncCompute очереди имеют разный queue family, что требует выполнить queue family ownership transfer операцию и использовать семафор для синхронизации между очередями. AMD и ARM позволяют делать queue present из AsyncCompute очереди, что удобно для постпроцессов, а в NVidia такой возможности нет из-за чего придется передавать результат обратно в Graphics очередь и там уже делать презент, целесообразность такого подхода весьма сомнительна.
PCI-E Transfer - отдельная очередь для тяжелых операций копирования из host visible памяти в device local, например для загрузки текстур или подгрузки нулевого мипмэпа (который занимает 66% памяти всей текстуры), для копирования небольших объемов данных лучше использовать Graphics очередь. Так как transfer queue и graphics queue имеют разный queue family, то нужно делать захват и освобождение ресурса, это также снижает производительность.


Command Buffer.
  Особенность command buffer'а - он привязан к конкретной очереди (на самом деле command pool привязан к очереди, а command buffer к пулу), его можно переиспользовать только после того как он выполнится на девайсе - нужно ожидать fence. Можно хранить ресуры, что хорошо для производительности при повторном использовании, а можно сбрасывать, что сэкономит память, с этим стоит поэкспериментировать.
  Рекомендуют разбивать кадр на несколько command buffer'ов и сразу после заполнения сабмитить их, это снизит нагрузку. Также некоторые реализации копируют secondary command buffer в primary, что несколько снижает их эффективность.



Memory Manager.
  Как всегда, управление памятью достаточно сложная задача. Есть несколько логических типов памяти:
HostVisibleAllocator - эта память выделяется для стриминга из RAM в VRAM и обратно. При формировании кадра память маппится и в нее пишутся данные, перед началом рисования первого command buffer память надо сделать видимой для GPU, для этого делается flush и вставляется memory barrier. После рисования кадра содержимое памяти не важно, все буферы, на которые забиндина память перезаписываются.
DedicatedAllocator - оптимизация GPU памяти, когда один memory object биндится только к конкретному image или buffer object. В некоторых случаях обещают до 15% увеличение производительности, но лучше тестировать на конкретном рендерере, поэтому использовать или нет dedicated allocation бедет решать фреймграф, но руководствуясь пользовательскими настройками. В vulkan есть ограничение на количество аллокаций (гарантированный максимум 4096), а при dedicated allocation каждый раз нужно создавать новый memory object, что снижает количество доступных аллокаций, а также приводит к сильному снижению производительности, так как выделение памяти работает медленно.
PoolAllocator - используется для размещения множества image или buffer ресурсов без пересечения их памяти, позволяет удалять ресурсы и переиспользовать их память, что очень хорошо для производительности, так как нет частых аллокаций. Негативная сторона такого подхода - фрагментация памяти, это лечится например дефрагментациями в рантайме, либо несколькими аллокаторами под разные размеры внутренних блоков.
TransientAllocator - идея основана на презентации Dice. Суть в том, что во время рисования кадра есть множество временных ресурсов (чаще всего рендер таргеты), их память используется только короткое время, поэтому, имея представление всего кадра, можно переиспользовать один и тот же рендер таргет в разных местах, либо переиспользовать память если у рендер таргетов отличается формат или размеры, но примерно совпадает размер данных.

Комментариев нет:

Отправить комментарий