Experimental GPU Support

There’s Vulkan 1.4 based GPU support available to test in the vukvuk2 branch, just install the most recent artifact. It’s feature and API complete and can be used for testing. Changes to the API and how it works will only be made if someone has high quality feedback.

All internal core filters now also have GPU support and apart from a few of the most esoteric resize transfer options there is complete feature parity in every filter with one exception, GPU filters don’t accept variable format/size input. Also filters that accept multiple nodes require all nodes to be either local or GPU, no mixing. If you don’t use any GPU filters these builds will work more or less exactly like the latest R79 release.

General structure for users

Basically the available types have been extended with GPU video nodes and GPU video frames. These are more or less identical to the good old video frames except that they’re stored on the GPU. GPU nodes are simply nodes that output GPU frames.

To transfer between local and GPU type for nodes you simply do:

clip.std.GPUUpload()
clip.std.GPUDownload()

Uploading or downloading a clip already in the desired location does nothing. Note that implicit transfers are inserted when needed, for example if you do something like:

clip = core.bs.VideoSource("filename", gpu=False)
clip = clip.nnedi3vk.NNEDI3()
clip = clip.vivtc.VFM(order=1)

This will work but due to the implicit transfers before nnedi3vk and VFM you’ll get two warnings. It’s also possible to set call set_output() on GPU nodes, they’ll simply get a GPUDownload() inserted implicitly and work like you expect.

By default the best GPU is selected. Best means that discrete devices are picked first and if multiple are found the amount of VRAM is the tie breaker. If you don’t like this you can enumerate all available using core.vulkan_devices() and specify which one to use with core.set_vulkan_device(device index) BEFORE running any GPU filters.

Resource management and performance quirks

In addition to the normal max_cache_size() there is now a max_vram_cache_size() that works much in the same way. There are however a few important quirks to mention, since there’s no swap for GPU memory you literally crash when it runs out. VapourSynth internally retries allocations a few times with delays to reduce the chance of it happening but you will need more conservative numbers here. The default is ~2/3 of discrete GPU VRAM.

Note how I said discrete GPU VRAM. Integrated GPUs with unified memory use much more conservative numbers since the two pools effectively compete for the same RAM. It’s also important to mention that most internal filters run faster on the CPU than on an integrated GPU due to this, the CPU algorithms have much better memory access patterns in general and you otherwise end up memory bandwidth limited very fast.

The internal frame structure

The model chosen is somewhat inspired by FFmpeg’s Vulkan GPU filter framework but has many notable differences. Frames are stored in vkBuffer objects with exactly the same memory layout as a normal frame including the stride and other padding. One vkBuffer per plane, individually reference counted and possible to pass through. Everything works and looks like a normal VSFrame with one exception, getWritePtr() doesn’t have the rarely used copy-on-write semantics, instead you have to explicitly make a copy yourself in a new frame before modifying frame data due to how the GPU pipeline works. For those of you wondering FFmpeg instead uses vkImage as the underlying frame type.

Writing filters and Vulkan usage

Generally it can be said that 3 levels of abstraction are provided. Plain Vulkan (NOT RECOMMENDED), the VSGPUExecPool and other machinery provided in VSVULKANAPI and finally the gpudriver.h header.

Many filters can use gpudriver.h, for example all core filters except resize use this so you can simply look up how the most similar filter was implemented in the core and base your code off that. You can find the invert sample here. As you can see it’s mostly about declaring inputs, output and the shaders. To see more clearly which helper macros (STORE, SRCN and so on) are predefined look up how vsgpu::SimpleFilter::prelude is set. This header will probably be promoted to public at some point with minor changes but for now everyone’s allowed to freely copy and relicense it.

The second also recommended way is to use the VSGPUExecPool object and its related functions. An example of this can be found in the normal GPU invert sample here. Basically it’s normal Vulkan with some additional helpers so the core can keep track of resources. Note that the GPUExecPool is an important part of the memory management machinery. You can still write mostly raw Vulkan API plugins however then the core can’t account for its resource usage and hence is why it’s not recommended even if technically possible.

CUDA users and other feature/extension quirks

There are many versions of the invert sample, including one for CUDA. Basically you import all the input and output frame memory from Vulkan using the export handles. It looks quite simple and I have no idea how CUDA works at all so if you need help go ask AI or something.

