Skip to content

Repository files navigation

vhs-decode-dotnet

English | 简体中文 | 日本語

A .NET 11 rewrite of the decode-facing parts of oyvindln/vhs-decode, targeting upstream release v0.4.0 at commit 43155200da87c0d49eb37d8ec09b1372075ee8e4.

The current .NET port release is v0.4.0-2.10.0 (application version 2.10.0).

Important

This remains a compatibility work in progress. The top-level decode paths are implemented and heavily tested, but every real capture and rare option combination has not yet been certified byte-for-byte.

Read the detailed English reference for the full compatibility matrix, implementation notes, historical benchmarks, validation evidence, and remaining gaps.

Contents

Overview

  • Decode-only scope: VHS, CVBS, LaserDisc, and HiFi.
  • Release 4.0 command names, options, aliases, defaults, diagnostics, and output lifecycle are the compatibility target.
  • VHS-family routing includes VHS/S-VHS, Betamax, Video8/Hi8, U-matic, Type C, EIAJ, and supported PAL/NTSC variants.
  • TBC utility tools, the double-click GUI, and developer plotting windows are intentionally out of scope.
  • The Visual Studio 2026 .slnx solution has 1,615 standard xUnit v3 tests that are visible in Test Explorer and runnable with dotnet test.

Get started

Download the current binary-only Windows x64 package from GitHub Releases. The package is built as a single-file decode.exe.

decode.exe vhs [upstream options] input.lds output
decode.exe cvbs [upstream options] input.lds output
decode.exe ld [upstream options] input.lds output
decode.exe hifi [upstream options] input.lds output.wav

Standalone command aliases such as vhs-decode.exe and ld-decode.exe are also supported. Use decode.exe <command> --help for the complete compatible option set.

The release workflow also builds a self-contained, multi-file glibc linux-x64 tar. It supports only the portable exact backend and bundles the Linux SQLite, libsndfile, and libsoxr native assets; Windows IPP/CUDA DLLs are excluded. After installing FFmpeg and the documented OS libraries:

tar -xzf vhs-decode-dotnet-linux-x64.tar.gz
cd vhs-decode-dotnet-linux-x64
./vhs-decode --pal --dsp-backend exact input.lds output

See Linux x64 release for the Ubuntu 22.04/glibc 2.35 baseline, runtime packages, checksums, build provenance, and release gates.

Seekable RF preview server

VHS and LaserDisc can expose a local, seekable HTTP preview without an output base name:

Run decode.exe vhs --preview-server --pal input.lds for tape RF, or decode.exe ld --preview-server --pal input.ldf for LaserDisc RF.

The command prints a loopback player URL and a standard HLS/fMP4 playlist URL. The default address starts at 127.0.0.1:8080; if occupied, startup increments the port through 8180 until one binds. An explicit --preview-port is strict, while --preview-port 0 asks the operating system for a dynamic port. Preview mode creates no TBC, JSON, SQLite, EFM, audio, or decoder log artifacts.

This is intentionally a low-accuracy navigation mode. It retains colour through a cheap 4fSC one-dimensional demodulator, derives the PAL V-switch from neighbouring burst lines to avoid four-field hue flicker, applies lightweight dropout concealment, skips audio and the expensive export comb/repair stages, and decodes the full continuous frame count for every two-second preview window. The muted web player starts automatically and keeps two windows of lookahead buffered. Top-field-first source fields are deinterlaced at field rate: NTSC is served as progressive 640x480 at 60000/1001 fps and PAL as progressive 768x576 at 50 fps. At startup the preview validates complete fMP4 pipelines in this order: NVENC with CUDA YADIF, QSV with advanced VPP deinterlacing, AMF with CPU YADIF, then libx264 with CPU YADIF. --preview-crf accepts 0 through 51 and defaults to 31; hardware encoders map it to their closest quality/QP control, so bitrate is not identical across backends. Eligible native-rate 40 MSPS PAL/NTSC VHS preview first performs a lightweight CUDA-driver/device preflight; this does not load cuFFT, create a CUDA context, or initialize NVENC. A passing device then gets one full CUDA/cuFFT/NVENC initialization attempt. If preflight or full startup is unavailable, preview falls back to ipp-fast when available, then to the portable managed backend. Other preview inputs start with that same IPP-to-managed CPU order. Standard 40 MSPS VHS preview also applies a fixed anti-alias filter and decodes its internal RF stream at 20 MSPS. Native 20 MSPS VHS input stays at 20 MSPS. In other words, supported VHS preview routes force the same behavior as the full-decode --decode-at-20msps switch. Full VHS decode can opt into that switch with ipp-fast, cuda-fast, or approx-fast; Exact, S-VHS, other tape formats, and LaserDisc retain their existing sample-rate behavior. Startup reports the selected video pipeline, IPP-FAST initialization, active decoder thread count, and separate in-place window-ID and real-time-FPS lines. A matching FFmpeg build is required on PATH; VHSDECODE_FFMPEG and VHSDECODE_FFPROBE can select explicit binaries.

