paint-brush
Un moyen simple de développer votre propre plugin Apple Metal et de l'intégrer dans DaVinci Resolvepar@denissvinarchuk
3,614 lectures
3,614 lectures

Un moyen simple de développer votre propre plugin Apple Metal et de l'intégrer dans DaVinci Resolve

par Denis Svinarchuk13m2024/03/13
Read on Terminal Reader

Trop long; Pour lire

OFX, alias OFX Image Processing API, est un standard ouvert pour la création d'effets visuels 2D et la composition vidéo. Il fonctionne dans un modèle de développement d'applications de type plugin. Essentiellement, il sert à la fois d'hôte - une application fournissant un ensemble de méthodes, et de plug-in - une application ou un module implémentant cet ensemble. Cette configuration offre un potentiel d'extension illimitée des fonctionnalités de l'application hôte.
featured image - Un moyen simple de développer votre propre plugin Apple Metal et de l'intégrer dans DaVinci Resolve
Denis Svinarchuk HackerNoon profile picture

OFX, alias OFX Image Processing API , est un standard ouvert pour la création d'effets visuels 2D et la composition vidéo. Il fonctionne dans un modèle de développement d'applications de type plugin. Essentiellement, il sert à la fois d'hôte - une application fournissant un ensemble de méthodes, et de plug-in - une application ou un module implémentant cet ensemble.


Cette configuration offre un potentiel d'extension illimitée des fonctionnalités de l'application hôte.

DaVinci Resolve et Métal

Des applications telles que Final Cut X et DaVinci Resolve Studio, à partir de la version 16, prennent entièrement en charge les pipelines Apple Metal. Semblable à OpenCL et Cuda, dans le cas d'OFX, vous pouvez obtenir un descripteur ou un gestionnaire d'une file d'attente de commandes spécifique à la plate-forme. Le système hôte prend également la responsabilité d'attribuer un pool de telles files d'attente et d'équilibrer les calculs sur celles-ci.


De plus, il place les données des clips d'image source et cible dans la mémoire GPU, simplifiant considérablement le développement de fonctionnalités extensibles.

Prise en charge de la version OFX dans Resolve

Avec Resolve, les choses sont un peu plus compliquées. DaVinci annonce le support d'OFX v1.4, mais avec quelques limitations. Plus précisément, certaines méthodes permettant d'utiliser les fonctions d'interface ne sont pas disponibles. Pour déterminer quelle méthode est disponible, OFX vous permet d'examiner la suite prise en charge via des requêtes clé/valeur.


Les méthodes de publication dans le code du plugin sont basées sur des appels C . Mais nous utiliserons le shell OpenFXS C++ adapté pour C++17. Pour plus de commodité, j'ai tout compilé dans un seul référentiel : dehancer-external tiré du projet open source Dehancer .

Concept OFXS

Dans ce projet, j'utiliserai OpenFXS, une extension C++ d'OpenFX initialement écrite par Bruno Nicoletti et devenue populaire au fil du temps dans les projets de traitement vidéo commerciaux et open source.


L' OpenFXS original n'était pas adapté aux dialectes C++ modernes, je l'ai donc mis à jour pour le rendre compatible avec C++17 .


OFX, et par conséquent OFXS, est un module logiciel autonome chargé dynamiquement par le programme hôte. Il s'agit essentiellement d'une bibliothèque dynamique qui est chargée au démarrage de l'application principale. OpenFXS, comme OFX, doit publier les signatures de méthodes. Par conséquent, nous utilisons une méthode C du code.


Pour commencer à développer dans OpenFXS, vous devez accepter quelques ensembles communs de classes utilisées pour créer de nouvelles fonctionnalités dans votre application. En règle générale, dans un nouveau projet, vous devez hériter de ces classes et implémenter ou remplacer certaines méthodes virtuelles.


