Skip to content

Streaming diffusion

Restyle a rendered texture with a text prompt using StreamDiffusionV2. Try charcoal, ink or barbed wire, or describe an appearance of your own. The source and returned image stay on the GPU, and the result is a Field render-to-texture target (RTT) you can sample in another shader or composite into a scene.

Open the streaming diffusion starting point ↗

Spinning curves rendered in Field, with the current source on the left and returned charcoal on the right

The example draws five rotating curves at 512 × 512. The left panel shows the current source; the right shows stylized output. The model processes groups of frames and introduces a causal delay; the motion controls can reproject that output onto current geometry between results. While it starts, the right panel shows the source. The example starts with charcoal at strength 0.85 and enables the motion controls.

Open the example

Search for streaming_diffusion under Starting points on Field's welcome screen, then open an editable copy in your workspace. Run 2 · renderer, then 3 · spinning curves. Start with the charcoal preset or enter any text in Prompt override. A blank override uses the selected material preset. Press Enter or leave the text field to apply your prompt. The controls also let you adjust strength, pause rotation, and reset the model's state.

For white marks on black paper, set whiteOnBlack = true in the curves box and run it again. Adapt prompt for white marks on black switches between the original material prompt and its white-on-black version. A custom prompt takes precedence over this switch. The input geometry stays the same across prompts. The model can invent detail and change the shapes; strength controls how much freedom it has.

Lower strength retains more of the input; higher strength gives the model more room to add texture, smudges and splatter. Start at 0.45 for restrained marks, 0.60 for texture, and try 0.85–0.90 for expressive ink or charcoal. These are styling controls, not a guarantee of exact geometry. For narrower marks, use StreamDiffusionPresets.FINE_INK or FINE_CHARCOAL in the box.

Optional local backend

Local inference currently requires Linux with an NVIDIA CUDA GPU and a Vulkan driver that exports GPU memory and timeline semaphores. The starting point opens on Macs and other platforms; its curves box shows an availability message before attempting any CUDA or Unix-handle setup. Ordinary Field startup and builds do not require Python, TensorRT or model downloads.

Install the backend

Run the following from your Field checkout on Linux. Replace /path/to/sd2 with an installation directory. This uses the compatible upstream snapshot and Field's adapter patch; the Python environment is separate from Field's own dependencies.

git clone https://github.com/chenfengxu714/StreamDiffusionV2.git /path/to/sd2
git -C /path/to/sd2 checkout 6961a5cf2045d1dda05a04ef229698bdc04e873a
git -C /path/to/sd2 apply "$PWD/optional/streamdiffusion/upstream/field.patch"
cp optional/streamdiffusion/upstream/local_rtx4090.yaml /path/to/sd2/configs/
uv venv --python 3.10 /path/to/sd2/.venv
uv pip install --python /path/to/sd2/.venv/bin/python torch==2.6.0 torchvision==0.21.0 --index-url https://download.pytorch.org/whl/cu124
uv pip install --python /path/to/sd2/.venv/bin/python -e /path/to/sd2
uv pip install --python /path/to/sd2/.venv/bin/python -r optional/streamdiffusion/upstream/requirements-local.txt -r optional/streamdiffusion/requirements.txt
cp optional/streamdiffusion/upstream/download-models.py /path/to/sd2/
/path/to/sd2/.venv/bin/python /path/to/sd2/download-models.py

The model download includes the Wan encoder, T5 text encoder, distilled causal checkpoint and TAEHV decoder. The recorded setup uses Python 3.10, Torch 2.6 with CUDA 12.4, FlashAttention 2.7.4 and TensorRT 10.9 on Linux x86_64. Allow space for these weights and the TensorRT engine cache. Wan and distilled weights use pinned revisions; the TAEHV download follows its upstream main branch.

Build the small native handle-sharing helper with a JDK containing JNI headers, then set the backend path in the environment used to start Field:

cmake -S native/field-fdpass -B native/field-fdpass/build -DJAVA_HOME=/path/to/jdk
cmake --build native/field-fdpass/build
export FIELD_STREAMDIFFUSION_ROOT=/path/to/sd2

