paint-brush
Uma maneira fácil de desenvolver seu próprio plug-in Apple Metal e integrá-lo ao DaVinci Resolvepor@denissvinarchuk
3,614 leituras
3,614 leituras

Uma maneira fácil de desenvolver seu próprio plug-in Apple Metal e integrá-lo ao DaVinci Resolve

por Denis Svinarchuk13m2024/03/13
Read on Terminal Reader

Muito longo; Para ler

OFX, também conhecido como OFX Image Processing API, é um padrão aberto para criação de efeitos visuais 2D e composição de vídeo. Ele opera em um modelo de desenvolvimento de aplicativos semelhante a um plugin. Essencialmente, ele serve tanto como Host - um aplicativo que fornece um conjunto de métodos, quanto como Plug-in - um aplicativo ou módulo que implementa esse conjunto. Essa configuração oferece potencial para expansão ilimitada da funcionalidade do aplicativo host.
featured image - Uma maneira fácil de desenvolver seu próprio plug-in Apple Metal e integrá-lo ao DaVinci Resolve
Denis Svinarchuk HackerNoon profile picture

OFX, também conhecido como OFX Image Processing API , é um padrão aberto para criação de efeitos visuais 2D e composição de vídeo. Ele opera em um modelo de desenvolvimento de aplicativos semelhante a um plugin. Essencialmente, ele serve tanto como Host - um aplicativo que fornece um conjunto de métodos, quanto como Plug-in - um aplicativo ou módulo que implementa esse conjunto.


Essa configuração oferece potencial para expansão ilimitada da funcionalidade do aplicativo host.

DaVinci Resolve e Metal

Aplicativos como Final Cut X e DaVinci Resolve Studio, a partir da versão 16, oferecem suporte total aos pipelines Apple Metal. Semelhante ao OpenCL e Cuda, no caso do OFX, você pode obter um descritor ou manipulador de uma fila de comandos específica da plataforma. O sistema host também assume a responsabilidade de alocar um conjunto dessas filas e equilibrar os cálculos sobre elas.


Além disso, ele coloca os dados do clipe de imagem de origem e de destino na memória da GPU, simplificando significativamente o desenvolvimento de funcionalidades extensíveis.

Suporte à versão OFX no Resolve

Com o Resolve, as coisas são um pouco mais complicadas. DaVinci anuncia suporte para OFX v1.4, embora com algumas limitações. Especificamente, alguns métodos para trabalhar com funções de interface não estão disponíveis para uso. Para determinar qual método está disponível, o OFX permite examinar o conjunto compatível por meio de consultas de chave/valor.


Os métodos de publicação no código do plugin são baseados em chamadas C. Mas usaremos o shell OpenFXS C++ adaptado para C++17. Por conveniência, compilei tudo em um repositório: dehancer-external retirado do projeto Dehancer de código aberto.

Conceito OFXS

Neste projeto, usarei OpenFXS, uma extensão C++ para OpenFX que foi originalmente escrita por Bruno Nicoletti e que se tornou popular ao longo do tempo em projetos comerciais e de processamento de vídeo de código aberto.


O OpenFXS original não foi adaptado aos dialetos C++ modernos, então eu o atualizei para torná-lo compatível com C++17 .


OFX e, consequentemente, OFXS, é um módulo de software independente que é carregado dinamicamente pelo programa host. Essencialmente, é uma biblioteca dinâmica que é carregada quando o aplicativo principal é iniciado. OpenFXS, assim como OFX, deve publicar assinaturas de métodos. Portanto, usamos um método C do código.


Para começar a desenvolver no OpenFXS, você precisa concordar com alguns conjuntos comuns de classes que são usados para criar novas funcionalidades em seu aplicativo. Normalmente, em um novo projeto, você precisa herdar dessas classes e implementar ou substituir alguns métodos virtuais.


Para criar seu próprio plugin no sistema host, vamos começar nos familiarizando com as seguintes classes públicas e o mesmo método:


  • OFX::PluginFactoryHelper é um modelo básico para criar um conjunto de estrutura de dados e painel de controle de um plugin (embora possa ser deixado vazio). A classe herdada cria um objeto singleton que registra um conjunto de parâmetros e predefinições no sistema host, com o qual o desenvolvedor registra seu módulo;


  • OFX::ParamSetDescriptor - classe contêiner base para criar e armazenar propriedades de estrutura;


  • OFX::ImageEffectDescriptor - um contêiner de propriedades usado ao manipular dados gráficos ao chamar procedimentos de processamento de dados. Utilizado pela aplicação host para salvar o contexto dos parâmetros de processamento no banco de dados interno e trabalhar com propriedades do plugin definidas para cada uma de suas instâncias;


  • OFX::ParamSet - um conjunto de configurações que permite manipular a estrutura de dados cadastrada;


  • OFX::ImageEffect - um conjunto de configurações para efeitos em dados gráficos, herdado de OFX::ParamSet;


  • OFX::MultiThread::Processor - na classe filha é necessário implementar processamento de fluxo de dados: imagens ou vídeos;


  • OFX::Plugin::getPluginIDs - método de registro de um plugin (fábrica) na aplicação host;