Pour créer votre propre plugin sur le système hôte, commençons par nous familiariser avec les classes publiques suivantes et la même méthode :


  • OFX::PluginFactoryHelper est un modèle de base pour créer la suite de structures de données et le panneau de contrôle d'un plugin (bien qu'il puisse être laissé vide). La classe héritée crée un objet singleton qui enregistre un ensemble de paramètres et de préréglages dans le système hôte, avec lequel le développeur enregistre son module ;


  • OFX::ParamSetDescriptor - classe conteneur de base pour créer et stocker les propriétés de structure ;


  • OFX::ImageEffectDescriptor - un conteneur de propriétés utilisé lors de la manipulation de données graphiques lors de l'appel de procédures de traitement de données. Utilisé par l'application hôte pour sauvegarder le contexte des paramètres de traitement dans la base de données interne et travailler avec les propriétés du plugin définies pour chacune de ses instances ;


  • OFX::ParamSet - un ensemble de paramètres qui vous permet de manipuler la structure de données enregistrée ;


  • OFX::ImageEffect - un ensemble de paramètres pour les effets sur les données graphiques, hérité de OFX::ParamSet ;


  • OFX::MultiThread::Processor - dans la classe enfant, il est nécessaire d'implémenter le traitement des flux de données : images ou vidéos ;


  • OFX::Plugin::getPluginIDs - méthode d'enregistrement d'un plugin (usine) dans l'application hôte ;

Fausse couleur

Une caractéristique qui distingue le processus de prise de vue vidéo de la simple capture d'une image dans une photo est le changement dynamique des scènes et de l'éclairage des scènes dans leur ensemble et des zones de l'image. Cela détermine la manière dont l'exposition est contrôlée pendant le processus de prise de vue.


En vidéo numérique, il existe un mode moniteur de contrôle pour les opérateurs dans lequel le niveau d'exposition des zones est cartographié en un ensemble limité de zones, chacune teintée de sa propre couleur.


Ce mode est parfois appelé « prédateur » ou mode Fausses Couleurs. Les échelles sont généralement référencées à l’échelle IRE.


Un tel moniteur vous permet de voir les zones d'exposition et d'éviter des erreurs importantes lors du réglage des paramètres de prise de vue de l'appareil photo. Quelque chose de similaire dans sa signification est utilisé lors de l'exposition en photographie - le zonage selon Adams, par exemple.


Vous pouvez mesurer une cible spécifique avec un posemètre et voir dans quelle zone elle se trouve, et en temps réel nous voyons les zones, soigneusement teintées pour faciliter la perception.


Le nombre de zones est déterminé par les objectifs et les capacités du moniteur de contrôle. Par exemple, un moniteur utilisé avec les caméras Arri Alexa peut intégrer jusqu'à 6 zones.


Logiciel version « prédateur » avec 16 zones


Ajout d'extensions

Avant de passer à l'exemple, nous devons ajouter quelques classes proxy simples pour implémenter OpenFXS en tant que plate-forme de traitement des données sources, telles que les textures Metal. Ces cours comprennent :


  • imétalling::Image : Une classe proxy pour les données de clip OFX.


  • imétalling::Image2Texture : Un foncteur pour transférer les données du tampon de clip vers une texture Metal. Depuis DaVinci, vous pouvez extraire un tampon de n'importe quelle structure et emballage de valeurs de canal d'image dans le plugin, et il doit être renvoyé sous une forme similaire.


    Pour faciliter le travail avec le format de flux dans OFX, vous pouvez demander à l'hôte de préparer à l'avance les données d'un type spécifique. J'utiliserai des flotteurs emballés en RGBA - rouge/vert/bleu/alpha.


  • imétalling::ImageFromTexture : Un foncteur inverse pour transformer un flux en tampon du système hôte. Comme vous pouvez le constater, il existe un potentiel d'optimisation significative des calculs si vous apprenez aux cœurs de calcul Metal à travailler non pas avec la texture, mais directement avec le tampon.


