diff --git a/CLAUDE.md b/CLAUDE.md index c99bddb..03721cb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 @@ -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 @@ -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 | diff --git a/README.md b/README.md index 0e3903e..69da48e 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/Scripts/download-deps-linux.sh b/Scripts/download-deps-linux.sh index 87ec312..2ee017f 100755 --- a/Scripts/download-deps-linux.sh +++ b/Scripts/download-deps-linux.sh @@ -646,13 +646,48 @@ 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 ) @@ -660,13 +695,13 @@ content = re.sub( # 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() { @@ -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" \ diff --git a/Scripts/download-deps-macos.sh b/Scripts/download-deps-macos.sh index 90b40a6..36e2536 100755 --- a/Scripts/download-deps-macos.sh +++ b/Scripts/download-deps-macos.sh @@ -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" @@ -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" @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/Scripts/download-deps-windows.ps1 b/Scripts/download-deps-windows.ps1 index a6567c6..3af20d1 100644 --- a/Scripts/download-deps-windows.ps1 +++ b/Scripts/download-deps-windows.ps1 @@ -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. ) @@ -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 @@ -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) { diff --git a/app/assets/deps-version.json b/app/assets/deps-version.json index 71f33b6..6537b71 100644 --- a/app/assets/deps-version.json +++ b/app/assets/deps-version.json @@ -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" } diff --git a/app/lib/views/about_dialog.dart b/app/lib/views/about_dialog.dart index 28fef5f..7079627 100644 --- a/app/lib/views/about_dialog.dart +++ b/app/lib/views/about_dialog.dart @@ -396,8 +396,10 @@ class _AboutDialogState extends State { _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', diff --git a/app/macos/Runner/Info.plist b/app/macos/Runner/Info.plist index 0a56c98..b19806b 100644 --- a/app/macos/Runner/Info.plist +++ b/app/macos/Runner/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.5.1 + 1.6.0 CFBundleVersion - 1.5.1 + 1.6.0