We also have another case where the core’s vkDevice isn’t created with the features and extensions required by a plugin. Or for codebases that really prefer to keep their own vkDevice internally. One example of both is BestSource when using FFmpeg’s Vulkan decoding. When using hardware decoding each available decoder needs its own extensions enabled in addition to several general hardware decoding ones. Due to how Vulkan works you can only specify the enabled features/extensions when a vkDevice is created. You can however have multiple vkDevices on the same physical GPU to allow fast memory copies/sharing between them. They’re also not extensions that are generally useful to video processing filters like atomic floats and such so in these cases you just have to have your own vkDevice.

Over time I plan to enable more generally useful extensions when available and do an API version bump to indicate it.

MVUtensils – Faster and cleaner MVTools

How much faster you ask? For a 16 bit Degrain3 script it’s 95% FASTER ON A 9800X3D!

It was done by cleaning up the code structure, using better algorithms, using new CPU instructions, optimizing everything and fixing really stupid bugs.

Go RTFM on GitHub or install it from PyPi (vapoursynth-mvutensils)

Stupid bugs

Let’s start with the most stupid bug of them all: The motion estimation code in Analyse generates 4 predictors, guesses on what the motion vector for the current block should be. These come from motion estimation at higher levels, neighboring blocks that already have known motion vectors and some other similar guesswork. Very often these are identical, especially in low motion video, where all 4 predictions can be (0,0). Instead of pruning the obvious duplicates the code would still test all 4 identical values and reach the same result. No longer checking the same motion vector over and over again gave an instant 7% speedup.

Did I say it generates 4 predictors? That’s not quite true, the code generated 5 but the 5th one was never used and now never will be.

Better algorithms

A common anti-pattern in the MVTools codebase is to allocate huge temporary buffers. Usually the size of a whole frame. Sometimes twice the size. In some cases 6 buffers the size of a frame. Apart from RAM being very expensive nowadays this has another major downside, the working set doesn’t fit in the CPU cache and as such you become memory bandwidth limited very quickly. What has been done in every filter is to instead process parts of the frame in tiles/chunks to greatly improve cache performance.

In the original MVTools most of the Flow* filters also need to reassemble the super clip into one large frame (the Super output doesn’t store the pel image as a single image, instead it’s stored as 2×2 or 4×4 images with the different offsets) which would take time and for pel=4 require a huge amount of memory. Now they all directly use the Super clip.

A lot of the C code was also modified to allow compilers to auto-vectorize it or generate better code in general.

New CPU instructions

New CPUs have AVX512 and many of its additions. These instructions can help quite a bit. For example the SAD16 code was sped up by 50% extra by using the VNNI instructions. I don’t believe anyone else has used this trick yet. And that was after the first speedup of 50% after using the applicable techniques from x264’s code. Also note that x264 has a much easier SAD calculation problem since not all 16 bits are actually used. Just like x265.

Optimizing everything

Avisynth-descended code usually shows another anti-pattern: The simple functions have intrinsics/assembler written for them while the more complicated ones don’t. This generally means that things the compiler could (sometimes with a small nudge) auto vectorize better than poorly thought out intrinsics become slower. And the really complicated functions where the compiler runs out of good ideas go untouched. Not here. Some functions like FlowBlur require AVX512 to make the optimization dream happen but every filter is optimized. It may take a week or two to think of a sane algorithm for a variable fetch memory gather bound filter but it’s worth it. I mean QTGMC uses it in like one place. Must be important.

R76 – Respecting Cache Limits

R76 completely reworks how the cache limits work and actually much more strictly adheres to them if it all possible. This means that scripts that previously were running just fine may slow down due to memory constraints. For example even with the default limit of 4GB scripts like mc_degrain could actually use 16GB+ of memory when processing 4k material. That doesn’t sound so bad until the Linux out of memory killer strikes and many scripts can’t complete at all.

This version improves things by limiting the number of concurrent threads running at the same time when memory is constrained. For example in the case of MVTools (super-analyze-degrain3) with 4k 10bit material an additional thread can mean approximately 500MB more memory is required. Previously all limiting would fail when there were no more frames stored in the caches and only the working set of the running filters remained. When this state is reached the number of running threads is reduced. Note that if you have a 16 core CPU that can run 32 hardware threads at once decreasing the number down to 8-16 actually running threads usually has very little real effect since consumer CPUs will dual channel ram most of the time will be memory bandwidth limited anyway.

