Tool Development: Resolve, Fusion, Custom Code #0: Introduction
Welcome to this six-part blog series on developing custom colour grading tools using DaVinci Resolve, Fusion, and DCTL. Throughout this series, you'll learn how to build five powerful tools—each one a gateway into a deeper rabbit hole of colour manipulation. We’ll give you just enough insight to dive fully into each concept, helping you explore the rich and complex world of colour tool development. Along the way, we’ll create a photometric exposure tool, an RGB mixer with dynamic colour crosstalk (introducing independent channel manipulation), a custom built saturation tool, a luma vs sat curve for working across non-RGB spaces using curves, and a custom masking system for generating mattes—essential components you can combine to craft your own bespoke grading tools. All of this will be built across three powerful platforms, each offering its own unique strengths in precision, control, and creative potential.
We’ll begin in the Colour page, where we use native nodes and operators. This is the most accessible entry point—especially if you're already comfortable with Resolve’s interface—and offers an intuitive way to prototype and understand tool behaviour. However, while flexible, this approach can become complex and cumbersome for real-world workflows.
Next, we’ll transition into the Fusion page, where you'll begin working directly with the mathematics behind your tools. Fusion offers much more granular control, helping you understand exactly how adjustments affect the image. It has a steeper learning curve, but it unlocks tremendous technical and creative power.
Finally, we’ll step into DCTL coding, where we write custom tools from scratch in a text editor. Here, you'll translate your mathematical ideas into code that runs natively in Resolve’s Colour page—just like built-in effects.
By the end of this series, you won’t just have recreated some of Resolve’s native tools—you’ll have the knowledge and confidence to build your own. Each stage introduces core concepts in tool development and gives you a solid foundation to expand your creative and technical toolkit as a colourist.
But before we dive in and start building, this post will focus on the big picture—what tool development really is, why it matters, and how the process works across the three platforms.
What actually are colour grading tools?
One of the most eye-opening moments in my colour grading journey was realising that every colour grading tool is essentially a mathematical operation applied to image data.
Every image is made up of pixels, and each pixel contains three values: Red, Green, and Blue. A colour grading tool modifies those values in some way—sometimes uniformly, sometimes selectively, sometimes based on thresholds or through interactions with the other channels.
That’s the core idea. No matter how complex a tool appears, at its heart, it’s just doing maths to RGB values. That simplicity is empowering. It means if you understand the maths, you can build the tool.
Step 1: The Colour Page
The Colour page is the most familiar to many users, and that’s where we’ll begin—building custom tools using Resolve’s native nodes and operations.
Despite its limitations, there’s still a lot of power here. By combining tools like layer mixers, composite modes, and curves, we can create surprisingly sophisticated operations. These node setups can become complex and impractical for everyday grading, but they’re perfect for prototyping and understanding how tools behave.
Think of this stage as using node trees to represent mathematical formulas. It’s a great starting point for grasping the structure and logic of a grading tool—even if it’s not the most efficient for everyday work.
Step 2: The Fusion Page
Fusion is still node-based, so it feels familiar—but it also introduces a powerful node called the Custom Tool. This is a game-changer. It lets you write mathematical operations directly and apply them to your image—all in one node.
The Custom Tool has six tabs, but we’ll focus mainly on:
Controls: where you define sliders and curves
Channels: where you manipulate the red, green, and blue image channels
Inter: where you store intermediate formulas for reuse
Here’s how it works in practice:
Let’s say we want to build an Offset tool (which is just uniform addition across all channels). We can reference the term n1 which is the name of the first slider in the Controls tab like so:
red = r1 + n1 green = g1 + n1 blue = b1 + n1
Just like that, we’ve built an offset control which will dynamically adjust our image when the slider labelled ‘Number in 1’ (n1) in the controls tab is adjusted. The Custom Tool is a perfect middle ground—it lets us go deeper with maths while still giving us instant visual feedback and a familiar workspace.
Step 3: Custom DCTL Coding
Now for the deep dive: custom tool creation with DCTL (DaVinci Color Transform Language). This is the most advanced stage, but also the most powerful.
The good news is: if you’ve made it through the Colour and Fusion pages, you already understand the underlying maths. Now, it’s just a matter of translating those formulas into code.
However, unlike Fusion, DCTL doesn’t give you any prebuilt components—no sliders, no automatic channel access. You have to define everything yourself. But in return, you get full control and customisation.
The Basics of Writing a DCTL
To create a working DCTL file, you need to:
Install a text editor application - I personally like using Sublime text.
Save the file with a .dctl extension—otherwise, Resolve won’t recognise it.
Include a main transform function, which is required for the code to compile. This is a block of standard boilerplate that we’ll reuse across all our tools - you can copy this block of code below.
__DEVICE__ float3 transform(int p_Width, int p_Height, int p_X, int p_Y, float p_R, float p_G, float p_B){ float3 out = make_float3(p_R, p_G, p_B); return out; }
Before we jump in to writing our DCTL, there’s a few things we should go over about the basics of writing a DCTL that actually runs. What you’ll inevitably find when you start writing DCTL is that it virtually never works first time - compilers are notoriously picky, and they’ll refuse to run your code if there is a single element of the syntax written incorrectly—there’s no autocorrect, it will simply spit out an error message, which may help you pinpoint where the issue lies, or it may not. So here are a couple basic things to know to ensure your code works. There’ll be another checklist at the end of this post to recap a few more troubleshooting steps—but you can check those once you have the basics down.
Every actionable line must be terminated with a semi-colon. This is the equivalent of a full-stop, it tells the compiler that this is the end of this operation.
After you’ve declared a function, which we’ll learn about soon, you must wrap the contents of this function within curly brackets.
Now we have everything we need to get started—so let’s build that same Offset tool again, this time in DCTL.
Step 3.1: Define the Slider
To create a slider control with decimal-point precision in DCTL, we use the following template:
DEFINE_UI_PARAMS(Variable_name, Label, DCTLUI_SLIDER_FLOAT, Default, Min, Max, Step)
Let’s break this down:
Variable_name: is is the internal name you'll use when referencing the slider in your mathematical formulas. Unlike Fusion (where sliders are fixed as n1, n2, etc.), you can name this anything you like.
Label: This is the user-facing name of the control—what will be displayed in Resolve’s interface.
Default: The initial value the slider starts at.
Min/Max: The lowest and highest values the slider can be set to.
Step: The amount the value changes when the user drags the slider. Setting this to
0.001
allows for fine control.
So for our offset control, we can adjust this appropriately:
DEFINE_UI_PARAMS(offset_control, Offset, DCTLUI_SLIDER_FLOAT, 0, -10, 10, 0.001)
I personally like to sit this line of code above the main transform function in your DCTL. Once defined, you can reference the variable_name directly in your colour operations to dynamically control the effect via the slider.
DEFINE_UI_PARAMS(offset_control, Offset, DCTLUI_SLIDER_FLOAT, 0, -10, 10, 0.001) __DEVICE__ float3 transform(int p_Width, int p_Height, int p_X, int p_Y, float p_R, float p_G, float p_B){ float3 out = make_float3(p_R, p_G, p_B); return out; }
Step 3.2: Hook the slider into the operation
In DCTL, applying your mathematical formula to the image is conceptually similar to what we did in Fusion—but with a few important differences.
First, it’s crucial that the image manipulation occurs between the main transform function and the “return out;” line, which marks the end of the function and returns the modified image.
Here’s the key line we’ll insert into that space:
out = OffsetRGB(out, offset_control);
So our current full block of code looks like this:
DEFINE_UI_PARAMS(offset_control, Offset, DCTLUI_SLIDER_FLOAT, 0, -10, 10, 0.001) __DEVICE__ float3 transform(int p_Width, int p_Height, int p_X, int p_Y, float p_R, float p_G, float p_B){ float3 inRGB = make_float3(p_R, p_G, p_B); float3 out = inRGB; out = OffsetRGB(out, offset_control); return out; }
At first glance, this new line might seem cryptic, and that’s perfectly normal—it doesn't mean much yet. Let’s unpack it.
What is “out”?
Beneath the main transfer function DCTL code, you’ll see these two lines:
float3 inRGB = make_float3(p_R, p_G, p_B); float3 out = inRGB;
Let’s break this down:
inRGB is a variable that contains our incoming red, green, and blue pixel values.
out is simply a copy of inRGB.
At this point in the code, inRGB and out are identical—they both represent the unmodified image data.
So why do we need both?
Technically, for this basic Offset tool, we don’t. But keeping both variables is a helpful habit to form:
inRGB can serve as a reference to the original image—a snapshot of the input state, untouched.
out becomes the working copy—the version of the image that we’ll progressively modify.
This pattern becomes especially useful when you’re building more complex tools. Having inRGB always available means you can refer back to the original state at any point, without undoing any previous changes made to out.
And remember, the terms inRGB and out are completely arbitrary. You, as the author, can name them anything you like. I personally prefer these names because they’re clear and intuitive: inRGB tells me it’s the raw input image, and out signals the final result after processing. So even though they’re just labels, choosing meaningful names helps keep your code readable—especially as your tools grow in complexity.
So, when we write:
out = OffsetRGB(out, offset_control);
We’re saying: Take the current image (out) - in other words, the red, green and blue values of every pixel, run it through a function we’ve named OffsetRGB, and modify it based on the value of the user-controlled slider (offset_control). And this result will become our new ‘out’ going forward.
Since we haven't yet defined what OffsetRGB is, this line alone won’t do anything meaningful —except return a compilation error if we tried to load it up inside Resolve. But now that we’ve used it, we can work backwards and define that function next.
This structure—calling a function before defining it—is a common and powerful coding pattern, and one I find more intuitive than writing functions in advance. It’s a bit like skipping the harder questions on a maths test and tackling the easy ones first. By banking some early wins, you build momentum—and when you circle back to the tougher parts (like writing the function itself), you do so with more confidence and mental clarity.
Step 3.3: Define the function
Now that we’ve referenced a function called OffsetRGB in our main code, it’s time to actually define it - which is where the maths we’ve already learned in fusion comes in.
In DCTL, a function is like a container—very similar to the Inter tab in the Fusion Custom Tool. It holds mathematical operations that you can call and reuse throughout your code.
Here’s how we define the OffsetRGB function at the top of our DCTL file to create an offset effect:
__DEVICE__ float3 OffsetRGB(float3 in, float offset) { in.x = in.x + offset; in.y = in.y + offset; in.z = in.z + offset; return in; }
At first glance, this might look a little complicated, so let’s break it down step by step:
__DEVICE__
This keyword is required at the start of every function in DCTL. It tells Resolve that the function is designed to run on the GPU.
float3
This indicates that the function operates on three floating-point values—important because this will allow us to adjust the red, green, and blue channels with high precision.
OffsetRGB(float3 in, float offset)
This is the name of the function (OffsetRGB) and the parameters it takes:
float3 in is a placeholder for the image data being passed into the function. It contains three components - which will soon become the red, green, and blue values of each pixel.
float offset is a second parameter—a control value from our slider (offset_control), which we’ll use to adjust all three channels.
Note: Both in and offset are just names. You could call them img and adjustment if you wanted—the names don’t matter as long as you’re consistent within the function.
in.x, in.y, in.z
You might be wondering why we’re not using red, green, and blue here. At this point in the DCTL - this block of code has no idea that we’re modifying our red, green, and blue data. It only understands that we’re modifying a variable which contains three components: .x, .y, and .z. Everything here is an empty meaningless placeholder which we’ll eventually substitute out for our image data, But for practical purposes when writing DCTLs, you can safely think of it like this:
.x
= Red.y
= Green.z
= Blue
So the lines:
in.x = in.x + offset; in.y = in.y + offset; in.z = in.z + offset;
will simply add the same value to the red, green, and blue channels of the input pixel.
Finally, the line:
return in;
returns the final result of the container which holds .x, .y, and .z, and finishes off our function.
Quick sidenote:
One thing that really tripped me up when I first started learning DCTL was understanding how variable names work when substituting the user parameters into a function.
Specifically, I couldn’t wrap my head around why the variable inside the function (which we’ve named offset) can have a completely different name from the parameter we pass in (in this case, offset_control). I initially assumed those names had to match exactly in order for the function to work—but that’s not the case.
Here’s the key idea:
When you're defining a function, the variable names used inside it are local to that function — and must match. However, when that function is later re-used - those names no longer matter. What does matter is the order in which values are passed in.
So, as long as the internal logic of the function is consistent (i.e. using offset inside the function), the name of the input that you later pass into it—like offset_control —can be totally different.
The connection is made based on the position of each variable in the list—not its name. For example, take a look at these two lines: the first is the header of the offsetRGB function, and the second is where we call and reuse it:
__DEVICE__ float3 offsetRGB(float3 in, float offset){
out = offsetRGB(out, offset_control);
offset_control is passed into the function as the second argument, so it gets assigned to the offset placeholder within the function. That’s all that matters. And similarly out is substituted for in—since they’re both the first term in the list.
Once I understood that, everything clicked into place. Of course, you're free to use the same name for both the user parameter and the placeholder variable—but I personally prefer to keep them slightly different. It helps create a clear mental distinction between the proxy used inside the function and the actual user-facing control being passed in.
And That’s It!
You’ve just written your first working DCTL—an Offset tool that can now be loaded into DaVinci Resolve and used just like any built-in effect.
__DEVICE__ float3 offsetRGB (float3 in, float offset){ in.x = in.x + offset; in.y = in.y + offset; in.z = in.z + offset; return x; } DEFINE_UI_PARAMS(offset_control, Offset, DCTLUI_SLIDER_FLOAT, 0, -1, 1, 0.001) __DEVICE__ float3 transform(int p_Width, int p_Height, int p_X, int p_Y, float p_R, float p_G, float p_B){ float3 inRGB = make_float3(p_R, p_G, p_B); float3 out = inRGB; out = offset(out, offset_control); return out; }
Let’s quickly recap the flow of logic from top to bottom:
We began by defining a mathematical function—an operation that modifies three internal components (representing the red, green, and blue channels) by adding an unknown variable we named offset.
Next, we processed our image through that function by:
Substituting in the actual RGB values of each pixel for those abstract placeholders—via the out variable, which is the one that actually contains our red, green, and blue channels.
Hooking in our slider control via the offset_control variable, so the unknown value is now controlled by the user in real time via the UI.
This setup gives the user direct, interactive control over the effect—just like any native tool in Resolve.
Once this is set up, all that’s left is to save the file with a .dctl extension if you haven’t already and install it into Resolve.
If you’re unsure how to do that, I’ve included a link to my tools manual. At the top, it walks you through the installation process step by step, so you can get up and running with your custom tool right away.
Wrapping Up
That’s the core process I use for every tool I build:
Start in the Colour page—prototype using native nodes
Move to Fusion—refine the maths using Custom Tool
Finalise in DCTL—write reusable, performant code
In our next blogpost, we’ll follow this process to build a photometric exposure tool—and it’s already live, so feel free to check it out right now.
One Final Note
You’ll make mistakes when writing DCTL—frequently. But that’s not failure; it’s just a natural and expected part of the process.
That said, debugging can often be one of the more frustrating parts of the process. So here’s a quick checklist of common issues to run through when your code fails to compile:
Missing a semicolon at the end of a line
Forgetting a closing parenthesis somewhere in the code
Leaving out a return statement at the end of a function
(The code might still run, but the image will appear heavily broken)Defining two functions with the same name
Calling a function with the wrong number of arguments
Calling a function below where it has been declared
Missing a comma between function arguments
Declaring a variable in a function header without specifying its type (e.g., float, int)
Typos or inconsistent capitalisation in variable names within a function
Errors are inevitable. But each one you troubleshoot builds your confidence and sharpens your skills. With practice, every tool you create will make you a faster, more creative colourist—and a stronger coder.