Cor Falsa

Um recurso que distingue o processo de gravação de vídeo da simples captura de uma imagem em uma foto é a mudança dinâmica de cenas e a iluminação das cenas como um todo e de áreas da imagem. Isso determina a forma como a exposição é controlada durante o processo de filmagem.


No vídeo digital, existe um modo de monitor de controle para operadores, no qual o nível de exposição das áreas é mapeado em um conjunto limitado de zonas, cada uma tingida com sua própria cor.


Este modo às vezes é chamado de modo "predador" ou cor falsa. As escalas são geralmente referenciadas à escala IRE.


Esse monitor permite ver as zonas de exposição e evitar erros significativos ao definir os parâmetros de disparo da câmera. Algo semelhante em significado é usado na exposição em fotografia - zoneamento de acordo com Adams, por exemplo.


Você pode medir um alvo específico com um medidor de exposição e ver em que zona ele está localizado, e em tempo real vemos as zonas, bem coloridas para facilitar a percepção.


O número de zonas é determinado pelos objetivos e capacidades do monitor de controle. Por exemplo, um monitor usado com câmeras Arri Alexa pode incorporar até 6 zonas.


Versão “predador” do software com 16 zonas


Adicionando extensões

Antes de prosseguir com o exemplo, precisamos adicionar algumas classes de proxy simples para implementar o OpenFXS como uma plataforma para processamento de dados de origem, como texturas Metal. Essas aulas incluem:


  • imetalling::Image : Uma classe proxy para dados de clipe OFX.


  • imetalling::Image2Texture : Um functor para transferir dados do buffer de clipe para uma textura Metal. Do DaVinci, você pode extrair um buffer de qualquer estrutura e empacotamento de valores de canal de imagem para o plugin, e ele deve ser retornado de forma semelhante.


    Para facilitar o trabalho com o formato de fluxo no OFX, você pode solicitar ao host que prepare dados de um tipo específico com antecedência. Usarei carros alegóricos embalados em RGBA - vermelho/verde/azul/alfa.


  • imetalling::ImageFromTexture : Um functor reverso para transformar um fluxo em um buffer do sistema host. Como você pode ver, há potencial para uma otimização significativa dos cálculos se você ensinar os núcleos de computação do Metal a trabalhar não com a textura, mas diretamente com o buffer.


Herdamos as classes base OFXS e escrevemos nossa funcionalidade sem entrar em detalhes de como o núcleo Metal funciona:


  • imetalling::falsecolor::Processor : Aqui, implementamos a transformação do fluxo e iniciamos o processamento.


  • imetalling::falsecolor::Factory : Esta será nossa parte específica da descrição do pacote do plugin. Precisamos implementar diversas chamadas obrigatórias relacionadas à montagem da estrutura e criar uma instância da classe OFX::ImageEffect com funcionalidades específicas, que dividimos em duas subclasses na implementação: Interaction e Plugin.


  • imetalling::falsecolor::Interaction : Implementação da parte interativa de trabalhar com efeitos. Essencialmente, esta é a implementação apenas de métodos virtuais do OFX::ImageEffect relacionados ao processamento de alterações nos parâmetros do plugin.


  • imetalling::falsecolor::Plugin : Implementação de renderização de thread, ou seja, lançamento de imetalling::Processor.


Além disso, precisaremos de várias classes utilitárias construídas sobre Metal para separar logicamente o código host e o código do kernel no MSL. Esses incluem:


  • imetalling::Function : Uma classe base que obscurece o trabalho com a fila de comandos Metal. O parâmetro principal será o nome do kernel no código MSL e o executor da chamada do kernel.


  • imetalling:Kernel : Uma classe geral para transformar uma textura de origem em uma textura de destino, estendendo Function para simplesmente definir os parâmetros para chamar o kernel MSL.


  • imetalling::PassKernel : Ignora o kernel.


  • imetalling::FalseColorKernel : Nossa principal classe funcional, um emulador "predador" que posteriza (reduz a resolução) para um número especificado de cores.


O código do kernel para o modo "predador" poderia ser assim:

 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); }


Inicialização do plugin OFX

Começaremos definindo a classe imetalling::falsecolor::Factory. Nesta aula, definiremos um único parâmetro - o status do monitor (ligado ou desligado). Isto é necessário para o nosso exemplo.