However if you see it go down to 1-4 threads from 32 you should REALLY increase the maximum cache size.

Two serious bugs were also fixed, one caused corrupt output from the “generic filters” (maxium, minimum, 3×3 convolution and so on) in some compiles in the avx2 code path. The other bug is much more specific and could cause memory leaks if an API4 filter requested frames from a node not properly declared as a dependency. If that sounds very specific it’s just what MVTools v25&v26 did.

R75 – Sanding off the R74 Edges and Plugin Manifests

Note that this post has been amended for the R78 release that introduced the avx512 level and an additional zen4 level compiler options.

R74 had some bugs and issues related to the new packaging and now the worst is fixed. But that’s not really all that interesting. Instead let’s talk about what manifests and plugin loading changes in R75 can do for you. R74 already introduced recursive loading of all plugins in a directory. This meant that a multi-file plugin like znedi3 could have both its DLL and its data file as a neat group in a separate directory since they belong together.

R75 added manifests, these files are useful if your plugin consist of several support libraries (DLLs) because then VapourSynth can skip wasting time loading the unrelated DLLs. When a manifest file is encountered in a subdirectory only the filenames listed will be loaded and and everything else skipped.

[VapourSynth Manifest V1]
bestsource

The platform specific library ending (.dll/.so/.dylib) is appended to the listed filenames. Multiple filenames with one on each line is allowed. It’s also possible to use relative paths as long as they point to subdirectories. The file must be named manifest.vs.

The other new feature is that a plugin now can have multiple versions compiled for different CPU instruction set levels and automatically load the optimal one for the current system. This is automatically done on autoloading when manifests are present or when explicitly calling LoadPlugin on the base name (such as base.dll) of a plugin. Only the base version has to exist and if any level is missing it will try the other ones in order.

The levels chosen are:

LevelGCC/ClangMSVCFilename
Plain x64 (x86_64_v1)base.dll
AVX2 level (Intel Haswell)-march=x86-64-v3/arch:AVX2base.avx2.dll
AVX512 level (Intel Skylake)-march=x86-64-v4/arch:AVX512base.avx512.dll
Zen4 level without AMD only instructions and no bf16-march=znver4 -mno-sse4a -mno-avx512bf16No equivalentbase.zn4.dll

If you’re familiar with the defined x86_64 “levels” you may wonder where v2 and v4 went. The short answer is that v2 is mostly pointless (SSE4.2-ish) due to AVX2 (v3) CPUs being so widely available. Intel’s Haswell CPUs were released in 2013.

For v4 the answer is a lot more convoluted because Intel had no plan and no idea about what they were doing. Basically it’s the instruction set of an Intel Skylake CPU with AVX512 enabled. The first consumer CPU with AVX512. And also almost last. Many of the following CPUs from Intel had AVX512 disabled and only enabled for sever parts which nobody actually has at home. This combined with crippling downclocking issues when AVX512 instructions were used means that ironically when you tell most modern compilers to make code for Skylake CPUs (-march=skylake or -march=x86_64_v4) they won’t actually use the wider registers and only use the same register width as AVX2 code. That’s how bad early AVX512 was on consumer CPUs.

Over time more and more AVX512 instructions have also been added that are quite useful. However Intel CPUs would add one new group of instructions and remove another. Meaning that there’s no clear path forward and code compiled for these more modern Intel CPUs wouldn’t necessarily work with later ones. A complete mess. It wasn’t until AMD introduced reasonably priced AVX512 support for consumers with Zen4 things started to fall into place for normal people. At this point, 7 years after the original Skylake, many additional AVX512 instructions had been added. Not only that, Zen5 is a proper superset so it’s fully compatible with Zen4. And so will Zen6 also be. Meaning that from now there’s a consistent target with full width AVX512 used that NORMAL PEOPLE CAN ACTUALLY BUY! This also fits very well with Intel’s coming CPUs with AVX10.2 support that adds very few new instructions of value.

New Packaging and Install Methods in R74

Everything has changed! Your old installations will break! Encodings will fail!

