paint-brush
Простой способ разработать собственный плагин Apple Metal и интегрировать его в DaVinci Resolveк@denissvinarchuk
3,589 чтения
3,589 чтения

Простой способ разработать собственный плагин Apple Metal и интегрировать его в DaVinci Resolve

к Denis Svinarchuk13m2024/03/13
Read on Terminal Reader

Слишком долго; Читать

OFX, также известный как OFX Image Processing API, представляет собой открытый стандарт для создания 2D-визуальных эффектов и видеокомпозиции. Он работает по модели разработки приложений, подобной плагинам. По сути, он выполняет функции как Хоста — приложения, предоставляющего набор методов, так и Плагина — приложения или модуля, реализующего этот набор. Эта конфигурация предлагает возможность неограниченного расширения функциональности хост-приложения.
featured image - Простой способ разработать собственный плагин Apple Metal и интегрировать его в DaVinci Resolve
Denis Svinarchuk HackerNoon profile picture

OFX, он же OFX Image Processing API , представляет собой открытый стандарт для создания 2D-визуальных эффектов и видеокомпозиции. Он работает по модели разработки приложений, подобной плагинам. По сути, он выполняет функции как Хоста — приложения, предоставляющего набор методов, так и Плагина — приложения или модуля, реализующего этот набор.


Эта конфигурация предлагает возможность неограниченного расширения функциональности хост-приложения.

DaVinci Resolve и металл

Такие приложения, как Final Cut X и DaVinci Resolve Studio, начиная с версии 16, полностью поддерживают конвейеры Apple Metal. Подобно OpenCL и Cuda, в случае OFX вы можете получить дескриптор или обработчик очереди команд для конкретной платформы. Хост-система также берет на себя ответственность за выделение пула таких очередей и балансировку вычислений по ним.


Более того, он помещает исходные и целевые данные клипов изображений в память графического процессора, что значительно упрощает разработку расширяемых функций.

Поддержка версий OFX в Resolve

С Resolve дела обстоят немного сложнее. DaVinci объявляет о поддержке OFX v1.4, хотя и с некоторыми ограничениями. В частности, некоторые методы работы с функциями интерфейса недоступны для использования. Чтобы определить, какой метод доступен, OFX позволяет вам проверить поддерживаемый пакет с помощью запросов «ключ-значение».


Методы публикации в коде плагина основаны на вызовах C. Но мы будем использовать оболочку OpenFXS C++, адаптированную для C++17. Для удобства я собрал всё в один репозиторий: dehancer-external, взятый из открытого проекта Dehancer .

Концепция OFXS

В этом проекте я буду использовать OpenFXS, расширение C++ для OpenFX, которое изначально было написано Бруно Николетти и со временем стало популярным в коммерческих проектах и проектах обработки видео с открытым исходным кодом.


Исходный OpenFXS не был адаптирован к современным диалектам C++, поэтому я обновил его, чтобы сделать совместимым с C++17 .


OFX и, следовательно, OFXS — это автономный программный модуль, динамически загружаемый хост-программой. По сути, это динамическая библиотека, которая загружается при запуске основного приложения. OpenFXS, как и OFX, должен публиковать сигнатуры методов. Следовательно, мы используем один метод C из кода.


Чтобы начать разработку в OpenFXS, вам необходимо согласиться с несколькими общими наборами классов, которые используются для создания новых функций в вашем приложении. Обычно в новом проекте вам необходимо наследовать эти классы и реализовать или переопределить некоторые виртуальные методы.


Чтобы создать собственный плагин в хост-системе, давайте начнем с ознакомления со следующими общедоступными классами и тем же методом:


  • OFX::PluginFactoryHelper — это базовый шаблон для создания набора структур данных и панели управления плагина (хотя его можно оставить пустым). Унаследованный класс создает одноэлементный объект, регистрирующий в хост-системе набор параметров и пресетов, с помощью которых разработчик регистрирует свой модуль;


  • OFX::ParamSetDescriptor — базовый класс-контейнер для создания и хранения свойств структуры;


  • OFX::ImageEffectDescriptor — контейнер свойств, используемый при манипулировании графическими данными при вызове процедур обработки данных. Используется хост-приложением для сохранения контекста параметров обработки во внутренней базе данных и работы со свойствами плагина, определенными для каждого его экземпляра;


  • OFX::ParamSet — набор настроек, позволяющий манипулировать зарегистрированной структурой данных;


  • OFX::ImageEffect — набор настроек эффектов на графические данные, унаследованный от OFX::ParamSet;


  • OFX::MultiThread::Processor — в дочернем классе необходимо реализовать обработку потока данных: изображений или видео;


  • OFX::Plugin::getPluginIDs - метод регистрации плагина (фабрики) в хост-приложении;

