Custom Operators with the TouchDesigner CDK
Build native C++ operators using the Component Development Kit for performance-critical paths, proprietary hardware interfaces, and GPU compute.
The Component Development Kit (CDK) is Derivative’s C++ SDK that lets you author operators that compile to native .dll files and load directly into TouchDesigner as first-class citizens. From TD’s perspective, a CDK plugin is indistinguishable from a built-in operator — it appears in the OP Create dialog, it has typed pins, it can cook on the GPU, and it participates in the dependency graph like everything else.
When to Use CDK vs Python
Python Script OPs are adequate for most logic, but three scenarios justify the overhead of a C++ plugin:
Performance-critical inner loops. Python’s GIL and interpreter overhead make it unsuitable for per-sample or per-pixel work at production frame rates. A Python Script CHOP calling numpy is fine for occasional batch operations, but a CHOP that runs every frame against 32,000 samples at 60 Hz needs native code.
Proprietary hardware interfaces. Manufacturers of LED processors, motion-capture systems, and industrial sensors ship C or C++ SDKs. Calling these from Python via ctypes works for prototype work, but you lose type safety, you can’t share CUDA contexts, and the FFI overhead is non-trivial. A CDK wrapper links directly against the vendor library.
GPU compute beyond what GLSL TOPs expose. GLSL TOP fragment shaders are powerful, but you cannot issue compute dispatches, use CUDA, or manage multi-pass pipelines with complex synchronisation from GLSL alone. A CDK TOP can call into CUDA, DirectX 12 compute, or Vulkan compute and present the result as a TD texture.
Setting Up the Build Environment
You need Visual Studio 2022 (Community edition is fine) with the “Desktop development with C++” workload installed. The CDK download lives on Derivative’s website under each build’s release page — match the CDK version exactly to the TD build you’re targeting because the ABI is versioned per build.
Unzip the CDK to a stable path, for example C:\TD_CDK\. The distribution contains:
TD_CDK/
CPlusPlus_Common.h # Core TD type definitions
TOP_CPlusPlusBase.h # TOP base class
CHOP_CPlusPlusBase.h # CHOP base class
DAT_CPlusPlusBase.h # DAT base class
SOP_CPlusPlusBase.h # SOP base class
samples/
CPlusPlusCHOPExample/
CPlusPlusTOPExample/
Open CPlusPlusTOPExample.sln in Visual Studio 2022. The project is pre-configured with include paths pointing to the CDK headers. Set the platform to x64 and build configuration to Release before doing anything else. Verify the post-build event copies the compiled .dll to %USERPROFILE%\Documents\Derivative\Plugins — that path is where TD scans for plugins at startup.
The CPlusPlus TOP Template
Every TOP plugin must implement three entry points that TD resolves from the DLL:
// Required DLL exports — these are what TD's plugin loader calls
extern "C" {
DLLEXPORT TOP_CPlusPlusBase* CreateTOPInstance(const OP_NodeInfo* info, TOP_Context* context);
DLLEXPORT void DestroyTOPInstance(TOP_CPlusPlusBase* instance, TOP_Context* context);
DLLEXPORT int32_t GetTOPAPIVersion(void);
}
GetTOPAPIVersion must return TOP_CPLUSPLUS_API_VERSION from the header — this is how TD detects ABI incompatibility and refuses to load a plugin built against a mismatched CDK.
The execute() method is where your cooking logic lives:
void
MyInvertTOP::execute(TOP_OutputFormatSpecs* outputFormat,
const OP_Inputs* inputs,
TOP_Context* context,
void* reserved1)
{
const TOP_Input* input = inputs->getInputTOP(0);
if (!input) return;
TOP_UploadInfo info;
info.colorBufferIndex = 0;
// Request a CPU buffer to write into
uint8_t* buf = (uint8_t*)context->beginCPUMemoryTransfer(
outputFormat->width * outputFormat->height * 4,
TOP_TransferType::CPU_Write,
&info);
if (!buf) return;
// Download the input texture to CPU
const OP_TOPInput* topInput = inputs->getInputTOP(0)->getCPUData(
TOP_CPUMemPixelType::BGRA8Fixed, 0, 0);
if (topInput)
{
const uint8_t* src = (const uint8_t*)topInput->data;
int64_t totalBytes = outputFormat->width * outputFormat->height * 4;
for (int64_t i = 0; i < totalBytes; i++)
{
// Invert R, G, B but leave alpha unchanged
buf[i] = (i % 4 == 3) ? src[i] : 255 - src[i];
}
}
context->endCPUMemoryTransfer(buf, &info);
}
getOutputFormat() tells TD what resolution and pixel format to allocate for the output texture:
void
MyInvertTOP::getOutputFormat(TOP_OutputFormat* format,
const OP_Inputs* inputs,
void* reserved1)
{
const TOP_Input* input = inputs->getInputTOP(0);
if (input)
{
format->width = input->width;
format->height = input->height;
}
format->pixelFormat = TOP_PixelFormat::BGRA8Fixed;
}
The CPlusPlus CHOP Template
CHOP plugins are simpler because they deal with flat arrays of floating-point samples rather than 2D texture memory. The two critical methods are execute() and getNumInfoCHOPChans().
void
MySerialCHOP::execute(CHOP_Output* output,
const OP_Inputs* inputs,
void* reserved)
{
// output->numChannels and output->numSamples are set by getOutputInfo()
for (int chan = 0; chan < output->numChannels; chan++)
{
float* chanData = output->channels[chan];
for (int sample = 0; sample < output->numSamples; sample++)
{
chanData[sample] = mLatestValues[chan];
}
}
}
int32_t
MySerialCHOP::getNumInfoCHOPChans(void* reserved1)
{
// How many channels appear in the Info CHOP
return 2;
}
void
MySerialCHOP::getInfoCHOPChan(int32_t index, OP_InfoCHOPChan* chan, void* reserved1)
{
if (index == 0) { chan->name->setString("bytes_received"); chan->value = (float)mBytesReceived; }
if (index == 1) { chan->name->setString("parse_errors"); chan->value = (float)mParseErrors; }
}
Full Example: 32-Channel Custom Serial Protocol CHOP
This is the real motivation for CDK: a proprietary sensor array that streams a binary packet at 2,000 Hz. Python can’t keep up; C++ handles it trivially.
// In the constructor, open the COM port and start a reader thread
MySerialCHOP::MySerialCHOP(const OP_NodeInfo* info)
: mRunning(true), mBytesReceived(0), mParseErrors(0)
{
memset(mLatestValues, 0, sizeof(mLatestValues));
mPort = CreateFileA("\\\\.\\COM7",
GENERIC_READ, 0, nullptr,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED, nullptr);
DCB dcb = {};
dcb.DCBlength = sizeof(DCB);
dcb.BaudRate = CBR_115200;
dcb.ByteSize = 8;
dcb.StopBits = ONESTOPBIT;
dcb.Parity = NOPARITY;
SetCommState(mPort, &dcb);
mReaderThread = std::thread(&MySerialCHOP::readerLoop, this);
}
void
MySerialCHOP::readerLoop()
{
// Packet format: 0xAA 0xBB [32 × float32 LE] 0xCC 0xDD
constexpr int PACKET_SIZE = 2 + 32 * 4 + 2;
uint8_t buf[PACKET_SIZE];
DWORD bytesRead = 0;
OVERLAPPED ov = {};
ov.hEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
while (mRunning)
{
ReadFile(mPort, buf, PACKET_SIZE, &bytesRead, &ov);
WaitForSingleObject(ov.hEvent, 16); // max 16 ms wait
GetOverlappedResult(mPort, &ov, &bytesRead, FALSE);
if (bytesRead == PACKET_SIZE &&
buf[0] == 0xAA && buf[1] == 0xBB &&
buf[PACKET_SIZE-2] == 0xCC && buf[PACKET_SIZE-1] == 0xDD)
{
std::lock_guard<std::mutex> lock(mMutex);
memcpy(mLatestValues, buf + 2, 32 * sizeof(float));
mBytesReceived += bytesRead;
}
else
{
mParseErrors++;
}
ResetEvent(ov.hEvent);
}
CloseHandle(ov.hEvent);
}
The execute() method simply copies the latest values from the mutex-protected array into the output channels. This runs in TD’s cook thread without any I/O blocking — the reader thread handles all serial communication independently.
Compiling and Loading
Build in Release / x64. The post-build event should produce MyPlugin.dll in %USERPROFILE%\Documents\Derivative\Plugins\. Restart TD (plugins load at startup). Create a new OP — type “CPlusPlusCHOP” or the custom menu name you registered — and the plugin appears.
Versioning and ABI Compatibility
The CDK major version must match the TD build. Derivative increments the ABI version with each official release. If you load a plugin compiled against CDK 2023.11600 into TD 2023.12000, TD will log an error and refuse to cook the OP. Maintain a build matrix: a separate VS solution configuration per supported TD build, each pointing to the corresponding CDK headers. Use Git tags to mark which commit built against which CDK version. Automate this with a CI pipeline (GitHub Actions on a Windows runner) that builds .dll artifacts per CDK version on every push.