VAE Decode (C2C)
The Wan decode node that fixes banding and flicker — fp32 precision and spatial-only tiling, one swap
- samples
- vae
- image
- reference
- IMAGE
- report
- cast_strength
- saturation
If you generate Wan video in ComfyUI and the frames come out with banding in gradients or a subtle flicker across time, the usual suspect isn't the sampler - it's the VAE decode. Wan's VAE is trained and behaves noticeably better when decoded in fp32, and tiling the decode wrong can make each tile flicker independently. C2CVAEQualityDecode is a drop-in replacement for the standard VAE Decode node that handles both: it forces fp32, uses spatial-only tiling so temporal coherence is preserved, and can optionally apply ACES tonemapping on the way out for that HDR-ish finish.
It's aimed squarely at Wan video, but it's a standard LATENT + VAE in, IMAGE out node, so it slots anywhere you currently have a VAE Decode. The community's own guidance on Wan (from the corpus) already points at sigma shifts and careful decode settings for flicker; this node automates the decode side of that fight.
How it works
The implementation in nodes/hdr_color_science.py wraps the normal VAE decode with quality upgrades. With force_fp32 on (the default), it converts the latent to float32 and - if it can reach the VAE's first-stage model - temporarily moves the VAE itself to fp32, decodes, then restores the original dtype so you don't leak a half-converted model into the rest of your graph. When a tile_size is set on a 5D (video) latent, it hands off to a spatial-tiled decode that tiles only the spatial dimensions, leaving the temporal axis intact - the point being that temporal tiling is what causes frame-to-frame flicker. If that tiled path throws for any reason, it logs a warning and falls back to a standard decode instead of crashing your run. With apply_aces on, it finishes with the ACES filmic curve and exposure before encoding back to sRGB.
The inputs
- samples - your
LATENTfrom the sampler. - vae - the
VAE. - force_fp32 (true) - leave on. This is the whole quality win.
- tile_size (0) - spatial tile size. 0 = auto/no tiling; the tooltip recommends 256+ for 1080p output, i.e. when the decode would otherwise OOM on a big frame.
- apply_aces (false) - filmic tonemapping after decode.
- exposure (1.0) - only used when ACES is on.
Output is a single IMAGE.
Installing it
Part of Code2Collapse/ComfyUI-CustomNodePacks. ComfyUI Manager → search "CustomNodePacks", or:
cd ComfyUI/custom_nodes
git clone https://github.com/Code2Collapse/ComfyUI-CustomNodePacks.git
Restart ComfyUI. No extra model downloads and no extra pip packages for the core path - fp32 decode and ACES are pure torch. (Skip the pack root's blanket pip install -r requirements.txt; install only what you're missing.)
Gotchas
Two things to know before you adopt it. First, fp32 decode is slower and hungrier than the fp16 default - that's the price of the quality, and it's why tile_size exists. On a big 720p+ video, set a tile size rather than letting the whole frame decode in one shot, or you may trade banding for an OOM. Second, the ACES step is off by default for a reason: only enable it if you actually want the filmic look, because once it's baked into the decoded frames it's hard to undo. And don't be surprised if your first 5D latent run takes a moment longer - the node's being careful, and that's the entire point.
Inputs (18)
| Name | Type | Default | Description |
|---|---|---|---|
| force_fp32 | BOOLEAN | true | Decode in fp32. Measured gain: at most 0.17 dB on Flux/Wan 2.1 round-trip (~2x VAE memory). |
| tile_size | INT | 00–1024 | Spatial tile size in PIXELS, used when tile_mode is manual (0 = no tiling). Converted with the VAE spatial factor (usually 8). 512 measured best for Wan on 8 GB. |
| apply_aces | BOOLEAN | false | Apply ACES filmic tone mapping after decode (the decode is treated as sRGB: linearised, tone-mapped with the ACES fit, encoded once). |
| exposure | FLOAT | 1.000.01–10 | Exposure for ACES (only used if apply_aces=True). |
| clamp_outputopt | BOOLEAN | true | Core clamps decoder output to 0..1. Off keeps out-of-range values (4.6x / 5.3x more accurate there on Flux/Wan, measured M2). For HDR/EXR chains. |
| tile_modeopt | COMBO | auto | auto: tile only when the untiled decode would not fit the GPU (measured on an 8 GB card: a 2K Wan decode took 502 s untiled, spilling into system RAM, and 37 s tiled, with the same picture). off: never tile. manual: tile at tile_size. Video is tiled in space only - splitting a video VAE in time costs ~21 dB. |
| samplesopt | LATENT | The latent to decode (with vae). Leave empty to clean 'image'. | |
| vaeopt | VAE | — | |
| imageopt | IMAGE | An already-decoded picture to clean instead of decoding a latent. | |
| cleanopt | BOOLEAN | false | Measure the decode and correct what is wrong with it: colour cast, oversaturation, crushed shadows, chroma speckle. The report says what it found, including clipping it cannot undo. |
| balance_modeopt | COMBO | off | Clean (when clean is on): off = measure only. grey world = drive the AVERAGE neutral. Fast and wrong on any shot that is genuinely one colour - a sunset averages red and this will throw the sunset away. white point = drive the BRIGHTEST 1% neutral. Safer on coloured scenes, because a red room still has a neutral highlight. match reference = copy a plate's channel means. Use when you have the real footage. |
| balance_strengthopt | FLOAT | 1.000–1 | Clean (when clean is on): How far toward neutral. 1.0 is full correction; back it off when the cast is partly the lighting you wanted. |
| saturationopt | FLOAT | 1.000–2 | Clean (when clean is on): Chroma scale about luma, so pulling it down does not also darken the picture. 1.0 leaves it alone. If the render is oversaturated AND contrasty, read the report first - that combination is usually CFG, and lowering CFG keeps detail that desaturating here cannot bring back. |
| contrast_restoreopt | FLOAT | 0.000–1 | Clean (when clean is on): Lift crushed shadows, which brings contrast DOWN - the fix for a render that came back too hard. Raises the floor rather than stretching the range, so nothing can clip. It does NOT recover detail already crushed to black: those pixels just stop being pure black. The report says how much is in that state. |
| chroma_cleanupopt | FLOAT | 0.000–1 | Clean (when clean is on): Smooth colour noise without touching detail. Blurs CHROMA only - the eye carries detail in luma, which is why every codec subsamples chroma and not luma, and why this can go quite hard before anything looks soft. The fix for VAE decode speckle. |
| referenceopt | IMAGE | Clean (when clean is on): Required by 'match reference'. The plate, or any frame whose colour you want copied. | |
| restore_unchangedopt | FLOAT | 0.0000–0.5 | Clean (when clean is on): Experimental. 0 = off. Every VAE round trip loses detail everywhere, even where nothing was meant to change. With the original plate wired to 'reference', this writes the original pixels back wherever the picture did not really change, and keeps the edit. The value is how far the local average may move and still count as unchanged. Measured on the Flux VAE: one round trip moves it by up to 0.037, three by up to 0.050 - so start near 0.05-0.07. Too low leaves VAE loss in place; too high also reverts subtle intended changes. |
| restore_radiusopt | INT | 81–64 | Clean (when clean is on): Window for the write-back, in pixels (8 = one latent cell of an 8x VAE). An edit's influence reaches about 3x this far past its edge, so the boundary blends rather than cuts. Experimental. |
Outputs (4)
| Name | Type | Description |
|---|---|---|
| IMAGE | IMAGE | The decoded (and, with clean on, corrected) picture. |
| report | STRING | Clean: what was measured and corrected (empty when clean is off). |
| cast_strength | FLOAT | Clean: how strong the colour cast was. |
| saturation | FLOAT | Clean: measured saturation. |