Historical recipe — not current setup guidance. Measurements and implementation links below belong to their original workload and revision. Deployment labels and defaults may be obsolete. Use the reviewed performance recipes; browse historical evidence.
AVPlumber · Mixer optimization cookbook
cuda_transform, the shared geometry node
Implementation links are pinned to develop at c20464a5 (2026-10-09). The mixer has changed since; this page was checked against develop at 8fa15c92 on 2026-10-13. Measurements retain their original workload and scope. Watch the current 192-input L4 demo.
Current scope. Source transforms are implemented. GPU smoke checks now cover source normalization and rendition conversion as well as the primitive. No full 188-source performance comparison is claimed.
cuda_transform takes one CUDA frame and gives one frame per output canvas. Each output has its own size, sw_format and layers that crop, scale and place the input, and may take a lower rate. It is the rect compositor's drawing without input matching or a clock. An output that would only repeat the input gets the input frame itself: no draw, no copy. The 88-source show it was written beside had no stage for it, and nothing was measured in a show.
pass_arrays.What it is, and what it shares with the compositors
One input edge, one named device (hwaccel) and a list of outputs. Each output names its edge or edges (dst), a canvas (width, height, sw_format, default nv12) and layers: the compositors' layer objects with crop, destination rectangle and fit, all reading the one input. Where no layer draws, the canvas keeps its background. Every input frame gives one frame on each output that is due, with the input's timestamp and metadata.
fps(optional, a ratio as a string such as"15/1"): the output takes the first frame of each 1/fps slot of the input's timestamps, so the choice follows timestamps, not arrival order. It never repeats a frame; an empty slot gives none.drop(optional): discard the output's frame when its edge is full instead of waiting.pass_arrays(optional): see the pass-through rule below.
The node holds no drawing code of its own. It calls CudaRectDraw, the draw module and kernels of cuda_rect_overlay, mixer_compositor and mixer_keyer, with layers parsed and resolved by the same functions. What a node does around that module to own a canvas, the canvas parameters, the pool of linear CUDA frames, a blank frame from it and the source's properties copied onto it, moved out of the compositor into cuda_rect_frame.hpp, which the compositor nodes (many inputs, one canvas) and this node (one input, many canvases) both include. The compositor nodes' behaviour did not change.
The pass-through rule
An output gets the input frame itself, a new reference to the same buffers, when its canvas would only repeat the input:
- one layer, not blended;
- the whole frame, uncropped, to the whole canvas at 1:1: the frame and the canvas have the same size;
- the input's
sw_formatis the canvas's; - the input is a linear CUDA frame, or a CUarray and the output sets
pass_arrays.
The rule is checked for every frame, so a source that changes size is scaled when it has to be, and the node logs when an output starts or stops passing frames on (cuda_transform: output 2 passes the input frame on, or draws the input frame). A passed frame costs no draw, no copy and no stream synchronisation.
pass_arrays is off by default because a CUarray is a different kind of storage from the linear CUDA frames the node draws. Set it only for consumers that read arrays as well. Without it, a CUarray input to a 1:1 output is drawn into a linear frame.
Run on the L4
tests/cuda/smoke_transform.py passes on the L4 host. It feeds a 192×108 NV12 source at 30 fps into one node with four outputs: a letterbox on a 64×48 canvas, a crop scaled onto 96×64 at 15 fps, a 1:1 copy and a 1:1 copy at 15 fps. Beside the node, four cuda_rect_overlay nodes draw the same layers from the same frames.
- Every output is byte-identical, after download, to its
cuda_rect_overlay, matched by timestamp. - The two same-size outputs logged
passes the input frame on; that they were not copied shows only in that log. - The 15 fps outputs took the first frame of each slot.
tests/cpp/test_aux_layerscovers the decision without a GPU: whole frame at 1:1 passes; a scaled, cropped, offset, blended, two-layer or letterboxed canvas does not.
Not covered by the smoke test: a CUarray or browser-texture input, 10-bit formats, 1080p sizes, a full output edge, and any CPU, latency or VRAM figure. The comparison is against the same kernels, so it says nothing about picture quality against scale_cuda.
Limits as built
- No colour conversion or tone mapping;
coloron an output is rejected. - 4-sample bilinear only, and a black background only.
- One input: a layer can only read input 0. Layers, sizes and rates are fixed when the node is created.
- It synchronises the stream of the device it names once per input frame it draws. It cannot create a stream of its own, as
cuda_rect_overlayandmixer_compositorcan withoutput_hwaccel(One main CUDA stream, one per monitor). - It declares its size and
sw_formatdownstream only when all its outputs have one canvas, and never a rate. An encoder below it still needsforce_fps, andassume_video_formatwherever the outputs differ in size orsw_format. The mixer integration smoke has exercised downstream format declarations. - The parameters are documented in the source header and the shared Python transform builder.
Where the mixer can and cannot use it today
The 88-source show had no stage for it. All scaling happens inside the compositors, which draw with the same module, and every output is canvas-size, so its graph holds no cuda_transform. Nothing about the node was measured in a show: no CPU, GPU, VRAM or latency figure exists.
- Where the mixer builds it, source normalization and rendition conversion now GPU-smoke-tested:
- A rendition whose size differs from the canvas. One node per feed scales for all such renditions, one draw per distinct size; each rendition's colour conversion follows on the smaller picture. A feed with two or more renditions, all scaled, swaps its split for the node; any other feed with a scaled rendition gains one node and one thread.
- More than 32
--inputsources. One node per source fits it into 1920×1080 with black bars, in place of ascale_cudaandpad_cudagraph: the same number of nodes and threads. A source already of that size and storage is passed on undrawn. Browser pages can now be among those sources, unless--input-colordeclares HDR: a routed slot applies that one colour setting to every source it shows. A 10-bit source needs a 10-bit--working-formatthere, because the node does not reduce bit depth.
- Also implemented: a configured per-source transform, before scene aliases and AUX fan-out.
- Where it cannot stand in, from a reading of the mixer's graph code: anything that needs colour (the per-source colour nodes, a tone-mapped rendition, an aux bus's SDR conversion on an HLG canvas); anything that repeats or re-stamps frames (
force_fpspacing and the constant-rate output chains); CPU frames (raw uploads); and pictures with alpha (the wipe clip).
At a glance
- What
A node,
cuda_transform: one CUDA or CUarray frame in, one frame per output canvas, drawn by the rect compositor's module. An output that would only repeat the input gets the input frame itself. - Why
The compositors' geometry (crop, scale, place) existed only inside a compositor. A stage that turns one frame into several canvases had to use FFmpeg filter graphs, a split and one
scale_cudagraph per canvas. The node offers the compositors' own draw code for that, in one node. - Latency
Not measured. By construction: one blocking node, so one thread hop per frame, and one stream synchronisation per frame that draws. A passed-on output adds neither a draw nor a synchronisation.
- Side effects
None in the 88-source show, which builds no such node. In a graph that does,
fpsselects frames and never fills a gap, and an output withoutdropwaits on a full edge and delays the outputs after it. The mixer sets neitherfpsnordrop. - Cost
Per node: 1 thread. Per output: one draw module and one frame pool. VRAM not measured. Code: 235 lines in
cuda_transform.cppand an 88-line header,cuda_rect_frame.hpp, split out of the compositor. - Risk
Low for the 88-source show: its graph is unchanged. A show with a scaled rendition or more than 32
--inputsources now depends on the node, both covered by the mixer integration GPU smoke. The compositor nodes now take their canvas code from the shared header. Pass-through hands a consumer the input's own buffers; withpass_arraysthat can be a CUarray, which the consumer must be able to read. - Switch it off
The mixer has no switch: it builds the node for a rendition of another size than the canvas and for more than 32
--inputsources, and for configured source transforms. In a graph that uses the node, pass-through follows from the layers: give the output a different size orsw_formatand it is drawn.pass_arraysis off unless set.
Where in the code
- src/nodes/hwaccel/cuda_transform.cpp: what the node does and its parameters; source
due(), the slot rule offps; source the format it declares; source the pass-through decision and its log line; source the draw and the one stream synchronisation; sourcecolorrejected; sourcefps,dropandpass_arrays. - src/mixer/primitives/compositor_layers.hpp:
copiesWholeFrame, the rule; sourceresolveDrawOps, shared with the compositors. - src/nodes/hwaccel/cuda_rect_frame.hpp: what the header holds; source
parseCanvas, source the canvas frame pool, sourcecopySourceProps. - tests/cuda/smoke_transform.py: the four outputs; source the comparisons. tests/cpp/test_aux_layers.cpp: the pass-through cases.
- pyplumber/node.py:
CudaTransform, the Python node class. pyplumber/transform.py:transform_outputandtransform_params, which build the node's parameters for every application. - pyplumber/mixer/application.py: the normalisation of a large catalogue; source the renditions of another size.
- src/nodes/hwaccel/cuda_rect_overlay.cpp:
output_hwaccelon the generic overlay node, the private stream this node does not have.
Smoke test run on the L4 host (one NVIDIA L4, 16 vCPUs). The list of where the node cannot stand in comes from reading the mixer's graph code, not from a build or a run. The 2026-10-06 mixer integration smoke also passed source normalization and rendition conversion. This does not establish large-show throughput or validate every optional transform configuration.