Run Field from its checkout. An existing compatible backend can be reused: install optional/streamdiffusion/requirements.txt into its environment and set the same variable. If Field is already running, write the path into ~/.afield/streamdiffusion-root.txt instead and run the box again. Nothing is downloaded merely by opening the document.

TAEHV is the decoder for this integration. useTensorRT = true is the default; the first run may build engines. To use the Torch decoder path instead, set useTensorRT = false in the configuration. view.session.tensorRtActive says whether an engine actually executed, and view.session.status notes it when TensorRT was requested and is not running.

Process your own rendered texture

Create a single-layer, single-sample RGBA8 or BGRA8 target, with colorTransferSrc = true. UNORM and sRGB are both accepted. The source dimensions must be multiples of 32; start with 512 × 512 and 16 input frames per second. See Render to texture for creating and displaying targets.

In a box connected beneath your renderer, this wraps an existing source RTT. The processor schedules the source render and supplies a sampled output target:

import afield.media.video.diffusion.*

val root = StreamDiffusionSupport.configuredRoot()
val config = root?.let {
    StreamDiffusionConfig(
        it,
        prompt = StreamDiffusionPresets.CHARCOAL,
        strength = .45,
        fps = 16,
        useTensorRT = true,
    )
}
val support = StreamDiffusionSupport.probe(renderer, config)
if (support.available) {
    prop<StreamDiffusionView>(diffusion)
    diffusion = StreamDiffusionView(renderer, source, checkNotNull(config))
    prop<RenderToTexture>(stylized) = diffusion.output
} else {
    markdown(support.reason)
}

Sample stylized through the same image and sampler bindings as any other RTT. Remove the source's separate render-list registration when the processor schedules it. If your graph already renders it earlier in the same frame, pass renderSource = false. The caller owns the source; diffusion.close() releases the worker, shared buffers and output. Construct and close between graphics frames. The starting point shows how to retain the object in a box property and close it when its frame loop ends or the box is run again.

Once the session is ready, change material or strength without replacing the RTT:

if (diffusion.session.ready) {
    diffusion.reset(prompt = StreamDiffusionPresets.INK, strength = .85, seed = 0)
}
print(diffusion.session.status)
print(diffusion.session.lastStats)

Reset clears causal history and holds the last valid image during warm-up. session.status reports loading and errors; session.logFile contains the worker log. StreamDiffusionSupport.probe checks the platform, selected Vulkan device, export features, native helper and installation files. The worker then checks CUDA device matching, packages and model loading. A startup or inference failure leaves the source or last completed output visible.

Use motion from your renderer

The starting point enables all three motion controls. It generates correspondence from the same ribbon geometry that draws the source, retaining earlier transforms and checking reference object IDs at crossings. Reproject output updates the right panel between model results. History blend mixes preceding stylized results into each new result. Noise advection anchors some of the model's randomness to moving surfaces; changing it resets the model.

The following clips are native Field RTT captures at 512 × 512 with active TAEHV/TensorRT and the charcoal prompt at strength 0.85. Each has the current source on the left and processed output on the right. They show successive eight-second intervals of the same rotating scene; their source poses are not identical. Clip playback is 10 FPS; diffusion requests 16 FPS.

All three enabled — display reprojection, history blend .35, noise advection .8:

All three disabled after reset — the model's delayed output is held between updates:

The enabled run fills rejected pixels with paper reconstructed from nearby output, so old curve positions do not leave a pale outline. Generated marks beyond the supplied correspondence can still leave trails. The clips show the controls working together; they do not measure how much each one improves temporal stability. With the controls enabled the run presented about 16 FPS once steady; the run data and worker log record the settings, resets, engine loads and chunk timings.

For your own scene, pass a StreamDiffusionMotionProvider to the processor and opt into the controls. The provider records a GPU correspondence target in the supplied command buffer:

import afield.graphics.vulkan.device.CommandBuffer
import afield.media.video.diffusion.*

