# Thor Codec Administrator & Developer Guide

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](https://tools.ietf.org/html/draft-fuldseth-netvc-thor).
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](https://github.com/cisco/thor/issues/5). Upgrade to gcc 4.7+. |
| gcc 5.2 | Broken | `restrict`/inline regression; see [issue #11](https://github.com/cisco/thor/issues/11). Upgrade to gcc 5.3+. |
| Older clang | Broken | Some intrinsics unavailable; see [issue #6](https://github.com/cisco/thor/issues/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

```text
; 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 `#define`s
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

```text
-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.

```bash
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:

```bash
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:

```dockerfile
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:

```text
   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:

```text
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 `cmp`s 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](https://github.com/cisco/thor/issues/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:

```bash
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](https://github.com/cisco/thor/issues/40) | NULL pointer dereference | `interpolate_frame` (in `common/temporal_interp.c`) |
| [#39](https://github.com/cisco/thor/issues/39) | Stack buffer overflow | `read_block()` (in `dec/read_bits.c`) |
| [#38](https://github.com/cisco/thor/issues/38) | Invalid read | `vg_replace_strmem.c:210` (Valgrind report) |
| [#37](https://github.com/cisco/thor/issues/37) | Stack buffer overflow | `common_kernels.c` |
| [#36](https://github.com/cisco/thor/issues/36) | SIGSEGV in Thordec | Symbol unspecified |
| [#30](https://github.com/cisco/thor/issues/30) | Infinite loop | `get_vlc()` on invalid streams |
| [#15](https://github.com/cisco/thor/issues/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-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`.

### 7.4 Recommended fuzz-test recipe

Use AFL or libFuzzer over `Thordec`'s entry point:

```bash
# 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. `cmp`s 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](https://github.com/cisco/thor/issues/14) | Add a build system generator (CMake or Meson) for non-Make environments |
| [#7](https://github.com/cisco/thor/issues/7) | Integrate offered hand-written assembly optimisations |
| [#13](https://github.com/cisco/thor/issues/13) | Resolve reserved-identifier violations (`_FOO_H_` style include guards reserved in C99) |
| [#33](https://github.com/cisco/thor/issues/33) | Use `%u` for unsigned ints in printf instead of `%d` |
| [#15](https://github.com/cisco/thor/issues/15) | Complete error handling — many decoder paths swallow errors silently |
| [#36](https://github.com/cisco/thor/issues/36)–[#40](https://github.com/cisco/thor/issues/40) | Memory-safety hardening of the decoder against malformed bitstreams |
| [#2](https://github.com/cisco/thor/issues/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.

---

_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. Unofficial; not affiliated with or reviewed by the project._