But in the end you’ll be able to install VapourSynth EVERYWHERE by typing pip install vapoursynth followed by vapoursynth config.

After that there are a few more optional commands you can find detailed in the updated installation instructions.

No more installers needed. Things will be neatly separated in every Python virtual environment. Everything will just work. Apart from migrating from your old installation. Plugins are now stored in a a subdirectory of the VapourSynth Python package. To get the new location run vapoursynth.get_plugin_dir() in Python. This will in the future allow all plugins and scripts to be installed through pip instead of VSRepo which over time will be phased out. NO PLUGINS WILL BE LOADED FROM THE PREVIOUS LOCATIONS!

Other Important Things

If you want to use VSRepo you now have to install it separately using pip (yes, pip install vsrepo is now possible). Likewise AVFS has been split off into its own repository and is now a separate download.

There still is a normal windows installer and the portable version install script if you prefer to still use them. Do however note that simply “upgrading” from R73 or earlier probably will break a lot of things due to the changes.

Why this didn’t happen earlier

This change has been requested for many years. Almost from the start ever since the Python module was packaged into a proper wheel. Due to a lot of factors this wasn’t even technically possible until recently. Or at least not without an insane testing workload.

The main improvement to Python that made this possible was the limited API which allows binary wheels to work on multiple Python versions without needing to compile a separate version for every release. This reduces the number of packages from over a gazillion to about 5. The limited API did however not have enough features until around Python 3.11-3.12 to accomplish this since VapourSynth uses memory views and such in its bindings.

And even if the pre-requisites are there you still have to figure out how to do it. VapourSynth is split into two main components, the core library written in C++ and VSScript, a library that embeds Python and abstracts script evaluation which is very hard to get right. If you’re paying attention you’ve now realized that VapourSynth is a library used by a Python module that is used by another library (VSScript) that embeds Python and is then in turn loaded by vspipe.

This is an almost circular dependency. Probably nothing else in existence does this. There are no Python tutorials covering this. Embedding Python is in and of itself something that feels more like an afterthought than something intended and creates many problems. Or to put it simply: it took this long to figure it out after Python 3.12 was released.

R73 – The Last Windows 7 Release

The time has finally come. I’ve been busy the past few months so all you get are a few bug fixes in another maintenance release. Also Visual Studio 2026 was released and its toolchain only support Windows 10 and newer. Likewise Cython is planning to soon drop support for Python 3.8 which is the last Python version to support Windows 7. Or to put it simply: Windows 7 is now getting much harder to support. The number of Windows 7 users is also much smaller now and just like the removal of 32 bit x86 builds this streamlines the build process quite a bit.

If anyone decides to produce legacy compiles tell me and I’ll link them but I suspect the market for those are very limited. I think that’s it. The upside is that clang-cl probably can be used to compile the next release and make it a bit faster.

Website Migration and Changes

I was forced to move the domain and change hosting provider for the first time in 13 years due to the previous providers having been bought by Newfold Digital, a company that turns everything it buys into overpriced shit. As a result of this I’ve also implemented a few changes:

  • Comments have been disabled here and all old comments have been wiped
  • I’ve enabled discussions on Github where comments and discussions can be posted instead

You can also check the Help and Chat page for IRC, Discord and forum links.

R72 – Named Pipes and Python 3.12+ Support on Windows

Many poorly coded but popular Python modules like to write lots of junk directly to stdout. This is a big problem when piping from vspipe to another application and will break everything. On “not Windows” it was possible to use named pipes to get around this however on Windows you were stuck until now. To create a named pipe for output simply do vspipe script.vpy "\\.\pipe\<your pipe name>" and use the same “filename” as input for FFmpeg or recent x265 builds. A patch has also been submitted to x264 but hasn’t been accepted yet.

As mentioned in the previous posts every Python version starting with 3.12 is now supported in the Windows installer. This of course also includes Python 3.13 and 3.14 and will help people using poorly coded but popular Python modules that are slow to get support for for new Python versions.

Supporting All Recent Python Versions!

There’s now an experimental version of VapourSynth that support Python 3.12 and later. This means 3.13 and the 3.14 pre-release as well. Windows binaries can be found here. It’s safe to use and is apart from the better Python support identical to the R71 release.

If no major issues are found this will become the standard distribution method for the windows installer in future releases starting with R72.