val config = StreamDiffusionConfig(
    checkNotNull(root),
    displayReprojection = true,
    historyBlend = .35,
    noiseAdvection = .8,
)
val motion = object : StreamDiffusionMotionProvider {
    override fun onFrame(frame: StreamDiffusionFrame) {
        // Retain transforms / camera / object state keyed by frame.id and generation.
    }

    override fun record(
        command: CommandBuffer,
        request: StreamDiffusionMotionRequest,
    ): RenderToTexture? {
        // Render current pixels into reference-frame UV coordinates.
        // Return null if this pair's retained geometry is unavailable.
        return renderCorrespondence(command, request.current, request.reference)
    }
}
val diffusion = StreamDiffusionView(renderer, source, config, motion = motion)

renderCorrespondence here stands for your renderer's pass. It must return a single-layer, single-sample RGBA32F RTT at the source resolution, with a sampler and colorSampled = true, colorTransferSrc = true. You own that target and its retained geometry; close them after the processor. The same target can be reused across calls: each consumer records its work before the next request.

Channel Meaning
R, G Absolute normalized UV in the requested reference frame
B Confidence, 0 to 1; zero rejects history
A Reactive rejection, 0 to 1; one rejects history

Use texture-row coordinates: X right, Y down, pixel centres ((x + .5) / width, (y + .5) / height). Field's negative-height viewport puts NDC Y = +1 at texture row zero. A backward displacement (dx, dy) in pixels becomes referenceUV = currentUV + (dx / width, dy / height). Forward flow needs to be converted into backward correspondence with holes and visibility handled. The alpha channel is a reactive mask, rather than opacity; ordinary geometry can use confidence one and reactive zero.

Match the requested frames

onFrame runs after every source draw, including draws that are not accepted by the model. Retain renderer state under its frame ID; timestamps and generation are provided too. record can be called several times in one graphics frame, with different frame pairs:

Request use Current → reference
DISPLAY Current source → source frame of the latest resolved result
HISTORY Source frame of a new result → preceding resolved result's source frame
NOISE Newly accepted source → previous accepted source

A single previous-frame vector buffer usually cannot cover the model's delay. Re-render previous surface positions using retained transforms, or compose backward flow with confidence checks across the requested interval. Match the actual IDs, including skipped input captures. Use depth, object IDs or your G-buffer to reject occlusion, newly revealed surfaces, topology changes and unreliable pixels. Return null for missing retained state; write confidence zero for individual pixels that cannot be matched.

The demo retains 512 display-frame transforms, about 8.5 seconds at 60 FPS. Your retention window should cover your actual model lag. It extends correspondence to a wider ribbon around each curve to carry stylized edges. Its static paper mapping feathers rejection around old ribbons and fills revealed paper using nearby generated paper. This local fill can soften paper texture, and generated mark trails remain possible: it cannot identify every generated splatter pixel as part of a moving surface.

What each control does

Display reprojection samples the delayed stylized result at reference UVs on every Field draw. Rejected pixels show the current source unless your provider supplies a fallback image. Missing correspondence holds the latest resolved result. It improves geometric responsiveness; model inference still has the same causal delay.

A provider can override recordFallback(command, request, source, referenceImage) to render an image used where confidence or reactivity rejects display history. It is called immediately after a successful DISPLAY request, with the resolved stylized reference image borrowed for that call. Return a sampled, single-layer, single-sample RTT matching the source's dimensions and colour format; you own it. Return null for the default source fallback. This leaves noise and history blending unchanged. The demo uses this hook to reconstruct paper from nearby reference pixels outside old ribbons, so the flat source paper does not show through as a brighter patch. It is a local approximation and does not predict new stylized content.

History blending warps the previous resolved result into the new result's frame, clamps its colours to the fresh result's 3 × 3 neighbourhood, and blends using historyBlend × confidence × (1 − reactive). Missing or invalid history uses the fresh result. High blending can soften marks or leave trails; begin near .35.

Noise advection warps a CUDA noise field between accepted source frames and mixes it with fresh noise. Nearest sampling and square-root weights preserve marginal noise variance. The encoder compresses four RGB frames into one temporal latent; the adapter uses the group's ending noise field. This is an experimental spatial approximation, not exact flow through the video VAE. The model has no new learned flow or depth conditioning, and generated marks can move independently of the source geometry.