Nous héritons des classes de base OFXS et écrivons nos fonctionnalités sans entrer dans les détails du fonctionnement du noyau Metal :


  • immetalling::falsecolor::Processor : Ici, nous implémentons la transformation du flux et lançons le traitement.


  • imétalling::falsecolor::Factory : Ce sera notre partie spécifique de la description de la suite pour le plugin. Nous devons implémenter plusieurs appels obligatoires liés à la configuration de la structure et créer une instance de la classe OFX::ImageEffect avec des fonctionnalités spécifiques, que nous divisons en deux sous-classes dans l'implémentation : Interaction et Plugin.


  • imétalling::falsecolor::Interaction : Implémentation de la partie interactive du travail avec les effets. Essentiellement, il s'agit de l'implémentation uniquement de méthodes virtuelles d'OFX::ImageEffect liées au traitement des modifications des paramètres du plugin.


  • immetalling::falsecolor::Plugin : Implémentation du rendu des threads, c'est-à-dire lancer immetalling::Processor.


De plus, nous aurons besoin de plusieurs classes utilitaires construites sur Metal pour séparer logiquement le code hôte et le code noyau sur MSL. Ceux-ci inclus:


  • imétalling::Function : Une classe de base qui masque le travail avec la file d'attente de commandes Metal. Le paramètre principal sera le nom du noyau dans le code MSL, et l'exécuteur de l'appel du noyau.


  • imétalling:Kernel : Une classe générale pour transformer une texture source en texture cible, étendant Function pour définir simplement les paramètres d'appel du noyau MSL.


  • immetalling::PassKernel : Contourner le noyau.


  • imétalling::FalseColorKernel : Notre classe fonctionnelle principale, un émulateur "prédateur" qui postérise (sous-échantillonne) un nombre spécifié de couleurs.


Le code du noyau pour le mode "prédateur" pourrait ressembler à ceci :

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


Initialisation du plugin OFX

Nous commencerons par définir la classe imetalling::falsecolor::Factory. Dans ce cours, nous définirons un seul paramètre : l'état du moniteur (activé ou éteint). Ceci est nécessaire pour notre exemple.

Nous hériterons de OFX::PluginFactoryHelper et surchargerons cinq méthodes :


  • load() : Cette méthode est invoquée pour configurer globalement l'instance lors du premier chargement du plugin. La surcharge de cette méthode est facultative.


  • unload() : Cette méthode est invoquée lorsqu'une instance est déchargée, par exemple pour vider la mémoire. La surcharge de cette méthode est également facultative.


  • décrire(ImageEffectDescriptor&) : C'est la deuxième méthode que l'hôte OFX appelle lorsque le plugin est chargé. Il est virtuel et doit être défini dans notre classe. Dans cette méthode, nous devons définir toutes les propriétés du plugin, quel que soit son type de contexte. Pour plus de détails sur les propriétés, reportez-vous au code ImageEffectDescriptor .


  • décrireInContext(ImageEffectDescriptor&,ContextEnum) : Semblable à la méthode describe , cette méthode est également appelée lors du chargement du plugin et doit être définie dans notre classe. Il doit définir les propriétés associées au contexte actuel.


    Le contexte détermine le type d'opérations avec lesquelles l'application travaille, telles que le filtre, la peinture, l'effet de transition ou le retimer d'image dans un clip.


  • createInstance(OfxImageEffectHandle, ContextEnum) : C'est la méthode la plus cruciale que nous surchargeons. Nous renvoyons un pointeur vers un objet de type ImageEffect . En d'autres termes, notre imetalling::falsecolor::Plugin dans lequel nous avons défini toutes les fonctionnalités, tant en ce qui concerne les événements utilisateur dans le programme hôte que le rendu (transformation) de la trame source en celle cible :
 OFX::ImageEffect *Factory::createInstance(OfxImageEffectHandle handle,OFX::ContextEnum) {     return new Plugin(handle);   }


Gestion des événements

A ce stade, si vous compilez un bundle avec le module OFX, le plugin sera déjà disponible dans l'application hôte, et dans DaVinci, il pourra être chargé sur le nœud de correction.


Cependant, pour fonctionner pleinement avec une instance de plugin, vous devez définir au moins la partie interactive et la partie associée au traitement du flux vidéo entrant.


