Multi-Machine TouchDesigner Systems
Design and deploy distributed TouchDesigner rigs across multiple machines using Perform Sync, TouchEngine, OSC, and hot-standby redundancy for large-scale installations.
Single-machine TD rigs hit practical limits at around 7680×1080 continuous output, or when rendering load exceeds what one GPU can sustain at frame rate. Multi-machine architectures distribute that load, but they introduce synchronisation, latency, and failure-mode complexity that must be designed explicitly rather than hoped away.
Why Multiple Machines
Output scale: most GPUs top out at four independent display outputs at 4K60. A nine-projector permanent installation needs at least three machines. A 360-degree cylindrical mapping with twelve projectors needs at least four.
Rendering load: a single 8K generative simulation at 60 fps is often beyond what even the fastest consumer GPU can sustain. Splitting the simulation into tiles — each machine renders its viewport — distributes the load horizontally.
Redundancy: a permanent installation that runs 8 hours a day cannot afford a single point of failure. A hot-standby machine that can take over in under 5 seconds is the minimum viable redundancy model.
TD Perform Sync
Perform Sync is TD’s built-in frame-synchronisation mechanism for LAN environments. It uses a UDP broadcast protocol: one machine is the master, all others are slaves. The master broadcasts a sync signal at the start of each frame; slaves hold their render until they receive it, then cook and present simultaneously.
Setting Up Perform Sync
On the master machine:
- Add a Perform Sync CHOP: Operators > CHOP > Perform Sync
- Set Role to Master
- Set Network Interface to the IP of the LAN adapter connected to the sync network (use a dedicated gigabit switch — never run sync over Wi-Fi)
- Note the Sync Port (default: 9930)
On each slave machine:
- Add a Perform Sync CHOP
- Set Role to Slave
- Set Master IP to the master’s LAN IP address
- Set Sync Port to 9930 (must match master)
The Perform Sync CHOP outputs channels sync_received, sync_latency_ms, and frame_offset. Monitor sync_latency_ms — values above 2 ms indicate network congestion or a switch bottleneck.
Network Latency Budget
For 60 fps operation (16.67 ms frame budget), the sync signal must propagate and be acted upon within 2 ms. This requires:
- Unmanaged or IGMP-snooping gigabit switch — managed switches with STP spanning-tree can introduce 30+ ms delays during topology changes
- All machines on the same switch — do not route through an uplink
- Jumbo frames disabled — large MTU increases per-frame latency
- Network interface interrupt moderation disabled on Windows (Device Manager > NIC advanced settings)
Ping between master and each slave should be consistently below 0.3 ms on a properly configured network.
Scene Distribution Architecture
The canonical pattern for a multi-machine panoramic installation:
Master machine handles:
- All logic (cue state, Python controllers)
- Audio analysis
- CHOP data processing and generation
- No display output (or a single operator monitor output)
Slave machines handle:
- Receiving parameter data from master via OSC or shared state
- Rendering their assigned viewport
- Driving their assigned projectors/displays
The separation is intentional: the master is never under GPU pressure, so it can run Python and logic at full speed; slaves are under maximum GPU pressure (rendering) and should run minimal logic.
OSC for High-Level Sync vs Perform Sync for Frame-Level Sync
Use both simultaneously, at different layers:
Perform Sync operates at the GPU vsync level — it ensures all machines present their rendered frame at the same physical moment. It carries no data, only a timing signal.
OSC (Open Sound Control) carries the actual data — parameter values, cue indices, CHOP channel values. OSC runs over UDP on a separate port and is dispatched from master to slaves every frame using an Execute DAT on the frameEnd callback:
# In an Execute DAT on the master, frameEnd callback
def frameEnd(frame):
# Send current scene parameters to all slaves
osc_out = op('osc_out_chop')
params = {
'/scene/index': op('master_controller').par.Sceneindex.val,
'/audio/rms': op('audio_rms')['chan1'][0],
'/audio/bass': op('audio_bass')['chan1'][0],
'/anim/phase': (absTime.seconds * 0.1) % 1.0,
}
for address, value in params.items():
osc_out.sendOSC(address, [value])
On slaves, an OSC In CHOP with matching port receives the data and routes it to local parameter expressions.
TouchEngine
TouchEngine is a separate runtime (installed with TD) that allows a TD network to run as a headless service — it exposes inputs and outputs to a host application via a C++ API, C API, or via Unreal Engine and Unity plugins. Use TouchEngine when:
- You need TD’s processing inside a game engine environment
- You want to run multiple TD “services” on one machine without multiple full TD instances
- A client wants TD functionality embedded in their own application
TouchEngine licences are separate from TD licences — check the current Derivative pricing. TouchEngine runs the TD Cook/render pipeline without a window; outputs are accessed as textures or CHOP arrays from the host application.
Redundancy: Hot-Standby
A hot-standby configuration keeps a second master machine in a ready state, receiving all the same OSC data as the live master, running the same .toe file, and able to take over in one network reconfiguration step.
# Watchdog on the standby machine — runs in an Execute DAT
# If we haven't received an OSC heartbeat from master in 2 seconds, promote ourselves
HEARTBEAT_TIMEOUT = 2.0 # seconds
last_heartbeat = 0.0
def onReceiveOSC(dat, rowIndex, message, bytes, timeStamp, address, args, peer):
global last_heartbeat
if address == '/heartbeat':
last_heartbeat = absTime.seconds
def frameEnd(frame):
global last_heartbeat
elapsed = absTime.seconds - last_heartbeat
if elapsed > HEARTBEAT_TIMEOUT:
# Master is gone — promote this machine
promote_to_master()
def promote_to_master():
# Switch Perform Sync role from Slave to Master
op('perform_sync').par.Role = 'Master'
# Notify operators via Pushbullet or a local alarm
print(f'[{absTime.seconds:.1f}] STANDBY PROMOTED TO MASTER')
# Reset heartbeat to avoid repeated promotions
global last_heartbeat
last_heartbeat = absTime.seconds
The master sends /heartbeat every frame. The standby monitors this. On promotion, the standby’s Perform Sync CHOP switches to Master role and begins broadcasting sync — slaves switch to the new master automatically because their Perform Sync CHOP is set to Auto-Discover Master mode.
Full Example: 3-Machine Panoramic Installation
Hardware: three machines on a dedicated gigabit switch, each driving two 4K displays edge-blended into a 7680×2160 panoramic canvas.
Master (no display output): runs logic, audio analysis, OSC dispatch, Perform Sync Master.
Slave A (left half, 3840×2160): receives OSC, renders left viewport, drives displays 1 and 2.
Slave B (right half, 3840×2160): receives OSC, renders right viewport, drives displays 3 and 4.
The viewport split is achieved by setting each Render TOP’s camera to have its aspect and horizontal offset calculated for its half of the panorama:
# On Slave A — camera horizontal offset for left half
# Full canvas: 7680 wide, this slave: 3840 wide, starting at x=0
# Camera FOV covers left half, centred at x = -0.5 (in normalised canvas space)
op('camera').par.tx = -1920 # pixels from centre in world units calibrated to canvas size
A Python module on both slaves imports the shared parameter state from OSC into local variables, and all GLSL TOPs and SOPs reference those variables via expressions. This means the same .toe file runs on both slaves — the only difference is which camera offset is active, set by an environment variable read at startup:
# In a startup Execute DAT (frameStart, first frame only)
import os
machine_role = os.environ.get('TD_MACHINE_ROLE', 'slave_a')
if machine_role == 'slave_a':
op('camera').par.tx = -1920
elif machine_role == 'slave_b':
op('camera').par.tx = 1920
Set TD_MACHINE_ROLE in Windows System Environment Variables on each slave machine before deployment.