This guide is the source-of-truth reference for engineers who deploy, audit, or modify the Thor reference codec. It covers the architecture of the encoder and decoder, every encoder configuration parameter, the bitstream syntax, the security posture of the reference implementation, and the internals you need to land an upstream patch. It complements the User Guide, which focuses on day-to-day usage and the shipped configuration presets.
Thor is a hybrid block-based video codec authored by Cisco Systems for the
IETF NETVC working group. The repository contains a reference encoder
(Thorenc) and decoder (Thordec) written in portable C99, hand-tuned
SIMD kernels for SSE2/SSSE3/SSE4/AVX2 (x86) and NEON (ARM), and 30 preset
configuration files that mirror the operating points described in the
Thor Internet-Draft.
There are no runtime dependencies beyond libm.
1. Architecture Overview
Thor is structured as three top-level source directories:
common/— code shared by both encoder and decoder: block-coding helpers, intra and inter prediction, transform/quantisation, deblocking, CDEF, CLPF, weight matrices, frame buffer management, SNR computation, and SIMD abstractions.enc/— encoder-only code: rate-distortion optimisation loop, motion estimation, mode decisions, rate control, bitstream writer.dec/— decoder-only code: bitstream parser, mode dispatcher, inverse transform driver, frame reconstruction loop.
Each binary links a single main() (enc/mainenc.c or dec/maindec.c)
plus the relevant common code. The Makefile compiles 80+ .c files into
two standalone executables, with common/ shared between them.
High-level encoder pipeline
+-----------+ +-----------+ +-----------+
raw YUV ---> | Read & | ---> | Frame- | ---> | Per-block |
(or Y4M) | configure| | level | | RDO loop |
| (strings | | setup | | (encode_ |
| .c) | | (mainenc | | block.c) |
+-----------+ | .c) | +-----------+
+-----------+ |
v
+------------+ +------------+ +-----------+ +------------+
| Bitstream |<--| Inverse |<--| In-loop |<--| Predict + |
| (write_ | | quantise+ | | filters | | transform |
| bits.c + | | inverse | | (deblock, | | + entropy |
| putvlc.c) | | transform | | CDEF, | | code coeff |
+------------+ +------------+ | CLPF) | +------------+
+-----------+
High-level decoder pipeline
+-----------+ +-----------+ +-----------+
bitstream --> | initbits_ | ---> | read_seq_ | ---> | decode_ |
| dec | | header / | | frame |
| (getbits | | read_ | | (decode_ |
| .c) | | frame_hdr | | frame.c) |
+-----------+ +-----------+ +-----------+
|
v
+------------+ +------------+ +-----------+ +------------+
| YUV / Y4M |<--| In-loop |<--| Inverse |<--| Per-block |
| output | | filters | | quantise+ | | mode |
| (maindec | | (deblock, | | inverse | | dispatch + |
| .c) | | CDEF, | | transform | | predict |
+------------+ | CLPF) | +-----------+ | (decode_ |
+------------+ | block.c) |
+------------+
Both pipelines share common/: prediction, transform, kernels, deblocking
and SIMD abstractions. The encoder additionally drives an
inverse-transform path on its own output to keep the encoder's
reconstruction byte-identical with the decoder's output.
Note
The reference implementation is single-threaded by design. There is no tile-level parallelism, no slice-level parallelism, no multi-threaded encoder, and no decoder thread pool. This makes the codec easy to reason about for research but limits throughput; expect ~real-time 1080p30 on a modern x86 core at low-complexity settings.
Component inventory
| Component | Path | Role |
|---|---|---|
| Encoder entry point | enc/mainenc.c |
Argument parsing wrapper, frame loop, sub-GOP scheduling, QP selection, sequence-header emission. |
| Decoder entry point | dec/maindec.c |
Two-arg CLI, sequence-header parse, frame loop, statistics dump. |
| Encoder per-block | enc/encode_block.c, enc/encode_block_hbd.c |
RDO over modes/sizes; chooses intra / inter / skip / merge / bipred per block. |
| Decoder per-block | dec/decode_block.c, dec/decode_block_hbd.c |
Mode dispatcher; reconstructs the residual for each block from coded coefficients. |
| Encoder per-frame | enc/encode_frame.c, enc/encode_frame_hbd.c |
Frame-level encoder: superblock loop, deblock/CDEF/CLPF tying, statistics. |
| Decoder per-frame | dec/decode_frame.c |
Frame-level decoder: superblock loop, in-loop filter application. |
| Bitstream writer | enc/write_bits.c, enc/putbits.c, enc/putvlc.c |
Sequence/frame header emission, MV/coefficient VLC coder. |
| Bitstream reader | dec/read_bits.c, dec/getbits.c, dec/getvlc.c |
Sequence/frame header parser, MV/coefficient VLC decoder. |
| Argument parser | enc/strings.c |
parse_config_params() (CLI + -cf recursive config files), check_parameters() (validation). |
| Rate control | enc/rc.c |
Per-superblock rate controller used when -bitrate > 0. |
| Encoder kernels | enc/enc_kernels.c, enc/enc_kernels_hbd.c, enc/enc_kernels_gen.c |
SIMD kernels (SAD, SSE, transforms, motion compensation) — encoder-only. |
| Common kernels | common/common_kernels.c, common/common_kernels_hbd.c, common/common_kernels_gen.c |
SIMD kernels shared by enc and dec. |
| Intra prediction | common/intra_prediction.c, common/intra_prediction_hbd.c |
Intra mode generators. |
| Inter prediction | common/inter_prediction.c, common/inter_prediction_hbd.c |
6-tap luma / 4-tap chroma motion compensation, OBMC. |
| Transform | common/transform.c |
Forward + inverse DCT/DST. |
| Frame buffers | common/common_frame.c, common/common_frame_hbd.c |
Allocation, padding, read/write of YUV planes. |
| Block-level helpers | common/common_block.c, common/common_block_hbd.c |
MV prediction, skip/merge candidates, deblocking, CDEF/CLPF. |
| Quantisation matrices | common/wt_matrix.c |
Per-QM-level weight matrix tables. |
| Temporal interpolation | common/temporal_interp.c, common/temporal_interp_hbd.c |
Builds synthetic interpolated reference frames. |
| SNR | common/snr.c, common/snr_hbd.c |
Per-frame Y/U/V PSNR. |
| SIMD abstraction | common/simd.c, common/simd.h, common/simd/*.h |
Portable v64/v128/v256 wrappers over native SSE/AVX/NEON. |
| Round-trip harness | check.sh, top-level Makefile |
make check driver. |
Technology stack
| Layer | Technology |
|---|---|
| Language | C99 (-std=c99 -pedantic) |
| Build | GNU make Makefile + Visual Studio solution at build/Thor.sln |
| Library deps | libm only (-lm) |
| SIMD | x86 SSE2/SSSE3/SSE4/AVX2, ARM NEON, portable C fallback |
| HBD generator | bash (scripts/lbd_to_hbd.sh) — produces _hbd_gen.c files at build time |
| Test harness | check.sh POSIX shell script invoked via make check |
| Test runtime | Optional valgrind for memory checks |
| Container/IO | Raw planar YUV; YUV4MPEG2 / Y4M for self-describing input/output |
| License | 2-clause BSD (Cisco Systems, Inc., 2015) |
Data flow for a single encode-decode round trip
Thorencreads the configuration viaparse_config_params()(recursive over-cffiles), thencheck_parameters()validates ranges.Thorencopens-if, peeks at the first 256 bytes, and if the magicYUV4MPEG2is present, parses W/H/F/I/C/A/X tags into the encoder parameters.Thorencwrites a sequence header into the output bitstream (write_sequence_header()— see §8 for syntax) and emits a per-frame header for each frame, followed by mode/coefficient payloads.Thorencruns an encode-then-reconstruct cycle per block, so its in-memoryrecbuffer matches whatThordecwill produce. PSNR Y/U/V is computed against the original input frame.Thordecreads the bitstream, extracts width/height/bit-depth from the sequence header, allocates frame buffers, and decodes each frame using the same in-loop filters in the same order as the encoder.- The decoder writes raw YUV (or Y4M with a
.y4moutput extension) and prints exhaustive bit-allocation and mode-distribution statistics.
2. System Requirements
| Component | Minimum | Recommended |
|---|---|---|
| OS | Linux, macOS, or Windows (WSL/MSYS2 or Visual Studio) | Ubuntu 20.04+ or RHEL 8+ |
| Compiler | gcc 5.3 / clang 6.0 with C99 | gcc 9+ or clang 12+ |
| Make | GNU make 3.81+ | GNU make 4+ |
| RAM | 1 GB for 1080p, 4 GB for 4K | 8 GB+ for parallel runs |
| CPU | x86-64 with SSE2 or ARM with NEON | Modern x86 with AVX2 (Haswell+) |
| Disk | 100 MB for build + sources; YUV inputs dominate | NVMe SSD for I/O-bound rate sweeps |
| Optional | bash for check.sh and HBD generator; valgrind for memory tests |
Both pre-installed |
Note
Thor's
-march=native -mtune=nativedefault makes the build sensitive to the host micro-architecture. For portable distribution, pin to a baseline (ARCH=core2for legacy x86,ARCH=avx2for modern x86,ARCH=neonfor ARM). The portable C SIMD fallback atcommon/simd/compiles when noARCHflag matches.
Tested toolchains (per upstream issue history)
| Toolchain | Status | Notes |
|---|---|---|
| gcc 4.6.1 (ARM) | Broken | NEON intrinsics missing; see issue #5. Upgrade to gcc 4.7+. |
| gcc 5.2 | Broken | restrict/inline regression; see issue #11. Upgrade to gcc 5.3+. |
| Older clang | Broken | Some intrinsics unavailable; see issue #6. Upgrade to clang 6+. |
| gcc 6+ | Working | Recommended baseline. |
| clang 6+ | Working | Recommended baseline. |
| Visual Studio | Working | Use build/Thor.sln; HBD _gen.c files must be present. |
3. Configuration Reference
Thorenc is driven by 70+ command-line flags, each settable on the command
line or inside a -cf config file. Every flag has a hard-coded default in
enc/strings.c (parse_config_params()); explicit -cf flags or
command-line flags override defaults.
3.1 Configuration file format
; comments start with a semicolon and run to end of line
-flag_name value ; inline comment
-another_flag "value with spaces" ; quoted string
-int_list 1, 2, 3, 4 ; comma-separated ints
-cf another_config.txt ; recursive include
The parser at enc/strings.c:read_config_file() skips whitespace, supports
double-quoted strings for paths/values containing spaces, and treats ; as
a line-comment marker. Flags defined later override earlier ones; command
line wins over -cf files.
3.2 File I/O parameters
| Flag | Type | Default | Description |
|---|---|---|---|
-cf |
path | — | Read a config file recursively. May be repeated. |
-if |
path | — | Input YUV (planar) or Y4M file. Required. |
-of |
path | — | Output Thor bitstream. Required. |
-rf |
path | — | Optional reconstructed YUV (Y4M if .y4m extension). |
-stat |
path | — | Optional stat-file path for downstream tools. |
-ph |
int | 0 |
File header length in bytes to skip in -if. Auto-set for Y4M. |
-fh |
int | 0 |
Per-frame header length to skip in -if. Auto-set to 6 for Y4M. |
3.3 Geometry parameters
| Flag | Type | Default | Description | Validation |
|---|---|---|---|---|
-width |
int | 1920 |
Frame width (luma samples). | width % 8 == 0 |
-height |
int | 1080 |
Frame height (luma samples). | height % 8 == 0 |
-n |
int | 600 |
Number of frames to encode. | n > 0 |
-skip |
int | 0 |
Frames to skip at start of -if. |
— |
-f |
float | 60 |
Frame rate. | — |
-subsample |
int | 420 |
Chroma subsampling. | One of 400, 420, 422, 444 |
-bitdepth |
int | 8 |
Internal codec bit depth. | One of 8, 10, 12 |
-input_bitdepth |
int | 8 |
Bit depth of -if. |
One of 8, 10, 12 |
-frame_bitdepth |
int | 8 |
Bit depth of internal buffers (8 or 16). | Forced to 16 if bitdepth>8 |
3.4 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. |
-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. |
-dqpP |
int | 0 |
QP offset for low-quality P-frames. |
-dqpB |
int | 0 |
QP offset for non-dyadic B-frames. |
-dqpB0/B1/B2/B3 |
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/B1/B2/B3 |
float | 1.0 |
QP multipliers for hierarchical B levels 0–3. |
-max_delta_qp |
int | 0 |
Per-superblock delta-QP search range. Range 0–7 (≥8 fatal). |
-delta_qp_step |
int | 1 |
Step size for the delta-QP search. |
-bitrate |
int | 0 |
Target bitrate in bps. 0 disables rate control. Mutually exclusive with -num_reorder_pics > 0. |
-lambda_coeffI/P/B/B0/B1/B2/B3 |
float | 1.0 |
Multipliers for the rate-distortion lambda per frame type / B level. |
-early_skip_thr |
float | 0.0 |
Early-skip threshold. Higher = more aggressive skip. |
Important
-bitrate > 0together with-num_reorder_pics > 0aborts withCurrent rate control doesn't work with frame reordering. The reference rate controller inenc/rc.cdoes not support hierarchical B frames yet. For variable-bitrate hierarchical encodes, use the-mqp*/-dqp*framework on a single base QP instead.
3.5 Coding tool parameters
| Flag | Type | Default | Description |
|---|---|---|---|
-log2_sb_size |
int | 7 |
Log2 superblock size. Range 6–7. |
-enable_tb_split |
int | 0 |
1 allow 4-way transform-block split. |
-enable_pb_split |
int | 0 |
1 allow 4-way prediction-block split. |
-max_num_ref |
int | 1 |
Max reference frames per inter block. Range 1–4. |
-HQperiod |
int | 1 |
Period of high-quality (anchor) frames. Must divide num_reorder_pics+1 or be 1. |
-num_reorder_pics |
int | 0 |
Sub-GOP size minus 1. With -dyadic_coding 1, must be 0/1/3/7/15. |
-dyadic_coding |
int | 1 |
1 hierarchical B (dyadic); 0 flat B. |
-interp_ref |
int | 0 |
1/2 enable a synthetic interpolated reference frame. |
-intra_period |
int | 0 |
Force I-frame every N frames. Must be a multiple of num_reorder_pics+1. 0 = single initial I-frame. |
-intra_rdo |
int | 0 |
1 full-RDO over all intra modes; 0 use 4 most likely. |
-encoder_speed |
int | 0 |
Speed preset: 0 slow, 1 moderate, 2 fast. |
-sync |
int | 0 |
1 deterministic-output mode (requires -encoder_speed 2). |
-deblocking |
int | 1 |
1 run in-loop deblocking filter. |
-cdef |
int | 2 |
CDEF: 0 off, 1 slow, 2 medium, 3 fast. |
-clpf |
int | 0 |
CLPF: 0 off, 1 SB-level, 2 frame-level. |
-max_clpf_strength |
int | 4 |
Max CLPF filter strength. |
-snrcalc |
int | 1 |
1 compute and print PSNR per frame. |
-use_block_contexts |
int | 0 |
1 use spatial context for block-level coding. |
-enable_bipred |
int | 0 |
1 allow bipred; 2 bipred without single-frame prediction. |
-qmtx |
int | 0 |
1 enable per-block quantisation matrices. |
-qmtx_offset |
int | 0 |
QP offset (-32..31) for QM-level pick. |
-enable_cfl_intra |
int | 1 |
1 chroma-from-luma for intra blocks. |
-enable_cfl_inter |
int | 0 |
1 chroma-from-luma for inter blocks. |
3.6 Build-time parameters
The build system reads one environment variable:
| Variable | Type | Default | Description |
|---|---|---|---|
ARCH |
string | native |
Sets SIMD baseline. Values: native, avx2, sse4, ssse3, core2, neon. |
There are no other build-time configuration knobs. Compile-time #defines
in common/global.h (MAX_QP, MAX_REF_FRAMES, CDEF,
NEW_DEBLOCK_FILTER, DYADIC_CODING, etc.) select compile-time variants
during research but are not part of the supported configuration surface.
3.7 Annotated walkthrough — a high-efficiency 4K config
-cf config_RA_high_efficiency.txt ; baseline preset
-width 3840
-height 2160
-f 60
-bitdepth 10 ; 10-bit internal codec
-input_bitdepth 10 ; 10-bit YUV source
-frame_bitdepth 16 ; (auto-forced) 16-bit frame buffers
-num_reorder_pics 7 ; 8-frame sub-GOP
-max_num_ref 4 ; up to 4 references per inter block
-intra_period 64 ; intra refresh every 64 frames
-qp 24 ; aggressive base QP
-max_delta_qp 4 ; per-superblock QP wobble of +-4
-cdef 1 ; slow but better-quality CDEF
-clpf 2 ; frame-level CLPF
-qmtx 1 ; quantisation matrices on
-snrcalc 1
3.8 Decoder configuration
Thordec has no configuration. Every parameter is read from the
bitstream's sequence header (see §8). The only runtime input is
the path to the bitstream and an optional output path.
4. Deployment & Scaling
Thor ships as two standalone CLI binaries with no daemon, no socket, no worker pool, and no orchestrator. Deployment patterns reflect those constraints.
4.1 Single-host research deployment
The default. Build on the target host; invoke Thorenc and Thordec
directly from shell scripts, Python, or research notebooks.
make -j8 ARCH=avx2
./build/Thorenc -cf config_RA_medium_complexity.txt -if src.yuv \
-of out.bit -rf rec.yuv -width 1920 -height 1080 -f 30 -n 300 -qp 27
./build/Thordec out.bit out.yuv
Tip
For reproducible research, fix
ARCHto a baseline (e.g.,core2) and document toolchain version (gcc --version) alongside every result. Floating-point reduction order on-march=nativebuilds can drift across CPU vendors and produce ~0.01 dB PSNR differences.
4.2 Batch farm — fan-out via shell
Thor is single-threaded per process, so horizontal scaling means running
multiple Thorenc processes in parallel. GNU parallel is the cleanest
driver:
ls inputs/*.yuv | parallel -j $(nproc) \
'./build/Thorenc -cf config_RA_medium_complexity.txt \
-if {} -of out/{/.}.bit -rf out/{/.}_rec.yuv \
-width 1920 -height 1080 -f 30 -qp 32 -n 300 \
> out/{/.}.log 2>&1'
The encoder uses ~250 MB of RSS at 1080p with default settings, scaling
roughly linearly with frame area. Plan for (num_jobs * 250 MB) + OS overhead at 1080p, or (num_jobs * 1 GB) at 4K.
Note
Do not run more parallel jobs than you have physical cores. Thor saturates a single core; oversubscription costs throughput because of SIMD register-file pressure and cache contention. With AVX2 builds in particular, 1 process per physical core (not hyperthread) is the throughput sweet spot.
4.3 Containerisation
A single-stage Dockerfile is sufficient for the binaries:
FROM ubuntu:22.04 AS build
RUN apt-get update && apt-get install -y build-essential bash git
RUN git clone https://github.com/cisco/thor.git /thor
WORKDIR /thor
RUN make -j$(nproc) ARCH=avx2
FROM ubuntu:22.04
COPY --from=build /thor/build/Thorenc /usr/local/bin/Thorenc
COPY --from=build /thor/build/Thordec /usr/local/bin/Thordec
COPY --from=build /thor/config_*.txt /etc/thor/
ENTRYPOINT ["Thorenc"]
4.4 Scaling limits
| Bottleneck | Effect | Mitigation |
|---|---|---|
| Single-threaded | Throughput is per-process; can't scale within a frame. | Run multiple processes; one per physical core. |
| Memory bandwidth | At high resolution, MC and SAD kernels are bandwidth-bound. | Use AVX2 builds; pin processes to NUMA nodes (numactl --cpunodebind). |
| File I/O | Raw YUV inputs are large (1080p = 3 MB/frame at 8-bit 4:2:0). | Use NVMe SSDs for input/output; avoid network filesystems for hot paths. |
| Determinism | Floating-point reduction order varies across -march=native builds. |
Pin ARCH and toolchain. |
5. Operations
The reference codec has no daemon, no logs, no metrics endpoint. Operations
boils down to "watch stdout and stderr from each Thorenc/Thordec
invocation."
5.1 Standard stdout/stderr
Per-frame, the encoder writes one stdout line of the form:
12 P 30 28591 34.2811 39.1450 39.7902 0 1 2 3 | 12 11 10 9
Columns: frame_num, frame type (I/P/B), QP, total frame bits,
PSNR-Y, PSNR-U, PSNR-V, then the chosen reference indices, then the
display frame numbers of those references.
After the run, the encoder prints aggregate bit/byte counts, average PSNR per channel, and the realised bitrate in kbps.
The decoder writes one stdout line per decoded frame:
decode_frame_num= 12 display_frame_num= 9 input_file_size= 3829512 bitcnt= 421376
After the run, it prints exhaustive bit-allocation, mode-distribution, size-distribution, super-mode, and reference-index statistics.
5.2 Health checks for batch jobs
For a batch farm, the canonical "encode succeeded" check is:
Thorencexit code is 0.-offile exists and is non-empty.- The trailing summary line is present in stdout.
- (Optional)
Thordec out.bit /tmp/test.yuv && cmp /tmp/test.yuv rec.yuvto confirm the decoder agrees.
Tip
Drive
make checkin CI after every patch. Thecheck.shharness already encodes, decodes, andcmps four resolutions; a pass gives you reasonable assurance no regression slipped in. Pair withvalgrind(make check test-valgrind=1) on a slower CI lane to catch memory-safety regressions.
5.3 Operational checklist for a release
- [ ]
make clean && make -j$(nproc)completes with no warnings under gcc and clang. - [ ]
make check test-valgrind=0passes for at least four configs:config_LDB_low_complexity.txt,config_LDB_high_efficiency.txt,config_RA_medium_complexity.txt,config_HDB16_high_efficiency.txt. - [ ]
make check test-valgrind=1passes for at least one config (slow but catches leaks). - [ ] PSNR Y/U/V at QP 32 on a known reference clip is within ±0.05 dB of the previous release.
- [ ] Total bitrate at QP 32 on the same clip is within ±1% of the previous release.
5.4 Common operational errors
| Symptom | Likely cause | Remedy |
|---|---|---|
Could not open in-file for reading. |
Missing/unreadable -if path |
Verify file exists and reader has POSIX read access |
Could not open out-file for writing. |
-of directory missing or no write access |
Create directory, fix perms |
Width and height must be a multiple of 8 |
Non-multiple-of-8 geometry | Pad source to multiple of 8 |
max_delta_qp too large |
-max_delta_qp >= 8 |
Use 1–7 |
Subgop length (num_reorder_pics+1) must divide HQperiod. |
Mismatched -HQperiod and -num_reorder_pics |
Set HQperiod to a multiple of sub-GOP, or to 1 |
Sync requires encoder_speed=2 |
-sync 1 with non-fast preset |
Add -encoder_speed 2 |
Encoder/Decoder mismatch detected. Frame nr.: N |
Codec bug — reconstructed frame differs from decoder output | Reduce reproducer; file upstream issue |
| Process spinning forever in decoder | Malformed bitstream — see issue #30 | Wrap with timeout; sandbox decoder |
6. Performance
Thor is hand-tuned C99 with a v64/v128/v256 SIMD abstraction over native SSE/AVX2/NEON intrinsics, and a portable C fallback. Performance varies by configuration and hardware.
6.1 Encoder hot paths
The dominant cost in encoder time is mode/RDO search:
| Hot path | Trigger | Tuning knob |
|---|---|---|
| Motion estimation | Inter-frame coding | -encoder_speed, -max_num_ref |
| Intra mode RDO | I-frames or -intra_rdo 1 |
-intra_rdo, -encoder_speed |
| Bi-prediction RDO | -enable_bipred 1, B-frames |
-enable_bipred |
| Block split RDO | -enable_pb_split 1/-enable_tb_split 1 |
-enable_pb_split, -enable_tb_split |
| CDEF param search | -cdef 1 (slow) or -cdef 2 (medium) |
-cdef 3 (fast) for real-time |
| QP delta search | -max_delta_qp > 0 |
Reduce -max_delta_qp |
Tip
For real-time constrained encoding, the canonical preset is
config_LDB_low_complexity.txt -encoder_speed 2 -snrcalc 0 -cdef 3. Disabling-snrcalcremoves a per-frame full-frame walk; setting-encoder_speed 2reduces RDO search depth across all coding decisions.
6.2 Decoder cost model
Decoder cost scales mostly with bitrate (more coded bits = more VLC parsing) and pixel rate (more pixels = more inverse transform / motion compensation work). It is significantly cheaper than the encoder. The hot paths are:
decode_block_lbd()/decode_block_hbd()— per-block residual reconstruction. Branch-heavy.- Inverse transform (
common/transform.c). - Motion compensation (
common/inter_prediction.c— 6-tap luma, 4-tap chroma). - In-loop filters: deblock → CDEF → CLPF in
decode_frame.c.
6.3 Tuning knobs for throughput
| Goal | Knobs |
|---|---|
| Minimise encoder latency | -encoder_speed 2, -num_reorder_pics 0, -cdef 3, -clpf 0, -snrcalc 0 |
| Maximise encoder quality | -encoder_speed 0, -num_reorder_pics 7, -max_num_ref 4, -intra_rdo 1, -enable_pb_split 1, -enable_tb_split 1, -enable_bipred 1 |
| Minimise decoder cost | Encode with -cdef 0 -clpf 0 -deblocking 0; bitstream consumers run fewer in-loop filters |
| Maximise SIMD throughput | Build with ARCH=avx2 (x86) or ARCH=neon (ARM); avoid ARCH=core2 unless distributing |
6.4 Benchmarking methodology
Thor's per-frame stdout already reports PSNR Y/U/V and frame size in bits. To produce an RD curve:
for qp in 22 27 32 37; do
./build/Thorenc -cf config_RA_high_efficiency.txt \
-if src_1920x1080.yuv -of /dev/null -width 1920 -height 1080 \
-f 30 -n 300 -qp $qp \
2>&1 | tee log_qp${qp}.txt
done
# Extract avg-PSNR-Y and bitrate for each QP
for qp in 22 27 32 37; do
psnr=$(grep -E "Average" log_qp${qp}.txt | awk '{print $3}')
rate=$(grep -E "Bitrate" log_qp${qp}.txt | awk '{print $2}')
echo "qp=$qp psnr=$psnr rate=$rate"
done
Plot rate vs. PSNR; compare with x265 / libaom output on the same source at matching QP/CRF for a fair RD comparison.
7. Security
The Thor reference implementation is research-grade software. It is not hardened against adversarial input and should not be exposed to untrusted bitstreams without sandboxing. Cisco published the codec under a permissive 2-clause BSD license to advance video coding research; it does not include the input validation, fuzz infrastructure, or defensive coding that a production decoder requires.
Warning
Treat
Thordecas you would a research C parser: assume any attacker-controlled.bitfile can crash, hang, or corrupt memory in the decoding process. Run it inside a process boundary (bwrap --ro-bind, Docker, gVisor, seccomp-bpf) before parsing bitstreams from untrusted sources.
7.1 Known security issues
The upstream issue tracker explicitly lists several memory-safety
problems in the reference decoder, all unresolved as of commit
18de8f9:
| Issue | Type | Location |
|---|---|---|
| #40 | NULL pointer dereference | interpolate_frame (in common/temporal_interp.c) |
| #39 | Stack buffer overflow | read_block() (in dec/read_bits.c) |
| #38 | Invalid read | vg_replace_strmem.c:210 (Valgrind report) |
| #37 | Stack buffer overflow | common_kernels.c |
| #36 | SIGSEGV in Thordec | Symbol unspecified |
| #30 | Infinite loop | get_vlc() on invalid streams |
| #15 | Incomplete error handling | Decoder error paths in general |
7.2 Input validation posture
Encoder: validates parameters in enc/strings.c:check_parameters(). The
checks cover positive frame counts, multiple-of-8 dimensions,
max_num_ref in [1, 4], max_delta_qp < 8, sub-GOP power-of-two with
dyadic coding, sub-GOP divisibility of HQperiod and intra_period,
encoder_speed = 2 when sync = 1, mutual exclusivity of bitrate and
reordering, log2_sb_size in [6, 7], qmtx_offset in [-32, 31],
subsample in {400, 420, 422, 444}, and bitdepth/input_bitdepth in
{8, 10, 12}. The encoder calls fatalerror() and exits on violation.
Decoder: trusts the bitstream. The sequence header is read with
get_flc(width_bits, stream) calls and clamped only for log2_sb_size
(via clip(...) against MIN_BLOCK_SIZE/MAX_SB_SIZE). Width, height,
max_num_ref, num_reorder_pics, and other fields are trusted as-is.
This is the root cause of issues #30, #36–#40.
7.3 Recommended hardening for production use
If you must run Thor decode on untrusted bitstreams:
- Sandbox the process. Use
seccomp-bpfto drop syscalls beyondread/write/mmap/munmap/exit_group. Run inside a minimal Docker container with read-only root and no network. - Wall-clock cap. Wrap with
timeout 60s ./build/Thordec ...to bound parser-spin attacks (issue #30). - Memory cap.
ulimit -v 1048576(1 GB virtual memory) bounds malloc-bomb attempts. - Filesystem cap. Run from a tmpfs with a strict size limit; the decoder can be coerced to write large outputs.
- Pre-parse with a hardened decoder. Validate the sequence header
with a custom fuzzer-hardened parser before passing the bitstream to
Thordec.
7.4 Recommended fuzz-test recipe
Use AFL or libFuzzer over Thordec's entry point:
# Build with AFL
CC=afl-gcc make -j8 ARCH=core2
# Seed corpus: a few small known-good .bit files
mkdir -p corpus
./build/Thorenc -cf config_LDB_low_complexity.txt \
-if seed_640x360_30.yuv -of corpus/seed.bit \
-width 640 -height 360 -f 30 -n 5 -qp 32
# Fuzz
afl-fuzz -i corpus -o findings -- ./build/Thordec @@ /dev/null
Caution
Fuzz output will produce crashes within minutes. The decoder's lack of defensive bounds checking means ~every malformed bitstream that survives the sequence header to reach a per-frame parse is a potential crash. Triaging crashes requires manual reduction; do not assume a crash is unique without
afl-cminandafl-tmin.
7.5 Untrusted-input handling rules of thumb
| Source of bitstream | Safe to decode directly? |
|---|---|
Output of your own Thorenc at known commit |
Yes |
Output of a peer-controlled Thorenc |
Sandboxed only |
| Bitstream from a public dataset | Sandboxed only |
| Bitstream from a chat / email attachment | No — never, even sandboxed; fingerprint first |
| Network-fetched bitstream | Validate signature first; sandbox always |
8. Developer Internals
8.1 Repository layout
thor/
├── Makefile GNU make build
├── README.md Brief usage hint
├── LICENSE 2-clause BSD
├── check.sh Round-trip test driver
├── config_*.txt 30 preset configurations
├── build/ Output dir (Makefile creates Thorenc/Thordec here)
├── common/ Shared code
│ ├── common_block.c/h Block-level helpers, MV prediction, deblock
│ ├── common_block_hbd.c High-bit-depth variant
│ ├── common_frame.c/h Frame buffer alloc/read/write
│ ├── common_frame_hbd.c HBD variant
│ ├── common_kernels.c/h Shared SIMD kernels (SAD/SSE/transform/MC/CDEF)
│ ├── common_kernels_gen.c Generated HBD kernel templates
│ ├── common_kernels_hbd.c HBD kernels
│ ├── common_tables.c Static tables (zigzag, VLC, QM)
│ ├── global.h MAX_QP, MAX_SB_SIZE, feature switches
│ ├── intra_prediction.c/h Intra mode generators
│ ├── intra_prediction_hbd.c
│ ├── inter_prediction.c/h 6-tap luma, 4-tap chroma MC, OBMC
│ ├── inter_prediction_hbd.c
│ ├── simd.c/h Portable SIMD entry points
│ ├── simd/ v64/v128/v256 wrappers (arm/c/x86)
│ ├── snr.c/h PSNR Y/U/V
│ ├── snr_hbd.c
│ ├── temporal_interp.c/h Synthetic interpolated reference frames
│ ├── temporal_interp_hbd.c
│ ├── transform.c/h Forward + inverse DCT/DST
│ ├── types.h Shared typedefs
│ └── wt_matrix.c/h Quantisation matrix tables
├── dec/ Decoder-only code
│ ├── decode_block.c/h
│ ├── decode_block_hbd.c
│ ├── decode_frame.c/h
│ ├── getbits.c/h Bitstream reader (FLC)
│ ├── getvlc.c/h VLC decoder
│ ├── maindec.c/h main(); CLI = Thordec infile [outfile]
│ └── read_bits.c/h Sequence header, frame header, block syntax parse
├── enc/ Encoder-only code
│ ├── enc_kernels.c/h
│ ├── enc_kernels_gen.c
│ ├── enc_kernels_hbd.c
│ ├── encode_block.c/h
│ ├── encode_block_hbd.c
│ ├── encode_frame.c/h
│ ├── encode_frame_hbd.c
│ ├── encode_tables.c Static encoder-only tables
│ ├── mainenc.c/h main(); 70+ flag CLI; sub-GOP scheduling
│ ├── putbits.c/h Bitstream writer (FLC)
│ ├── putvlc.c/h VLC encoder
│ ├── rc.c/h Per-superblock rate controller
│ ├── strings.c/h parse_config_params, check_parameters
│ └── write_bits.c/h Sequence header, frame header, block syntax write
└── scripts/
└── lbd_to_hbd.sh bash script to generate _hbd.c from _lbd.c
8.2 Bitstream syntax — sequence header
The sequence header is a fixed-length payload at the start of every
bitstream, written by enc/write_bits.c:write_sequence_header() and read
by dec/read_bits.c:read_sequence_header(). Field widths are bits.
| Field | Width (bits) | Meaning |
|---|---|---|
width |
16 | Frame width in luma samples |
height |
16 | Frame height in luma samples |
log2_sb_size |
3 | Log2 superblock size (clamped to [log2(MIN_BLOCK_SIZE), log2(MAX_SB_SIZE)] = [3, 7]) |
pb_split |
1 | enable_pb_split |
tb_split_enable |
1 | enable_tb_split |
max_num_ref - 1 |
2 | Max reference frames, minus 1 |
interp_ref |
2 | 0/1/2 — synthetic interpolated reference enable |
max_delta_qp_or_bitrate |
1 | 1 if either max_delta_qp > 0 or bitrate > 0 |
deblocking |
1 | In-loop deblock enable |
clpf_present |
1 | 1 if any CLPF mode active |
use_block_contexts |
1 | Spatial context coding enable |
enable_bipred |
2 | 0/1/2 — biprediction enable |
qmtx |
1 | Quantisation matrices on |
qmtx_offset + 32 |
6 (only if qmtx) |
QM-level QP offset, biased by +32 |
subsample_code |
2 | Encodes 400/420/422/444 in 2 bits via a parity-based mapping |
num_reorder_pics |
4 | Sub-GOP size minus 1 |
cfl_intra |
1 (only if subsample != 400) |
Chroma-from-luma intra |
cfl_inter |
1 (only if subsample != 400) |
Chroma-from-luma inter |
bitdepth_gt8 |
1 | 0 = 8-bit, 1 = 10/12-bit |
bitdepth_eq12 |
1 (only if previous = 1) | 0 = 10-bit, 1 = 12-bit |
input_bitdepth_gt8 |
1 | Same encoding for input bit depth |
input_bitdepth_eq12 |
1 (only if previous = 1) | 10-bit vs 12-bit input |
Note
The subsample encoding is unusual. The 2-bit code maps as follows: bit 0 =
(subsample & 4) == 4(i.e. 4:?:?), bit 1 (when set with bit 0) selects 422 vs 444. The mapping0=400, 1=420, 2=422, 3=444is reconstructed in the decoder by(code & 1) * 20 + (code & 2) * 22 + ((code & 3) == 3) * 2 + 400.
8.3 Bitstream syntax — frame header
Written by enc/write_bits.c:write_frame_header() and read by
dec/read_bits.c:read_frame_header().
| Field | Width | Meaning |
|---|---|---|
frame_type != I_FRAME |
1 | 0 = I-frame, 1 = non-I |
qp |
8 | Frame QP (range 0–51) |
num_intra_modes |
4 | 4 or up to MAX_NUM_INTRA_MODES |
num_ref - 1 |
2 (only if non-I) | Number of references, minus 1 |
ref_array[r] + 1 |
6 (per ref) | Reference index, biased by +1 |
interp_ref_signal |
5 (only if num_ref==2 && ref_array[0]==-1) |
Synthetic interp ref |
frame_num |
16 | Display frame number |
cdef_damping - 3 |
2 (if CDEF) | CDEF damping selector |
cdef_bits |
2 (if CDEF) | log2 number of CDEF presets |
| Per CDEF preset | (variable) | pri_strength[c] (4 bits) + skip_condition[c] (1) + sec_strength[c] (2) per channel |
8.4 Transform and quantisation
Block-coded residuals go through:
- Forward transform (
common/transform.c). Uses block-size-adaptive DCT/DST. Quantisation block size capped atMAX_QUANT_SIZE = 16regardless of coding-block size. - Quantisation with optional per-block weight matrices (
common/wt_matrix.c).NUM_QM_LEVELS = 12,WEIGHT_SHIFT = 6,INV_WEIGHT_SHIFT = 6. - Zigzag scan (
zigzag16,zigzag64,zigzag256tables inenc/encode_tables.cfor 4x4, 8x8, 16x16 quant blocks). - VLC entropy code (
enc/putvlc.c). Two coefficient modes: level-mode (CABAC-free fixed-length levels) and run-mode (run-length over zero coefficients).
The decoder mirrors this: VLC decode → inverse zigzag → inverse quantise (with QM if signalled) → inverse transform → add to predictor.
8.5 Motion estimation and compensation
- Motion compensation uses a 6-tap luma filter and a 4-tap chroma
filter with quarter-pel precision (from
common/inter_prediction.c). For high-bit-depth, the same filter taps run throughcommon/inter_prediction_hbd.c. - Up to 4 reference frames simultaneously
(
MAX_REF_FRAMES = 33is the buffer cap;max_num_ref = 4is the per-block cap). - Skip mode allows up to
MAX_NUM_SKIP = 2candidates (LIMITED_SKIPdefine). - Merge mode allows up to
MAX_NUM_MERGE = 2candidates. - Synthetic interpolated references (when
-interp_ref 1/2): build a virtual frame between two real references viacommon/temporal_interp.c:interpolate_frames(), signal it withref_idx = -1.
8.6 In-loop filters
Three filters run in this order on each reconstructed frame:
- Deblocking filter (
common/common_block.c:deblock_frame_y()/deblock_frame_uv()). Toggled by-deblocking(default on). TheNEW_DEBLOCK_FILTER,NEW_DEBLOCK_TEST,NEW_MV_TEST, andMODIFIED_DEBLOCK_TESTcompile-time flags are all on by default. - CDEF (Constrained Directional Enhancement Filter). Per-superblock
primary + secondary direction-aware filter inherited from AV1
research. Strength selection encoded via 4+1+2 bits per superblock.
CDEF_BLOCKSIZE = 64,CDEF_PRI_STRENGTHS = 16,CDEF_SEC_STRENGTHS = 4. - CLPF (Constrained Low-Pass Filter). Optional follow-on filter
with per-superblock or per-frame strength control. Strength capped
by
-max_clpf_strength(default 4).
8.7 Frame management — sub-GOP scheduling
enc/mainenc.c outer loop iterates over the input file in chunks of
sub_gop = num_reorder_pics + 1 frames. Inside each chunk it picks
frame types and reference structures:
-num_reorder_pics 0: pure P-frame coding (and forced I atintra_periodboundaries). Reference is always the most recent encoded frame, optionally a high-quality anchor everyHQperiodframes.-num_reorder_pics > 0with-dyadic_coding 1: hierarchical B with a dyadic structure. Sub-GOP sizes 2/4/8/16 (powers of two). The first frame of each sub-GOP is a P-frame; remaining frames are B-frames atb_level = log2(coded_phase + 1). Each B-level gets its own QP multiplier (mqpB0..mqpB3) and offset (dqpB0..dqpB3).-num_reorder_pics > 0with-dyadic_coding 0: flat B coding (uniform QP across all B-frames).
The display ↔ coding order remap is in
enc/encode_tables.c:dyadic_reorder_display_to_code[]. The decoder
follows the same schedule by reading display_frame_num from each frame
header and reordering its MAX_REORDER_BUFFER = 32 slot output buffer.
Note
The encoder removes duplicate references in
mainenc.cafter picking them — when two slots inref_array[]resolve to the same frame number, the duplicate is dropped, sonum_refmay shrink at runtime belowmax_num_ref. The encoder also drops references that crossed the most recent intra boundary (last_intra_frame_num) to preserve random-access semantics.
8.8 Code patterns and conventions
- HBD/LBD code duplication. Each high-bit-depth source file has a
matching low-bit-depth source. The HBD versions are partially
generated from the LBD versions via
scripts/lbd_to_hbd.sh, which preprocesses#define SAMPLEand template-style macros. TheTEMPLATE(name)macro inmainenc.c/maindec.cselects between_lbdand_hbdsymbol pairs at runtime based ondecoder_info.bitdepth == 8. - No dynamic dispatch. Function pointers are used for SIMD kernels
but not for codec tools. Tool toggling happens at the bitstream-syntax
level (e.g.,
pb_splitin the sequence header) and is read into fields ofenc_params/decoder_info_t. - Single-file
main()s. Bothmainenc.candmaindec.chold the full encode/decode loop in a singlemain()— no abstract encoder/decoder objects. This is intentional for research readability. - C99 with
restrict. The codec usesrestrictheavily on pointer parameters in inner loops to enable autovectorisation. With Visual Studio 2015, a compatibility shim#define restrict __restrictlives incommon/global.h. static consttables. All large lookup tables (VLC, zigzag, weight matrices, intra mode predictors) arestatic constincommon/common_tables.candenc/encode_tables.c.
8.9 Testing strategy
make checkdrivescheck.sh, which:- Generates random YUV files at four resolutions
(1920x1080, 1280x720, 640x480, 640x360) — or uses files passed via
test-files=. - Encodes each with the supplied config preset.
- Decodes the resulting bitstream.
cmps the decoder output against the encoder's reconstructed buffer. A mismatch reports the offending frame number and exits.
- Generates random YUV files at four resolutions
(1920x1080, 1280x720, 640x480, 640x360) — or uses files passed via
- Optionally wraps each
ThorencandThordecinvocation invalgrind --leak-check=full --max-stackframe=5000000 --error-exitcode=123(settest-valgrind=1). - There is no unit-test suite. All testing is end-to-end round-trip.
8.10 Potential improvements (from upstream issue tracker)
| Issue | Improvement |
|---|---|
| #14 | Add a build system generator (CMake or Meson) for non-Make environments |
| #7 | Integrate offered hand-written assembly optimisations |
| #13 | Resolve reserved-identifier violations (_FOO_H_ style include guards reserved in C99) |
| #33 | Use %u for unsigned ints in printf instead of %d |
| #15 | Complete error handling — many decoder paths swallow errors silently |
| #36–#40 | Memory-safety hardening of the decoder against malformed bitstreams |
| #2 | Support arbitrary sequence lengths (currently every frame must round-trip cleanly through the sub-GOP scheduler) |
8.11 Contributing guide
- Fork the upstream repository.
- Make changes in a topic branch off
master. - Build with both gcc and clang at
ARCH=core2andARCH=avx2(andARCH=neonif you have ARM hardware). - Run
make checkagainst at least two presets (LDB + RA). - Run
make check test-valgrind=1against one preset. - Verify that PSNR / bitrate doesn't regress on a known reference clip.
- Open a pull request describing the change. Include the affected bitstream-syntax fields if any.
Note
Bitstream-changing patches must update both
enc/write_bits.canddec/read_bits.cin the same commit — otherwise the encoder and decoder disagree andcheck.shwill report mismatches. Themake checkstep in CI catches this.
9. Integration Points
Thor is designed as a standalone CLI tool, but its file-based interface makes it integration-friendly.
9.1 Input integration
| Source | Method |
|---|---|
| Raw planar YUV | Pass directly to -if. Specify -width, -height, -subsample, -input_bitdepth, -f to match. |
| Y4M (YUV4MPEG2) | Pass directly to -if. The encoder peeks at the first 256 bytes, parses the W/H/F/I/C/A/X tags, and overrides -width/-height/-f/-subsample/-input_bitdepth. Sets -ph and -fh automatically. |
| MP4/MKV/etc. | Pre-decode with FFmpeg to YUV: ffmpeg -i in.mp4 -pix_fmt yuv420p out.yuv then pass to -if. |
| Live stream | Capture to a YUV pipe; Thorenc reads sequentially via fseek+fread. Pipe-style input requires a pre-known frame size (the encoder uses fseek with a computed offset). |
9.2 Output integration
| Sink | Method |
|---|---|
| Raw bitstream | Default — Thor writes its own byte stream to -of. There is no IVF/MP4/MKV wrapper. |
| Decoded raw YUV | Default decoder output. |
| Decoded Y4M | Use .y4m extension on the decoder's second positional argument. |
| FFmpeg pipeline | Decode with Thordec to Y4M, then ffmpeg -i decoded.y4m -c:v libx264 out.mp4 (or any other codec). |
9.3 No public C API
Thor does not expose a library API. There is no libthor.a, no
libthor.so, no public header. The only programmatic interface is the
CLI. Embedding Thor in a streaming pipeline means invoking the CLI as a
subprocess.
9.4 No daemon / no network
Thor has no server mode, no socket interface, no HTTP API. Process lifecycle is one-shot per invocation. Running it under a job scheduler (systemd, Kubernetes Jobs, AWS Batch, Slurm) is the canonical deployment pattern.
9.5 Process boundary contract
| Direction | Protocol |
|---|---|
| Encoder reads | -if (open binary; fseek random-access; never streamed) |
| Encoder reads | -cf (open text; sequential read; recursive include) |
| Encoder writes | -of (open binary; sequential write; flushes per frame) |
| Encoder writes | -rf (optional; binary or Y4M) |
| Encoder writes | -stat (optional, text) |
| Encoder writes | stdout — per-frame status line + final summary |
| Encoder writes | stderr — fatal errors (fatalerror()) |
| Decoder reads | argv[1] — bitstream path |
| Decoder writes | argv[2] (optional) — YUV or Y4M output |
| Decoder writes | stdout — per-frame status + statistics dump |
| Decoder writes | stderr — Run-time error... messages, then exits |
Both binaries return exit code 0 on success, non-zero on any error.
Important
Both binaries call
exit(1)(encoder viafatalerror()→abort()in some paths; decoder viarferror()→exit(1)) on any failure. They do not flush partial output on error. A wrapper script must clean up partial-of/-rffiles itself.