Releases: theChaosCoder/vapoursynth-it
v1.4.0
[1.4.0] — 2026-06-10
Feature release: high bit depth and additional chroma samplings, plus the
fixes from a full audit. 8-bit 4:2:0 output with default parameters is
unchanged (all 198 pre-existing golden hashes reproduce bit-identically);
diMode=1 scene-change frames and diMode=2 edge columns intentionally
changed toward Avisynth parity (see Fixed).
Added
- 10/12/16-bit integer YUV input (u16 storage). IVTC decisions
(field matching, decimation, scene logic) are bit-depth-deterministic:
the decision kernels round at the 8-bit map level (pavgbScore), so a
shifted 8-bit clip decides identically at any depth, while output
pixels keep full precision. No external oracle exists for HBD
(upstream is YV12-only); guarded by cross-bit-depth consistency tests. - 4:2:2 and 4:4:4 chroma sampling across all kernels (4:1:1/4:4:0
remain rejected). - Real-content robustness + bit-depth-consistency harness
(scripts/test_real_content.py). thresholdparameter is now actually wired (upstream accepted it but
hardcodedAdjPara(20); identical behaviour at the default 20).- Golden coverage for the previously oracle-free axes:
ref
BOTTOM/ALL/NONE,diMode0/1/2,blend0/1 (newmotion_flicker
fixture — the first whose blocks actually pass the blend motion gate);
compCnunit tests incl. the upstream uppercase-'N' quirk; the oracle
parameter grid now lives in one module (scripts/param_grid.py).
Fixed
- 4:2:0 heights with
height % 4 == 2(e.g. 720×486) are now
rejected. The field-interleaved chroma row mapping requires an even
chroma-plane height; such clips previously caused out-of-bounds chroma
reads on the default path, an out-of-bounds write in the blend path,
and an uninitialized final chroma row (visible as cross-plane pixel
contamination). Upstream has the identical latent bug, so nothing
bit-compatible is lost. - diMode=1 now always runs the full deinterlacer (Avisynth parity:
only SIMPLE_BLUR/ONE_FIELD have the DrawPrevFrame scene-change
shortcut). Previously a scene-change frame emitted a copy of the
previous frame's matched fields instead. - blend=1 is access-order independent: the blend kernel's reach into
the previous block (base-1, weight 37/256) is now decided before
rendering, so seeking produces the same pixels as linear playback. - diMode=2 edge columns match upstream: the 3-tap motion test now
replicates upstream's cross-row pointer wraps at x=0/x=w−1 instead of
substituting 0. - The 1.3.2 "Known issue" (diMode=1 SIMD pavgb vs scalar truncated
average) was already fixed in the 4:2:2/4:4:4 work; the SIMD and
scalar paths are verified identical across all kernels. - Hardening:
threshold/pthresholdvalidated to [0, 100000]
(previously overflowed i32 internally in release builds); VFR clips
and fps=24 on <2-frame clips rejected with clear errors; height capped
at 8192; out-of-nominal-range 10/12-bit samples saturate instead of
invoking UB; allocation-failure paths now set a proper filter error
instead of returning garbage frames.
CI / packaging
- The integration suite (golden hashes, upstream comparison, determinism,
bit depth) is now gating — it previously ran with a job-level
continue-on-errorand could never fail CI; a missing plugin now fails
the run instead of silently skipping every test. - Releases run the full test gate (Debug + ReleaseFast unit tests +
integration suite against the ReleaseFast artifact) before anything is
published; unit tests also run under ReleaseFast in CI. - PyPI wheels now carry the full manylinux compatibility alias tags in
WHEEL (the comment claimed it; the code wrote only the first alias). - GitHub release bodies use the latest changelog section, not the whole
file;pytestcollection pinned totests/integration.
v1.3.3
Changelog
All notable changes to this project are documented here. Format loosely
follows Keep a Changelog, versioning is
SemVer.
[1.3.3] — 2026-05-24
Packaging-only release. Same Binaries.
Fixed
- PyPI wheel should install into site-packages/vapoursynth/plugins/
[1.3.2] — 2026-05-24
Patch release. Pixel output unchanged from 1.3.1 on every supported
parameter combination — verified bit-identical against the
vapoursynth-cpp-api4 reference across the full upstream-compare grid
(8 fixtures × parameter combos) and across the 3-mode smoke
(default / ref=ALL / blend=1, 150 frames each, sha256 prefix
63e6227182560887 unchanged).
Performance
The hot algorithm functions (motion.makeMotionMap2{Min,Max},
output.{copyCPNField, deinterlace, simpleBlur, deintOneField}) now
take their plane-view arguments as *const plane.PlaneView instead of
by value. Each PlaneView is 48 bytes; the 24fps path threads several
through several calls per output frame, so the reduction in stack
copies shows up clearly:
| Pipeline | 1.3.1 | 1.3.2 | Δ |
|---|---|---|---|
| fps=24 (default) | 2395 fps | 2701 fps | +12.8% |
| fps=24 + blend | 2391 fps | 2699 fps | +12.9% |
| fps=24 ref=ALL | 2490 fps | 2795 fps | +12.3% |
| fps=30 diMode=1 | 716 fps | 715 fps | — |
| fps=30 diMode=2 | 3409 fps | 3447 fps | +1.1% |
(720x480 NTSC, 10 000 measured frames per run × 3 runs, taskset -c 1,
ReleaseFast.) The 30fps modes don't move because they do one
makeOutput per output frame; the 24fps path runs getFrameSub 5×
per output frame in the decimation block, which is where the saved
copies add up.
Internal restructuring (no behaviour change)
- Bindings: dropped the hand-rolled
@cImportlayer insrc/c.zig
and the three vendored VS4 headers in favour of the upstream
vapoursynth-zig
package. Filter + plugin entry now useZAPI/ZMaphelpers with
type-safe enums and typed prop setters (setFieldBased,
setCombed,setDuration*). Net diff ~−1450 lines. PlaneView/PlaneViewMutintroduced inplane.zig— one
canonical 6-field struct replaces the 18–28 separate
[*]const u8+usizeparameters the algorithm functions used to
take.scalar.zigmodule for tiny u8-math helpers (absDiff,
pavgb,subSat) that were previously redefined per-file (6
inline copies across 5 files). Vector versions stay insimd.zig.- Deinterlace dedupe: extracted
output.deinterlacePixelScalar— one canonical inline kernel for
the per-pixel scoring + pick + motion-override that was previously
written twice inoutput.deinterlace(~75 lines inside the SIMD
body's chroma loop + ~109 lines in the scalar tail). - Audit cleanup: dead fields (
CFrameInfo.{out, matchAcc},
CallState.{realFrame, iUsePrev, iUseNext}) removed; redundant
per-frame@memsetcalls inCallState.resetForFramedropped
(consumers either fully overwrite or pair partial writes with
matching partial reads); 12var ... = undefinedplane-row locals
insimpleBlurpaths becameconstexpressions. - Centralised constants:
MAX_WIDTH(3 copies) and
CHROMA_LANES(2 copies) consolidated intoplane.zig.
Tooling
scripts/bench.py— wall-clock fps benchmark across the
relevant filter configurations. Used to validate the
PlaneView-by-pointer change above; sub-1% spread on SIMD-bound
paths once pinned withtaskset -c 1.- CI workflow (
.github/workflows/ci.yml) — consolidated three
duplicatedcurl | tarZig installs intomlugg/setup-zig@v2;
collapsed the lint/unit/cross jobs into a singlebuildjob;
replaced the PPA-based VapourSynth install on the integration job
withpip install VapourSynth(the PyPI wheel ships
libvapoursynth.so+vspipeon Linux). −50 lines. - Release workflow (
.github/workflows/release.yml) — same
mlugg/setup-zig@v2swap; tag-resolve collapsed to a one-liner via
inputs.tag || github.ref_name; the
gh release view || create; upload --clobber; editchoreography
became a singlesoftprops/action-gh-release@v2step. −20+ lines.
Known issue (carried forward, not new)
The SIMD body of output.deinterlace (diMode=1) uses pavgb (rounded
average) for the motion-override luma override, while the scalar tail
and the Avisynth C upstream both use truncated (t+b)>>1. At pixels
where the motion-override fires and (pT[x]+pB[x]) is odd, our SIMD
path differs by ±1 from the scalar path. The vapoursynth-cpp-api4
reference plugin hardcodes one_field (no diMode parameter), so
this can't yet be validated against ground truth — a TODO marker in
the source records the one-liner fix for whoever builds the Avisynth
oracle (or hand-computes reference fixtures).
[1.3.1] — 2026-05-23
Patch release. Performance-only — pixel output unchanged from 1.3.0
on every supported parameter combination.
Changes
- SIMD coverage extended to the three output-stage functions in
src/output.zig:simpleBlur(diMode=2): 32-lane motion-hit count + 16-lane luma
body with overlap-loaded 3-tap motion neighbours.deintOneField(diMode=3, default): 16-lane luma body using
cross-row field-map OR-mask + pavgb +@select; chroma
unconditional via tight LANES/2 pass.deinterlace(diMode=1): 32-lane luma with all five IV scores +
@select-chain min-pick + motion override.
- ARM64 binaries included by default (Linux aarch64 + macOS
aarch64) since the 1.3.0 release-workflow rework.
Speed delta vs 1.3.0 (720x480 NTSC, ReleaseFast, best-of-3)
| Pipeline | 1.3.0 | 1.3.1 | Δ |
|---|---|---|---|
| fps=30 | 3217 fps | 3175 fps | ~0 (noise) |
| fps=24 | 3148 fps | 3023 fps | ~0 |
| fps=24 diMode=1 | 3041 fps | 3215 fps | +5.7% |
| fps=24 blend=1 | 2353 fps | 2301 fps | ~0 |
The default fps=24/fps=30 paths don't move measurably because most
frames on telecined NTSC content classify as ip='P' and skip the
deinterlacer entirely. Workloads that hit ip='I' frequently
(interlaced-heavy content, or pthreshold=1-forced runs) see the
diMode=1 gain on every frame.
Bit-exactness
Default-path 720-frame regression against the API-4 upstream port
still passes byte-for-byte. diMode=1 SIMD was independently
verified against the pre-SIMD scalar build (80 frames, all md5s
identical).
[1.3.0] — 2026-05-23
Initial release. A from-scratch Zig port of the VapourSynth-IT
inverse-telecine filter, bit-exact to the upstream C reference path
and with the original Avisynth-only parameters restored.
Version numbering picks up the IT lineage:
- Avisynth
IT_YV12 v0.1.03(minamina, 2003) - VapourSynth
VS_IT.dll v0103.1.2(msg7086, 2014) - This Zig port:
v1.3.0.
Features
- Filter
core.zit.IT(clip, fps=24, threshold=20, pthreshold=75, ref="TOP", blend=0, diMode=3)registered for VapourSynth API 4
(R55+). Accepts YUV420P8 input withwidth % 16 == 0,height % 2 == 0,
width ≤ 8192. - Decimation modes:
fps=24— 3:2-pulldown removal, output 24000/1001 fps (5→4
frames per cycle).fps=30— field-matching only, input rate preserved.
refparameter ("TOP"/"BOTTOM"/"ALL"/"NONE"): the
Avisynth original's field-match-direction switch, fully reimplemented.
The VapourSynth upstream had stripped this down to a hardcoded
"TOP".blendparameter: pure-Zig port of Avisynth'sBlendFrame_YV12,
triangular kernel weighted blend of post-matched frames, motion-gated
on the sameminD/avgDheuristic as the original. Only active when
fps=24.diModeparameter with all four Avisynth deinterlace modes:0DI_MODE_NONE— straight field copy, no deinterlace.1DI_MODE_DEINTERLACE— full per-pixel five-candidate scorer
with motion-gated vertical-average fallback (~500 LoC of Avisynth
MMX re-expressed as pure scalar Zig).2DI_MODE_SIMPLE_BLUR— vertical(T+2C+B)/4blur on
motion-flagged pixels.3DI_MODE_ONE_FIELD— default; field-interpolation using the
simple-blur and motion2max maps.
Frame properties
- Standard, always set on output:
_FieldBased=0,_Combedper
frame,_DurationNum/_DurationDenderived from output rate. All
source props (_Matrix,_Transfer,_Primaries,
_ChromaLocation,_SARNum/Den,_Range/_ColorRange, …)
inherited viapropSrcinnewVideoFrame. - Diagnostic (per-frame inspection for power-user scripts):
ITMatch,ITMflag,ITIpFlag(utf8 1-char each);ITIvC/P/N/M,
ITDiffP0/P1/S0/S1,ITBlended(ints).
Verification
- Bit-exact to a mechanically API-3 → API-4-ported build of the
upstream C reference (reference/vapoursynth-cpp-api4/). 198 frames
across 10 fixture × parameter combinations, plus 720 frames of two
real telecined NTSC VOB samples in bothfps=24andfps=30mode,
byte-for-byte identical. - 35 Zig unit tests for the algorithm primitives + SIMD helpers.
- 58 Python/VapourSynth integration tests — invariants,
property checks, error paths, the regression-pinned md5 hashes, the
upstream-compare matrix, and the new parameter sweeps.
Performance
500 frames of 720×480 NTSC, ReleaseFast build, single-threaded
fmParallelRequests:
| Pipeline | fps |
|---|---|
core.zit.IT(clip, fps=30) |
~2700 |
core.zit.IT(clip, fps=24) |
~2200 |
core.zit.IT(clip, fps=24, diMode=1) |
~2350 |
core.zit.IT(clip, fps=24, blend=1) |
~1700 |
core.vivtc.VFM(clip) (reference) |
~600 |
core.vivtc.VFM + VDecimate (ref) |
~1270 |
~4× faster than vivtc.VFM a...
v1.3.2
Changelog
All notable changes to this project are documented here. Format loosely
follows Keep a Changelog, versioning is
SemVer.
[1.3.2] — 2026-05-24
Performance
The hot algorithm functions (motion.makeMotionMap2{Min,Max},
output.{copyCPNField, deinterlace, simpleBlur, deintOneField}) now
take their plane-view arguments as *const plane.PlaneView instead of
by value. Each PlaneView is 48 bytes; the 24fps path threads several
through several calls per output frame, so the reduction in stack
copies shows up clearly:
| Pipeline | 1.3.1 | 1.3.2 | Δ |
|---|---|---|---|
| fps=24 (default) | 2395 fps | 2701 fps | +12.8% |
| fps=24 + blend | 2391 fps | 2699 fps | +12.9% |
| fps=24 ref=ALL | 2490 fps | 2795 fps | +12.3% |
| fps=30 diMode=1 | 716 fps | 715 fps | — |
| fps=30 diMode=2 | 3409 fps | 3447 fps | +1.1% |
(720x480 NTSC, 10 000 measured frames per run × 3 runs, taskset -c 1,
ReleaseFast.) The 30fps modes don't move because they do one
makeOutput per output frame; the 24fps path runs getFrameSub 5×
per output frame in the decimation block, which is where the saved
copies add up.
Internal restructuring (no behaviour change)
- Bindings: dropped the hand-rolled
@cImportlayer insrc/c.zig
and the three vendored VS4 headers in favour of the upstream
vapoursynth-zig
package. Filter + plugin entry now useZAPI/ZMaphelpers with
type-safe enums and typed prop setters (setFieldBased,
setCombed,setDuration*). Net diff ~−1450 lines. PlaneView/PlaneViewMutintroduced inplane.zig— one
canonical 6-field struct replaces the 18–28 separate
[*]const u8+usizeparameters the algorithm functions used to
take.scalar.zigmodule for tiny u8-math helpers (absDiff,
pavgb,subSat) that were previously redefined per-file (6
inline copies across 5 files). Vector versions stay insimd.zig.- Deinterlace dedupe: extracted
output.deinterlacePixelScalar— one canonical inline kernel for
the per-pixel scoring + pick + motion-override that was previously
written twice inoutput.deinterlace(~75 lines inside the SIMD
body's chroma loop + ~109 lines in the scalar tail). - Audit cleanup: dead fields (
CFrameInfo.{out, matchAcc},
CallState.{realFrame, iUsePrev, iUseNext}) removed; redundant
per-frame@memsetcalls inCallState.resetForFramedropped
(consumers either fully overwrite or pair partial writes with
matching partial reads); 12var ... = undefinedplane-row locals
insimpleBlurpaths becameconstexpressions. - Centralised constants:
MAX_WIDTH(3 copies) and
CHROMA_LANES(2 copies) consolidated intoplane.zig.
Known issue (carried forward, not new)
The SIMD body of output.deinterlace (diMode=1) uses pavgb (rounded
average) for the motion-override luma override, while the scalar tail
and the Avisynth C upstream both use truncated (t+b)>>1. At pixels
where the motion-override fires and (pT[x]+pB[x]) is odd, our SIMD
path differs by ±1 from the scalar path. The vapoursynth-cpp-api4
reference plugin hardcodes one_field (no diMode parameter), so
this can't yet be validated against ground truth — a TODO marker in
the source records the one-liner fix for whoever builds the Avisynth
oracle (or hand-computes reference fixtures).
v1.3.1
Changelog
All notable changes to this project are documented here. Format loosely
follows Keep a Changelog, versioning is
SemVer.
[1.3.1] — 2026-05-23
Patch release. Performance-only — pixel output unchanged from 1.3.0
on every supported parameter combination.
Changes
- SIMD coverage extended to the three output-stage functions in
src/output.zig:simpleBlur(diMode=2): 32-lane motion-hit count + 16-lane luma
body with overlap-loaded 3-tap motion neighbours.deintOneField(diMode=3, default): 16-lane luma body using
cross-row field-map OR-mask + pavgb +@select; chroma
unconditional via tight LANES/2 pass.deinterlace(diMode=1): 32-lane luma with all five IV scores +
@select-chain min-pick + motion override.
- ARM64 binaries included by default (Linux aarch64 + macOS
aarch64) since the 1.3.0 release-workflow rework.
Speed delta vs 1.3.0 (720x480 NTSC, ReleaseFast, best-of-3)
| Pipeline | 1.3.0 | 1.3.1 | Δ |
|---|---|---|---|
| fps=30 | 3217 fps | 3175 fps | ~0 (noise) |
| fps=24 | 3148 fps | 3023 fps | ~0 |
| fps=24 diMode=1 | 3041 fps | 3215 fps | +5.7% |
| fps=24 blend=1 | 2353 fps | 2301 fps | ~0 |
The default fps=24/fps=30 paths don't move measurably because most
frames on telecined NTSC content classify as ip='P' and skip the
deinterlacer entirely. Workloads that hit ip='I' frequently
(interlaced-heavy content, or pthreshold=1-forced runs) see the
diMode=1 gain on every frame.
Bit-exactness
Default-path 720-frame regression against the API-4 upstream port
still passes byte-for-byte. diMode=1 SIMD was independently
verified against the pre-SIMD scalar build (80 frames, all md5s
identical).
[1.3.0] — 2026-05-23
Initial release. A from-scratch Zig port of the VapourSynth-IT
inverse-telecine filter, bit-exact to the upstream C reference path
and with the original Avisynth-only parameters restored.
Version numbering picks up the IT lineage:
- Avisynth
IT_YV12 v0.1.03(minamina, 2003) - VapourSynth
VS_IT.dll v0103.1.2(msg7086, 2014) - This Zig port:
v1.3.0.
Features
- Filter
core.zit.IT(clip, fps=24, threshold=20, pthreshold=75, ref="TOP", blend=0, diMode=3)registered for VapourSynth API 4
(R55+). Accepts YUV420P8 input withwidth % 16 == 0,height % 2 == 0,
width ≤ 8192. - Decimation modes:
fps=24— 3:2-pulldown removal, output 24000/1001 fps (5→4
frames per cycle).fps=30— field-matching only, input rate preserved.
refparameter ("TOP"/"BOTTOM"/"ALL"/"NONE"): the
Avisynth original's field-match-direction switch, fully reimplemented.
The VapourSynth upstream had stripped this down to a hardcoded
"TOP".blendparameter: pure-Zig port of Avisynth'sBlendFrame_YV12,
triangular kernel weighted blend of post-matched frames, motion-gated
on the sameminD/avgDheuristic as the original. Only active when
fps=24.diModeparameter with all four Avisynth deinterlace modes:0DI_MODE_NONE— straight field copy, no deinterlace.1DI_MODE_DEINTERLACE— full per-pixel five-candidate scorer
with motion-gated vertical-average fallback (~500 LoC of Avisynth
MMX re-expressed as pure scalar Zig).2DI_MODE_SIMPLE_BLUR— vertical(T+2C+B)/4blur on
motion-flagged pixels.3DI_MODE_ONE_FIELD— default; field-interpolation using the
simple-blur and motion2max maps.
Frame properties
- Standard, always set on output:
_FieldBased=0,_Combedper
frame,_DurationNum/_DurationDenderived from output rate. All
source props (_Matrix,_Transfer,_Primaries,
_ChromaLocation,_SARNum/Den,_Range/_ColorRange, …)
inherited viapropSrcinnewVideoFrame. - Diagnostic (per-frame inspection for power-user scripts):
ITMatch,ITMflag,ITIpFlag(utf8 1-char each);ITIvC/P/N/M,
ITDiffP0/P1/S0/S1,ITBlended(ints).
Verification
- Bit-exact to a mechanically API-3 → API-4-ported build of the
upstream C reference (reference/vapoursynth-cpp-api4/). 198 frames
across 10 fixture × parameter combinations, plus 720 frames of two
real telecined NTSC VOB samples in bothfps=24andfps=30mode,
byte-for-byte identical. - 35 Zig unit tests for the algorithm primitives + SIMD helpers.
- 58 Python/VapourSynth integration tests — invariants,
property checks, error paths, the regression-pinned md5 hashes, the
upstream-compare matrix, and the new parameter sweeps.
Performance
500 frames of 720×480 NTSC, ReleaseFast build, single-threaded
fmParallelRequests:
| Pipeline | fps |
|---|---|
core.zit.IT(clip, fps=30) |
~2700 |
core.zit.IT(clip, fps=24) |
~2200 |
core.zit.IT(clip, fps=24, diMode=1) |
~2350 |
core.zit.IT(clip, fps=24, blend=1) |
~1700 |
core.vivtc.VFM(clip) (reference) |
~600 |
core.vivtc.VFM + VDecimate (ref) |
~1270 |
~4× faster than vivtc.VFM at field-matching, ~2× faster than
vivtc.VFM + VDecimate at IVTC, using the pure-C path + portable
Zig @Vector(N, u8) SIMD (pavgb, psubusb, pmaxub/pminub
equivalents).
Differences from the VapourSynth upstream
The upstream
VapourSynth-IT
plugin (msg7086, 2014) was a partial port of the Avisynth original.
This port reintroduces what was stripped and fixes four framework-level
issues:
ref,blend,diMode=0/1/2reinstated (upstream hardcoded
ref="TOP",blend=false,diMode=3).- Frame properties: upstream passes
propSrc=nullto
newVideoFrame, so output frames carry no metadata at all. We
inherit source props and set_FieldBased/_Combed/_Duration*
explicitly. - Threading mode:
fmParallelRequests. Upstream registered as
fmParalleldespite sharing mutable per-instance state — a latent
race condition the modern VS scheduler hits more often. - Frame-request range widened to
[base-2, base+6](fps=24) /
[n-2, n+2](fps=30) so all reads throughgetFrameFilterare
satisfiable. Upstream relied on API 3's syncgetFrameto pull
neighbours from cache; under API 4 that is not allowed inside a
filter callback. - Edge clamping:
getFrameFilter(n, …)is called withnclipped
to[0, numFrames-1]. Under API 3 the core silently clamped; under
API 4 it returns null and a deref crashes.
Build & packaging
zig build --release=fastproduceszig-out/lib/libzit.soon the
host platform.zig build crossproduces release binaries for all three targets
underzig-out/{linux,macos,windows}/:libzit-linux-x86_64.solibzit-macos-x86_64.dylibzit-windows-x86_64.dll
- GitHub Actions workflow under
.github/workflows/ci.ymlruns
lint + unit + cross-build + integration onworkflow_dispatchonly;
push/pull-request triggers can be enabled by appending to theon:
block.
Credits
Algorithm: thejam79 (IT 0.051, 2002), minamina (IT_YV12 0.1.03, 2003),
poodle (64-bit / 8k mod), msg7086 (VapourSynth port, 2014). Zig port:
this repo.
v1.3.0 — initial Zig port
Changelog
All notable changes to this project are documented here. Format loosely
follows Keep a Changelog, versioning is
SemVer.
[1.3.0] — 2026-05-23
Initial release. A from-scratch Zig port of the VapourSynth-IT
inverse-telecine filter, bit-exact to the upstream C reference path
and with the original Avisynth-only parameters restored.
Version numbering picks up the IT lineage:
- Avisynth
IT_YV12 v0.1.03(minamina, 2003) - VapourSynth
VS_IT.dll v0103.1.2(msg7086, 2014) - This Zig port:
v1.3.0.
Features
- Filter
core.zit.IT(clip, fps=24, threshold=20, pthreshold=75, ref="TOP", blend=0, diMode=3)registered for VapourSynth API 4
(R55+). Accepts YUV420P8 input withwidth % 16 == 0,height % 2 == 0,
width ≤ 8192. - Decimation modes:
fps=24— 3:2-pulldown removal, output 24000/1001 fps (5→4
frames per cycle).fps=30— field-matching only, input rate preserved.
refparameter ("TOP"/"BOTTOM"/"ALL"/"NONE"): the
Avisynth original's field-match-direction switch, fully reimplemented.
The VapourSynth upstream had stripped this down to a hardcoded
"TOP".blendparameter: pure-Zig port of Avisynth'sBlendFrame_YV12,
triangular kernel weighted blend of post-matched frames, motion-gated
on the sameminD/avgDheuristic as the original. Only active when
fps=24.diModeparameter with all four Avisynth deinterlace modes:0DI_MODE_NONE— straight field copy, no deinterlace.1DI_MODE_DEINTERLACE— full per-pixel five-candidate scorer
with motion-gated vertical-average fallback (~500 LoC of Avisynth
MMX re-expressed as pure scalar Zig).2DI_MODE_SIMPLE_BLUR— vertical(T+2C+B)/4blur on
motion-flagged pixels.3DI_MODE_ONE_FIELD— default; field-interpolation using the
simple-blur and motion2max maps.
Frame properties
- Standard, always set on output:
_FieldBased=0,_Combedper
frame,_DurationNum/_DurationDenderived from output rate. All
source props (_Matrix,_Transfer,_Primaries,
_ChromaLocation,_SARNum/Den,_Range/_ColorRange, …)
inherited viapropSrcinnewVideoFrame. - Diagnostic (per-frame inspection for power-user scripts):
ITMatch,ITMflag,ITIpFlag(utf8 1-char each);ITIvC/P/N/M,
ITDiffP0/P1/S0/S1,ITBlended(ints).
Verification
- Bit-exact to a mechanically API-3 → API-4-ported build of the
upstream C reference (reference/vapoursynth-cpp-api4/). 198 frames
across 10 fixture × parameter combinations, plus 720 frames of two
real telecined NTSC VOB samples in bothfps=24andfps=30mode,
byte-for-byte identical. - 35 Zig unit tests for the algorithm primitives + SIMD helpers.
- 58 Python/VapourSynth integration tests — invariants,
property checks, error paths, the regression-pinned md5 hashes, the
upstream-compare matrix, and the new parameter sweeps.
Performance
500 frames of 720×480 NTSC, ReleaseFast build, single-threaded
fmParallelRequests:
| Pipeline | fps |
|---|---|
core.zit.IT(clip, fps=30) |
~2700 |
core.zit.IT(clip, fps=24) |
~2200 |
core.zit.IT(clip, fps=24, diMode=1) |
~2350 |
core.zit.IT(clip, fps=24, blend=1) |
~1700 |
core.vivtc.VFM(clip) (reference) |
~600 |
core.vivtc.VFM + VDecimate (ref) |
~1270 |
~4× faster than vivtc.VFM at field-matching, ~2× faster than
vivtc.VFM + VDecimate at IVTC, using the pure-C path + portable
Zig @Vector(N, u8) SIMD (pavgb, psubusb, pmaxub/pminub
equivalents).
Differences from the VapourSynth upstream
The upstream
VapourSynth-IT
plugin (msg7086, 2014) was a partial port of the Avisynth original.
This port reintroduces what was stripped and fixes four framework-level
issues:
ref,blend,diMode=0/1/2reinstated (upstream hardcoded
ref="TOP",blend=false,diMode=3).- Frame properties: upstream passes
propSrc=nullto
newVideoFrame, so output frames carry no metadata at all. We
inherit source props and set_FieldBased/_Combed/_Duration*
explicitly. - Threading mode:
fmParallelRequests. Upstream registered as
fmParalleldespite sharing mutable per-instance state — a latent
race condition the modern VS scheduler hits more often. - Frame-request range widened to
[base-2, base+6](fps=24) /
[n-2, n+2](fps=30) so all reads throughgetFrameFilterare
satisfiable. Upstream relied on API 3's syncgetFrameto pull
neighbours from cache; under API 4 that is not allowed inside a
filter callback. - Edge clamping:
getFrameFilter(n, …)is called withnclipped
to[0, numFrames-1]. Under API 3 the core silently clamped; under
API 4 it returns null and a deref crashes.
Build & packaging
zig build --release=fastproduceszig-out/lib/libzit.soon the
host platform.zig build crossproduces release binaries for all three targets
underzig-out/{linux,macos,windows}/:libzit-linux-x86_64.solibzit-macos-x86_64.dylibzit-windows-x86_64.dll
- GitHub Actions workflow under
.github/workflows/ci.ymlruns
lint + unit + cross-build + integration onworkflow_dispatchonly;
push/pull-request triggers can be enabled by appending to theon:
block.
Credits
Algorithm: thejam79 (IT 0.051, 2002), minamina (IT_YV12 0.1.03, 2003),
poodle (64-bit / 8k mod), msg7086 (VapourSynth port, 2014). Zig port:
this repo.