huff processes video through a sequential chain of effect passes each frame. Understanding the architecture helps you predict how parameter changes interact and why certain combinations produce the results they do.


Effect Pipeline

Each frame, source material passes through up to eight independent effect passes. Each pass is independently toggleable. When a pass is disabled it is completely skipped — there is no bleed-through. The order is fixed.

EFFECT PIPELINE — PER FRAME SOURCE video file · webcam · ring seed FRAME RING BUFFER 192 MB cap · up to 60 frames · ImageData[] [t-1] [t-2] [t-3] ··· [t-N] PASS 1 · TRAILS ghost frames composited PASS 2 · FEEDBACK zoom / translate / rotate PASS 3 · GLITCH tile displacement from ring PASS 4 · SCANLINES horizontal band displacement PASS 5 · FLOW WARP noise UV distortion PASS 6 · SOLARIZE luma-threshold inversion PASS 7 · SYMMETRY mirror fold at axis OUTPUT FRAME → Canvas window · Syphon · Spout · WS relay pushed to ring → becomes [t-1] next frame LEGEND main data flow ring read (past frames)

Reading the diagram: The frame ring sits at the centre. Every time-based effect (Trails, Glitch, Scanlines, Flow Warp) reads from it. The ring is updated at the end of each frame — passes always read the previous frame’s ring state, never the current one being assembled.


UI → Effect Data Flow

All parameter control — sliders, MIDI CC, OSC messages — converges on the same DOM element values. The p5.js draw loop reads these values directly on every frame. There is no debounce, no interpolation layer, and no intermediate state object.

UI → EFFECT DATA FLOW SLIDER / TOGGLE controls window (WebView) MIDI CC USB controller / IAC Bus midir (Rust native) OSC MESSAGE TouchOSC / Max / Pd / TD UDP port 9000 · rosc (Rust) PRESET LOAD JSON snapshot localStorage Tauri "midi-event" Tauri "osc-message" DOM ELEMENT (input / checkbox) e.g. els.corrupt.value = "0.35" ← all inputs converge here p5 draw() loop reads els.* on every requestAnimationFrame tick Effect chain executes applyGlitch() · applyTrails() · applyFlowWarp() ··· KEY INSIGHT No debounce. No smoothing layer. Next frame = live value.

What this means in practice:

  • Moving a slider and receiving a MIDI CC produce identical results — both write the same DOM element
  • There is no latency between receiving a MIDI CC and it affecting the output; the next draw() frame picks it up
  • Smoothing/interpolation of parameter changes is not built in — if you need it, do it in your MIDI controller or OSC sender

Frame Output Pipeline

At the end of each draw() call, huff routes the composited output frame to one or more destinations. Syphon and Spout use a magic-byte prefix on the same WebSocket connection as the canvas mirror — Rust inspects the first bytes and routes accordingly.

FRAME OUTPUT PIPELINE p5 gBuf composited output canvas SCREEN PREVIEW controls window · direct draw WS MIRROR JPEG encode → port 8787 canvas.html receives SYPHON macOS · HUFFSYPH prefix port 8787 → Rust intercept SPOUT Windows · HUFFSPOUT prefix port 8787 → Rust intercept syphon::push_frame() MTLTexture CPU upload SyphonMetalServer → Resolume · VDMX · MadMapper ··· spout::push_frame() spoutdx_send_image() D3D11 UpdateSubresource → Resolume · TouchDesigner ··· ⚠ CPU ROUND-TRIP (Syphon + Spout) getImageData() → WS → Rust → GPU · throttled to configured FPS cap all three routes use port 8787 · magic-byte prefix distinguishes them

The CPU round-trip note: Both Syphon and Spout currently use getImageData() to read pixels from the WebView canvas back to CPU, send them over localhost WebSocket, then re-upload to GPU in Rust. This is a full GPU→CPU→GPU trip per frame. It works reliably at 720p/30fps but is not zero-copy. See Caveats for details.


Frame Ring Buffer

The ring buffer is the shared memory between passes. It stores raw ImageData objects — RGBA pixel arrays at canvas resolution.

FRAME RING BUFFER current (building) t [t-1] prev frame DEPTH 0 [t-2] DEPTH 0.03 [t-3] ··· [t-N/2] DEPTH 0.5 ··· [t-N] 192MB cap ring wraps QUALITY SLIDER → ring depth 0 = 4 frames · 0.5 = 30 frames · 1.0 = 60 frames (or 192MB cap) DEPTH + SCATTER → which slot effects read DEPTH 0 = only [t-1] · DEPTH 0.5 = up to [t-N/2] · SCATTER = per-tile randomisation
Parameter Effect on ring
QUALITY Controls ring depth: 0→4 frames, 1→60 frames
DEPTH How far back effects sample into the ring (0–50% of ring depth)
DEPTH SCATTER Per-tile randomisation of the back index
192 MB cap Ring depth is also capped by pixel budget: at 1080p (~8 MB/frame) max ≈ 24 frames regardless of quality