Same as std.Expr()
, but takes C++ functions instead of MaskTools RPN expressions.
The following examples yield the same results as
clip = core.std.Expr((clip_a, clip_b), 'x y - abs 4 > x y * x x * ?')
assuming both sources are 8-bit.
Inline mode is the same as with Expr.
user_func = '''
#include <cmath>
uint16_t func(uint8_t x, uint8_t y)
{
if (std::abs(x - y) > 4) {
return x * y;
} else {
return x * x;
}
}
'''
clip = core.expr.expr_cpp((clip_a, clip_b), (user_func,))
Separate source mode allows to place all C++ code in a separate file, picking functions by their names.
user.cpp
:
#include <cmath>
uint16_t func(uint8_t x, uint8_t y)
{
if (std::abs(x - y) > 4) {
return x * y;
} else {
return x * x;
}
}
Vapoursynth script:
clip = core.expr.expr_cpp((b1, b2), ('func',), source_path='/path/to/user.cpp')
Currently ExprCpp mimics Expr user interface to the extent possible:
clips: Sequence[VideoNode]
— input clips.code: Sequence[str]
— user code (inline mode) or user function name (separate source mode) for each corresponding plane. As with Expr, empty string means copying, and no string at all means using the last one. See details in the next section.format: Optional[VSFormat]
— output format. Defaults to the first input clip's format.source_path: Optional[str]
— path to file with user code. Enabled separate source mode. Debug options:cxxflags: Optional[Sequence[str]]
— override optional flags supplied to compiler. Can be an empty sequence. Defaults are("-std=C++17", "-O3", "-march=native")
. They're parsed byclang++
driver even on Windows, socl
flags won't work.dump_path: Optional[str]
— folder to place dumps to. Default to currend working directory.dump_source: bool = false
— dump full source that goes to JIT.dump_bitcode: bool = false
— dump LLVM IR bitcode outputted by Clang frontend. Usellvm-dis
to get readable LLVM IR source.dump_binary: bool = false
— dump native binary outputted by backend. Useobjdump -d
orllvm-objdump -d
to get readable assembly.
Requirements:
- Valid C++ code.
- (for inline mode) Exactly one function (or overload set) in global namespace.
- With signature compatible (in terms of C++) with input clips and output format.
Name doesn't matter for the purpose of evaluation. It's used only for naming dumps, if they're requested. Every piece of code is separated from others in runtime.
Restrictions:
- Namespaces
exprcpp
andexpr
are reserved. Don't even mention them. - Half-precision floating-point (FP16) is not supported (yet).
- Operator
new
is not supported (yet), as well as anyting that depends on it, including much of STL. But you can still get away with it ifnew
will be optimized away. - Exceptions are currently not expected to work.
Things to keep in mind:
- Unsigned types wrap on overflow and underflow. For your generic 8-bit source that uses
uint8_t
underneath it means thatuint8_t{-1} == 255
anduint8_t{256} == 0
, for instance. - If output type in integer and you want your return values to be clamped (saturated), beware returning the type that exactly matches output type — return a wider type instead (note
uint16_t
in example in spite of 8-bit source). - Arithmetic in C++ has many other sharp corners, especially when it comes to floating-point. Stack Overflow and en.cppreference.com help a lot.
- Unlike Expr, ExprCpp doesn't require input clips to have constant dimensions, but it still requires dimensions of input clips to match on every processed frame.
Usage tips:
- You don't have to include
<cstdint>
to get access touint*_t
types — it's included by default. - (for inline mode) If you need to split your processing into multiple functions, place additional ones into namespace. Common choices are
details
,impl
or even unnamed, but stay out of reserved namespaces. - Use overloading or templates to generalize your functions for different outputs.
Tips to boost performance:
- Measure. Intuition is among your worst enemies.
- Avoid conversions. Take inputs using clip format's native type, mind your return type (also see (2) above).
- Don't be clever. The better optimizer "understands" your code, the better output it can produce. A bit of testing I've done using the example suggests that ExprCpp can be on par with Expr from performance standpoint.
- Compiler with C++17 support
- CMake 3.16.3+
- LLVM 10+ (
llvm-10-dev
) - Clang 10+ (
libclang-cpp10-dev
)
mkdir build
cd build
cmake ..
cmake --build .
Windows distibution of LLVM doesn't expose C++ API nor as dynamic library neither as static one, so it's required to build it from source, and to let ExprCpp to statically link with it. ExprCpp supports building as an LLVM external project to achieve this:
- Clone ExprCpp repo
- Clone LLVM monorepo
- Open Visual Studio Developer Console x64 and navigate to LLVM monorepo:
mkdir build
cd build
cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_PROJECTS=clang -DLLVM_EXTERNAL_PROJECTS=ExprCpp -DLLVM_EXTERNAL_EXPRCPP_SOURCE_DIR="path\to\exprcpp\root" -DCMAKE_CXX_STANDARD=17 -DLLVM_ENABLE_RTTI=ON -DLLVM_ENABLE_EH=ON ../llvm
ninja exprcpp
specifying path to ExprCpp root directory.
Notes on CMake arguments:
- It doesn't have to be Ninja, but I haven't tested other build systems.
- Omit
-DCMAKE_BUILD_TYPE=Release
for debug build (or specifyDebug
explicitly). - Add
-DLLVM_ENABLE_LLD=ON
to enablelld
linker. Highly recommend it for debug builds. - Add
-DLLVM_ENABLE_LTO=Full
to enable link-time optimization. - Add
-DLLVM_TARGETS_TO_BUILD=X86
to limit range of LLVM targets to just x86 (includes x86_64).
- Migrate to C++20.
- FP16 support.
- STL support.
- Look into GPU acceleration.
- Consider using
vsxx
. - Consider moving to
LLVM-C
andlibClang
.
Thank you for getting this far. User feedback is what I'm always lacking, so feel free to join this Telegram chat to contact me on anything. Feedback could also make future plans become a reality sooner.