Pour ce faire, nous héritons de la classe OFX::ImageEffect et surchargeons les méthodes virtuelles :


  • changesParam(const OFX::InstanceChangedArgs&, const std::string&) - Cette méthode nous permet de définir la logique de gestion de l'événement. Le type d'événement est déterminé par la valeur de OFX::InstanceChangedArgs::reason et peut être : eChangeUserEdit, eChangePluginEdit, eChangeTime - l'événement s'est produit à la suite d'une propriété modifiée par l'utilisateur, modifiée dans un plugin ou une application hôte, ou à la suite d'un changement de calendrier.


    Le deuxième paramètre spécifie le nom de chaîne que nous avons défini lors de l'étape d'initialisation du plugin, dans notre cas, il s'agit d'un paramètre : false_color_enabled_check_box .


  • isIdentity(...) - Cette méthode nous permet de définir la logique de réaction à un événement et de renvoyer un état qui détermine si quelque chose a changé et si le rendu a du sens. La méthode doit renvoyer false ou true. C'est un moyen d'optimiser et de réduire le nombre de calculs inutiles.


Vous pouvez lire l'implémentation de l'interaction interactive avec OFX dans le code Interaction.cpp . Comme vous pouvez le voir, nous recevons des pointeurs vers les clips : celui source et la zone mémoire dans laquelle nous allons mettre la transformation cible.

Implémentation du lancement du rendu

Nous ajouterons une autre couche logique sur laquelle nous définirons toute la logique de lancement de la transformation. Dans notre cas, c'est la seule méthode de remplacement jusqu'à présent :


  • render(const OFX::RenderArguments& args) - Ici, vous pouvez découvrir les propriétés des clips et décider comment les rendre. De plus, à ce stade, la file d'attente de commandes Metal et certains attributs utiles associés aux propriétés actuelles de la timeline deviennent disponibles.

Traitement

Au stade du lancement, un objet avec des propriétés utiles s'est mis à notre disposition : nous disposons d'au moins un pointeur vers le flux vidéo (plus précisément, une zone mémoire avec des données d'image image) et, surtout, une file d'attente de commandes Metal.


Nous pouvons désormais construire une classe générique qui nous rapprochera d’une forme simple de réutilisation du code du noyau. L'extension OpenFXS possède déjà une telle classe : OFX::ImageProcessor ; nous avons juste besoin de le surcharger.


Dans le constructeur, il a le paramètre OFX::ImageEffect, c'est-à-dire que nous y recevrons non seulement l'état actuel des paramètres du plugin, mais aussi tout le nécessaire pour travailler avec le GPU.


A ce stade, il suffit de surcharger la méthode processImagesMetal() et de lancer le traitement des noyaux déjà implémentés sur 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());      }    }


Construire le projet

Pour construire le projet, vous aurez besoin de CMake, et il doit être au moins en version 3.15. De plus, vous aurez besoin de Qt5.13, qui facilite l'assemblage simple et pratique du bundle avec le programme d'installation du plugin dans le répertoire système. Pour lancer cmake, vous devez d'abord créer un répertoire de construction.


Après avoir créé le répertoire build, vous pouvez exécuter la commande suivante :


 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 


Votre « prédateur » personnel


Ensuite, le programme d'installation, appelé IMFalseColorOfxInstaller.app , apparaîtra dans le répertoire que vous avez spécifié dans le paramètre PLUGIN_INSTALLER_DIR . Allons-y et lançons-le ! Une fois l'installation réussie, vous pouvez démarrer DaVinci Resolve et commencer à utiliser notre nouveau plugin.


Vous pouvez le trouver et le sélectionner dans le panneau OpenFX sur la page de correction des couleurs, et l'ajouter en tant que nœud.



Travailler les fausses couleurs



Liens externes

  1. Code du plugin OFX en fausses couleurs
  2. L’association Open Effects
  3. Téléchargez DaVinci Resolve - Version du fichier d'en-tête OFX et code de la bibliothèque OFXS sous Resolve + exemples