Native-rate 40 MSPS PAL/NTSC VHS therefore selects the independent GPU preview path automatically on a compatible machine. The same path can be pinned explicitly:

decode.exe vhs --preview-server --dsp-backend cuda-fast --pal input.ldf

This keeps one CUDA context across windows, performs the anti-aliased 40-to-20 MSPS reduction, sync, FM/chroma/dropout processing, NV12 bob rendering, and NVENC H.264 encoding on the GPU. The renderer writes a block-linear NV12 CUDA array that NVENC registers directly, avoiding its pitch-linear conversion. Each bounded RF batch is uploaded once, while full luma, chroma, and NV12 frames are never downloaded; only small sync/field-order control metadata and compressed H.264 packets cross the host/device boundary. FFmpeg only copy-muxes the H.264 into HLS/fMP4. An explicit --dsp-backend cuda-fast request requires a compatible NVIDIA GPU and never falls back to the CPU preview or another encoder. Automatic default selection falls back only if GPU startup fails; it never changes backend after a preview session has started. The existing GPU bob deinterlacer is unchanged. Preview-only cross-field dropout substitution uses a clean opposite-parity field when one exists in the bounded batch, and a one-field 75/25 current/previous chroma blend resets at every seek window.

A sustained local resource matrix on 2026-08-20 used the same real 40 MSPS PAL capture, an Intel Core Ultra 7 265K (20 logical processors), and an RTX 4070. The first five rows came from source commit 41bfd92; the corrected IPP preview row came from 1fb1455. Each row is the mean of two independent process launches; ranges are the two observed source-frame rates.

Path Source fps (range) decode.exe CPU Whole-system CPU GPU SM avg/peak NVENC avg/peak Peak GPU FB
Full, CUDA 40 MSPS 35.30 (35.28-35.33) 10.81% / 2.16 cores 33.89% 32.44% / 72% 0% / 0% 7,038 MiB
Full, IPP 40 MSPS 23.79 (23.71-23.87) 22.15% / 4.43 cores 28.76% 0.10% / 4% 0% / 0% 3,102 MiB
Full, CUDA 20 MSPS 35.80 (35.60-35.99) 10.92% / 2.18 cores 34.74% 27.67% / 54% 0% / 0% 5,288 MiB
Full, IPP 20 MSPS 26.86 (26.25-27.48) 24.19% / 4.84 cores 48.39% 0% / 0% 0% / 0% 3,102 MiB
Preview, CUDA 20 MSPS 47.03 (46.59-47.48) 4.06% / 0.81 cores 24.75% 26.91% / 61% 3.12% / 8% 4,795 MiB
Preview, IPP 20 MSPS 34.33 (34.05-34.61) 24.66% / 4.93 cores 43.85% 1.52% / 9% 1.48% / 4% 3,292 MiB

Full runs requested 500 source frames and verified exactly 1,000 output fields. Preview runs requested 20 distinct two-second windows, or 1,000 source frames, after a separate cold W5. Source fps does not count the two output fields/bob frames as two source frames. Process CPU is normalized against all 20 logical processors; whole-system CPU includes FFmpeg, drivers, the sampler, and other machine work. GPU values are global 100 ms NVML samples. The first five rows' idle baseline was 5.05% system CPU, 0% GPU SM, 0% NVENC, and 3,103 MiB GPU FB; the separately corrected IPP preview row used 6.85% system CPU and 3,110 MiB GPU FB. CUDA delivered 1.48x/1.33x/1.37x the IPP source throughput in full-40/full-20/preview-20 respectively. See the detailed method and evidence.

