Feedback Loops
With the layer↔texture bridge in place, Lux’s two rendering worlds can talk to each other. Now it’s time for the technique that makes that bridge sing.
Feedback is the single most important creative technique in real-time visual programming. Take the output of the previous frame, transform it slightly — decay, translate, rotate, scale — and composite it with the current frame’s input. The result accumulates over time. Trails form. Echoes build. Patterns emerge that no single frame could produce.
Every visual programming environment worth using has feedback. TouchDesigner has it. Cables.gl has it. Now Lux has it.
The node
- Inputs:
texture(Texture),decay(Number, 0.95),translate_x(Number, 0),translate_y(Number, 0),scale(Number, 1.0),rotation(Number, degrees) - Output:
out(TextureHandle)
Feedback composites the current input texture with the transformed previous frame. Every frame, it:
- Takes the previous frame’s output texture.
- Applies UV transforms — translate, scale, rotate — to shift the old content.
- Multiplies by the decay factor to fade it.
- Composites the current input on top using source-over blending.
- Outputs the result and saves it as the next frame’s “previous.”
The decay parameter is the key creative control. At 0.95, each frame retains 95% of the previous content — trails fade over about 60 frames (one second). At 1.0, nothing fades, and content accumulates forever. At 0.5, trails vanish almost instantly.
The transform parameters shift the feedback content. Translate and you get motion trails. Scale below 1.0 and content spirals inward. Rotate and you get kaleidoscope-like accumulation. Combine all three and things get wild.
The WGSL shader
The feedback composite shader does the actual work:
// Transform UV for the previous frame
let centered = uv - vec2f(0.5);
let cos_r = cos(rotation);
let sin_r = sin(rotation);
let rotated = vec2f(
centered.x * cos_r - centered.y * sin_r,
centered.x * sin_r + centered.y * cos_r
);
let transformed = rotated * (1.0 / scale) + vec2f(0.5) + translate;
let prev = textureSample(prev_tex, prev_sampler, transformed) * decay;
let curr = textureSample(curr_tex, curr_sampler, uv);
// Source-over composite: current on top of decayed previous
let out_a = curr.a + prev.a * (1.0 - curr.a);
let out_rgb = (curr.rgb * curr.a + prev.rgb * prev.a * (1.0 - curr.a)) / max(out_a, 0.001);
return vec4f(out_rgb, out_a);
The transform is applied to the previous frame’s UV coordinates, not the current input. That means the previous content drifts, shrinks, or rotates while the new content always arrives at its natural position. This is the correct feedback behaviour — the echo moves, not the source.
The GC problem
Here’s the tricky part. The Feedback node holds a texture handle from the previous frame. The TexturePool runs garbage collection every 300 frames, freeing textures that haven’t been used recently. If the Feedback node’s previous-frame texture gets GC’d between frames, the node reads from a freed handle and gets garbage pixels — or worse, a different texture that reused the same handle.
The solution is TextureOp::MarkInUse. During process(), the Feedback node calls ctx.mark_texture_in_use(prev_handle), which pushes a MarkInUse op that tells the pool to refresh the texture’s last_used_frame timestamp. The GC sees it as recently used and leaves it alone.
// Protect previous frame's texture from GC
if !self.prev_texture.is_invalid() {
ctx.mark_texture_in_use(self.prev_texture);
}
One line in the node, one new TextureOp variant, and feedback loops work reliably. The FrameDelay node in the animation crate got the same treatment — any node that holds texture handles across frames needs to mark them.
What it feels like
Connect a Circle → Translate (with a Mouse driving position) → RenderTarget → Feedback → TextureToLayer. Move the mouse. The circle leaves trails that fade into the background, drifting and decaying as the feedback loop accumulates frame over frame.
Set decay to 1.0 and the trails never fade — every position the mouse has ever visited is permanently etched into the texture. Set scale to 0.99 and the trails spiral gently inward. Set rotation to 0.01 and they twist.
This is the node that makes a visual programming tool feel alive. Static patches make images. Feedback makes experiences.
The built-in filters and feedback cover a lot of ground, but creative coding demands the custom and the weird. Next: letting users write their own WGSL shaders.