paint-brush
Eine einfache Möglichkeit, Ihr eigenes Apple Metal Plugin zu entwickeln und es in DaVinci Resolve zu integrierenvon@denissvinarchuk
3,610 Lesungen
3,610 Lesungen

Eine einfache Möglichkeit, Ihr eigenes Apple Metal Plugin zu entwickeln und es in DaVinci Resolve zu integrieren

von Denis Svinarchuk13m2024/03/13
Read on Terminal Reader

Zu lang; Lesen

OFX, auch bekannt als OFX Image Processing API, ist ein offener Standard für die Erstellung visueller 2D-Effekte und Video-Compositing. Es arbeitet in einem Plugin-ähnlichen Anwendungsentwicklungsmodell. Im Wesentlichen dient es sowohl als Host – eine Anwendung, die eine Reihe von Methoden bereitstellt, als auch als Plug-in – eine Anwendung oder ein Modul, die diese Reihe implementiert. Diese Konfiguration bietet das Potenzial für eine unbegrenzte Erweiterung der Funktionalität der Host-Anwendung.
featured image - Eine einfache Möglichkeit, Ihr eigenes Apple Metal Plugin zu entwickeln und es in DaVinci Resolve zu integrieren
Denis Svinarchuk HackerNoon profile picture

OFX, auch bekannt als OFX Image Processing API , ist ein offener Standard für die Erstellung visueller 2D-Effekte und Video-Compositing. Es arbeitet in einem Plugin-ähnlichen Anwendungsentwicklungsmodell. Im Wesentlichen dient es sowohl als Host – eine Anwendung, die eine Reihe von Methoden bereitstellt, als auch als Plug-in – eine Anwendung oder ein Modul, die diese Reihe implementiert.


Diese Konfiguration bietet die Möglichkeit einer unbegrenzten Erweiterung der Funktionalität der Host-Anwendung.

DaVinci Resolve und Metal

Anwendungen wie Final Cut X und DaVinci Resolve Studio unterstützen ab Version 16 Apple Metal Pipelines vollständig. Ähnlich wie bei OpenCL und Cuda können Sie im Fall von OFX einen Deskriptor oder Handler einer plattformspezifischen Befehlswarteschlange erhalten. Das Hostsystem übernimmt auch die Verantwortung für die Zuweisung eines Pools solcher Warteschlangen und den Ausgleich der Berechnungen darauf.


Darüber hinaus werden die Quell- und Ziel-Bildclipdaten im GPU-Speicher abgelegt, was die Entwicklung erweiterbarer Funktionen erheblich vereinfacht.

OFX-Versionsunterstützung in Resolve

Bei Resolve sind die Dinge etwas komplizierter. DaVinci kündigt Unterstützung für OFX v1.4 an, allerdings mit einigen Einschränkungen. Insbesondere stehen einige Methoden zum Arbeiten mit Schnittstellenfunktionen nicht zur Verfügung. Um festzustellen, welche Methode verfügbar ist, können Sie mit OFX die unterstützte Suite durch Schlüssel-/Wertabfragen untersuchen.


Veröffentlichungsmethoden im Plugin-Code basieren auf C-Aufrufen . Wir werden jedoch die für C++17 angepasste OpenFXS C++-Shell verwenden. Der Einfachheit halber habe ich alles in einem Repository zusammengestellt: dehancer-external aus dem Open-Source -Projekt Dehancer .

OFXS-Konzept

In diesem Projekt verwende ich OpenFXS, eine C++-Erweiterung von OpenFX, die ursprünglich von Bruno Nicoletti geschrieben wurde und im Laufe der Zeit in kommerziellen und Open-Source-Videoverarbeitungsprojekten populär geworden ist.


Das ursprüngliche OpenFXS wurde nicht an moderne C++-Dialekte angepasst, daher habe ich es aktualisiert, um es mit C++17 kompatibel zu machen.


OFX und folglich OFXS ist ein eigenständiges Softwaremodul, das vom Hostprogramm dynamisch geladen wird. Im Wesentlichen handelt es sich um eine dynamische Bibliothek, die beim Start der Hauptanwendung geladen wird. OpenFXS muss wie OFX Methodensignaturen veröffentlichen. Daher verwenden wir eine C-Methode aus dem Code.


Um mit der Entwicklung in OpenFXS zu beginnen, müssen Sie einigen gemeinsamen Klassensätzen zustimmen, die zum Erstellen neuer Funktionen in Ihrer Anwendung verwendet werden. Normalerweise müssen Sie in einem neuen Projekt von diesen Klassen erben und einige virtuelle Methoden implementieren oder überschreiben.


