Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

Thor Codec Administrator & Developer Guide

Generated April 2026 from cisco/thor at 18de8f9

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

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

  1. Thorenc reads the configuration via parse_config_params() (recursive over -cf files), then check_parameters() validates ranges.
  2. Thorenc opens -if, peeks at the first 256 bytes, and if the magic YUV4MPEG2 is present, parses W/H/F/I/C/A/X tags into the encoder parameters.
  3. Thorenc writes 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.
  4. Thorenc runs an encode-then-reconstruct cycle per block, so its in-memory rec buffer matches what Thordec will produce. PSNR Y/U/V is computed against the original input frame.
  5. Thordec reads 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.
  6. The decoder writes raw YUV (or Y4M with a .y4m output 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=native default makes the build sensitive to the host micro-architecture. For portable distribution, pin to a baseline (ARCH=core2 for legacy x86, ARCH=avx2 for modern x86, ARCH=neon for ARM). The portable C SIMD fallback at common/simd/ compiles when no ARCH flag 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 > 0 together with -num_reorder_pics > 0 aborts with Current rate control doesn't work with frame reordering. The reference rate controller in enc/rc.c does 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 ARCH to a baseline (e.g., core2) and document toolchain version (gcc --version) alongside every result. Floating-point reduction order on -march=native builds 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:

  1. Thorenc exit code is 0.
  2. -of file exists and is non-empty.
  3. The trailing summary line is present in stdout.
  4. (Optional) Thordec out.bit /tmp/test.yuv && cmp /tmp/test.yuv rec.yuv to confirm the decoder agrees.

Tip

Drive make check in CI after every patch. The check.sh harness already encodes, decodes, and cmps four resolutions; a pass gives you reasonable assurance no regression slipped in. Pair with valgrind (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=0 passes 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=1 passes 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 -snrcalc removes a per-frame full-frame walk; setting -encoder_speed 2 reduces 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 Thordec as you would a research C parser: assume any attacker-controlled .bit file 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.

If you must run Thor decode on untrusted bitstreams:

  • Sandbox the process. Use seccomp-bpf to drop syscalls beyond read/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.

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-cmin and afl-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 mapping 0=400, 1=420, 2=422, 3=444 is 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:

  1. Forward transform (common/transform.c). Uses block-size-adaptive DCT/DST. Quantisation block size capped at MAX_QUANT_SIZE = 16 regardless of coding-block size.
  2. Quantisation with optional per-block weight matrices (common/wt_matrix.c). NUM_QM_LEVELS = 12, WEIGHT_SHIFT = 6, INV_WEIGHT_SHIFT = 6.
  3. Zigzag scan (zigzag16, zigzag64, zigzag256 tables in enc/encode_tables.c for 4x4, 8x8, 16x16 quant blocks).
  4. 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 through common/inter_prediction_hbd.c.
  • Up to 4 reference frames simultaneously (MAX_REF_FRAMES = 33 is the buffer cap; max_num_ref = 4 is the per-block cap).
  • Skip mode allows up to MAX_NUM_SKIP = 2 candidates (LIMITED_SKIP define).
  • Merge mode allows up to MAX_NUM_MERGE = 2 candidates.
  • Synthetic interpolated references (when -interp_ref 1/2): build a virtual frame between two real references via common/temporal_interp.c:interpolate_frames(), signal it with ref_idx = -1.

8.6 In-loop filters

Three filters run in this order on each reconstructed frame:

  1. Deblocking filter (common/common_block.c:deblock_frame_y() / deblock_frame_uv()). Toggled by -deblocking (default on). The NEW_DEBLOCK_FILTER, NEW_DEBLOCK_TEST, NEW_MV_TEST, and MODIFIED_DEBLOCK_TEST compile-time flags are all on by default.
  2. 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.
  3. 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 at intra_period boundaries). Reference is always the most recent encoded frame, optionally a high-quality anchor every HQperiod frames.
  • -num_reorder_pics > 0 with -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 at b_level = log2(coded_phase + 1). Each B-level gets its own QP multiplier (mqpB0..mqpB3) and offset (dqpB0..dqpB3).
  • -num_reorder_pics > 0 with -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.c after picking them — when two slots in ref_array[] resolve to the same frame number, the duplicate is dropped, so num_ref may shrink at runtime below max_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 SAMPLE and template-style macros. The TEMPLATE(name) macro in mainenc.c / maindec.c selects between _lbd and _hbd symbol pairs at runtime based on decoder_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_split in the sequence header) and is read into fields of enc_params / decoder_info_t.
  • Single-file main()s. Both mainenc.c and maindec.c hold the full encode/decode loop in a single main() — no abstract encoder/decoder objects. This is intentional for research readability.
  • C99 with restrict. The codec uses restrict heavily on pointer parameters in inner loops to enable autovectorisation. With Visual Studio 2015, a compatibility shim #define restrict __restrict lives in common/global.h.
  • static const tables. All large lookup tables (VLC, zigzag, weight matrices, intra mode predictors) are static const in common/common_tables.c and enc/encode_tables.c.

8.9 Testing strategy

  • make check drives check.sh, which:
    1. Generates random YUV files at four resolutions (1920x1080, 1280x720, 640x480, 640x360) — or uses files passed via test-files=.
    2. Encodes each with the supplied config preset.
    3. Decodes the resulting bitstream.
    4. cmps the decoder output against the encoder's reconstructed buffer. A mismatch reports the offending frame number and exits.
  • Optionally wraps each Thorenc and Thordec invocation in valgrind --leak-check=full --max-stackframe=5000000 --error-exitcode=123 (set test-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

  1. Fork the upstream repository.
  2. Make changes in a topic branch off master.
  3. Build with both gcc and clang at ARCH=core2 and ARCH=avx2 (and ARCH=neon if you have ARM hardware).
  4. Run make check against at least two presets (LDB + RA).
  5. Run make check test-valgrind=1 against one preset.
  6. Verify that PSNR / bitrate doesn't regress on a known reference clip.
  7. 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.c and dec/read_bits.c in the same commit — otherwise the encoder and decoder disagree and check.sh will report mismatches. The make check step 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 via fatalerror() → abort() in some paths; decoder via rferror() → exit(1)) on any failure. They do not flush partial output on error. A wrapper script must clean up partial -of/-rf files itself.