paint-brush
Una forma sencilla de desarrollar su propio complemento Apple Metal e integrarlo en DaVinci Resolvepor@denissvinarchuk
680 lecturas
680 lecturas

Una forma sencilla de desarrollar su propio complemento Apple Metal e integrarlo en DaVinci Resolve

por Denis Svinarchuk13m2024/03/13
Read on Terminal Reader

Demasiado Largo; Para Leer

OFX, también conocido como OFX Image Processing API, es un estándar abierto para crear efectos visuales 2D y composición de vídeo. Opera en un modelo de desarrollo de aplicaciones similar a un complemento. Básicamente, sirve como Host (una aplicación que proporciona un conjunto de métodos) y como Complemento (una aplicación o módulo que implementa este conjunto). Esta configuración ofrece la posibilidad de una expansión ilimitada de la funcionalidad de la aplicación host.
featured image - Una forma sencilla de desarrollar su propio complemento Apple Metal e integrarlo en DaVinci Resolve
Denis Svinarchuk HackerNoon profile picture

OFX, también conocido como OFX Image Processing API , es un estándar abierto para crear efectos visuales 2D y composición de vídeo. Opera en un modelo de desarrollo de aplicaciones similar a un complemento. Básicamente, sirve como Host (una aplicación que proporciona un conjunto de métodos) y como Complemento (una aplicación o módulo que implementa este conjunto).


Esta configuración ofrece la posibilidad de una expansión ilimitada de la funcionalidad de la aplicación host.

DaVinci Resolve y Metal

Aplicaciones como Final Cut X y DaVinci Resolve Studio, a partir de la versión 16, son totalmente compatibles con las canalizaciones Apple Metal. De manera similar a OpenCL y Cuda, en el caso de OFX, puede obtener un descriptor o controlador de una cola de comandos específica de la plataforma. El sistema anfitrión también asume la responsabilidad de asignar un conjunto de colas de este tipo y equilibrar los cálculos sobre ellas.


Además, coloca los datos del clip de imagen de origen y de destino en la memoria de la GPU, lo que simplifica significativamente el desarrollo de funciones extensibles.

Soporte de versión OFX en resolución

Con Resolve, las cosas son un poco más complicadas. DaVinci anuncia soporte para OFX v1.4, aunque con algunas limitaciones. Específicamente, algunos métodos para trabajar con funciones de interfaz no están disponibles para su uso. Para determinar qué método está disponible, OFX le permite examinar el conjunto admitido a través de consultas clave/valor.


Los métodos de publicación en el código del complemento se basan en llamadas C. Pero usaremos el shell OpenFXS C++ adaptado para C++17. Para mayor comodidad, he compilado todo en un repositorio: dehancer-external tomado del proyecto de código abierto Dehancer .

Concepto OFXS

En este proyecto, usaré OpenFXS, una extensión C++ para OpenFX que fue escrita originalmente por Bruno Nicoletti y que se ha vuelto popular con el tiempo en proyectos comerciales y de procesamiento de video de código abierto.


El OpenFXS original no se adaptó a los dialectos modernos de C++, así que lo actualicé para hacerlo compatible con C++17 .


OFX, y en consecuencia OFXS, es un módulo de software independiente que el programa anfitrión carga dinámicamente. Básicamente, es una biblioteca dinámica que se carga cuando se inicia la aplicación principal. OpenFXS, al igual que OFX, debe publicar firmas de métodos. Por lo tanto, utilizamos un método C del código.


Para comenzar a desarrollar en OpenFXS, debe aceptar algunos conjuntos comunes de clases que se utilizan para crear nuevas funciones en su aplicación. Normalmente, en un proyecto nuevo, es necesario heredar de estas clases e implementar o anular algunos métodos virtuales.


Para crear su propio complemento en el sistema host, comencemos familiarizándonos con las siguientes clases públicas y el mismo método:


  • OFX::PluginFactoryHelper es una plantilla básica para crear un conjunto de estructura de datos y un panel de control de un complemento (aunque se puede dejar vacío). La clase heredada crea un objeto singleton que registra un conjunto de parámetros y ajustes preestablecidos en el sistema host, con el cual el desarrollador registra su módulo;


  • OFX::ParamSetDescriptor - clase contenedora base para crear y almacenar propiedades de estructura;


  • OFX::ImageEffectDescriptor : un contenedor de propiedades utilizado al manipular datos gráficos al llamar a procedimientos de procesamiento de datos. Utilizado por la aplicación host para guardar el contexto de los parámetros de procesamiento en la base de datos interna y trabajar con las propiedades del complemento definidas para cada una de sus instancias;


  • OFX::ParamSet : un conjunto de configuraciones que le permite manipular la estructura de datos registrada;


  • OFX::ImageEffect : un conjunto de configuraciones para efectos en datos gráficos, heredado de OFX::ParamSet;


  • OFX::MultiThread::Processor - en la clase secundaria, es necesario implementar el procesamiento de flujo de datos: imágenes o vídeos;


  • OFX::Plugin::getPluginIDs - método para registrar un complemento (fábrica) en la aplicación host;

