Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1180,6 +1180,11 @@ rate is `FrameRateParameters.customFps` (a plain number, default 48).
NTSC source. Anything else is taken to three decimals. Out-of-range input is
clamped to 1–240 rather than refused: the panel's number box doesn't
enforce the schema's bounds. The Dart enum carries no numbers.
- **FlowFPS's length is set by the script, not MVTools.** macOS arm64 and
Linux build MVTools `master` (`floor(n * ratio)` frames), Windows and macOS
x64 ship v24 (`floor((n - 1) * ratio) + 1`). Both templates trim or extend
to `floor(n * ratio)` after the call (`test_184`); a frame-count assertion
that passes on one platform proves nothing about the others.
- **Don't check an NTSC output rate on an MKV.** Matroska's 1/1000 timebase
can't hold 60000/1001, so ffprobe reports an estimate (`19001/317`) for any
NTSC rate. `integration_frame_mapping_test.dart` encodes its custom-rate
Expand Down Expand Up @@ -1370,10 +1375,20 @@ the simple slots are full at four).
`include_directories('include', run_command(py, '-c', 'import vapoursynth…'))`,
and macOS x64 never ran `build_plugin`. Both failed silently because the
plugin wasn't in their `deps-expected-plugins.json` lists.
- **KNLMeansCL is Jaded Encoding Thaumaturgy's fork, v1.3, on every
platform** (issue #144): tag `v1.3` from source on macOS/Linux (`KNLM_TAG`),
PyPI wheel 1.3 on Windows; keep them in step. Never Khanattila's v1.1.1
release (refuses 9–15-bit except packed YUV444P10, so every 10-bit 4:2:0
source failed on Windows) or master (races its buffer fills on Apple's
OpenCL: unrepeatable, sometimes garbage). No runner has OpenCL, so after
touching it run the KNLMeans cases of
`integration_high_bit_depth_filters_test.dart` on a machine with a GPU —
they skip otherwise.
- **KNLMeans has its own device probe**, separate from `knlm_available`.
havsfunc calls KNLMeansCL with the default `device_type="auto"`, which fails
on Apple Silicon (`CL_INVALID_VALUE`) where `"gpu"` works. So `knlm` still
answers for QTGMC's call, and `DependencyLocator::knlm_device` (gpu, then
havsfunc calls KNLMeansCL with the default `device_type="auto"`, which
failed on Apple Silicon (`CL_INVALID_VALUE`) before v1.3, where `"gpu"`
worked; v1.3's `auto` works there, so QTGMC's `knlmeanscl` now runs on a Mac
instead of falling back to DFTTest. `knlm` still answers for QTGMC's call, and `DependencyLocator::knlm_device` (gpu, then
auto; `knlm-device-probe.json`) is the device our own call passes. With no
device the job is refused with `KNLM_NO_DEVICE_MESSAGE`, the same text the
panel shows (`knlmDevice`/`knlmMessage` from `--probe-opencl`), through
Expand Down Expand Up @@ -2289,3 +2304,4 @@ Full write-ups (root causes, measurements) for each entry are in
| 1.12.0 | 2026-10-04 | **Issue #101**: akarin 1.4.1 → **1.5.0** on every platform. The 1.4.1 Linux wheel put JIT code on the `brk` heap, where SELinux denies `PROT_EXEC`, so every job segfaulted on Fedora/RHEL from deps 1.8.0 on. `download-deps-linux.sh` now fails if `libakarin.so` lacks the mmap allocator |
| 1.13.0 | 2026-10 | One new plugin; every other binary is built from the same sources as 1.12.0. Adds **vszip** 22.1.0 (MIT) for `vszip.CLAHE`, Color Correction's local contrast (issue #90). Baseline build only, flat in the plugin directory on every platform (the wheels' AVX2/AVX-512 variants are left out); macOS x64 builds the sdist with Zig 0.16.0 for the 12.0 floor, with header padding so it can be signed. The v2 CPU tier is no longer a second full bundle: each x64 platform publishes its v3 bundle plus `…-x64-v2-delta.zip`, the files in `_tierFiles` (zsmooth; on macOS also MVTools), extracted over it (`"tierFormat": "delta"`). Every x64 machine downloads the same bundle; only an older CPU fetches the few MB more. This is also what lets the Linux AppImage carry its deps for both tiers without doubling in size |
| 1.14.0 | 2026-10-25 | Adds **RIFE** 4.26 (`r9_mod_v33`, MIT) + its model in `vapoursynth/rife/`, and **VSNCNN** (vs-mlrt v15.16, GPL-3.0) + Real-ESRGAN / Real-CUGAN models in `vapoursynth/ncnn/`, on macos-arm64, windows-x64 and linux-x64, with **MoltenVK** 1.4.2 (Apache-2.0) in `vapoursynth/moltenvk/` on macOS. Not autoloaded; see "GPU Plugins". **BM3D** r12 (MIT) on every platform; it had been macOS arm64 only (see "BM3D and KNLMeans Noise Reduction"). **descale** r12 (MIT) on every platform, for the Descale pass. (AnimeJaNai's models, used by the same VSNCNN, are an app add-on, not in the bundle). Published as the tested `deps-v1.14.0-rc1` bundles with only the version changed; every other file is byte-identical |
| 1.15.0 | 2026-11-03 | **Issue #144**: KNLMeansCL moves to Jaded Encoding Thaumaturgy's fork **v1.3** on every platform (Windows: PyPI wheel, replacing Khanattila's 2018 v1.1.1 zip, which refused 10-bit 4:2:0; macOS/Linux: the `v1.3` tag, replacing unpinned master, which gave unrepeatable results on Apple's OpenCL). Both meson patchers learn v1.3's assigned `run_command` include probe. Published as the tested `deps-v1.15.0-rc1` bundles with only the version changed |
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ VapourBox is a front end to other people's work. Thanks in particular to:
- **dubhater** — VapourSynth ports of MVTools, nnedi3, AWarpSharp2, TemporalMedian, FluxSmooth, Bifrost and `adjust`
- **HolyWu** — VapourSynth ports of DFTTest, TTempSmooth, TCanny, CTMF, DCTFilter, Deblock, AddGrain, CAS, EEDI3 and NNEDI3CL
- **mawen1250** — Retinex, BM3D and mvsfunc
- **Edoardo Brunetti** — KNLMeansCL
- **Edoardo Brunetti** (maintained by Jaded Encoding Thaumaturgy) — KNLMeansCL
- **Frechdachs** (maintained by Jaded Encoding Thaumaturgy) — descale
- **vszip** by Julek — CLAHE, the local-contrast lift in Color Correction
- **RIFE** by hzwer (with Megvii) — the frame interpolation network, run through HolyWu's VapourSynth plugin (built on nihui's rife-ncnn-vulkan and Tencent's ncnn), with **MoltenVK** for Vulkan on macOS
Expand Down
63 changes: 52 additions & 11 deletions Scripts/download-deps-linux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -646,27 +646,62 @@ FAILED_PLUGINS=()
# which fails with our bundled Python. Replace with hardcoded include path.
patch_meson_vs_include() {
[ ! -f meson.build ] && return
python3 -c "
import re
with open('meson.build', 'r') as f:
VS_INCLUDE_DIR="$VS_INCLUDE_DIR" python3 - meson.build <<'PYEOF' 2>/dev/null || true
import os, re, sys
path = sys.argv[1]
inc = os.environ["VS_INCLUDE_DIR"]
with open(path) as f:
content = f.read()

def call_end(s, i):
"""Index just past the ')' closing the call whose '(' is at s[i],
skipping parentheses inside quoted strings."""
depth, q = 0, None
while i < len(s):
c = s[i]
if q:
if c == "\\":
i += 1
elif c == q:
q = None
elif c in "'\"":
q = c
elif c == "(":
depth += 1
elif c == ")":
depth -= 1
if depth == 0:
return i + 1
i += 1
return -1

# The probe assigned to a variable, used later as VAR.stdout().strip(), as
# KNLMeansCL v1.3 writes it: r = run_command(py, '-c', 'import vapoursynth ...').
for m in list(re.finditer(r"^[ \t]*(\w+)\s*=\s*run_command\s*\(", content, flags=re.M))[::-1]:
end = call_end(content, m.end() - 1)
if end < 0 or "import vapoursynth" not in content[m.start():end]:
continue
var = m.group(1)
content = content[:m.start()] + content[end:]
content = re.sub(r"\b%s\.stdout\(\)\.strip\(\)" % re.escape(var), repr(inc), content)

content = re.sub(
r'incdir\s*=\s*include_directories\s*\(\s*run_command\s*\(.*?\)\.stdout\(\)\.strip\(\)\s*,?\s*\)',
\"incdir = include_directories('$VS_INCLUDE_DIR')\",
"incdir = include_directories(%r)" % inc,
content,
flags=re.DOTALL
)
# The same probe nested among other include dirs, as BM3D r12 writes it:
# include_directories('include', run_command(py, '-c', 'import vapoursynth ...')).
# Only a vapoursynth include probe is replaced; other run_command calls stay.
content = re.sub(
r\"run_command\(\s*[^,()]+,\s*'-c',\s*'import vapoursynth[^']*'[^)]*\)\.stdout\(\)\.strip\(\)\",
\"'$VS_INCLUDE_DIR'\",
r"run_command\(\s*[^,()]+,\s*'-c',\s*'import vapoursynth[^']*'[^)]*\)\.stdout\(\)\.strip\(\)",
repr(inc),
content,
)
with open('meson.build', 'w') as f:
with open(path, 'w') as f:
f.write(content)
" 2>/dev/null || true
PYEOF
}

build_plugin() {
Expand Down Expand Up @@ -1068,11 +1103,17 @@ build_plugin "bm3d" \
"$PLUGIN_BUILD_ENV meson setup build --buildtype=release && ninja -C build" \
"$BM3D_TAG"

# KNLMeansCL (OpenCL denoiser)
# KNLMeansCL (OpenCL denoiser - Noise Reduction's KNLMeans method and QTGMC's
# "knlmeanscl" denoiser). Jaded-Encoding-Thaumaturgy's maintained fork, pinned:
# keep KNLM_TAG in step with download-deps-macos.sh and the wheel version in
# download-deps-windows.ps1 (v1.3 == wheel 1.3). Khanattila's master, built
# here before, had no tag to pin and races its buffer fills (issue #144).
KNLM_TAG="v1.3"
build_plugin "knlmeanscl" \
"https://github.com/Khanattila/KNLMeansCL.git" \
"https://github.com/Jaded-Encoding-Thaumaturgy/vapoursynth-knlmeanscl.git" \
"libknlmeanscl.so" \
"$PLUGIN_BUILD_ENV meson setup build --buildtype=release && ninja -C build"
"$PLUGIN_BUILD_ENV meson setup build --buildtype=release && ninja -C build" \
"$KNLM_TAG"

# TemporalMedian (core.tmedian.TemporalMedian - used by the SpotLess filter)
build_plugin "tmedian" \
Expand Down
109 changes: 82 additions & 27 deletions Scripts/download-deps-macos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,14 @@ VS_TAG="R78"
# build_plugin). Keep in step with download-deps-linux.sh and the wheel version
# in download-deps-windows.ps1 (r12 == wheel 12.0).
BM3D_TAG="r12"
# KNLMeansCL is Jaded-Encoding-Thaumaturgy's maintained fork, built from source
# on both arches. Keep in step with download-deps-linux.sh and the wheel version
# in download-deps-windows.ps1 (v1.3 == wheel 1.3). Not Khanattila's master:
# that fills its OpenCL buffers without waiting on the fill, which Apple's
# OpenCL does not complete in time - KNLMeans then denoises with a stale
# max-weight buffer, and some frames come out as garbage (issue #144).
KNLM_REPO="https://github.com/Jaded-Encoding-Thaumaturgy/vapoursynth-knlmeanscl.git"
KNLM_TAG="v1.3"
VS_BUILD_DIR="$BUILD_DIR/vapoursynth"
VS_INSTALL_DIR="$BUILD_DIR/vapoursynth-install"

Expand Down Expand Up @@ -813,6 +821,68 @@ cd "$BUILD_DIR"
BUILT_PLUGINS=()
FAILED_PLUGINS=()

# Some plugins' meson.build locates the VapourSynth headers via a run_command
# the clean build machine can't satisfy, in one of three forms:
# (a) `import vapoursynth as vs; print(vs.get_include())` in the host python
# (e.g. mvtools, bm3d, eedi3m) - the host python has no `vapoursynth`
# module (the runtime VS module is built against our embedded python).
# (b) `run_command('vapoursynth', 'get-include', ...)` - invokes the
# `vapoursynth` console script, which isn't on PATH here (e.g. CAS, which
# upstream switched to this form). (a) and (b) end in `.stdout().strip()`.
# (c) either probe assigned to a variable whose `.stdout().strip()` is used
# later (KNLMeansCL v1.3: `r = run_command(py, '-c', '...')`).
# Replace the probe with the from-source VS include dir (same trick used for
# vivtc below). Match the program form too, not just "import vapoursynth".
# A no-op when the file has no such probe or VS_INC_DIR is not known yet.
# Always returns 0, so it can sit in an && chain.
patch_meson_vs_include() {
local file="${1:-meson.build}"
[ -n "$VS_INC_DIR" ] && [ -f "$file" ] || return 0
grep -qE "import vapoursynth|run_command\('vapoursynth'" "$file" || return 0
VS_INC_DIR="$VS_INC_DIR" python3 - "$file" <<'PYEOF'
import os, re, sys
path = sys.argv[1]
inc = os.environ["VS_INC_DIR"]
s = open(path).read()

def call_end(s, i):
"""Index just past the ')' closing the call whose '(' is at s[i],
skipping parentheses inside quoted strings."""
depth, q = 0, None
while i < len(s):
c = s[i]
if q:
if c == "\\":
i += 1
elif c == q:
q = None
elif c in "'\"":
q = c
elif c == "(":
depth += 1
elif c == ")":
depth -= 1
if depth == 0:
return i + 1
i += 1
return -1

# (c) first: VAR = run_command(...vapoursynth...), then VAR.stdout().strip().
for m in list(re.finditer(r"^[ \t]*(\w+)\s*=\s*run_command\s*\(", s, flags=re.M))[::-1]:
end = call_end(s, m.end() - 1)
if end < 0 or "vapoursynth" not in s[m.start():end]:
continue
var = m.group(1)
s = s[:m.start()] + s[end:]
s = re.sub(r"\b%s\.stdout\(\)\.strip\(\)" % re.escape(var), repr(inc), s)

# (a)/(b): run_command(...).stdout().strip() inline.
s = re.sub(r"run_command\(.*?\)\.stdout\(\)\.strip\(\)", repr(inc), s, flags=re.S)
open(path, "w").write(s)
PYEOF
return 0
}

build_plugin() {
local name="$1"
local repo="$2"
Expand All @@ -837,27 +907,9 @@ build_plugin() {

cd "$name"

# Some plugins' meson.build locates the VapourSynth headers via a run_command
# the clean build machine can't satisfy, in one of two forms:
# (a) `import vapoursynth as vs; print(vs.get_include())` in the host python
# (e.g. mvtools, bm3d, eedi3m) - the host python has no `vapoursynth`
# module (the runtime VS module is built against our embedded python).
# (b) `run_command('vapoursynth', 'get-include', ...)` - invokes the
# `vapoursynth` console script, which isn't on PATH here (e.g. CAS, which
# upstream switched to this form). Both end in `.stdout().strip()`.
# Replace either probe with the from-source VS include dir (same trick used
# for vivtc below). Match the program form too, not just "import vapoursynth".
if [ -n "$VS_INC_DIR" ] && [ -f meson.build ] \
&& grep -qE "import vapoursynth|run_command\('vapoursynth'" meson.build; then
VS_INC_DIR="$VS_INC_DIR" python3 - meson.build <<'PYEOF'
import os, re, sys
path = sys.argv[1]
inc = os.environ["VS_INC_DIR"]
s = open(path).read()
s = re.sub(r"run_command\(.*?\)\.stdout\(\)\.strip\(\)", repr(inc), s, flags=re.S)
open(path, "w").write(s)
PYEOF
fi
# Point any VapourSynth include probe in meson.build at our own headers
# (see patch_meson_vs_include above).
patch_meson_vs_include meson.build

# Expose the from-source VapourSynth to pkg-config so plugins that use
# `dependency('vapoursynth')` (removegrain, cas, ...) resolve against our
Expand Down Expand Up @@ -1217,8 +1269,9 @@ PYEOF
cd "$BUILD_DIR"
fi

# KNLMeansCL (OpenCL denoiser - core.knlm.KNLMeansCL). Used by QTGMC when its
# Denoiser is set to "knlmeanscl" (havsfunc). Stefan-Olt doesn't ship an
# KNLMeansCL (OpenCL denoiser - core.knlm.KNLMeansCL), pinned by KNLM_TAG.
# Used by Noise Reduction's KNLMeans method and by QTGMC when its Denoiser
# is set to "knlmeanscl" (havsfunc). Stefan-Olt doesn't ship an
# x86_64 build, so build from source like the arm64 path does. Its meson.build
# needs BOTH the VapourSynth headers (via pkg-config) AND Boost
# filesystem/system (via Homebrew, same as the nnedi3cl build), so set
Expand All @@ -1228,7 +1281,8 @@ PYEOF
if [ "$FORCE" = true ] || [ ! -f "$PLUGINS_DIR/libknlmeanscl.dylib" ]; then
echo ""; echo "=== Building KNLMeansCL (x86_64) ==="
rm -rf knlmeanscl
if git clone --depth 1 https://github.com/Khanattila/KNLMeansCL.git knlmeanscl \
if git clone --depth 1 --branch "$KNLM_TAG" "$KNLM_REPO" knlmeanscl \
&& patch_meson_vs_include knlmeanscl/meson.build \
&& PKG_CONFIG_PATH="$VS_PC_DIR:${PKG_CONFIG_PATH:-}" BOOST_ROOT="$SRCLIB" \
meson setup knlmeanscl/build knlmeanscl --buildtype=release \
&& ninja -C knlmeanscl/build; then
Expand Down Expand Up @@ -1463,11 +1517,12 @@ build_plugin "bm3d" \
"meson setup build --buildtype=release && ninja -C build" \
"$BM3D_TAG"

# KNLMeansCL (OpenCL denoiser)
# KNLMeansCL (OpenCL denoiser), pinned: see KNLM_TAG
build_plugin "knlmeanscl" \
"https://github.com/Khanattila/KNLMeansCL.git" \
"$KNLM_REPO" \
"libknlmeanscl.dylib" \
"meson setup build --buildtype=release && ninja -C build"
"meson setup build --buildtype=release && ninja -C build" \
"$KNLM_TAG"

# DeScratch (vertical scratch removal - core.descratch.DeScratch)
# Built from source: not available pre-built from Stefan-Olt. The repo carries
Expand Down
22 changes: 11 additions & 11 deletions Scripts/download-deps-windows.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -417,16 +417,8 @@ $PluginsZip = @(
Name = "fmtconv"
Url = "https://ldesoras.fr/src/vs/fmtconv-r31.zip"
Check = "fmtconv.dll"
},
@{
# core.knlm.KNLMeansCL - OpenCL denoiser used by QTGMC when its Denoiser
# is set to "knlmeanscl". The release zip ships a 32-bit DLL at the root
# AND x64\KNLMeansCL.dll; the $Win64 filter below picks the x64 one (and
# the PE-arch verifier rejects the 32-bit one if it ever slipped through).
Name = "knlmeanscl"
Url = "https://github.com/Khanattila/KNLMeansCL/releases/download/v1.1.1/KNLMeansCL-v1.1.1.zip"
Check = "KNLMeansCL.dll"
}
# KNLMeansCL is NOT here: it is a PyPI wheel, see the wheel section below.
# zsmooth is NOT here: which build ships depends on the CPU tier. See
# section 4a below.
)
Expand Down Expand Up @@ -774,7 +766,7 @@ if (-not (Test-Path "$PluginsDir\libakarin.dll")) {
}

# =============================================================================
# Bwdif + DeDot + vszip + BM3D + descale - PyPI wheels
# Bwdif + DeDot + vszip + BM3D + descale + KNLMeansCL - PyPI wheels
# =============================================================================
# None of these upstreams publishes a Windows release asset, so each comes from
# its PyPI wheel, resolved through the JSON API the same way akarin is (never
Expand Down Expand Up @@ -808,7 +800,15 @@ $WheelPlugins = @(
@{ Name = "bm3d"; Package = "vapoursynth-bm3d"; Version = "12.0"; Dll = "bm3d.dll" },
# core.descale (the Descale pass). Its AVX2 kernel is chosen at run time, so
# one DLL serves both tiers. Imports VCRUNTIME140, which Python ships.
@{ Name = "descale"; Package = "vapoursynth-descale"; Version = "12"; Dll = "libdescale.dll" }
@{ Name = "descale"; Package = "vapoursynth-descale"; Version = "12"; Dll = "libdescale.dll" },
# core.knlm.KNLMeansCL (Noise Reduction's KNLMeans method, and QTGMC's
# "knlmeanscl" denoiser). Jaded-Encoding-Thaumaturgy's maintained fork:
# wheel 1.3 is git tag v1.3, as built from source by
# download-deps-{macos,linux}.sh. It replaces Khanattila's v1.1.1 release
# zip (2018), which refuses every 9-15-bit clip except packed YUV444P10 /
# RGB30 - so any 10-bit 4:2:0 source failed (issue #144). OpenCL is linked
# statically; the DLL imports only Windows system libraries.
@{ Name = "knlmeanscl"; Package = "vapoursynth-knlmeanscl"; Version = "1.3"; Dll = "KNLMeansCL.dll" }
)

foreach ($W in $WheelPlugins) {
Expand Down
6 changes: 3 additions & 3 deletions app/assets/deps-version.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"version": "1.14.0",
"releaseTag": "deps-v1.14.0",
"releaseDate": "2026-10-25",
"version": "1.15.0",
"releaseTag": "deps-v1.15.0",
"releaseDate": "2026-11-03",
"tierFormat": "delta",
"githubRepo": "StuartCameronCode/VapourBox"
}
6 changes: 4 additions & 2 deletions app/lib/views/about_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -396,8 +396,10 @@ class _AboutDialogState extends State<AboutDialog> {
_ComponentTile(
name: 'KNLMeansCL',
license: 'GPL 3.0',
copyright: 'Edoardo Brunetti',
url: 'https://github.com/Khanattila/KNLMeansCL',
copyright:
'Edoardo Brunetti; maintained by Jaded Encoding Thaumaturgy',
url:
'https://github.com/Jaded-Encoding-Thaumaturgy/vapoursynth-knlmeanscl',
),
_ComponentTile(
name: 'DeScratch',
Expand Down
Loading
Loading