The final-source five-window quality recheck used corrected IPP coordinates. After trimming the first IPP output frame for one-field alignment, default CUDA preview averaged SSIM Y/U/V/All of 0.914657/0.957361/0.966698/0.930448. The forced line-phase guard averaged 0.926692 combined and disabling cross-field dropout plus chroma stabilization averaged 0.922357; default was higher on every tested window. These are capture- and hardware-specific preview results, not Exact-equivalence claims.

Profiles and backends

--compat-version selects upstream behavior:

Value Meaning
v0.4.0 Default. Targets the pinned Python release behavior.
current Opt-in staged behavior from upstream PR 341, including newer VHS sync and color-under processing.

The strict compatibility oracle is Python v0.4.0 commit g4315520 with --threads 0. Python output hashes are not stable across its worker counts, so multithreaded Python runs are used for speed measurements only.

--dsp-backend selects the DSP implementation:

Value Meaning
exact Default managed path for compatibility-sensitive decoding.
ipp-fast Experimental Windows x64 VHS and LaserDisc real-RF paths using Intel IPP. It can change floating-point bits and never silently falls back to exact.
cuda-fast Experimental Windows x64 NVIDIA CUDA 13 full-signal VHS path. It has an independent numerical contract, supports PAL/NTSC VHS at 40 MSPS normally or GPU 40-to-20/native-20 MSPS with --decode-at-20msps, and never silently falls back to a CPU backend.
approx-fast Experimental VHS-only FP32 path with selectable TBC resampler and precision contract. The default balanced precision keeps the existing v1/v2 behavior; explicit aggressive selects the faster, deliberately lossy field-FP32/burst-IQ-prefix/chroma-FFT v6 contract. Nonzero RF high boost is not supported.
decode.exe vhs --compat-version current --dsp-backend ipp-fast `
  --threads 20 input.lds output
decode.exe vhs --dsp-backend ipp-fast --decode-at-20msps `
  --pal input.lds output-20msps
decode.exe vhs --dsp-backend cuda-fast --pal `
  --decode-at-20msps --start 100 --length 20 input.ldf output
decode.exe vhs --dsp-backend approx-fast `
  --pal input.lds output-approx-balanced