Color falso

Una característica que distingue el proceso de grabar un vídeo de simplemente capturar una imagen en una fotografía es el cambio dinámico de escenas y la iluminación tanto de las escenas en su conjunto como de las áreas de la imagen. Esto determina la forma en que se controla la exposición durante el proceso de disparo.


En el vídeo digital, existe un modo de monitor de control para los operadores en el que el nivel de exposición de las áreas se asigna a un conjunto limitado de zonas, cada una teñida con su propio color.


Este modo a veces se denomina modo "depredador" o modo Falso Color. Las escalas suelen estar referenciadas a la escala IRE.


Un monitor de este tipo le permite ver las zonas de exposición y evitar errores importantes al configurar los parámetros de disparo de la cámara. Algo similar en significado se utiliza cuando se expone en fotografía: la zonificación según Adams, por ejemplo.


Puedes medir un objetivo específico con un exposímetro y ver en qué zona se encuentra, y en tiempo real vemos las zonas, cuidadosamente teñidas para facilitar la percepción.


El número de zonas está determinado por los objetivos y capacidades del monitor de control. Por ejemplo, un monitor utilizado con cámaras Arri Alexa puede incorporar hasta 6 zonas.


Versión software “predator” con 16 zonas


Agregar extensiones

Antes de continuar con el ejemplo, necesitamos agregar algunas clases de proxy simples para implementar OpenFXS como una plataforma para procesar datos de origen, como texturas metálicas. Estas clases incluyen:


  • imetalling::Image : una clase proxy para datos de clips OFX.


  • imetalling::Image2Texture : un functor para transferir datos desde el búfer de clip a una textura de metal. Desde DaVinci, puede extraer un búfer de cualquier estructura y empaquetado de valores de canales de imágenes en el complemento, y debe devolverse de forma similar.


    Para facilitar el trabajo con el formato de transmisión en OFX, puede solicitar al anfitrión que prepare datos de un tipo específico con anticipación. Usaré flotadores empaquetados en RGBA: rojo/verde/azul/alfa.


  • imetalling::ImageFromTexture : un funtor inverso para transformar una secuencia en un búfer del sistema host. Como puede ver, existe un potencial para una optimización significativa de los cálculos si enseña a los núcleos informáticos de Metal a trabajar no con la textura, sino directamente con el búfer.


Heredamos las clases base OFXS y escribimos nuestra funcionalidad sin entrar en detalles de cómo funciona el núcleo Metal:


  • imetalling::falsecolor::Processor : Aquí implementamos la transformación del flujo e iniciamos el procesamiento.


  • imetalling::falsecolor::Factory : esta será nuestra parte específica de la descripción de la suite para el complemento. Necesitamos implementar varias llamadas obligatorias relacionadas con la configuración de la estructura y crear una instancia de la clase OFX::ImageEffect con una funcionalidad específica, que dividimos en dos subclases en la implementación: Interacción y Complemento.


  • imetalling::falsecolor::Interaction : Implementación de la parte interactiva del trabajo con efectos. Básicamente, se trata de la implementación de sólo métodos virtuales de OFX::ImageEffect relacionados con el procesamiento de cambios en los parámetros del complemento.


  • imetalling::falsecolor::Plugin : Implementación de renderizado de subprocesos, es decir, lanzamiento de imetalling::Processor.


Además, necesitaremos varias clases de utilidades construidas sobre Metal para separar lógicamente el código del host y el código del kernel en MSL. Éstas incluyen:


  • imetalling::Function : una clase base que oscurece el trabajo con la cola de comandos de Metal. El parámetro principal será el nombre del kernel en el código MSL y el ejecutor de la llamada del kernel.


  • imetalling:Kernel : una clase general para transformar una textura de origen en una textura de destino, extendiendo Function para simplemente establecer los parámetros para llamar al kernel de MSL.


  • imetalling::PassKernel : Omitir el kernel.


  • imetalling::FalseColorKernel : nuestra clase funcional principal, un emulador "depredador" que posteriza (reduce la resolución) a un número específico de colores.


El código del kernel para el modo "depredador" podría verse así:

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


Inicialización del complemento OFX

Comenzaremos definiendo la clase imetalling::falsecolor::Factory. En esta clase, configuraremos un único parámetro: el estado del monitor (ya sea encendido o apagado). Esto es necesario para nuestro ejemplo.