Um Ihr eigenes Plugin auf dem Hostsystem zu erstellen, machen wir uns zunächst mit den folgenden öffentlichen Klassen und derselben Methode vertraut:


  • OFX::PluginFactoryHelper ist eine grundlegende Vorlage zum Erstellen der Datenstruktur-Suite und des Control Panels eines Plugins (obwohl es leer gelassen werden kann). Die geerbte Klasse erstellt ein Singleton-Objekt, das eine Reihe von Parametern und Voreinstellungen im Hostsystem registriert, bei dem der Entwickler sein Modul registriert;


  • OFX::ParamSetDescriptor – Basiscontainerklasse zum Erstellen und Speichern von Struktureigenschaften;


  • OFX::ImageEffectDescriptor – ein Container mit Eigenschaften, die beim Bearbeiten von Grafikdaten beim Aufrufen von Datenverarbeitungsprozeduren verwendet werden. Wird von der Host-Anwendung verwendet, um den Kontext der Verarbeitungsparameter in der internen Datenbank zu speichern und mit Plugin-Eigenschaften zu arbeiten, die für jede ihrer Instanzen definiert sind;


  • OFX::ParamSet – eine Reihe von Einstellungen, mit denen Sie die registrierte Datenstruktur bearbeiten können;


  • OFX::ImageEffect – eine Reihe von Einstellungen für Effekte auf grafische Daten, geerbt von OFX::ParamSet;


  • OFX::MultiThread::Processor – in der untergeordneten Klasse ist es notwendig, die Verarbeitung von Datenströmen zu implementieren: Bilder oder Videos;


  • OFX::Plugin::getPluginIDs – Methode zur Registrierung eines Plugins (Factory) in der Host-Anwendung;

Falsche Farbe

Ein Merkmal, das den Prozess der Videoaufnahme von der einfachen Aufnahme eines Bildes auf einem Foto unterscheidet, ist der dynamische Szenenwechsel und die Beleuchtung sowohl der Szenen als Ganzes als auch der Bereiche im Bild. Dies bestimmt die Art und Weise, wie die Belichtung während des Aufnahmevorgangs gesteuert wird.


Bei digitalen Videos gibt es einen Kontrollmonitormodus für Bediener, bei dem die Belichtungsstufe von Bereichen auf eine begrenzte Anzahl von Zonen abgebildet wird, die jeweils mit einer eigenen Farbe getönt sind.


Dieser Modus wird manchmal als „Raubtier“ oder Falschfarbenmodus bezeichnet. Die Skalen beziehen sich üblicherweise auf die IRE-Skala.


Mit einem solchen Monitor können Sie die Belichtungszonen sehen und erhebliche Fehler bei der Einstellung der Kameraaufnahmeparameter vermeiden. Eine ähnliche Bedeutung wird bei der Belichtung in der Fotografie verwendet – zum Beispiel die Zoneneinteilung nach Adams.


Sie können ein bestimmtes Ziel mit einem Belichtungsmesser messen und sehen, in welcher Zone es sich befindet. In Echtzeit sehen wir die Zonen, sauber getönt, um die Wahrnehmung zu erleichtern.


Die Anzahl der Zonen wird durch die Ziele und Fähigkeiten des Kontrollmonitors bestimmt. Beispielsweise kann ein Monitor, der mit Arri Alexa- Kameras verwendet wird, bis zu 6 Zonen integrieren.


Softwareversion „Predator“ mit 16 Zonen


Erweiterungen hinzufügen

Bevor wir mit dem Beispiel fortfahren, müssen wir einige einfache Proxy-Klassen hinzufügen, um OpenFXS als Plattform für die Verarbeitung von Quelldaten, wie z. B. Metalltexturen, zu implementieren. Zu diesen Kursen gehören:


  • imetalling::Image : Eine Proxy-Klasse für OFX-Clipdaten.


  • imetalling::Image2Texture : Ein Funktor zum Übertragen von Daten aus dem Clip-Puffer in eine Metal-Textur. Von DaVinci aus können Sie einen Puffer beliebiger Struktur und Paketierung von Bildkanalwerten in das Plugin extrahieren, der in ähnlicher Form zurückgegeben werden sollte.


    Um die Arbeit mit dem Stream-Format in OFX zu erleichtern, können Sie den Host auffordern, vorab Daten eines bestimmten Typs vorzubereiten. Ich werde in RGBA gepackte Floats verwenden – Rot/Grün/Blau/Alpha.


  • imetalling::ImageFromTexture : Ein umgekehrter Funktor zum Umwandeln eines Streams in einen Hostsystempuffer. Wie Sie sehen, besteht Potenzial für eine erhebliche Optimierung der Berechnungen, wenn Sie den Metal-Rechenkernen beibringen, nicht mit der Textur, sondern direkt mit dem Puffer zu arbeiten.