API defaults are false, 0, 0; supplying no provider retains the original RGB processing path. Display reprojection and history blending can be changed live through diffusion.displayReprojection and diffusion.historyBlend. Change noise through diffusion.reset(noiseAdvection = .8); reset clears noise history and prevents history blending across model generations. During reset, the last valid image can still be reprojected while new results warm up.

GPU cost and diagnostics

Display/history passes run on Vulkan. Motion for noise is copied into an additional RGBA32F plane in each shared input slot and imported directly as a CUDA tensor; RGB and motion do not pass through CPU memory. Six float planes add 24 MiB at 512 × 512. Resolved history, display and the invalid-map target add another 7 MiB for RGBA8 output; your motion targets, fallback target, CUDA copies for the current four/five-frame group and retained scene state are additional. Larger resolutions scale these allocations with pixel count.

displayReprojectionRequests, historyBlendRequests and noiseMotionFrames report requests/maps sent by Field. The worker's noise_motion_frames reports maps actually consumed by noise advection, and noise_advection reports its active setting. These are counts only; they say nothing about how good the correspondence was or how much of the image it covered.

Resolution

Width and height must each be multiples of 32. Rectangular images are accepted; there is no square-image requirement. Resolution is fixed when the processor is created. To resize, close it and create a new processor for the resized source.

The worker accepts dimensions up to 4096 × 4096, but the model has only been run at 512 × 512, on an RTX 4090. Larger images increase GPU memory and computation. Doubling both width and height gives four times the pixels and can cost more than four times the computation. A new resolution may require new TensorRT engines.

Duration, FPS and latency

There is no fixed clip-duration limit. The adapter streams with bounded buffers and a rolling model cache rather than storing the whole video. It has not been tested over runs lasting hours.

fps accepts 1–60 and controls capture and output pacing. It is a requested sampling rate; available GPU throughput determines whether it can be sustained. Changing it requires recreating the processor. Field can render at 60 FPS while diffusion updates at 16 FPS, holding the latest completed image between updates.

The worker collects five frames initially, then processes groups of four. At 16 FPS, four input frames span 250 ms; measured computation takes approximately 186 ms per group at 512 × 512. Collection, the two-step causal pipeline and output pacing add delay, so end-to-end latency is longer than the 62.5 ms a 16 FPS frame interval suggests; it has not been measured exactly. Model startup takes tens of seconds on the recorded machine; initial TensorRT engine building can take longer. startupTimeoutSeconds defaults to 240 and can be increased.

The model advances through accepted frames. Supplied timestamps identify the source frame; they do not condition the model on elapsed time. Changing the sampling rate, or skipping captures under load, therefore changes the motion seen by the model. The input scene can use its own animation clock independently of diffusion sampling.

Timing and memory

Field captures on an independent FPS grid. When the bounded input queue is full, it skips captures rather than growing a queue. The worker processes accepted frames in order; Field keeps displaying the last completed output while it is busy. capturedFrames, skippedCaptures, presentedFrames, droppedOutputs, lastSourceFrameId and lastSourceTimeNanos describe that behavior.

The native ink run on an RTX 4090 sustained 16.00 presented FPS at 512 × 512 over an 18-second steady interval. The median four-frame worker chunk took 186 ms. In an earlier run, total GPU memory use (including Field and the rest of the desktop) was about 12.3 GiB. Scene complexity, other GPU workloads and resolution change the available throughput and memory headroom.

The text encoder moves to CPU after initial prompt encoding by default. Later prompt changes can therefore take longer. Per-frame image processing and image transport stay on CUDA and Vulkan. Renderer correspondence can drive the three motion controls above. Depth and other G-buffer data can help your provider construct visibility and confidence; the model does not consume them directly.

Source and recorded run

The editable source is the operation transcript. The charcoal image and comparison clips are native Field RTT captures, with TAEHV and active TensorRT. Run data and the worker log retain settings and engine loads. The capture uses the starting point's geometry and prompt controls with strict checkpoint loading. The ink clip was recorded without the display fill and shows the pale paper outline it removes. The backend's setup and architecture are in optional/streamdiffusion/README.md.

See also the graphics starting point and render to texture.