Ложный цвет

Одной особенностью, отличающей процесс видеосъемки от простого захвата изображения на фото, является динамическая смена сцен и освещение как сцен в целом, так и участков на изображении. Это определяет способ управления экспозицией в процессе съемки.


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


Этот режим иногда называют «хищником» или режимом ложного цвета. Шкалы обычно относятся к шкале IRE.


Такой монитор позволяет видеть зоны экспозиции и избежать существенных ошибок при настройке параметров съемки камеры. Нечто похожее по смыслу используется при экспонировании в фотографии – зонирование по Адамсу, например.


Можно измерить экспонометром конкретную цель и посмотреть, в какой зоне она находится, причем в реальном времени мы видим зоны, аккуратно тонированные для удобства восприятия.


Количество зон определяется задачами и возможностями монитора управления. Например, монитор, используемый с камерами Arri Alexa , может включать до 6 зон.


Версия ПО «Хищник» с 16 зонами


Добавление расширений

Прежде чем приступить к примеру, нам нужно добавить несколько простых прокси-классов для реализации OpenFXS в качестве платформы для обработки исходных данных, таких как текстуры металла. Эти классы включают в себя:


  • Metalling::Image : прокси-класс для данных клипов OFX.


  • Metalling::Image2Texture : Функтор для передачи данных из буфера клипа в текстуру металла. Из DaVinci можно извлечь в плагин буфер любой структуры и упаковки значений каналов изображения, и он должен быть возвращен в аналогичном виде.


    Чтобы упростить работу с форматом потока в OFX, вы можете попросить хост заранее подготовить данные определенного типа. Я буду использовать поплавки, упакованные в RGBA — красный/зеленый/синий/альфа.


  • Metalling::ImageFromTexture : обратный функтор для преобразования потока в буфер хост-системы. Как видите, есть потенциал для существенной оптимизации вычислений, если научить вычислительные ядра Metal работать не с текстурой, а напрямую с буфером.


Мы наследуем базовые классы OFXS и пишем свою функциональность, не вдаваясь в подробности работы ядра Metal:


  • Metalling::falsecolor::Processor : Здесь мы реализуем преобразование потока и запускаем обработку.


  • Metalling::falsecolor::Factory : это будет наша специфическая часть описания пакета для плагина. Нам необходимо реализовать несколько обязательных вызовов, связанных с настройкой структуры, и создать экземпляр класса OFX::ImageEffect со специфическим функционалом, который в реализации мы разделяем на два подкласса: Interaction и Plugin.


  • Metalling::falsecolor::Interaction : Реализация интерактивной части работы с эффектами. По сути, это реализация только виртуальных методов из OFX::ImageEffect, связанных с обработкой изменений параметров плагина.


  • Metalling::falsecolor::Plugin : реализация рендеринга потоков, то есть запуск Metalling::Processor.


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


  • Metalling::Function : базовый класс, который скрывает работу с очередью команд Metal. Основным параметром будет имя ядра в коде MSL и исполнитель вызова ядра.


  • Metalling:Kernel : общий класс для преобразования исходной текстуры в целевую текстуру, расширяющий функцию для простой установки параметров для вызова ядра MSL.


  • Metalling::PassKernel : Обход ядра.


  • Metalling::FalseColorKernel : Наш основной функциональный класс, эмулятор «хищника», который постеризует (понижает разрешение) до заданного количества цветов.