Wir erben die OFXS-Basisklassen und schreiben unsere Funktionalität, ohne auf die Details der Funktionsweise des Metal-Kerns einzugehen:


  • imetalling::falsecolor::Processor : Hier implementieren wir die Stream-Transformation und initiieren die Verarbeitung.


  • imetalling::falsecolor::Factory : Dies wird unser spezifischer Teil der Suite-Beschreibung für das Plugin sein. Wir müssen mehrere obligatorische Aufrufe im Zusammenhang mit dem Aufbau der Struktur implementieren und eine Instanz der OFX::ImageEffect-Klasse mit spezifischer Funktionalität erstellen, die wir in der Implementierung in zwei Unterklassen unterteilen: Interaction und Plugin.


  • imetalling::falsecolor::Interaction : Implementierung des interaktiven Teils der Arbeit mit Effekten. Im Wesentlichen handelt es sich hierbei um die Implementierung ausschließlich virtueller Methoden von OFX::ImageEffect, die sich auf die Verarbeitung von Änderungen in Plugin-Parametern beziehen.


  • imetalling::falsecolor::Plugin : Implementierung des Thread-Renderings, d. h. Starten von imetalling::Processor.


Darüber hinaus benötigen wir mehrere auf Metal aufbauende Dienstprogrammklassen, um den Host-Code und den Kernel-Code auf MSL logisch zu trennen. Diese beinhalten:


  • imetalling::Function : Eine Basisklasse, die die Arbeit mit der Metal-Befehlswarteschlange verdeckt. Der Hauptparameter ist der Name des Kernels im MSL-Code und der Ausführer des Kernel-Aufrufs.


  • imetalling:Kernel : Eine allgemeine Klasse zum Umwandeln einer Quelltextur in eine Zieltextur, die die Funktion erweitert, um einfach die Parameter zum Aufrufen des MSL-Kernels festzulegen.


  • imetalling::PassKernel : Kernel umgehen.


  • imetalling::FalseColorKernel : Unsere Hauptfunktionsklasse, ein „Predator“-Emulator, der eine Posterisierung (Downsampling) auf eine bestimmte Anzahl von Farben durchführt.


Der Kernel-Code für den „Predator“-Modus könnte so aussehen:

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


Initialisierung des OFX-Plugins

Wir beginnen mit der Definition der Klasse imetalling::falsecolor::Factory. In dieser Klasse legen wir einen einzelnen Parameter fest – den Status des Monitors (entweder ein oder aus). Dies ist für unser Beispiel notwendig.

Wir werden von OFX::PluginFactoryHelper erben und fünf Methoden überladen:


  • Load() : Diese Methode wird aufgerufen, um die Instanz global zu konfigurieren, wenn das Plugin zum ersten Mal geladen wird. Das Überladen dieser Methode ist optional.


  • unload() : Diese Methode wird aufgerufen, wenn eine Instanz entladen wird, beispielsweise um Speicher zu löschen. Das Überladen dieser Methode ist ebenfalls optional.


  • beschreiben(ImageEffectDescriptor&) : Dies ist die zweite Methode, die der OFX-Host aufruft, wenn das Plugin geladen wird. Es ist virtuell und muss in unserer Klasse definiert werden. Bei dieser Methode müssen wir alle Eigenschaften des Plugins festlegen, unabhängig von seinem Kontexttyp. Weitere Einzelheiten zu den Eigenschaften finden Sie im ImageEffectDescriptor Code.


  • beschreibenInContext(ImageEffectDescriptor&,ContextEnum) : Ähnlich wie die describe wird diese Methode auch beim Laden des Plugins aufgerufen und muss in unserer Klasse definiert werden. Es sollte Eigenschaften definieren, die mit dem aktuellen Kontext verknüpft sind.


    Der Kontext bestimmt die Art der Vorgänge, mit denen die Anwendung arbeitet, z. B. Filter, Farbe, Übergangseffekt oder Frame-Retimer in einem Clip.


  • createInstance(OfxImageEffectHandle, ContextEnum) : Dies ist die wichtigste Methode, die wir überladen. Wir geben einen Zeiger auf ein Objekt vom Typ ImageEffect zurück. Mit anderen Worten, unser imetalling::falsecolor::Plugin , in dem wir alle Funktionalitäten definiert haben, sowohl im Hinblick auf Benutzerereignisse im Host-Programm als auch auf das Rendern (Umwandeln) des Quell-Frames in den Ziel-Frame:
 OFX::ImageEffect *Factory::createInstance(OfxImageEffectHandle handle,OFX::ContextEnum) {     return new Plugin(handle);   }


Umgang mit Ereignissen

Wenn Sie zu diesem Zeitpunkt ein Bundle mit dem OFX-Modul kompilieren, ist das Plugin bereits in der Host-Anwendung verfügbar und kann in DaVinci auf den Korrekturknoten geladen werden.


Um jedoch vollständig mit einer Plugin-Instanz arbeiten zu können, müssen Sie mindestens den interaktiven Teil und den Teil definieren, der mit der Verarbeitung des eingehenden Videostreams verbunden ist.


Dazu erben wir von der Klasse OFX::ImageEffect und überladen virtuelle Methoden:


  • changesParam(const OFX::InstanceChangedArgs&, const std::string&) – Mit dieser Methode können wir die Logik für die Behandlung des Ereignisses definieren. Der Ereignistyp wird durch den Wert von OFX::InstanceChangedArgs::reason bestimmt und kann sein: eChangeUserEdit, eChangePluginEdit, eChangeTime – das Ereignis ist als Ergebnis einer vom Benutzer bearbeiteten Eigenschaft, einer Änderung in einem Plugin oder einer Hostanwendung oder aufgetreten als Ergebnis einer Änderung in der Zeitleiste.


    Der zweite Parameter gibt den String-Namen an, den wir in der Plugin-Initialisierungsphase definiert haben. In unserem Fall handelt es sich um einen Parameter: false_color_enabled_check_box .


  • isIdentity(...) – Mit dieser Methode können wir die Logik für die Reaktion auf ein Ereignis definieren und einen Zustand zurückgeben, der bestimmt, ob sich etwas geändert hat und ob das Rendern sinnvoll ist. Die Methode muss false oder true zurückgeben. Dies ist eine Möglichkeit, die Anzahl unnötiger Berechnungen zu optimieren und zu reduzieren.


Sie können die Implementierung der interaktiven Interaktion mit OFX im Interaction.cpp- Code lesen. Wie Sie sehen, erhalten wir Zeiger auf die Clips: den Quellclip und den Speicherbereich, in den wir die Zieltransformation einfügen werden.

Implementierung von Rendering Launch

Wir werden eine weitere logische Ebene hinzufügen, auf der wir die gesamte Logik für den Start der Transformation definieren. In unserem Fall ist dies bisher die einzige Methode zum Überschreiben:


  • render(const OFX::RenderArguments& args) – Hier können Sie die Eigenschaften der Clips herausfinden und entscheiden, wie sie gerendert werden sollen. Außerdem stehen uns in dieser Phase auch die Metal-Befehlswarteschlange und einige nützliche Attribute zur Verfügung, die mit den aktuellen Timeline-Eigenschaften verknüpft sind.

wird bearbeitet

In der Startphase stand uns ein Objekt mit nützlichen Eigenschaften zur Verfügung: Wir haben mindestens einen Zeiger auf den Videostream (genauer gesagt einen Speicherbereich mit Frame-Bilddaten) und vor allem eine Warteschlange mit Metal-Befehlen.


Jetzt können wir eine generische Klasse erstellen, die uns einer einfachen Form der Wiederverwendung von Kernel-Code näher bringt. Die OpenFXS-Erweiterung verfügt bereits über eine solche Klasse: OFX::ImageProcessor; wir müssen es nur überladen.


Im Konstruktor gibt es den Parameter OFX::ImageEffect, d. h. wir erhalten darin nicht nur den aktuellen Status der Plugin-Parameter, sondern alles, was für die Arbeit mit der GPU notwendig ist.


Zu diesem Zeitpunkt müssen wir lediglich die Methode „processImagesMetal()“ überladen und die Verarbeitung der bereits auf Metal implementierten Kernel initiieren.

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


Aufbau des Projekts

Um das Projekt zu erstellen, benötigen Sie CMake und es muss mindestens Version 3.15 sein. Darüber hinaus benötigen Sie Qt5.13, das die einfache und bequeme Zusammenstellung des Bundles mit dem Plugin-Installer im Systemverzeichnis unterstützt. Um cmake zu starten, müssen Sie zunächst ein Build-Verzeichnis erstellen.


Nachdem Sie das Build-Verzeichnis erstellt haben, können Sie den folgenden Befehl ausführen:


 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 


Ihr persönlicher „Raubtier“


Anschließend wird das Installationsprogramm mit dem Namen IMFalseColorOfxInstaller.app in dem Verzeichnis angezeigt, das Sie im Parameter PLUGIN_INSTALLER_DIR angegeben haben. Lasst uns loslegen und es starten! Sobald die Installation erfolgreich ist, können Sie DaVinci Resolve starten und unser neues Plugin verwenden.


Sie können es im OpenFX-Bedienfeld auf der Farbkorrekturseite finden, auswählen und als Knoten hinzufügen.



Funktionierende Falschfarbe



Externe Links

  1. Falschfarbener OFX-Plugin-Code
  2. Die Open Effects Association
  3. Laden Sie DaVinci Resolve – OFX-Header-Dateiversion und OFXS-Bibliothekscode unter Resolve + Beispiele herunter