Thor is a perceptually-tuned video codec authored by Cisco, originally
developed as a candidate technology for the IETF NETVC working group. The
repository ships a reference encoder (Thorenc) and reference decoder
(Thordec) written in portable C99, plus a curated set of configuration
files that mirror the operating points described in the Thor Internet-Draft.
Thor is a hybrid block-based codec in the same lineage as H.264/AVC, HEVC,
and AV1: it builds a prediction (intra or inter), transforms and quantises
the residual, and entropy-codes the result, with deblocking, CDEF, and CLPF
as in-loop post-processing options.
This guide is written for video-codec researchers and engineers who want to encode and decode raw YUV (or Y4M) sequences with Thor, run rate-distortion comparisons against other codecs, and reproduce the operating points from Cisco's reference configurations. It documents every command-line flag the encoder and decoder accept, every supported colour format and bit depth, and the canonical directory of preset configuration files. It is paired with the Thor Codec Administrator & Developer Guide, which covers architecture, bitstream syntax, security posture, and developer internals.
1. Quick Start
You need a POSIX-style shell, make, a C99 compiler (gcc or clang), and
a short raw YUV input clip. Thor's reference build produces two binaries in
the build/ directory: Thorenc and Thordec.
git clone https://github.com/cisco/thor.git
cd thor
make -j8
./build/Thorenc -cf config_LDB_low_complexity.txt -if input_1280x720_30.yuv \
-of stream.bit -rf recon.yuv -width 1280 -height 720 -f 30 -qp 32 -n 30
./build/Thordec stream.bit decoded.yuv
The first call encodes 30 frames of input_1280x720_30.yuv with the
Low-Delay B (LDB) low-complexity preset at QP 32, writing the compressed
bitstream to stream.bit and the encoder's reconstructed YUV to
recon.yuv. The second call decodes the bitstream and writes raw YUV to
decoded.yuv.
Tip
If your input is a Y4M file, Thor reads the width, height, frame rate, chroma subsampling, and bit depth out of the Y4M header automatically, so the
-width,-height, and-fflags become optional. Likewise, naming the decoder output with a.y4mextension makesThordecemit a Y4M stream instead of headerless raw YUV — handy for pipelines that already expect Y4M.
What you get out of Thor
- A self-contained C99 encoder and decoder, no external runtime dependencies
beyond
libm. - Thirty ready-to-use configuration presets covering Random Access
(
RA*), Low-Delay B (LDB*), and high-bit-depth (HDB*,HDB16*) operating points, each inlow_complexity,medium_complexity, andhigh_efficiencyflavours, with optional_qm_(quantisation matrix) variants. - Optional Constrained Directional Enhancement Filter (CDEF) and Constrained Low-Pass Filter (CLPF) loop post-processing.
- Frame-level rate control with target bitrate, plus per-frame and per-superblock QP adaptation.
- Built-in PSNR Y/U/V reporting after every encoded frame and a per-block, per-mode, per-size statistics dump after every decoded sequence.
- A
check.shround-trip harness driven bymake checkthat runs an encode-decode-cmp loop on randomly generated YUV files (optionally under Valgrind) — perfect for catching regressions when modifying the codec.
Verify your build in 60 seconds
make check test-config=config_LDB_low_complexity.txt test-frames-count=2 test-valgrind=0
1920 1080 30 ./rnd_test_tmp_1920x1080_30.yuv 2
SH: 19 bits
0 I 30 162488 34.3416 39.0421 39.7245 ...
1 P 38 11425 33.6512 38.7104 39.4118 ...
1280 720 30 ...
640 480 30 ...
640 360 30 ...
check.sh generates four randomly-filled YUV files at 1920x1080,
1280x720, 640x480, and 640x360, encodes each one, decodes the resulting
bitstream, and byte-compares the decoder output against the encoder's
reconstructed buffer. Any mismatch — even a single byte — exits with
Encoder/Decoder mismatch detected and the offending frame number.
2. Installation
Thor has no package manager presence. You build it from source. The
reference build is a hand-written Makefile that targets POSIX systems
(Linux, macOS, WSL, MSYS2). Windows users without WSL can use the Visual
Studio solution at build/Thor.sln.
2.1 System requirements
| Component | Requirement |
|---|---|
| OS | Linux, macOS, or Windows (via WSL, MSYS2, or Visual Studio) |
| Compiler | gcc or clang with C99 support and -march=native capability |
| Linker | Standard POSIX toolchain; the only library link is -lm |
| RAM | ~1 GB for 1080p encoding at default settings |
| Disk | The repository checkout is roughly 3 MB; raw YUV inputs dominate disk use |
| Optional | valgrind (for make check test-valgrind=1); bash (for check.sh) |
Important
Thor's CFLAGS default to
-march=native -mtune=native. Binaries built on one host are not portable to a different micro-architecture. If you are distributing builds, passARCH=core2,ARCH=ssse3,ARCH=sse4,ARCH=avx2, orARCH=neon(ARM) tomaketo target a specific instruction-set baseline.
2.2 Build from source — Linux and macOS
git clone https://github.com/cisco/thor.git
cd thor
make -j8
cc -std=c99 -g -O3 -Wall -pedantic -I common -march=native -mtune=native -c -o common/common_block.o common/common_block.c
...
cc -o build/Thorenc enc/encode_block.o enc/encode_frame.o ... -lm
cc -o build/Thordec dec/decode_block.o dec/decode_frame.o ... -lm
After a clean build, you have:
| Binary | Purpose |
|---|---|
build/Thorenc |
Reference encoder. Reads raw YUV or Y4M, writes a Thor bitstream. |
build/Thordec |
Reference decoder. Reads a Thor bitstream, writes raw YUV or Y4M. |
Warning
The build emits two generated files,
common/common_kernels_gen.candenc/enc_kernels_gen.c, by runningscripts/lbd_to_hbd.shover the corresponding low-bit-depth source. The generator isbash, so a missing or non-executablebashinPATHwill fail the build with a confusing "no such file or directory" error frommake. Installbashand ensurescripts/lbd_to_hbd.shis executable (chmod +x scripts/lbd_to_hbd.sh).
2.3 Build with a specific instruction-set baseline
The Makefile recognises five ARCH values that swap the SIMD compiler
flags:
make -j8 ARCH=avx2 # x86-64 with AVX2 (Haswell or newer)
make -j8 ARCH=sse4 # x86-64 with SSE4
make -j8 ARCH=ssse3 # x86-64 with SSSE3 (older Intel)
make -j8 ARCH=core2 # SSE2 + SSSE3, tuned for Core2
make -j8 ARCH=neon # ARM with NEON SIMD
make -j8 ARCH=native # Default — march/mtune=native
The matching -mfpu=neon, -msse2 -mssse3 -mtune=core2, -mssse3,
-msse4, -mavx2, or -march=native -mtune=native flags are appended to
the C99 baseline (-std=c99 -g -O3 -Wall -pedantic -I common). Passing an
unrecognised ARCH value silently disables the SIMD-specific flag — you
get a portable build at the C99 baseline.
2.4 Build on Windows with Visual Studio
The repository ships a build/Thor.sln solution. Open it in Visual Studio,
choose a build configuration (Release recommended), and build the Thorenc
and Thordec projects. The solution mirrors the Makefile sources but does
not run scripts/lbd_to_hbd.sh; the generated _gen.c files must be
present in the working tree before you build (commit them or generate them
under WSL/MSYS2 first).
2.5 Verify the install
After the build, encode and decode a tiny clip end-to-end:
# Generate 3 frames of 640x480 random YUV
head --bytes $((640 * 480 * 3 * 3 / 2)) </dev/urandom > test.yuv
./build/Thorenc -cf config_LDB_low_complexity.txt -if test.yuv \
-of test.bit -rf test_rec.yuv -width 640 -height 480 -f 30 -n 3 -qp 32
./build/Thordec test.bit test_dec.yuv
cmp test_dec.yuv test_rec.yuv && echo OK
OK means the encoder's reconstructed reference matches the decoder
output byte-for-byte. Any other result is a regression.
Note
The encoder always prints a per-frame line of the form
<frame> <I|P|B> <qp> <bits> <psnr_y> <psnr_u> <psnr_v>. Final summary lines reportbitratein kbps (when-snrcalcis enabled) and average Y/U/V PSNR over the encoded sequence. The decoder prints exhaustive bit-allocation and mode-distribution statistics at the end of the run.
2.6 Install troubleshooting
Error: Compilation fails on ARM with gcc 4.6.1
Cause: Thor's NEON SIMD kernels rely on intrinsics that gcc 4.6 does not expose. Tracked upstream as issue #5.
Solution: Upgrade to gcc 4.7 or newer (gcc 6+ recommended), or build
with ARCH= set to a non-NEON value to fall back to the portable C SIMD
wrappers.
Prevention: Pin a known-good toolchain in CI; Thor's CI history exercises gcc 5+ and clang 6+.
Error: Code fail to build with GCC 5.2
Cause: A pre-2018 issue with restrict and inline rules under GCC 5.2.
Tracked as issue #11, resolved.
Solution: Use GCC 5.3 or newer, or pull master (commit 18de8f9 or
later).
Prevention: Verify your compiler version with gcc --version before
building. Thor uses -std=c99 -pedantic, which makes the build sensitive
to old language-rule corner cases.
Error: Compilation fails with clang due to intrinsics
Cause: Older clang versions did not provide every SIMD intrinsic Thor
expected. Tracked as issue #6,
resolved in current master.
Solution: Upgrade clang to 6.0 or newer.
Prevention: When porting Thor to a new platform, build first with
ARCH= left at the default to surface intrinsics issues; only opt into
specific SIMD baselines once the portable build succeeds.
3. Configuration
Thorenc is configured via a flat keyword=value list passed either on the
command line (e.g. -qp 32 -width 1920 -height 1080 ...) or through one or
more configuration files referenced with -cf <file>. Configuration files
hold one parameter per line in the form -name value and accept comments
introduced by a semicolon (;) up to the end of the line. Command-line
flags take precedence over config-file values; later -cf files override
earlier ones.
Thordec takes no configuration flags — it derives every parameter
(width, height, bit depth, chroma subsampling, deblocking, CDEF, CLPF, etc.)
from the sequence header carried in the bitstream itself.
3.1 The shipped preset configurations
The repository includes 30 ready-to-use preset configs in the root
directory. The naming scheme breaks down as
config_<profile>_[qm_]<complexity>.txt:
| Profile | Coding mode |
|---|---|
LDB |
Low-Delay B-frames (no reordering, B references only past frames) |
RA |
Random Access (hierarchical B-frames with reordering) |
RA16 |
Random Access with num_reorder_pics=15 (16-frame subgroup) |
HDB |
High-Delay B (longer reorder buffer) |
HDB16 |
High-Delay B with num_reorder_pics=15 |
*_qm_* |
Same operating point, with quantisation matrices enabled (-qmtx 1) |
Each profile also has three complexity flavours:
| Flavour | Encoder speed | Tools enabled |
|---|---|---|
low_complexity |
Fast (-encoder_speed 2) |
Block contexts, CFL-intra, deblock, no PB/TB split, no bipred |
medium_complexity |
Moderate (-encoder_speed 1) |
Adds -enable_pb_split 1 |
high_efficiency |
Slow (-encoder_speed 0) |
RDO intra, PB+TB split, bipred, max 4 references |
Tip
Pair
config_RA_high_efficiency.txtwith-qp 27(or 22 for higher quality) when you want the best compression efficiency Thor can deliver; useconfig_LDB_low_complexity.txtwith the encoder's default QP 32 for low-latency real-time scenarios. TheRA16andHDB16variants extend the GOP to 16 frames for offline VOD-style benchmarks.
3.2 Minimal annotated configuration
; Geometry
-width 1920
-height 1080
-f 30 ; frame rate in fps
; Coding tools
-num_reorder_pics 7 ; sub-GOP size minus 1 (8-frame sub-GOP)
-max_num_ref 4 ; up to 4 reference frames per inter frame
-intra_period 64 ; insert an I-frame every 64 frames
; Rate-distortion knobs
-qp 32 ; base QP
-dqpI -2 ; QP offset for I-frames (lower = better quality)
-mqpP 1.2 ; QP multiplier for low-quality P-frames
-lambda_coeffI 0.8 ; lambda multiplier for I-frames
; In-loop filters
-deblocking 1
-clpf 1 ; 0=off, 1=SB-level, 2=frame-level
-cdef 2 ; 0=off, 1=slow, 2=medium, 3=fast
Note
Default values come from the encoder itself, not from any of the shipped config files. Defaults set
-n 600,-width 1920,-height 1080,-qp 32,-f 60,-log2_sb_size 7(128x128 superblocks),-deblocking 1,-cdef 2,-clpf 0,-bitdepth 8,-input_bitdepth 8,-subsample 420,-max_num_ref 1,-snrcalc 1. If you launchThorencwith only-if,-of, and-rf, those defaults kick in.
3.3 Combining command-line flags with -cf
Configuration files are recursive: a flag inside a .txt config can
itself be -cf path/to/another.txt. Command-line arguments are parsed in
order, so the canonical "config + override" pattern is:
./build/Thorenc \
-cf config_RA_high_efficiency.txt \
-if input.yuv -of stream.bit -rf recon.yuv \
-width 3840 -height 2160 -f 60 \
-n 300 -qp 24 -intra_period 64
The preset establishes the coding-tool baseline; the trailing flags
override geometry, frame count, QP, and intra period. You can also point
-cf at a locally-tuned file that itself includes a preset:
; my_lowlatency.txt
-cf config_LDB_low_complexity.txt
-bitrate 4000000 ; target 4 Mbps
-max_delta_qp 4
See the Admin Guide's Configuration Reference for the full list of every encoder parameter, default value, and acceptable range.
4. Core Usage
The two binaries follow a strict separation of duties: Thorenc consumes
raw pixels and produces a Thor bitstream; Thordec consumes a Thor
bitstream and produces raw pixels. Neither tool muxes container formats —
there is no ISOBMFF, no Matroska, no IVF wrapper. The encoder writes a
byte stream that begins with a sequence header followed by per-frame
payloads.
4.1 The encoder: Thorenc
usage: Thorenc <parameters>
Parameters are zero or more flags from the table below, passed in any
order. At minimum you need an input file (-if) and an output bitstream
(-of). A reconstructed-frame output (-rf) is optional but strongly
recommended for testing — it lets check.sh byte-compare against the
decoder.
File I/O flags
| Flag | Type | Default | Description |
|---|---|---|---|
-cf |
path | — | Configuration file. May be specified more than once; later configs override earlier ones. |
-if |
path | — | Input file. Raw YUV (planar I420/I422/I444/Y400) or Y4M. Required. |
-of |
path | — | Output Thor bitstream. Required. |
-rf |
path | — | Optional reconstructed YUV (for verification). Append .y4m to emit a Y4M file. |
-stat |
path | — | Optional stat-file path (used by tools that consume per-frame stats). |
-ph |
int | 0 |
File header length in bytes to skip before the first frame in -if. Auto-set to the Y4M header length when -if is Y4M. |
-fh |
int | 0 |
Per-frame header length to skip in -if (set to 6 automatically for Y4M, which prefixes each frame with FRAME\n). |
Geometry and frame selection
| Flag | Type | Default | Description |
|---|---|---|---|
-width |
int | 1920 |
Frame width in luma samples. Must be a multiple of 8. |
-height |
int | 1080 |
Frame height in luma samples. Must be a multiple of 8. |
-n |
int | 600 |
Number of frames to encode. |
-skip |
int | 0 |
Number of frames to skip from the start of the input file. |
-f |
float | 60 |
Frame rate in frames per second. |
-subsample |
int | 420 |
Chroma subsampling: 400 (mono), 420, 422, or 444. |
-bitdepth |
int | 8 |
Internal codec bit depth: 8, 10, or 12. |
-input_bitdepth |
int | 8 |
Bit depth of pixels in -if: 8, 10, or 12. |
-frame_bitdepth |
int | 8 |
Bit depth of internal frame buffers: 8 or 16. Forced to 16 when -bitdepth>8. |
Important
Width and height must each be a multiple of 8. The encoder calls
fatalerror("Width and height must be a multiple of 8\n")and aborts otherwise. To encode an arbitrary-resolution sequence, pad the source width and height up to the nearest multiple of 8 and signal the active region out-of-band.
QP, lambda, and rate control
| Flag | Type | Default | Description |
|---|---|---|---|
-qp |
int | 32 |
Base QP applied to anchor frames. Range 0–51. |
-max_qp |
int | 51 |
Upper QP clamp for non-I frames during rate control. |
-min_qp |
int | 1 |
Lower QP clamp for non-I frames during rate control. |
-max_qpI |
int | 32 |
Upper QP clamp for I-frames. |
-min_qpI |
int | 32 |
Lower QP clamp for I-frames. |
-dqpI |
int | 0 |
QP offset for I-frames (negative = higher quality). |
-dqpP |
int | 0 |
QP offset for low-quality P-frames. |
-dqpB |
int | 0 |
QP offset for non-dyadic B-frames. |
-dqpB0..-dqpB3 |
int | 0 |
QP offsets for hierarchical B levels 0–3. |
-mqpP |
float | 1.0 |
QP multiplier for low-quality P-frames. |
-mqpB |
float | 1.0 |
QP multiplier for non-dyadic B-frames. |
-mqpB0..-mqpB3 |
float | 1.0 |
QP multipliers for hierarchical B levels 0–3. |
-max_delta_qp |
int | 0 |
Per-superblock delta-QP search range (max 7). |
-delta_qp_step |
int | 1 |
Step size for the delta-QP search. |
-bitrate |
int | 0 |
Target bitrate in bps (0 = no rate control). Mutually exclusive with -num_reorder_pics > 0. |
-lambda_coeffI |
float | 1.0 |
Multiplier for the rate-distortion lambda on I-frames. |
-lambda_coeffP |
float | 1.0 |
Multiplier for lambda on P-frames. |
-lambda_coeffB |
float | 1.0 |
Multiplier for lambda on non-dyadic B-frames. |
-lambda_coeffB0..B3 |
float | 1.0 |
Multipliers for lambda on hierarchical B levels 0–3. |
-early_skip_thr |
float | 0.0 |
Early-skip threshold (higher = more aggressive skip decisions). |
Coding tools
| Flag | Type | Default | Description |
|---|---|---|---|
-log2_sb_size |
int | 7 |
Log2 of superblock size. Range 6–7 (64x64 or 128x128). |
-enable_tb_split |
int | 0 |
1 = allow splitting a coding block into 4 transform blocks. |
-enable_pb_split |
int | 0 |
1 = allow splitting an inter coding block into 4 prediction blocks. |
-max_num_ref |
int | 1 |
Maximum number of reference frames per inter block. Range 1–4. |
-HQperiod |
int | 1 |
Period of high-quality (anchor) frames. |
-num_reorder_pics |
int | 0 |
Sub-GOP size minus 1. With >0 enables B-frames; subject to dyadic-coding power-of-two rule. |
-dyadic_coding |
int | 1 |
1 = hierarchical B with dyadic structure; 0 = flat B. |
-interp_ref |
int | 0 |
1/2 enable a synthetic interpolated reference frame. |
-intra_period |
int | 0 |
Force an I-frame every N frames; 0 = only one initial I-frame. |
-intra_rdo |
int | 0 |
1 = full RDO over all intra modes; 0 = use the four most likely modes. |
-encoder_speed |
int | 0 |
Speed preset. 0 = slow, 1 = moderate, 2 = fast. |
-sync |
int | 0 |
1 = require frame-by-frame deterministic output. Requires -encoder_speed 2. |
-deblocking |
int | 1 |
1 = run the in-loop deblocking filter. |
-cdef |
int | 2 |
Constrained Directional Enhancement Filter: 0 off, 1 slow, 2 medium, 3 fast. |
-clpf |
int | 0 |
Constrained Low-Pass Filter: 0 off, 1 SB-level, 2 frame-level. |
-max_clpf_strength |
int | 4 |
Maximum CLPF filter strength. |
-snrcalc |
int | 1 |
1 = compute and print PSNR every frame. |
-use_block_contexts |
int | 0 |
1 = use spatial context for block-level coding. |
-enable_bipred |
int | 0 |
1 = allow biprediction; 2 = bipred without single-frame prediction. |
-qmtx |
int | 0 |
1 = enable per-block quantisation matrices. |
-qmtx_offset |
int | 0 |
QP offset (-32..31) used when picking the QM level. |
-enable_cfl_intra |
int | 1 |
1 = chroma-from-luma prediction for intra blocks. |
-enable_cfl_inter |
int | 0 |
1 = chroma-from-luma prediction for inter blocks. |
Tip
The shipped preset configs already pick a coherent set of these tools. Start from a preset (
-cf config_RA_medium_complexity.txt) and only override the handful of flags you actually want to change.
4.2 The decoder: Thordec
usage: Thordec infile [outfile]
Thordec is intentionally minimal — it has exactly two positional
arguments and no flags.
| Position | Argument | Required | Description |
|---|---|---|---|
| 1 | infile |
Yes | Path to the Thor bitstream produced by Thorenc. |
| 2 | outfile |
No | Path to the decoded YUV output. If the path ends in .y4m, the decoder writes a Y4M stream including a YUV4MPEG2 header and a FRAME\n prefix per picture. Otherwise it writes headerless raw YUV. If omitted, the decoder still parses the bitstream and prints statistics, but discards pixels. |
The decoder reads the sequence header from the bitstream, allocates frame buffers from the embedded width / height / chroma subsampling / bit depth fields, then loops decoding frames until the input ends. After the run it prints a thorough breakdown of bit allocation, mode and size distribution, super-mode distribution, and reference-index distribution.
Note
The decoder does no rate control, no PSNR comparison, and no in-band scaling. To compare decoder output against the encoder's reconstruction, run
cmp out.yuv recon.yuv— they must match exactly. A mismatch is a codec bug.
5. Common Tasks
5.1 Encode and decode a raw YUV file
The canonical end-to-end path. input_1280x720_30.yuv is a planar I420
file at 30 fps:
./build/Thorenc -cf config_LDB_medium_complexity.txt \
-if input_1280x720_30.yuv -of stream.bit -rf recon.yuv \
-width 1280 -height 720 -f 30 -qp 30 -n 300
./build/Thordec stream.bit decoded.yuv
cmp decoded.yuv recon.yuv && echo "Decoder matches reconstruction"
Use -rf out.y4m instead of -rf recon.yuv if your downstream tools want
a Y4M-wrapped reconstructed output.
5.2 Batch-encode a directory of YUV files
The repository's check.sh already does this for round-trip testing. For
a plain encode-only pass over a directory, a small shell loop is enough:
#!/bin/bash
CONFIG=config_RA_medium_complexity.txt
for f in inputs/*.yuv; do
base=$(basename "$f" .yuv)
# Filename convention: name_WIDTHxHEIGHT_FPS.yuv
geom=$(echo "$base" | rev | cut -d_ -f2 | rev)
fps=$(echo "$base" | rev | cut -d_ -f1 | rev)
w=$(echo "$geom" | cut -dx -f1)
h=$(echo "$geom" | cut -dx -f2)
./build/Thorenc -cf "$CONFIG" -if "$f" \
-of "out/${base}_qp32.bit" -rf "out/${base}_qp32_rec.yuv" \
-width "$w" -height "$h" -f "$fps" -qp 32 -n 300 \
2> "out/${base}_qp32.log"
done
Tip
The encoder's per-frame stdout line ends with the cumulative bit count, so post-processing is as simple as
awk '{print $4}' out/<file>.logto pull bits per frame, or piping the final summary throughgrep -E '^Bitrate'on some sequences.
5.3 Compare PSNR against another codec
Thor prints PSNR Y/U/V on every frame when -snrcalc 1 (the default). To
benchmark Thor head-to-head with another codec on the same source:
# Encode with Thor
./build/Thorenc -cf config_RA_high_efficiency.txt -if src.yuv -of thor.bit \
-rf thor_rec.yuv -width 1920 -height 1080 -f 30 -qp 27 -n 300 \
| tee thor.log
# Capture Thor's average PSNR and bitrate from the trailing summary
grep -E "Bitrate|^PSNR" thor.log
# Decode and compute PSNR with an external tool against the original
./build/Thordec thor.bit thor_dec.yuv
ffmpeg -s 1920x1080 -pix_fmt yuv420p -i thor_dec.yuv \
-s 1920x1080 -pix_fmt yuv420p -i src.yuv \
-lavfi psnr=stats_file=thor_psnr.log -f null -
Thor's per-frame PSNR is computed against the original source; any other
codec compared on the same source at matching bitrate gives a fair RD
point. Sweep -qp over {22, 27, 32, 37} with each codec to draw an RD
curve.
Note
Thor's PSNR is computed in the encoder's
input_bitdepthdomain. If you use-bitdepth 10but-input_bitdepth 8, the encoder still reports PSNR in the 8-bit input domain. Match-input_bitdepthacross codecs before comparing.
5.4 Custom QP per frame
Thor exposes per-frame QP control via three layered mechanisms:
- Anchor QP with
-qp <N>— the I-frame and high-quality P-frame QP. - Frame-type offsets with
-dqpI,-dqpP,-dqpB, and the dyadic-dqpB0..-dqpB3. These are applied additively after the multipliers. - Frame-type multipliers with
-mqpP,-mqpB, and dyadic-mqpB0..-mqpB3. Multipliers are applied first, then offsets.
The effective QP for a non-I frame is computed inside mainenc.c as:
qp = clip(int(mqp<X> * base_qp) + dqp<X>, 0, 51)
For per-superblock variation within a frame, set -max_delta_qp <N>
(1–7). The encoder does an additional rate-distortion search over QP
offsets in [-N, +N] per superblock and signals the chosen delta in the
bitstream.
Warning
-max_delta_qpgreater than or equal to 8 triggersfatalerror("max_delta_qp too large\n"). Range 1–7 only. Higher values increase encoder work roughly linearly per superblock.
5.5 Multi-resolution rate ladder
A typical adaptive-streaming ladder encodes the same source at multiple spatial resolutions and target bitrates. Drive Thor with a tiny shell loop:
#!/bin/bash
CFG=config_RA_high_efficiency.txt
# rendition dst_w dst_h qp tag
RUNGS=(
"2160 3840 2160 22 4k_high"
"1080 1920 1080 24 1080p"
"720 1280 720 27 720p"
"540 960 540 30 540p"
"360 640 360 33 360p"
)
for rung in "${RUNGS[@]}"; do
set -- $rung
name=$1 w=$2 h=$3 qp=$4 tag=$5
# Optional: downscale outside Thor (e.g. with ffmpeg) before this loop;
# Thor itself does not resample.
./build/Thorenc -cf "$CFG" \
-if "scaled_${w}x${h}.yuv" \
-of "ladder/${tag}.bit" \
-rf "ladder/${tag}_rec.yuv" \
-width "$w" -height "$h" -f 60 -qp "$qp" -n 300 \
-intra_period 64 \
2>&1 | tee "ladder/${tag}.log"
done
Tip
Thor itself does not resample frames — it requires the input to already be at the target resolution. Use an external scaler (FFmpeg, swscale, dav1d-tools) to produce each rung's source YUV first. Match
-intra_periodacross rungs so segment boundaries align for ABR switching.
6. Troubleshooting
Error: Width and height must be a multiple of 8
Cause: The encoder validates params->width % 8 == 0 and
params->height % 8 == 0 in check_parameters() and aborts otherwise.
Solution: Pad the source to the next multiple of 8 in both dimensions
before passing it to Thorenc. Crop on the decoder side or signal the
active region out-of-band.
Prevention: Inspect raw YUV inputs with
echo $(($(stat -c %s in.yuv) / (W * H * 3 / 2))) to verify the assumed
geometry matches the file size.
Error: Number of frames must be positive
Cause: -n 0 or a negative -n value. The encoder aborts in
check_parameters().
Solution: Pass -n <positive integer>. The default is 600.
Prevention: When wrapping Thorenc in a script, always set -n
explicitly — the default of 600 may be larger than your test clip.
Error: Subgop length (num_reorder_pics+1) must divide HQperiod.
Cause: Hierarchical B-frame coding requires the high-quality period
(-HQperiod) to be an exact multiple of the sub-GOP size
(num_reorder_pics + 1). The encoder aborts in check_parameters().
Solution: Either set -HQperiod to a multiple of
num_reorder_pics+1 or drop -HQperiod to 1.
Prevention: Stick to the shipped RA* and HDB* presets, which
already satisfy this constraint.
Error: num_reorder_pics+1 must be a power of 2 with dyadic coding.
Cause: With -dyadic_coding 1 (default) the sub-GOP must be a power
of two. -num_reorder_pics 4 (sub-GOP 5) violates this.
Solution: Use -num_reorder_pics 0, 1, 3, 7, or 15 for
sub-GOPs of 1, 2, 4, 8, or 16, or set -dyadic_coding 0.
Prevention: Use the RA (num_reorder_pics=7) or RA16
(num_reorder_pics=15) presets to stay within the supported set.
Error: Illegal value for log2_sb_size
Cause: -log2_sb_size outside the supported range. The encoder
aborts in check_parameters().
Solution: Use -log2_sb_size 6 (64x64 superblocks) or
-log2_sb_size 7 (128x128, default).
Prevention: Don't override -log2_sb_size unless you specifically
need 64x64 superblocks for comparison studies.
Error: Illegal value for subsample. Only 444, 422, 420 and 400 supported.
Cause: -subsample set to a value other than 400, 420, 422, or 444.
Solution: Use one of the four supported chroma formats.
Prevention: Match -subsample to your input. Y4M files set this
automatically.
Error: Illegal value for bitdepth. Only 8, 10 and 12 supported.
Cause: -bitdepth, -input_bitdepth outside {8, 10, 12}.
Solution: Use one of the supported bit depths. With high bit depth, remember the input file is 16-bit per sample (planar) and twice as large.
Prevention: Use the HDB16 configs as a starting point for 10-bit
encodes; they bundle a sane -bitdepth 10, -input_bitdepth 10
combination.
Error: Could not open in-file for reading.
Cause: -if path does not exist, is unreadable, or the working
directory is wrong. Same applies to Thordec's positional input.
Solution: Verify with ls -l <path> that the file exists and is
readable by the user running Thor.
Prevention: Always pass absolute paths to Thorenc from scripts.
Decoder reports decode_frame_num increments but writes garbage
Cause: Mismatched bit depth, chroma subsampling, or geometry between
the encoder and the consumer of the decoder's output. The decoder reads
these from the bitstream's sequence header, so the output is correct —
but your downstream tool may be treating an I420 10-bit planar buffer
as I420 8-bit.
Solution: Check the encoder log for the subsample, bitdepth,
width, and height it used; configure your downstream tool to match.
Use the .y4m extension on the decoder output for self-describing
output.
Prevention: Always emit .y4m for downstream consumption when bit
depth or subsampling differs from 420 8-bit.
Crash: NULL Pointer Dereference in interpolate_frame
Cause: Known fuzzer-found bug parsing certain malformed bitstreams. Tracked as issue #40.
Solution: Treat Thordec as research-grade software. Do not feed
untrusted bitstreams without sandboxing. The reference decoder is not
hardened against adversarial input.
Prevention: Run Thordec under valgrind or ASan in CI, never on
a production server reachable from the public internet.
Crash: Stack buffer overflow in read_block()
Cause: Adversarial input with values outside the expected ranges. Tracked as issue #39.
Solution: Same as above — sandbox the decoder.
Prevention: Use a fuzzed/hardened decoder fork in any pipeline that processes untrusted bitstreams. The reference decoder prioritises clarity over hardening.
Crash: get_vlc(): infinite loop on invalid streams
Cause: Malformed VLC code in the bitstream causes the parser to spin. Tracked as issue #30.
Solution: Wrap Thordec invocations with a timeout 30s (or
similar) when parsing untrusted inputs.
Prevention: Validate bitstreams against a known-good encoder output before sharing them across systems.
Encoder error detected from check.sh
Cause: The encoder returned a non-zero exit code. Almost always a parameter-validation failure or a missing input file.
Solution: Re-run the encode standalone with the same arguments
check.sh would use; the parameter error message goes to stderr.
Prevention: Pre-validate config files with a dry encode of 1 frame before committing them.
Encoder/Decoder mismatch detected. Frame nr.: N
Cause: The decoder's output diverged from the encoder's reconstructed frame at frame N. This is always a codec bug; report it upstream with a minimal reproducer.
Solution: Reduce to the smallest -n and resolution that triggers
the mismatch. File a ticket with the input dimensions, config preset,
and encoder output log.
Prevention: Track the issue tracker for known mismatch reports (issue #29 discusses the encoder/decoder agreement check infrastructure).
7. FAQ
Q: Is Thor a finished standard?
A: No. Thor is research-grade reference software for the IETF NETVC working group, evolving alongside the draft-fuldseth-netvc-thor Internet-Draft. It is not a published video coding standard and the bitstream is not version-stable. Two builds at different commits may not interoperate.
Q: Does Thor support hardware acceleration?
A: No. Both encoder and decoder are CPU-only C99 with hand-written SIMD intrinsics for SSE2/SSSE3/SSE4/AVX2 on x86 and NEON on ARM. There is no GPU path.
Q: Can I package Thor in a Matroska/MP4 container?
A: Not directly. Thor produces a raw byte stream with no IVF, ISOBMFF,
or Matroska wrapper. To carry Thor inside a container you would need to
define your own packetisation. For research workflows, pair the raw
.bit file with a sidecar JSON describing geometry/framerate.
Q: What chroma formats does Thor support?
A: Four: 400 (luma only), 420, 422, and 444. The encoder
validates this in check_parameters() and the decoder reads it from
the sequence header.
Q: What bit depths does Thor support?
A: Three: 8, 10, and 12 bits. Both -bitdepth (internal) and
-input_bitdepth (source) accept the same set. -frame_bitdepth is
forced to 16 whenever the internal bit depth exceeds 8 — this affects
how the encoder allocates frame buffers.
Q: How do I disable PSNR computation to speed up encoding?
A: Set -snrcalc 0. By default PSNR is computed and printed every
frame, which adds a small per-frame cost. Disabling it is useful for
pure rate sweeps where you only want the bitrate.
Q: How do I enable rate control?
A: Set -bitrate <bps> (in bits per second, not kbps). This activates
the per-superblock rate controller in enc/rc.c. Note that rate
control is mutually exclusive with frame reordering —
-num_reorder_pics > 0 together with -bitrate > 0 triggers
fatalerror("Current rate control doesn't work with frame reordering\n").
Q: Why does my low-bit-depth output look slightly different on different machines?
A: Thor's default build is -march=native -mtune=native, which can
fold floating-point reductions differently on different CPUs. For
bit-exact reproducibility across machines, build with ARCH=core2
(the lowest common SSE baseline) and pin one toolchain version.
Q: How do I use a Y4M file as input?
A: Pass the .y4m file to -if. The encoder detects the YUV4MPEG2
header, populates -width, -height, -f, -subsample, and
-input_bitdepth from the header, and sets -ph and -fh to skip
the file and per-frame headers. Y4M's chroma tags map to Thor as
mono → subsample=400, 420 → 420, 422 → 422, 444 →
444. Interlaced Y4M (Im / It / Ib) is rejected with
Only progressive input supported.
Q: How do I get a Y4M decoder output?
A: Name the second positional argument with a .y4m extension:
./build/Thordec stream.bit decoded.y4m. The decoder writes a
YUV4MPEG2 header followed by a FRAME\n prefix per picture.
Q: What is CDEF? Should I leave it on?
A: Constrained Directional Enhancement Filter — an in-loop
post-processing filter inherited from AV1 research. It improves
perceptual quality at low QPs at a small encode-time cost. Default is
-cdef 2 (medium). Turn it off with -cdef 0 for compatibility
studies; turn to -cdef 3 (fast) for real-time encoding scenarios.
Q: What is CLPF?
A: Constrained Low-Pass Filter — an alternative in-loop filter that
runs before CDEF in the pipeline. Default is -clpf 0 (off). -clpf 1 applies it at superblock granularity; -clpf 2 applies it at frame
granularity. The maximum filter strength is set by
-max_clpf_strength (default 4).
Q: Where can I find the bitstream specification?
A: The IETF Internet-Draft
draft-fuldseth-netvc-thor
is the closest thing to a spec. The reference decoder's
dec/read_bits.c (read_sequence_header() and read_frame_header())
is the executable specification — when the draft and the code
disagree, the code wins. See the Admin Guide for a
syntax-element-by-syntax-element walk-through.
Q: Is the encoder deterministic?
A: Mostly. With -encoder_speed 0 and -snrcalc 1 on a single
thread, Thor is bit-deterministic given the same toolchain and ARCH
flags. -encoder_speed 2 -sync 1 enforces deterministic output across
configurations. Multi-threaded execution is not supported by the
reference encoder.
Q: Why does make check only test 4 resolutions?
A: The default check.sh hard-codes the four W/H pairs
(1920,1080), (1280,720), (640,480), (640,360). Pass
test-files=path/to/dir or test-files=path/to/file.yuv to use your
own inputs. Filenames must follow name_WIDTHxHEIGHT_RATE.yuv so the
script can extract geometry.
Q: Where do I report a security issue?
A: Through the upstream issue tracker at github.com/cisco/thor/issues, following Cisco's responsible-disclosure guidelines. Open issues #36–#40 already track several memory-safety problems in the reference decoder.