Skip to content

render: VHS Look, a clip-level analog tape look (#2516) - #2613

Open
danielbanariba wants to merge 1 commit into
storytold:mainfrom
danielbanariba:feat/vhs-look
Open

danielbanariba wants to merge 1 commit into
storytold:mainfrom
danielbanariba:feat/vhs-look

Conversation

@danielbanariba

@danielbanariba danielbanariba commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

VHS Look: a clip-level analog tape look (#2516)

Adds Video Effects > Stylize > VHS Look (id vhs_look), CPU only. It gives a whole clip the look of a VHS recording: soft brightness, colour that smears past edges, wobbling lines, tape noise. The VHS Damage transition plays between two clips; there was no effect for one clip.

Parameters

  • Luma Bandwidth (3.0 MHz) and Chroma Bandwidth (0.3 MHz): the detail brightness and colour keep. Colour gets a tenth of the band, which is what makes edges bleed.
  • Chroma Shift (2 px, -20 to 20) delays the colour, so it lands right of its edge. Line Jitter (1 px, 0 to 10) moves whole lines sideways.
  • Noise (20 %), Head Switching (0 lines, 0 to 30: a torn, noisy band at the bottom) and Dropouts (10 %: short bright streaks).
  • Random Seed.

How it works

Each line goes to display-encoded Y'CbCr (ITU-R BT.601). Luma and the two colour differences are low-passed separately: the MHz values become cycles per picture width through the active part of a 525/60 line (about 52.6 us), so 3 MHz is about 158 cycles and the look is the same at any resolution. The filter is a Gaussian run as three box passes with fractional end weights (Wells, IEEE PAMI 1986; Gwosdek et al., SSVM 2011), so it costs the same at any width. Then come the colour delay, the line jitter, tape noise (correlated by the channel bandwidth), the head-switching band and the dropouts. Tape bandwidths follow IEC 60774-1 and Poynton (Digital Video and HDTV Algorithms and Interfaces, 2003): about 3 MHz of luma and 0.3 to 0.5 MHz of colour. The head-switching band sits where the heads take turns, about 6.5 lines before vertical sync (US patent 6,650,825), so on a real tape overscan usually hides it. No implementation was consulted; the module docs have the details.

Vertically it uses the 480-line raster VHS records, and Chroma Shift and Line Jitter are full-size pixels that follow px_scale, so a proxy render matches the full-size one. Everything that varies is hashed from the frame time, the clip and Random Seed, never from a random generator, so preview and export agree bit for bit and each frame gets new noise (as in #906 and #956). Alpha is never changed, the filter is weighted by alpha so transparent pixels neither lend nor take colour, and non-finite pixels are left alone. Values outside 0 to 1 (super-white or negative) are clipped.

Not included: tracking errors, interlaced fields, edge ringing, luma/chroma crosstalk (dot crawl), vertical colour smear, PAL numbers and a GPU path.

Other changes

  • No Accelerated badge (no GPU op). In effect_cost_ms it sits in the heavy tier (50 ms, for effects that measure 30 ms or more).
  • es.tsv and ja.tsv get seven strings. The Japanese ones follow the vocabulary already in that file, but I am not a native speaker, so please check them.
  • BEYOND_CATALOGUE in the catalogue test (crates/project/src/effect/vfx.rs) lists effects FilmCraft adds beyond its reference list; the Video Effects count in vfx_tests.rs goes from 93 to 94.
  • Progress rows in ROADMAP.md and docs/gaps.md (G18).

Cost and result

On an idle machine with 16 cores, the release effect_costs calibration test (crates/render/src/preview_tests.rs) times the effect at 31 to 35 ms per 1920 x 1080 frame at its defaults (three runs), against 17 ms for Lumetri. That is just over the 30 ms heavy line, so it shares the heavy tier with Lumetri and the blurs: on a still or generated clip at 24 fps the render bar shows yellow, and with H.264 or HEVC footage red. A GPU path would be the follow-up.

City_Night_Drive from the demo project at 11 s: as is, defaults, and a stronger look (Luma 1.5 MHz, Chroma 0.12 MHz, Shift 5, Jitter 2.5, Noise 30 %, Head Switching 10, Dropouts 30 %).

City_Night_Drive: before, defaults, stronger

The colour bleed up close (window lights, red and white streaks), same order:

Chroma bleed: before, defaults, stronger

Tests

Fourteen tests in vfx::tests::vhs_look (crates/render/src/vfx/tests.rs) on synthetic pictures, plus vhs_look in NEW_IDS. They cover: the colour edge much wider than the brightness edge; Chroma Shift in both directions; the cutoff where the MHz setting says, at two resolutions, for luma and chroma; repeat frames identical and new frames, clips and seeds different; the head-switching band (place, tear, noise); jitter within its amount and scaled by px_scale; dropouts short, bright and colourless; grain following the bandwidth; half resolution matching full; neutral settings keeping the picture; alpha, transparent pixels and half-transparent pictures; values beyond a range acting as its end; and hostile input (NaN, infinite, overflowing or transparent pixels, 1 x 1, odd sizes, bad px_scale or time).

Against a stub that did nothing, 10 of the 14 fail; the other four check what must stay true, so a stub passes them. I then broke the implementation on purpose 35 ways (swapped shift sign, wrong unit for the cutoff, seed ignoring the frame, clip or Random Seed, head band at the top, no px_scale, wrong colour matrix, no clamp, and so on). Seven first passed, which led to stronger assertions; now every one turns a test red except a duplicate finite check on the output that no input can reach.

Checks

Check it by hand

  1. In the demo project, add VHS Look to City_Night_Drive and toggle the fx badge: the window lights soften and their yellow smears right.
  2. Set Luma Bandwidth 5, Chroma Bandwidth 1.5 and Shift, Jitter, Noise, Dropouts 0: the picture is back to the original.
  3. Set Head Switching 12: the bottom lines tear and fill with noise. Step frame by frame: noise changes each frame, and a frame you step back to is identical.
  4. Render at 50 %: the look matches the full-size render, only smaller.

Open questions

My contributors/people.toml entry comes with #2554; if this merges first, say so and I will add it here.

Closes #2516.

Summary by CodeRabbit

  • New Features

    • Added the VHS Look video effect for authentic analog-tape styling.
    • Includes adjustable luma and chroma bandwidth, chroma shift, jitter, noise, head switching, dropouts, and deterministic seed controls.
    • Supports repeatable frame and clip variation for consistent results.
    • Preserves transparency and handles varied image sizes and inputs safely.
  • Performance

    • VHS Look runs on the CPU and is classified as a higher-cost video effect for preview performance estimates.
  • Documentation

    • Added documentation describing the effect’s controls and supported behavior.

Adds Video Effects > Stylize > VHS Look (id `vhs_look`), a persistent
tape look for one clip: soft brightness, smeared colour, wobbling lines,
tape noise. CPU only. The VHS Damage transition plays between two clips;
this is the effect for a whole clip.

Each line of the picture goes through the stages a VHS recording puts
it through, on display-encoded Y'CbCr (ITU-R BT.601). Luma and the two
colour differences are low-passed separately, because VHS keeps colour
in a band a tenth as wide as brightness, so colour bleeds past an edge
while the edge itself stays fairly sharp. Chroma Shift then delays the
colour, Line Jitter moves whole lines sideways (part slow wobble, part
random per line), and tape noise, a head-switching band at the bottom
and dropout streaks go on last.

Bandwidths are in MHz and become a cutoff in cycles per picture width
through the active part of a 525/60 line (about 52.6 us), so 3 MHz is
about 158 cycles and the look is the same at any resolution. Lines
are those of the 480-line raster VHS records: jitter, noise, dropouts
and the head-switching band are keyed to it, so a 1080-line clip shows
each as two or three of its own lines. Chroma Shift and Line Jitter are
lengths in full-size pixels and follow px_scale, so a proxy render
matches the full-size one.

The low-pass is a Gaussian, run as three box passes of equal variance
with fractional end weights (Wells, IEEE PAMI 1986; Gwosdek et al., SSVM
2011), O(1) per pixel at any width. It is weighted by alpha, so a
transparent pixel never lends or takes colour, and alpha itself is never
changed. Everything time-varying is hashed from the frame time, the
clip and Random Seed (never a random generator), so preview and export
agree bit for bit and each frame gets new noise (storytold#906, storytold#956). Sources
are cited in the module docs; no implementation was consulted.

Parameters: Luma Bandwidth (3.0 MHz), Chroma Bandwidth (0.3 MHz), Chroma
Shift (2), Line Jitter (1), Noise (20 %), Head Switching (0 lines),
Dropouts (10 %), Random Seed. NaN, infinite and out-of-range values act
as the nearest end of the range, transparent and non-finite pixels are
left alone, and sizes are checked before anything is indexed.

The effect has no GPU op, so it gets no Accelerated badge, and it sits
in the heavy (50 ms) tier of effect_cost_ms: the effect_costs
calibration times it at 31 to 35 ms per 1080p frame on 16 cores, over
the 30 ms heavy line. Placement: Video Effects > Stylize. The catalogue test gets
BEYOND_CATALOGUE, a list of effects FilmCraft adds beyond its reference
list, so one can live in an existing folder; the folder question (storytold#1175)
is still open. The engine's count of Video Effects goes from 93 to 94,
and es.tsv and ja.tsv get the seven new strings. ROADMAP.md and
docs/gaps.md (G18) get their progress rows.

Tests (render vfx::tests::vhs_look): the colour edge of a red/black step
is more than 4x wider than the brightness edge; Chroma Shift moves the
colour edge by the set amount in either direction and red still shows
past the edge; a grating below the cutoff passes and one above it fades,
at two resolutions; the -3 dB point sits where the MHz setting says,
for luma and for chroma separately; a repeated frame is bit for bit the
same while the next frame, another clip and another Random Seed differ;
Head Switching stays in the bottom band, tears it sideways and adds
noise there; Line Jitter stays within the set amount and scales with
px_scale; Dropouts are short, bright, colourless streaks scaled by the
setting; noise grain follows the bandwidth, not the pixel count; half
resolution matches full resolution scaled down; neutral settings keep
the picture; alpha stays put, transparent pixels do not bleed and a
half-transparent picture keeps its colours; values beyond the range act
as its end; hostile input (NaN, infinite, transparent, 1 x 1, odd sizes,
a bad px_scale or time) neither panics nor returns garbage.
@coderabbitai

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: storytold/filmcraft/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 4369a93c-c5ed-473e-914f-deaf895a0894
📥 Commits

Reviewing files that changed from the base of the PR and between 70f0fc7 and 6ea935d.

⛔ Files ignored due to path filters: 2
  • crates/ui-egui/src/i18n/es.tsv is excluded by !**/*.tsv
  • crates/ui-egui/src/i18n/ja.tsv is excluded by !**/*.tsv
📒 Files selected for processing: 8
  • ROADMAP.md
  • crates/engine/src/vfx_tests.rs
  • crates/project/src/effect/vfx.rs
  • crates/render/src/preview.rs
  • crates/render/src/vfx/mod.rs
  • crates/render/src/vfx/tests.rs
  • crates/render/src/vfx/vhs.rs
  • docs/gaps.md

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

📝 Walkthrough

This pull request adds a CPU-only VHS Look video effect with controls for bandwidth, chroma shift, jitter, noise, head switching, dropouts, and seed. It registers the effect with the renderer and adds processing tests and documentation.

Changes: VHS Look effect

Layer / File(s) Summary
Effect contract and renderer wiring
crates/project/src/effect/vfx.rs, crates/render/src/vfx/mod.rs, crates/render/src/preview.rs, crates/engine/src/vfx_tests.rs
Adds the VHS Look controls to the Stylize catalogue, registers the renderer dispatch, sets its estimated cost to 50 ms per 1080p frame, and updates the video-effect count.
VHS image processing
crates/render/src/vfx/vhs.rs
Adds per-scanline processing with separate luma and chroma filtering, deterministic tape effects, alpha handling, and conversion between premultiplied linear RGB and BT.601 Y′CbCr.
Effect tests and documentation
crates/render/src/vfx/tests.rs, ROADMAP.md, docs/gaps.md
Adds tests for processing behavior and input handling. Records the effect in the roadmap and G18 documentation.

Priority: ⬇️ Low

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Feature

Suggested reviewers: echelon

Merge Risk: ⚪ Minimal · up to 6ea93

This adds a new, opt-in CPU VHS Look video effect with its controls, tests, and documentation. Existing effects and rendering paths are unchanged, and no outstanding defects were found, so the change is ready to merge.

Security architecture risk: 🔵 Low · up to 6ea93

VHS Look follows the existing effect-processing path, with bounded controls and isolated image processing. No new privilege boundary or material security issue was identified in the inspected paths. Broader resource limits and deployment exposure were not established.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — Within the traced paths, project-selected effects and controls reach CPU processing of a clip layer or an adjustment-layer snapshot, which can affect the composite below it. The demonstrated exposure is image output and renderer CPU/memory consumption; the inspected addition does not introduce a new authority or cross-service transition.

Trust Boundaries and Controls

  • observed — Project parameter declarations are not the only input control: the transform applies runtime bounds and validates checked width, height, and buffer-length relationships before processing. Existing disabled-effect and mask behavior is retained.

Resilience and Maintainability Implications

  • observed — The new source tests exercise malformed buffers, non-finite controls and pixels, transparency, parameter endpoints, and deterministic rendering from the same input. These checks support local failure containment; they were inspected, not executed during this review.
Pre-merge checks: Passed 4 · Inconclusive 1

Failed Failed checks: 1 inconclusive

Check name Status Explanation Resolution
Linked Issues check Inconclusive The implementation evidence covers the coding requirements in #2516: the CPU-only vhs_look effect, requested controls, BT.601 filtering, deterministic frame/clip/seed behavior, hostile-input handlin… Provide reviewable evidence that crates/ui-egui/src/i18n/es.tsv contains the required VHS Look localization rows, or remove the exclusion for that path and reassess.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely identifies the main change: adding the clip-level VHS Look analog tape effect. It is directly related to the pull request objectives.
Out of Scope Changes check Passed The listed changes remain within #2516. The effect implementation, registration, parameters, tests, localization and catalogue updates, heavy-tier cost entry, and progress documentation support the re…
Docstring Coverage Passed Docstring coverage is 84.62% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 52 functions across 6 files. (2 skipped: 2 …
Full details: Out of Scope Changes check

Explanation

The listed changes remain within #2516. The effect implementation, registration, parameters, tests, localization and catalogue updates, heavy-tier cost entry, and progress documentation support the requested feature. No unrelated implementation change is demonstrated. GPU support and excluded VHS features are not added.

Full details: Docstring Coverage

Explanation

Docstring coverage is 84.62% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 52 functions across 6 files. (2 skipped: 2 unsupported.)

Full details: Linked Issues check

Explanation

The implementation evidence covers the coding requirements in #2516: the CPU-only vhs_look effect, requested controls, BT.601 filtering, deterministic frame/clip/seed behavior, hostile-input handling, catalogue registration, cost classification, and synthetic tests. The issue also requires Spanish localization rows. crates/ui-egui/src/i18n/es.tsv is excluded by the !**/*.tsv review rule, and the available evidence does not independently verify those rows.

Resolution

Provide reviewable evidence that crates/ui-egui/src/i18n/es.tsv contains the required VHS Look localization rows, or remove the exclusion for that path and reassess.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Some tools did not complete. Review the errors below.

🔧 Clippy (1.98.1)

Clippy execution timed out

Comment @coderabbitai help to get the list of available commands.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature (Beyond Premiere): VHS Look, a clip-level analog tape effect

1 participant