decode.exe vhs --dsp-backend approx-fast `
  --approx-resampler catmull-rom4 --pal input.lds output-approx-catmull
decode.exe vhs --compat-version current --dsp-backend approx-fast `
  --approx-resampler catmull-rom4 `
  --approx-precision aggressive --pal input.lds output-approx-aggressive

--approx-resampler sinc16|catmull-rom4 and --approx-precision balanced|aggressive apply only to approx-fast; using any of them with another backend is rejected rather than ignored. Approx is a separate numerical contract from both exact and ipp-fast; use exact when release-compatible or byte-sensitive output is required.

If --approx-precision is omitted, balanced is selected. Balanced behavior is unchanged: omitted or explicit sinc16 retains vhs-rf-transform-f32-v1, while explicit catmull-rom4 selects vhs-rf-transform-f32-catmull-rom4-v2 and uses four-tap float32 Catmull-Rom for final luma/chroma TBC resampling; sync and phase-analysis prefix resampling remain sinc-based.

Explicit aggressive selects vhs-rf-transform-field-f32-catmull-rom4-burst-iq-prefix-chroma-fft-v6. It requires --compat-version current, --approx-resampler catmull-rom4, and a 40 MSPS decode. The route keeps field-sync VideoLowPass, its nine-tap boxcar, and the edge scan in FP32. It also skips current's 32-iteration nonlinear chroma-burst Tune, directly uses float32-derived I/Q phase, amplitude, and DC initial estimates, keeps the nominal burst frequency, and resamples chroma phase-analysis line prefixes with four-tap Catmull-Rom instead of the 16-tap sinc kernel.

The v6 chroma stage reuses the resident input RFFT, multiplies its half-spectrum by the zero-phase ChromaBurst |H|² response, and runs an IRFFT directly into the float32 chroma buffer. Relative to the older SOS forward/backward filtfilt path, this is an intentionally lossy numerical contract, including different FP32 rounding and block-edge behavior. Exact and Balanced remain unchanged. AFC/--cafc, chroma or video notches, --export_raw_tbc, non-color-under formats, configurations without the standard chroma filter, and other options that require the older chroma path fail closed; no partial fallback to that older path is performed. Aggressive precision does not disable CTI: --cti_mix 0 is a separate option. No fixed speedup or visual-equivalence claim is implied; compare complete same-input balanced/aggressive decodes, including TBC/chroma/JSON, ordered fields, and fileLoc, with the intended thread count.

--decode-at-20msps is a VHS preview-quality mode, not an Exact-equivalence mode. A 40 MSPS source is anti-alias filtered and decoded internally at 20 MSPS; native 20 MSPS input is decoded without another reduction. TBC metadata fileLoc remains in original input-sample coordinates. This is a preview-quality rate choice, not a universal throughput switch. In the current startup-inclusive 500-frame gate it raised CUDA throughput by 1.40% and IPP throughput by 12.91%. An earlier 100-frame gate showed IPP 6.83% slower because fixed startup and reduction costs dominated that short request; benchmark the intended capture and run length before selecting it for full decode.

The default Windows release includes the small CUDA-fast bridge but does not embed the 271 MiB cuFFT DLL. An explicit --dsp-backend cuda-fast request, or an eligible automatic VHS preview after its lightweight driver/device preflight passes, searches for a compatible CUDA 13/cuFFT 12 installation. If none is available, it verifies the NVIDIA driver first, downloads the pinned 202.2 MiB NVIDIA redistributable, validates both the archive and DLL with SHA-256, and installs it once under %LOCALAPPDATA%\vhs-decode-dotnet\cuda\cufft. A failed lightweight preflight never enters the resolver or accesses the network. Exact, IPP, and preview inputs outside the automatic CUDA support surface also remain offline. Set VHSDECODE_CUDA_RUNTIME_PATH for an offline/system runtime, VHSDECODE_CUDA_CACHE_PATH for a different cache root, or VHSDECODE_CUDA_AUTO_DOWNLOAD=0 to disable automatic downloads.

LaserDisc now routes its video, EFM, and analog-audio full-complex FFT stages through IPP. CVBS and HiFi still reject ipp-fast; use exact whenever release-compatible behavior is required. See the detailed backend notes before using IPP or CUDA for compatibility-sensitive work. On the tested RTX 4070 and one real PAL capture, the quality-corrected FP32 CUDA-full path is now visually much closer to Exact. In the sustained matrix above, CUDA measured 35.30 source fps versus IPP's 23.79 (1.48x) at 40 MSPS, and 35.80 versus 26.86 (1.33x) at 20 MSPS. A separate short same-source session measured materially higher CUDA throughput, so these figures are descriptive snapshots rather than evidence that later code alone reversed the older CUDA/IPP result. Each variant's two luma/chroma/JSON output sets were byte-identical within that variant. An aligned 79-frame lossless comparison with Exact using the default export-side dropout correction measured SSIM Y/U/V/All of 0.954905/0.988109/0.991285/0.972301 and PSNR Y/U/V/average of 33.196867/41.243137/43.586266/35.699053 dB. Manual inspection retained closely matching scene content, colour, and motion, while numerical equality is not claimed. This narrow result is hardware- and capture-specific; cuda-fast remains experimental and does not share the CPU numerical contract.

Latest performance

This startup-inclusive --start 100 --length 160 snapshot uses one fixed private local 40 MHz PAL VHS .ldf fixture; its filename is intentionally not published. It retains 30 fixed Python reference measurements from 2026-08-12. All 60 .NET measurements were refreshed together on 2026-08-26 with one self-contained .NET 11 Preview 7 candidate based on main commit b893bf7 plus the current RF chroma pass optimization described below. Every cell has three complete runs. Compatibility is evaluated separately from speed.

CLI mode (workers) Python v0.4.0 Python PR341 Exact + v0.4.0 Exact + current IPP-fast + v0.4.0 IPP-fast + current
default (5) 52.811 s 54.243 s 11.598 s / 4.553x 11.398 s / 4.759x 10.230 s / 5.162x 7.997 s / 6.783x
--threads 1 57.067 s 56.762 s 32.969 s / 1.731x 37.599 s / 1.510x 22.629 s / 2.522x 24.415 s / 2.325x
--threads 5 52.920 s 55.722 s 11.773 s / 4.495x 11.100 s / 5.020x 10.101 s / 5.239x 7.971 s / 6.991x
--threads 10 52.965 s 54.949 s 9.040 s / 5.859x 8.122 s / 6.765x 8.391 s / 6.312x 5.813 s / 9.453x
--threads 20 53.555 s 54.842 s 7.310 s / 7.327x 7.076 s / 7.751x 6.919 s / 7.741x 4.868 s / 11.265x

Each .NET cell shows median wall time and speedup versus its profile-matched Python column. The default is 5 workers; three-run ranges are in the detailed performance notes. A ratio moves when either the Python numerator or .NET denominator moves, and historical tables using another fixture or window are not directly comparable. Same-moment .NET revision A/B runs, rather than old ratio cells, determine causal regressions.

The current candidate fuses the first float32 quantization into the existing current RF chroma roll/DC pass and vectorizes its final centering conversion. The 32,768-sample kernel fell from 28.025 to 16.166 ms over 1,000 calls (42.317%). Three opposite-order 1,000-frame Exact current --threads 20 pairs retained all eight compatibility surfaces; paired median wall and CPU gains were 0.5385% and 0.6860%. Memory stayed bounded, but no reduction is claimed.

The refreshed 60-run Exact/IPP-fast matrix retained one hash for luma, chroma, raw JSON, stdout, normalized stderr/logs, and ordered fileLoc in every cell and across worker counts. The latest standard xUnit v3 1,615-test suite passed with 1,612 successes and 3 expected environment skips.

Every .NET profile/thread cell was deterministic across its three refreshed runs. Merged Python PR341 was deterministic in its pinned reference set; Python v0.4.0 produced 15 distinct luma, chroma, JSON, and normalized-log hashes in 15 runs, so the strict oracle remains Python v0.4.0 g4315520 --threads 0. Commands, ranges, binary hashes, memory bounds, and historical measurements are in the detailed performance reference.

Compatibility status

The main decode pipelines, streaming outputs, recovery behavior, and CLI surface are implemented. Focused tests and real-RF gates cover luma, chroma, JSON, ordered fileLoc, stdout, normalized stderr/logs, determinism, and bounded memory. Rare captures and uncommon option interactions remain ongoing work, so a successful build or equal file size alone is not treated as proof of compatibility.

TBC, chroma, JSON, and log files are opened for concurrent reading while a decode is running, allowing compatible preview tools to inspect partial output without blocking the writer.

On native-input routes, direct raw fLaC .ldf/.flac inputs that are 40 kHz mono PCM16 and contain at most Int32.MaxValue samples use the bundled libsndfile reader. Ordinary parallel VHS decode can also use libsndfile for a narrowly gated oversized fixed-block raw FLAC without a seek table; integer mapping reproduces the pinned FFmpeg/PyAV frame starts and rewind/restart boundaries, with one-way fallback at the same logical sample on any failure. --threads 0/1, debug-plot and GNU Radio AFE modes, nonzero --sharpness, other command families, default VHS .flac, CVBS, Ogg/FLAC, stereo, PCM24, other sample rates, and unfinished or ineligible headers retain FFmpeg.

Build and test

The pinned SDK is .NET 11.0.100-preview.7.26381.103.

dotnet restore VHSDecodeDotNet.slnx
dotnet build VHSDecodeDotNet.slnx -c Release --no-restore
dotnet test --solution VHSDecodeDotNet.slnx -c Release `
  --no-build --no-restore --minimum-expected-tests 1615

Open VHSDecodeDotNet.slnx in Visual Studio 2026 to build, debug, and run the xUnit v3 suite through Test Explorer.

On Ubuntu 22.04 x64, the complete native-build, test, multi-file publish, reproducible-tar, and final extracted-tar smoke pipeline is:

pwsh ./tools/build-linux-x64-release.ps1

More detail

License

GPL-3.0. See LICENSE.

About

A .NET 11 compatibility port of the decode-facing parts of oyvindln/vhs-decode.

Topics

Resources

Stars

6 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages