Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

DCC Oxidizer

Welcome to the docs for DCC Oxidizer.

DCC Oxidizer; in other words, making DCC's rusty so they can be used with the Rust programming language.

Why

My motivation for this crate is to enable image processing in a safer and more modern language. Rust offers the performance of a low-level language like C, combined with the advantages of robust safety features such as the borrow checker, no performance downsides caused by garbage collectors, and a modern package manager. Imho, this makes Rust an ideal choice for image processing development.

The only downside is that each DCC has quite some different ways of handling their images. And for using it in one format in Rust would require their buffers to be translated into a single format.

This crate takes care of the translations for the DCC buffers into the image crate. That makes it possible to write all image processing code in one single way, and then link it to all DCC's. Linking Rust to C and C++ is quite good, thanks to cbindgen. That allows the library in Rust to expose some functions which can then be called from their C or C++ counterparts, using FFI (foreign function interface).

Additionally, this crate provides ready to use examples to start with plug-in development, using the FFI bridge setup.

Note: This crate is still in active development, so expect API changes (these would be in the minor upgrade (v0.x.0)). Only Nuke is supported as of now, the plan is to add more.

Installation

Add this crate to your project by running

cargo add dcc-oxidizer
cargo add image
cargo add libc
cargo add cbindgen --build

Or add this to your Cargo.toml file

[dependancies]
dcc-oxidizer = "0.1.2"
image = ">=0.25.0"
libc = ">=0.2.0"

[build-dependencies]
cbindgen = ">=0.30.0"

Make sure your project is configured as a lib. For example:

[lib]
name = "example_plugin"
crate-type = ["staticlib"]
path = "src/lib.rs"

For a working example, please take a look at the Nuke example project Cargo.toml.

Examples

Currently only Nuke is supported, so the only example is available for Nuke.

Nuke

To start with Nuke, it is recommended to first setup your Rust project.

It is recommended to use the example provided in the repository as a quickstart. This serves as a ready to compile project.

Processing in Rust

First we start by creating the rendering functionality. We create a lib.rs file. This will expose a C function that the Nuke plugin can link to. This C function receives the ImagePlaneBuffer data, and calls the DCC Oxidizer functionality to translate that buffer data into a Rust native Image object. From that point on, you can do all the processing in Rust to how you'd like to do it. After that, the data is send back to Nuke, where it can finish the rendering.

As a starting point, it is recommended to use the example for Nuke. This contains the bare minimum code to start writing your image processing functionality.

If you want to do everything from scratch, please read the Create build.rs as well.

The Rust code:

#![allow(unused)]
fn main() {
use dcc_oxidizer::buffers::{converter::Converter, nuke::ImagePlaneBuffer};
use image::Rgba32FImage;
use libc::size_t;
use rayon::prelude::*;

#[unsafe(no_mangle)]
/// Render provided stripe from Nuke and modify provided buffer.
pub unsafe extern "C" fn render(
    buffer: *mut ImagePlaneBuffer,
    width: size_t,
    height: size_t,
    current_y_position: size_t,
) {
    let buffer_ref = unsafe {
        if buffer.is_null() {
            return;
        }
        buffer.as_mut().unwrap()
    };

    // In real scenarios make sure to handle the errors properly, as this will cause a panic if it fails.
    let mut processing_image: Rgba32FImage = unsafe { buffer_ref.get_image().unwrap() };

    processing_image
        .par_enumerate_pixels_mut()
        .for_each(|(x, y, pixel)| {
            pixel[0] = pixel[0] * 0.5 + x as f32 / width as f32;
            pixel[1] = pixel[1] * 0.5 + (current_y_position as f32 + y as f32) / height as f32;
        });

    unsafe {
        buffer_ref.apply_image(processing_image).unwrap();
    }
}
}

Basically the code does 4 things:

  1. Receive the ImagePlaneBuffer from Nuke.
  2. Convert the ImagePlaneBuffer into a native Rust Image object.
  3. Perform some image processing (just a simple UV overlay example).
  4. Copy the processed Image back into Nuke ImagePlane format.

Error handling

In a production ready scenario you would need to cover error handling as well. One possible way to do so is send send a CString back to Nuke. If the CString is empty, no error occured. If the ptr is not null, then call the Op::Error with the data to show to the user.

Also keep in mind that Nuke might abort the render while the render is still running. In this case you would need to make the abort() boolean available to Rust, and adjust the logic for rendering to abort when this is true. For the scope of simplicity this is not handled in this code.

Rayon

Optionally, I used Rayon in this example. Not really necessary for this UV example. It allows for multithreaded rendering with the Image crate. But as you are probably doing something more advanced than simple UV calculations, it is probably worth to do it this way.

Build

If you haven't used the default example, please read the Create build.rs file first.

Preparing NDK node

Nuke offers a few methods to handle image rendering. Usually for simple image processing this is the row based rendering. With DCC oxidizer only the PlanarIop is supported. This creates ImagePlane buffers. These can be translated to images in Rust. For simple image processing plugins, where simple pixel manipulations are done (row based), this FFI approach is a bit overkill. Just write those in native C++.

Create the node as usual. A ready to use example is available in the examples in the repository. It is recommended to use the entire example as a starting point, and change it to your needs. This should be ready to compile immediately.

In your PlanarIop C++ file, make sure to import your created header thats created in build.rs.

The important part of the file is the renderStripe method.

void ExampleUVOverlay::renderStripe(DD::Image::ImagePlane &output_plane) {
  input0().fetchPlane(output_plane);

  ImagePlaneBuffer image_plane_buffer;
  image_plane_buffer.channels = output_plane.channels().size();
  image_plane_buffer.data = output_plane.writable();
  image_plane_buffer.invert_rows = false;
  image_plane_buffer.length = calculate_buffer_size(&output_plane);
  image_plane_buffer.packed = output_plane.packed();
  image_plane_buffer.resolution[0] = output_plane.bounds().w();
  image_plane_buffer.resolution[1] = output_plane.bounds().h();

  render(&image_plane_buffer, resolution[0], resolution[1],
         output_plane.bounds().y());
}

What we do here is create a ImagePlaneBuffer struct to get the necessary data for Rust to understand the ImagePlane. Make sure all data is set correctly. If you prefer to do your image processing in Rust in the common y0x0 is top left format instead of bottom left, set invert_rows to true.

You can of course modify the render call in Rust to allow additional data to be passed to your code.

We then call the render function to render the image in Rust.

Running in Nuke

Make sure you've setup the CMake as in the examples or read the CMake example here.

Once setup, run this command (commands may vary on Windows):

cmake . -B build && cmake --build build

This will automatically build the Rust project and statically link it to the Nuke binary.

Test in Nuke

When built, before launching Nuke, add the build lib to the NUKE_PATH environment:

export NUKE_PATH="/path/to/this/directory/build/lib":$NUKE_PATH

Then once you've opened Nuke, you can press x and enter the node name ExampleUVOverlay.

When rendering, it will overlay the x and y coordinates in the red and green channels. And overlay it on input image. This processing is all done with the Rust library at src/lib.rs.

overlay

Create build.rs

It is necessary to create the header files to be able to link the compiled staticlib from Rust. For this, cbindgen is used to create these automatically.

This is an example configuration to do so:

use cbindgen;

fn main() {
    create_bindings();
}

/// Create the bindings for the FFI interface between the DCC and Rust
fn create_bindings() {
    let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
    cbindgen::Builder::new()
        .with_crate(crate_dir)
        .with_include("dcc_oxidizer.h")
        .generate()
        .expect("Unable to generate bindings")
        .write_to_file("./include/example.h");
}

I've chosen to write the headers to ./include/example.h. However, choose any directory you'd like. Most importantly is the line to include dcc_oxidizer.h. This will add a line to include the dcc-oxidizer header file. However, without any configuration this file will not exist.

That's why it is important to set the environment variable to create the dcc_oxidizer.h file as well. This is only necessary to do once, or when updating the crate.

This can be done automated with the .cargo/config.toml as shown in the examples. Or set it manually in your shell or CMake.

export DCC_OXIDIZER_HEADER_FILE=path/to/your/directory/dcc_oxidizer.h

After you've set this, run the build and the file will be generated.

Building (CMake)

This depends mostly on the way the DCC plug-in has to be built. For Nuke the common way to build the C++ plug-in is using CMake. This is an example configuration for that:

cmake_minimum_required(VERSION 3.25 FATAL_ERROR)
project(ExampleUVOverlayProject)

set(CMAKE_CXX_STANDARD ${CPP_BUILD_VERSION})
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
set(CMAKE_BUILD_TYPE "Release")
find_package(Nuke REQUIRED)

if(WIN32)
    add_compile_definitions(NOMINMAX)
    add_compile_definitions(_USE_MATH_DEFINES)
    add_compile_definitions(_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR)
    set(CMAKE_C_RUNTIME_LIBRARY "MultiThreadedDLL")
endif()

set(CMAKE_MODULE_PATH "CMake;${CMAKE_MODULE_PATH}")
set(CMAKE_POSITION_INDEPENDENT_CODE TRUE)

include(FetchContent)

FetchContent_Declare(
    Corrosion
    GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git
    GIT_TAG v0.5
    )
    FetchContent_MakeAvailable(Corrosion)
    corrosion_import_crate(MANIFEST_PATH Cargo.toml CRATE_TYPES "staticlib")
    
    
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)

include_directories(./include)

function(add_plugin PLUGIN_NAME)
    add_nuke_plugin(${PLUGIN_NAME} ${ARGN})
    target_link_libraries(${PLUGIN_NAME} PRIVATE example_uv_overlay )
    target_compile_definitions(${PLUGIN_NAME} PRIVATE FN_PLUGIN)
endfunction()

add_plugin(ExampleUVOverlay ExampleUVOverlay.cpp)

This also includes automatic building of the Rust code using corrosion-rs, which makes it easy to bridge the gap between CMake and Rust. After it has been built, it is also linked using target_link_libraries.

Note: For speed improvements when building it for multiple versions of Nuke: consider building the lib once, then link it manually in each build step per Nuke version.

DCC Oxidizer header files

To generate the header files for the data objects of DCC Oxidizer, built the project at least once with the environment DCC_OXIDIZER_HEADER_FILE set to the path where the header files should be written to. It will generate it automatically. More information at Create build.rs.