Herdaremos de OFX::PluginFactoryHelper e sobrecarregaremos cinco métodos:


  • load() : Este método é invocado para configurar globalmente a instância quando o plugin é carregado pela primeira vez. Sobrecarregar este método é opcional.


  • unload() : Este método é invocado quando uma instância é descarregada, por exemplo, para limpar a memória. Sobrecarregar este método também é opcional.


  • descreva(ImageEffectDescriptor&) : Este é o segundo método que o host OFX chama quando o plugin é carregado. É virtual e deve ser definido em nossa classe. Neste método, precisamos definir todas as propriedades do plugin, independente do seu tipo de contexto. Para obter mais detalhes sobre as propriedades, consulte o código ImageEffectDescriptor .


  • descriptionInContext(ImageEffectDescriptor&,ContextEnum) : Semelhante ao método describe , este método também é chamado quando o plugin é carregado e deve ser definido em nossa classe. Deve definir propriedades associadas ao contexto atual.


    O contexto determina o tipo de operações com as quais o aplicativo trabalha, como filtro, pintura, efeito de transição ou retimer de quadro em um clipe.


  • createInstance(OfxImageEffectHandle, ContextEnum) : Este é o método mais importante que sobrecarregamos. Retornamos um ponteiro para um objeto do tipo ImageEffect . Em outras palavras, nosso imetalling::falsecolor::Plugin no qual definimos todas as funcionalidades, tanto no que diz respeito aos eventos do usuário no programa host quanto na renderização (transformação) do quadro de origem no quadro de destino:
 OFX::ImageEffect *Factory::createInstance(OfxImageEffectHandle handle,OFX::ContextEnum) {     return new Plugin(handle);   }


Tratamento de eventos

Nesta fase, se você compilar um pacote com o módulo OFX, o plugin já estará disponível na aplicação host e, no DaVinci, poderá ser carregado no nó de correção.


No entanto, para trabalhar totalmente com uma instância de plug-in, você precisa definir pelo menos a parte interativa e a parte associada ao processamento do fluxo de vídeo recebido.


Para fazer isso, herdamos da classe OFX::ImageEffect e sobrecarregamos os métodos virtuais:


  • changeParam(const OFX::InstanceChangedArgs&, const std::string&) - Este método nos permite definir a lógica de tratamento do evento. O tipo de evento é determinado pelo valor de OFX::InstanceChangedArgs::reason e pode ser: eChangeUserEdit, eChangePluginEdit, eChangeTime - o evento ocorreu como resultado de uma propriedade sendo editada pelo usuário, alterada em um plugin ou aplicativo host, ou como resultado de uma mudança na linha do tempo.


    O segundo parâmetro especifica o nome da string que definimos na fase de inicialização do plugin, no nosso caso, é um parâmetro: false_color_enabled_check_box .


  • isIdentity(...) - Este método nos permite definir a lógica para reagir a um evento e retornar um estado que determina se algo mudou e se a renderização faz sentido. O método deve retornar falso ou verdadeiro. Essa é uma forma de otimizar e reduzir o número de cálculos desnecessários.


Você pode ler a implementação da interação interativa com OFX no código Interaction.cpp . Como você pode ver, recebemos ponteiros para os clipes: o de origem e a área de memória na qual colocaremos a transformação de destino.

Implementação do lançamento de renderização

Adicionaremos outra camada lógica na qual definiremos toda a lógica para lançar a transformação. No nosso caso, este é o único método de substituição até agora:


  • render(const OFX::RenderArguments& args) - Aqui, você pode descobrir as propriedades dos clipes e decidir como renderizá-los. Além disso, neste estágio, a fila de comandos do Metal e alguns atributos úteis associados às propriedades atuais da linha do tempo ficam disponíveis para nós.

Em processamento

Na fase de lançamento, um objeto com propriedades úteis ficou disponível para nós: temos pelo menos um ponteiro para o stream de vídeo (mais precisamente, uma área de memória com dados de imagem de quadro) e, o mais importante, uma fila de comandos Metal.


Agora podemos construir uma classe genérica que nos aproximará de uma forma simples de reutilizar o código do kernel. A extensão OpenFXS já possui essa classe: OFX::ImageProcessor; só precisamos sobrecarregá-lo.


No construtor possui o parâmetro OFX::ImageEffect, ou seja, nele receberemos não apenas o estado atual dos parâmetros do plugin, mas tudo o que é necessário para trabalhar com a GPU.


Nesta fase, basta sobrecarregar o método processImagesMetal() e iniciar o processamento dos kernels já implementados no 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());      }    }


Construindo o Projeto

Para construir o projeto, você precisará do CMake, e deve ser pelo menos a versão 3.15. Além disso, você precisará do Qt5.13, que auxilia na montagem fácil e conveniente do pacote com o instalador do plugin no diretório do sistema. Para iniciar o cmake, você deve primeiro criar um diretório de construção.


Depois de criar o diretório de construção, você pode executar o seguinte comando:


 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 


Seu “predador” pessoal


Posteriormente, o instalador, denominado IMFalseColorOfxInstaller.app , aparecerá no diretório que você especificou no parâmetro PLUGIN_INSTALLER_DIR . Vamos em frente e lançá-lo! Assim que a instalação for bem-sucedida, você poderá iniciar o DaVinci Resolve e começar a usar nosso novo plugin.


Você pode localizá-lo e selecioná-lo no painel OpenFX na página de correção de cores e adicioná-lo como um nó.



Trabalhando com cores falsas



links externos

  1. Código de plug-in OFX de cor falsa
  2. A Associação de Efeitos Abertos
  3. Baixe DaVinci Resolve - versão do arquivo de cabeçalho OFX e código da biblioteca OFXS em Resolve + exemplos