Код ядра для режима «хищник» может выглядеть так:

 static constant float3 kIMP_Y_YUV_factor = {0.2125, 0.7154, 0.0721}; constexpr sampler baseSampler(address::clamp_to_edge, filter::linear, coord::normalized); inline float when_eq(float x, float y) {  return 1.0 - abs(sign(x - y)); } static inline float4 sampledColor(        texture2d<float, access::sample> inTexture,        texture2d<float, access::write> outTexture,        uint2 gid ){  float w = outTexture.get_width();  return mix(inTexture.sample(baseSampler, float2(gid) * float2(1.0/(w-1.0), 1.0/float(outTexture.get_height()-1))),             inTexture.read(gid),             when_eq(inTexture.get_width(), w) // whe equal read exact texture color  ); } kernel void kernel_falseColor(        texture2d<float, access::sample> inTexture [[texture(0)]],        texture2d<float, access::write> outTexture [[texture(1)]],        device float3* color_map [[ buffer(0) ]],        constant uint& level [[ buffer(1) ]],        uint2 gid [[thread_position_in_grid]]) {  float4 inColor = sampledColor(inTexture,outTexture,gid);  float luminance = dot(inColor.rgb, kIMP_Y_YUV_factor);  uint     index = clamp(uint(luminance*(level-1)),uint(0),uint(level-1));  float4   color = float4(1);  if (index<level)    color.rgb = color_map[index];  outTexture.write(color,gid); }


Инициализация плагина OFX

Начнем с определения класса imetalling::falsecolor::Factory. В этом классе мы зададим единственный параметр — состояние монитора (включен или выключен). Это необходимо для нашего примера.

Мы унаследуем от OFX::PluginFactoryHelper и перегрузим пять методов:


  • load() : этот метод вызывается для глобальной настройки экземпляра при первой загрузке плагина. Перегрузка этого метода необязательна.


  • unload() : этот метод вызывается, когда экземпляр выгружается, например, для очистки памяти. Перегрузка этого метода также необязательна.


  • описать(ImageEffectDescriptor&) : это второй метод, который вызывает хост OFX при загрузке плагина. Он виртуальный и должен быть определен в нашем классе. В этом методе нам нужно установить все свойства плагина, независимо от типа его контекста. Дополнительные сведения о свойствах см. в коде ImageEffectDescriptor .


  • описатьInContext(ImageEffectDescriptor&,ContextEnum) : Подобно методу describe , этот метод также вызывается при загрузке плагина и должен быть определен в нашем классе. Он должен определять свойства, связанные с текущим контекстом.


    Контекст определяет тип операций, с которыми работает приложение, например фильтр, рисование, эффект перехода или таймер повтора кадра в клипе.


  • createInstance(OfxImageEffectHandle, ContextEnum) : это самый важный метод, который мы перегружаем. Мы возвращаем указатель на объект типа ImageEffect . Другими словами, наш imetalling::falsecolor::Plugin , в котором мы определили все функциональные возможности, как в отношении пользовательских событий в хост-программе, так и в отношении рендеринга (преобразования) исходного кадра в целевой:
 OFX::ImageEffect *Factory::createInstance(OfxImageEffectHandle handle,OFX::ContextEnum) {     return new Plugin(handle);   }


Обработка событий

На этом этапе, если вы скомпилируете связку с модулем OFX, плагин уже будет доступен в хост-приложении, а в DaVinci его можно будет загрузить на узел коррекции.


Однако для полноценной работы с экземпляром плагина необходимо определить как минимум интерактивную часть и часть, связанную с обработкой входящего видеопотока.


Для этого мы наследуем класс OFX::ImageEffect и перегружаем виртуальные методы:


  • измененоParam(const OFX::InstanceChangedArgs&, const std::string&) — этот метод позволяет нам определить логику обработки события. Тип события определяется значением OFX::InstanceChangedArgs::reason и может быть: eChangeUserEdit, eChangePluginEdit, eChangeTime — событие произошло в результате редактирования свойства пользователем, изменения в плагине или хост-приложении или в результате изменения временной шкалы.


    Второй параметр указывает имя строки, которое мы определили на этапе инициализации плагина, в нашем случае это один параметр: false_color_enabled_check_box .


  • isIdentity(...) — этот метод позволяет нам определить логику реагирования на событие и вернуть состояние, которое определяет, изменилось ли что-то и имеет ли смысл рендеринг. Метод должен возвращать false или true. Это способ оптимизировать и уменьшить количество ненужных вычислений.


Реализацию интерактивного взаимодействия с OFX вы можете прочитать в коде Interaction.cpp . Как видите, мы получаем указатели на клипы: исходный и область памяти, в которую мы поместим целевое преобразование.

Реализация запуска рендеринга

Добавим еще один логический слой, на котором определим всю логику запуска трансформации. В нашем случае это пока единственный метод переопределения:


  • render(const OFX::RenderArguments& args) — Здесь вы можете узнать свойства клипов и решить, как их визуализировать. Также на этом этапе нам становится доступна очередь команд Metal и некоторые полезные атрибуты, связанные с текущими свойствами временной шкалы.

Обработка

На этапе запуска нам стал доступен объект с полезными свойствами: у нас есть как минимум указатель на видеопоток (точнее, область памяти с данными изображения кадра), и, самое главное, очередь команд Metal.


Теперь мы можем создать универсальный класс, который приблизит нас к простой форме повторного использования кода ядра. В расширении OpenFXS уже есть такой класс: OFX::ImageProcessor; нам просто нужно его перегрузить.


В конструкторе у него есть параметр OFX::ImageEffect, т.е. в нем мы получим не только текущее состояние параметров плагина, но и все необходимое для работы с графическим процессором.


На этом этапе нам просто нужно перегрузить методprocessImagesMetal() и инициировать обработку уже реализованных на Metal ядер.

 Processor::Processor(            OFX::ImageEffect *instance,            OFX::Clip *source,            OFX::Clip *destination,            const OFX::RenderArguments &args,            bool enabled    ) :            OFX::ImageProcessor(*instance),            enabled_(enabled),            interaction_(instance),            wait_command_queue_(false),            /// grab the current frame of a clip from OFX host memory            source_(source->fetchImage(args.time)),            /// create a target frame of a clip with the memory area already specified in OFX            destination_(destination->fetchImage(args.time)),            source_container_(nullptr),            destination_container_(nullptr)    {      /// Set OFX rendering arguments to GPU      setGPURenderArgs(args);      /// Set render window      setRenderWindow(args.renderWindow);      /// Place source frame data in Metal texture      source_container_ = std::make_unique<imetalling::Image2Texture>(_pMetalCmdQ, source_);      /// Create empty target frame texture in Metal      destination_container_ = std::make_unique<imetalling::Image2Texture>(_pMetalCmdQ, destination_);      /// Get parameters for packing data in the memory area of the target frame      OFX::BitDepthEnum dstBitDepth = destination->getPixelDepth();      OFX::PixelComponentEnum dstComponents = destination->getPixelComponents();      /// and original      OFX::BitDepthEnum srcBitDepth = source->getPixelDepth();      OFX::PixelComponentEnum srcComponents = source->getPixelComponents();      /// show a message to the host system that something went wrong      /// and cancel rendering of the current frame      if ((srcBitDepth != dstBitDepth) || (srcComponents != dstComponents)) {        OFX::throwSuiteStatusException(kOfxStatErrValue);      }      /// set in the current processor context a pointer to the memory area of the target frame      setDstImg(destination_.get_ofx_image());    }    void Processor::processImagesMetal() {      try {        if (enabled_)          FalseColorKernel(_pMetalCmdQ,                           source_container_->get_texture(),                           destination_container_->get_texture()).process();        else          PassKernel(_pMetalCmdQ,                           source_container_->get_texture(),                           destination_container_->get_texture()).process();        ImageFromTexture(_pMetalCmdQ,                         destination_,                         destination_container_->get_texture(),                         wait_command_queue_);      }      catch (std::exception &e) {        interaction_->sendMessage(OFX::Message::eMessageError, "#message0", e.what());      }    }


Создание проекта

Для сборки проекта вам понадобится CMake версии не ниже 3.15. Дополнительно вам потребуется Qt5.13, который поможет легко и удобно собрать комплект с установщиком плагина в системном каталоге. Чтобы запустить cmake, вы должны сначала создать каталог сборки.


После создания каталога сборки вы можете выполнить следующую команду:


 cmake -DPRINT_DEBUG=ON -DQT_INSTALLER_PREFIX=/Users/<user>/Develop/QtInstaller -DCMAKE_PREFIX_PATH=/Users/<user>/Develop/Qt/5.13.0/clang_64/lib/cmake -DPLUGIN_INSTALLER_DIR=/Users/<user>/Desktop -DCMAKE_INSTALL_PREFIX=/Library/OFX/Plugins .. && make install 


Ваш личный «хищник»


После этого установщик с именем IMFalseColorOfxInstaller.app появится в каталоге, указанном вами в параметре PLUGIN_INSTALLER_DIR . Давайте продолжим и запустим его! После успешной установки вы можете запустить DaVinci Resolve и начать использовать наш новый плагин.


Вы можете найти и выбрать его на панели OpenFX на странице цветокоррекции и добавить в качестве узла.



Рабочий ложный цвет



Внешние ссылки

  1. Код плагина False Color OFX
  2. Ассоциация открытых эффектов
  3. Загрузите DaVinci Resolve — версию заголовочного файла OFX и код библиотеки OFXS в разделе Resolve + примеры.