Heredaremos de OFX::PluginFactoryHelper y sobrecargaremos cinco métodos:


  • load() : este método se invoca para configurar globalmente la instancia cuando se carga el complemento por primera vez. La sobrecarga de este método es opcional.


  • unload() : este método se invoca cuando se descarga una instancia, por ejemplo, para borrar la memoria. La sobrecarga de este método también es opcional.


  • describe(ImageEffectDescriptor&) : este es el segundo método que llama el host OFX cuando se carga el complemento. Es virtual y debe definirse en nuestra clase. En este método, necesitamos configurar todas las propiedades del complemento, independientemente de su tipo de contexto. Para obtener más detalles sobre las propiedades, consulte el código ImageEffectDescriptor .


  • describeInContext(ImageEffectDescriptor&,ContextEnum) : similar al método describe , este método también se llama cuando se carga el complemento y debe definirse en nuestra clase. Debe definir propiedades asociadas con el contexto actual.


    El contexto determina el tipo de operaciones con las que trabaja la aplicación, como filtro, pintura, efecto de transición o temporizador de cuadros en un clip.


  • createInstance(OfxImageEffectHandle, ContextEnum) : este es el método más importante que sobrecargamos. Devolvemos un puntero a un objeto de tipo ImageEffect . En otras palabras, nuestro imetalling::falsecolor::Plugin en el que hemos definido todas las funcionalidades, tanto con respecto a los eventos del usuario en el programa anfitrión como con la renderización (transformación) del fotograma de origen en el de destino:
 OFX::ImageEffect *Factory::createInstance(OfxImageEffectHandle handle,OFX::ContextEnum) {     return new Plugin(handle);   }


Manejo de eventos

En esta etapa, si compila un paquete con el módulo OFX, el complemento ya estará disponible en la aplicación host y, en DaVinci, se podrá cargar en el nodo de corrección.


Sin embargo, para trabajar completamente con una instancia de complemento, debe definir al menos la parte interactiva y la parte asociada con el procesamiento de la transmisión de video entrante.


Para hacer esto, heredamos de la clase OFX::ImageEffect y sobrecargamos los métodos virtuales:


  • changeParam(const OFX::InstanceChangedArgs&, const std::string&) - Este método nos permite definir la lógica para manejar el evento. El tipo de evento está determinado por el valor de OFX::InstanceChangedArgs::reason y puede ser: eChangeUserEdit, eChangePluginEdit, eChangeTime: el evento ocurrió como resultado de que una propiedad fue editada por el usuario, modificada en un complemento o aplicación host, o como resultado de un cambio en el cronograma.


    El segundo parámetro especifica el nombre de la cadena que definimos en la etapa de inicialización del complemento; en nuestro caso, es un parámetro: false_color_enabled_check_box .


  • isIdentity(...) : este método nos permite definir la lógica para reaccionar ante un evento y devolver un estado que determina si algo ha cambiado y si la representación tiene sentido. El método debe devolver falso o verdadero. Esta es una forma de optimizar y reducir la cantidad de cálculos innecesarios.


Puede leer la implementación de la interacción interactiva con OFX en el código Interaction.cpp . Como puede ver, recibimos punteros a los clips: el de origen y el área de memoria en la que colocaremos la transformación de destino.

Implementación del lanzamiento de renderizado

Añadiremos otra capa lógica sobre la que definiremos toda la lógica para lanzar la transformación. En nuestro caso, este es el único método de anulación hasta el momento:


  • render(const OFX::RenderArguments& args) : aquí puede conocer las propiedades de los clips y decidir cómo renderizarlos. Además, en esta etapa, la cola de comandos de Metal y algunos atributos útiles asociados con las propiedades actuales de la línea de tiempo estarán disponibles para nosotros.

Procesando

En la etapa de lanzamiento, tuvimos a nuestra disposición un objeto con propiedades útiles: tenemos al menos un puntero a la transmisión de video (más precisamente, un área de memoria con datos de imágenes de fotogramas) y, lo más importante, una cola de comandos de Metal.


Ahora podemos construir una clase genérica que nos acercará a una forma simple de reutilizar el código del kernel. La extensión OpenFXS ya tiene dicha clase: OFX::ImageProcessor; sólo necesitamos sobrecargarlo.


En el constructor tiene el parámetro OFX::ImageEffect, es decir, en él recibiremos no solo el estado actual de los parámetros del complemento, sino también todo lo necesario para trabajar con la GPU.


En esta etapa, sólo necesitamos sobrecargar el método ProcessImagesMetal() e iniciar el procesamiento de los kernels ya implementados en 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());      }    }


Construyendo el proyecto

Para construir el proyecto, necesitará CMake y debe tener al menos la versión 3.15. Además, necesitará Qt5.13, que ayuda a ensamblar fácil y convenientemente el paquete con el instalador del complemento en el directorio del sistema. Para iniciar cmake, primero debe crear un directorio de compilación.


Después de crear el directorio de compilación, puede ejecutar el siguiente 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 


Tu “depredador” personal


Luego, el instalador, llamado IMFalseColorOfxInstaller.app , aparecerá en el directorio que especificó en el parámetro PLUGIN_INSTALLER_DIR . ¡Sigamos adelante y lancemos! Una vez que la instalación sea exitosa, podrá iniciar DaVinci Resolve y comenzar a utilizar nuestro nuevo complemento.


Puede encontrarlo y seleccionarlo en el panel OpenFX en la página de corrección de color y agregarlo como nodo.



Trabajando color falso



enlaces externos

  1. Código de complemento OFX de color falso
  2. La Asociación de Efectos Abiertos
  3. Descargue DaVinci Resolve : versión del archivo de encabezado OFX y código de biblioteca OFXS en Resolve + ejemplos