oxideav-prores
Pure-Rust Apple ProRes codec — decoder + encoder for all six ProRes video profiles (422 Proxy / LT / Standard / HQ and 4444 / 4444 XQ). 8-bit, 10-bit, and 12-bit Y'CbCr; lossless alpha plane on the 4444 / 4444 XQ profiles.
Part of the oxideav framework but usable standalone. Implemented from the spec (no C codec libraries linked or wrapped, no *-sys crates).
Status
| Profile | FourCC | Pixel formats | State |
|---|---|---|---|
| 422 Proxy | apco |
Yuv422P, Yuv422P10Le, Yuv422P12Le |
decode + encode |
| 422 LT | apcs |
Yuv422P, Yuv422P10Le, Yuv422P12Le |
decode + encode |
| 422 Standard | apcn |
Yuv422P, Yuv422P10Le, Yuv422P12Le |
decode + encode |
| 422 HQ | apch |
Yuv422P, Yuv422P10Le, Yuv422P12Le |
decode + encode |
| 4444 | ap4h |
Yuv444P, Yuv444P10Le, Yuv444P12Le (+ alpha) |
decode + encode |
| 4444 XQ | ap4x |
Yuv444P, Yuv444P10Le, Yuv444P12Le (+ alpha) |
decode + encode |
Bit-depth selection follows the stream's CodecParameters::pixel_format
— RDD 36 §5 carries no per-frame bit-depth syntax element; §7.5.1
defines the conversion from reconstructed color component values to
pixel samples of arbitrary bit depth b. Pass Yuv422P10Le /
Yuv444P10Le to get 10-bit planar output (LE u16 pairs, valid range
0..=1023); pass Yuv422P12Le / Yuv444P12Le for 12-bit
(0..=4095); pass Yuv422P / Yuv444P (or omit pixel_format) for
8-bit.
Alpha plane
The 4444 / 4444 XQ profiles support a per-pixel alpha channel coded
losslessly per RDD 36 §5.3.3 + §7.1.2 (raster-scan run-length code +
differential VLC, Tables 12-14). The alpha is exposed as a 4th
VideoPlane on the decoded VideoFrame (after Y/Cb/Cr); the
encoder accepts the same shape on input via
[encoder::encode_frame_with_alpha].
The core PixelFormat enum does not yet carry Yuva422P / Yuva444P
variants, so the pixel-format reporting stays as Yuv4(2|4)4P* — the
caller checks frame.planes.len() == 4 to detect alpha.
The 4444 and 4444 XQ profiles share the same bitstream structure as 4444 — XQ is selected when the caller requests the highest quality tier and produces a larger packet at lower quantisation.
ffmpeg-encoded prores_ks apcn / apch (4:2:2) and ap4h /
ap4x (4444 ± alpha) streams decode interop-clean across 8-/10-/12-bit
and both progressive and interlaced (-flags +ildct -top {0,1})
modes. The picture-height-not-multiple-of-16 alpha edge case is
covered: the decoder reads the full padded macroblock-row alpha
(per RDD 36 §7.5.2) and crops on output. Streams produced by this
crate's own encoder use the spec's entropy coder for color, but the
encoder emits a plain run-length alpha (alternative path permitted by
§7.1.2); the coder is bit-exact with itself and decoder-compatible.
Frame-header metadata (RDD 36 §5.1.1 / §6.2)
The encoder fills the descriptive frame-header fields
(aspect_ratio_information, frame_rate_code, color_primaries,
transfer_characteristic, matrix_coefficients) automatically from
[CodecParameters::frame_rate] — e.g. Rational::new(30_000, 1001)
maps to frame_rate_code = 4 (29.97 fps NTSC). Set every field
explicitly via [encoder::EncoderConfig::with_meta]:
use ;
use FrameMeta;
let cfg = default.with_meta;
let enc = make_encoder_with_config?;
# Ok::
The mapping from Rational to frame_rate_code lives in
[frame::frame_rate_code_from_rational] and covers the 11 spec-named
rates (RDD 36 §6.2 Table 4); any other rate yields 0
("unknown"), which RDD 36 decoders treat as a hint only.
Configurable quantisation index (RDD 36 §7.3 / Table 15)
The encoder picks one quantization_index per profile by default
(8 / 6 / 4 / 2 / 2 / 1 for Proxy / LT / Standard / HQ / 4444 /
4444 XQ — see [frame::Profile::default_quant_index]). Lower index =
finer step = higher quality + larger packet. Override the default for
custom rate/quality trade-offs:
use ;
// Highest-quality Proxy: same profile selection (bit_rate hint), but
// the qscale floor of qi=2 instead of the Proxy default qi=8.
let cfg = default.with_quantization_index;
let enc = make_encoder_with_config?;
# Ok::
The valid range is 1..=224 (rejected at encoder construction
otherwise). The override applies to every slice in every encoded frame
and roundtrips through any RDD 36 decoder.
Configurable macroblocks-per-slice (RDD 36 §5.3)
Every encoded picture is partitioned into slices whose width in
macroblocks is signalled by picture_header.log2_desired_slice_size_in_mb
(a 2-bit field, so the legal set is {1, 2, 4, 8}). The default is
8 MBs/slice, matching every Apple-encoded fixture committed
under docs/video/prores/fixtures/. Lowering the value subdivides
each macroblock row into more, smaller slices — useful for finer
error resilience (a corrupted byte taints fewer macroblocks) at the
cost of a slightly larger packet (extra slice_header +
slice_size_table entries amortise over fewer macroblocks).
use ;
// Single-MB slices: maximum resilience, ~5% larger packet on a
// 128x64 4:2:2 source (5948 B → 6247 B in the regression test).
let cfg = default.with_mbs_per_slice;
let enc = make_encoder_with_config?;
# Ok::
Valid values are 1, 2, 4, 8; anything else returns
Error::invalid at encoder construction. The choice flows into
picture_header.log2_desired_slice_size_in_mb so any RDD 36 decoder
(including this crate's own) rebuilds the per-row template via
[frame::compute_slice_sizes]. The default path
(mbs_per_slice == None) is byte-identical to
with_mbs_per_slice(DEFAULT_MBS_PER_SLICE) (8) — the knob is
purely additive. ffmpeg's prores_ks exposes the same control
through -mbs_per_slice {1,2,4,8}.
Explicit profile selection
By default the encoder maps CodecParameters::bit_rate to one of the
six profiles via [encoder::pick_profile] (see the table below).
Override the heuristic with [encoder::EncoderConfig::with_profile] when
the caller wants a specific profile that the bitrate hint would not
pick — e.g. 4444 XQ at low bitrates (the heuristic only selects XQ
above 400 Mbit/s), or any 4:2:2 profile with no bit_rate hint at all.
use ;
use Profile;
// Force 4444 XQ for a 4:4:4 stream regardless of bit_rate hint.
let cfg = for_profile;
let enc = make_encoder_with_config?;
# Ok::
The override's [frame::Profile::chroma_format] must match the
requested PixelFormat (HQ/SD/LT/Proxy ↔ 4:2:2; 4444/4444 XQ ↔ 4:4:4) —
mismatches return Error::invalid at encoder construction. The bitstream
syntax itself only carries chroma_format per RDD 36 §5.1.1; the profile
choice influences only the encoder's default quantization_index.
Two-pass per-frame rate control
Set CodecParameters::bit_rate + frame_rate and call
EncoderConfig::with_rate_control() to enable per-frame binary-search
rate control. The encoder performs up to RATE_CTRL_MAX_PASSES (10)
trial encodes per frame, adjusting quantization_index to hit the
per-frame byte target derived from bit_rate / fps within
RATE_CTRL_TOLERANCE (±5 %). When the target is outside the
achievable range for the resolution the encoder returns the best
candidate (finest quality for targets above the maximum, coarsest for
targets below the minimum) — it never emits a broken stream.
use ;
// Hit the nominal 422 HQ bitrate (220 Mbit/s at 29.97 fps) within ±5%.
let cfg = default.with_rate_control;
let enc = make_encoder_with_config?;
# Ok::
Configurable quantisation matrices (RDD 36 §5.3.4 + §6.3.7 + §7.3)
The encoder defaults to the spec's flat all-4s quantisation matrix
(load_luma_qmat = load_chroma_qmat = 0, 20-byte frame header — same
as prores_ks for apcn / apch when no perceptual preset is
selected). Pass an [encoder::EncoderConfig] with a non-default
[quant::QuantMatrices] to load custom matrices into the frame header
(making the header 84 or 148 bytes per §7.3) — every RDD 36 decoder,
including ffmpeg's, uses the loaded matrices for dequantisation.
A built-in perceptual preset is provided via
[quant::QuantMatrices::perceptual]: JPEG K.1 / K.2 (ISO/IEC 10918-1
Annex K) normalised to a DC weight of 2 and clamped to the spec's
2..=63 weight range. At every quantization_index from 2 to 16 the
perceptual matrices cut packet size by 20-25% on broadband content
because the entropy coder's endOfData() semantics (RDD 36 §7.1.1)
turn HF zeros into trailing zero runs that cost no bits. PSNR trades
off slightly because flat is provably PSNR-optimal under uniform
quantisation, but JPEG-style CSF-rolloff matrices preserve perceptual
quality far better than the same byte-count flat encode.
use ;
use QuantMatrices;
let enc = make_encoder_with_config?;
// Or: EncoderConfig::default().with_quant_matrices(QuantMatrices { luma, chroma })
# Ok::
Profile-aware perceptual matrices
[quant::QuantMatrices::perceptual_for_profile] +
[encoder::EncoderConfig::perceptual_for_profile] take a [frame::Profile]
and blend the JPEG-derived perceptual matrix toward the flat all-4s
default in proportion to the profile's
[frame::Profile::default_quant_index] (blend = qi / 8). Higher-quality
profiles preserve more high-frequency precision; lower-quality profiles
get heavier HF rolloff for tighter packets at matched perceptual
quality.
| Profile | qi | blend | HF rolloff |
|---|---|---|---|
| Proxy | 8 | 8/8 | full (matches perceptual()) |
| LT | 6 | 6/8 | heavy |
| Standard | 4 | 4/8 | moderate |
| HQ | 2 | 2/8 | light |
| 4444 | 2 | 2/8 | light |
| 4444 XQ | 1 | 1/8 | minimal (matrix close to flat) |
The factory also pins the supplied profile so the chosen tier is
honoured regardless of the bit_rate → profile heuristic. Every
blended weight stays in 2..=63 (RDD 36 §7.3), and the matrices are
loaded into the frame header so any RDD 36 decoder dequantises
correctly.
use ;
use Profile;
// 4444 XQ with profile-aware perceptual rolloff — matrix sits close
// to flat, giving the encoder near-maximum HF precision while still
// loading custom weights into the frame header.
let cfg = perceptual_for_profile;
let enc = make_encoder_with_config?;
# Ok::
At matched quantization_index, the HQ-blended preset reconstructs
higher Y-PSNR than the Proxy-blended preset on broadband sources, and
the Proxy-blended preset emits a smaller packet — both monotonic in
the profile's quality tier (see tests/perceptual_profile.rs). The
mismatch between an explicit profile and the requested
PixelFormat (4:2:2 ↔ Yuv422P; 4:4:4 ↔ Yuv444P) is rejected at
encoder construction.
Cross-decode acceptance: bitstreams emitted by
encode_frame_with_qmats(..., QuantMatrices::perceptual_for_profile(p))
decode through ffmpeg's stock prores / prores_ks decoder at 58.0–63.8 dB
luma PSNR for every profile — Proxy / LT / Standard / HQ at 8-bit (with
HQ also exercised at 10-bit through the §7.5.1 b = 10 level shift on
the loaded-qmat path); 4444 / 4444 XQ at 8-bit (with 4444 also at 12-bit
through the §7.5.1 b = 12 level shift). The 4444 XQ corner is the
strongest guard — its 1/8 perceptual + 7/8 flat blend sits closest to
flat yet still triggers the load_*_qmat = 1 header path
(frame_header_size == 148), so a silent collapse to the flat default
would surface immediately. See tests/ffmpeg_cross_decode.rs.
Bitstream version compatibility (RDD 36 §6.4)
The decoder enforces every "decoder shall refuse" clause attached to the frame-header syntax elements:
| Spec clause | Constraint enforced |
|---|---|
§6.1.1 bitstream_version |
Reject any value > 1 — only versions 0 and 1 are specified. |
| §6.4 v0 stream rules | A bitstream_version = 0 stream must have chroma_format = 2 (4:2:2) AND alpha_channel_type = 0. Any v0 stream carrying 4:4:4 chroma or any alpha is malformed and rejected. |
§6.1.1 Table 2 interlace_mode |
Value 3 is reserved — rejected. |
| §6.1.1 qmat entries | Every entry of luma_quantization_matrix / chroma_quantization_matrix must lie in 2..=63 — out-of-range entries are rejected at parse time. |
The encoder picks the lowest legal bitstream_version per the spec's
own recommendation ("encoders should use the lowest bitstream version
appropriate for the frame being encoded"): 4:2:2 no-alpha streams emit
v0 for maximum legacy-decoder reach; any other combination emits v1.
See tests/spec_validation.rs (15 cases) for round-trip coverage.
ProRes RAW is detected and refused, never mis-decoded
Apple ProRes RAW (aprn / aprh) is a separate format that
wraps single-plane Bayer/CFA sensor data; it is outside the scope of
SMPTE RDD 36 (which covers only the six YUV/RGB profiles) and uses an
incompatible sample structure. This crate refuses ProRes RAW cleanly
at two layers rather than mis-routing it through the RDD 36 parser:
- FourCC level. [
is_prores_raw_fourcc] recognisesaprn/aprh(case-insensitive); [PRORES_RAW_FOURCCS] lists them. They deliberately resolve to neither aCodecIdnor aframe::Profile, so a demuxer/dispatcher can tell "ProRes RAW, unsupported" apart from "not ProRes at all" and surface a precise error to the user. - In-stream level. A sample carrying the ProRes RAW marker
aprhat theicpfoffset (just after the 4-byteframe_size) yields a specificUnsupportederror naming ProRes RAW — distinct from the generic "magic mismatch" returned for arbitrary non-ProRes bytes.
Decoding ProRes RAW would require Apple's proprietary bitstream
documentation (the staged Apple_ProRes_RAW_2023.pdf is a marketing
white paper with no syntax) and is not implemented.
Interlaced (RDD 36 §5.1, §6.2, §7.5.3)
A frame's interlace_mode (0 = progressive, 1 = top-field-first,
2 = bottom-field-first) controls whether the encoded bitstream carries
one picture or two (one per field). Source rows interleave across
fields per §7.5.3: top field = source rows 0, 2, 4, …; bottom field =
rows 1, 3, 5, …. The encoder splits the source into two field pictures
(each at (height + 1) / 2 or height / 2 rows) and emits them in
temporal order; the decoder reverses the deinterleave. Each field
picture uses the interlaced block scan (§7.2 Figure 5) instead of the
progressive Figure 4.
Interlaced output is reachable two ways: the free function
[encoder::encode_frame_interlaced], or — for callers that only touch
the high-level Encoder trait — [encoder::EncoderConfig::with_interlace_mode],
which threads the mode through make_encoder_with_config → send_frame
so a registry-built encoder emits interlaced ProRes (and respects the
mode under two-pass rate control). Value 3 is reserved (Table 2) and
rejected at encoder construction.
use ;
// Top-field-first interlaced output through the Encoder trait.
let cfg = default.with_interlace_mode;
let enc = make_encoder_with_config?;
# Ok::
Decoder side: a small synthetic interlaced apcn 10-bit yuv422p10le
fixture (128×128, 2 frames, TFF) ships in-tree at
docs/video/prores/fixtures/interlaced-tff-128x128-apcn/ alongside its
prores_ks reference YUV. The test
tests/docs_corpus.rs::corpus_interlaced_tff_128x128_apcn_per_field_psnr
splits the decoded luma plane into its top-field rows {0, 2, 4, …} and
bottom-field rows {1, 3, 5, …} per §7.5.3 and scores PSNR for EACH
field independently — both fields must clear 40 dB. Measured 78.19 /
79.42 dB (frame 0 top / bottom) and 77.84 / 78.69 dB (frame 1),
catching picture-order swap and silent second-picture skip that
whole-frame aggregate PSNR does not.
Streams produced by encode_frame_interlaced for apcn / apch cross-decode
through ffmpeg's prores_ks decoder at ≥ 64 dB luma PSNR — both 8-bit
(TFF and BFF, 64×48 and 128×96) and genuine 10-bit field-pair
packing (TFF/BFF at 64×48, TFF at 128×96; 64.40-64.47 dB). The 10-bit
cases drive a true 10-bit LE source through read_sample's
BitDepth::Ten branch (RDD 36 §7.5.1 level shift for b = 10) feeding
the §7.5.3 two-field deinterleave — not an 8-bit value padded into
10-bit storage. The high-level Encoder path is covered too: streams
emitted by make_encoder_with_config(... with_interlace_mode(m)) +
send_frame cross-decode through prores_ks at ≥ 58 dB luma PSNR
(apch TFF/BFF and apcn TFF at 64×48, apch TFF at 128×96), with the
even/odd field-bias check confirming TFF and BFF field order survives
the round-trip.
The interlaced 4444 + alpha path (ap4h / ap4x field-pair packing
with a per-pixel alpha plane) also cross-decodes through prores_ks at
65.24-65.26 dB luma PSNR (ap4h TFF/BFF at 64×48, ap4h TFF at 128×96,
ap4x TFF at 64×48). These cases combine the four hardest paths the
encoder owns at once — 4:4:4 full-resolution chroma, the genuine 12-bit
read_sample branch (RDD 36 §7.5.1 for b = 12, matching ffmpeg's
native ap4h/ap4x depth), the §5.3.3 / §7.1.2 / Table 14 16-bit-alpha
entropy coder (per-slice scanned-alpha blob at the padded MB-row height
per §7.5.2), and the §7.5.3 two-field deinterleave. The decoded alpha
gradient round-trips with sub-LSB mean-abs-error (the residual of
ffmpeg's 16→12-bit alpha resample; the bitstream alpha is lossless per
§7.1.2). See tests/ffmpeg_cross_decode.rs for the black-box
acceptance harness.
The symmetric progressive 4444 + alpha forward path
(encode_frame_with_alpha, single picture, interlace_mode == 0)
cross-decodes through prores_ks at 64.77-64.81 dB luma PSNR (ap4h at
64×48 and 128×96, ap4x at 64×48). It drives the same 4:4:4 + genuine
12-bit + 16-bit-alpha-entropy paths as the interlaced cases but
exercises the §7.2 Figure 4 progressive block scan instead of the
§7.5.3 field-pair deinterleave; the decoded alpha gradient again
round-trips with sub-LSB mean-abs-error (≈0.17).
The mainstream progressive 4:2:2 forward path (encode_frame_with_depth,
single picture, §7.2 Figure 4 scan) cross-decodes through ffmpeg's
prores decoder for all four base profiles — apco / apcs / apcn / apch —
at 62.4-64.2 dB luma PSNR across 8-, 10-, and 12-bit sources (64×48 and
128×96). The 10-/12-bit cases drive genuine high-bit-depth read_sample
input through the §7.5.1 level shift; since ffmpeg's 4:2:2 decode is
10-bit internally, all cases compare at 10-bit (8-bit ⟵ << 2, 12-bit
⟵ >> 2), and a left/right luma-ramp assertion guards against a
mis-scanned picture. See tests/ffmpeg_cross_decode.rs.
The symmetric progressive 4:4:4 without alpha forward path
(encode_frame_with_depth with ChromaFormat::Y444, single picture,
§7.2 Figure 4 scan, alpha_channel_type == 0) cross-decodes through
ffmpeg's prores_ks decoder for both 4444 profiles — ap4h and ap4x —
at 64.74-64.97 dB luma PSNR across 8-, 10-, and 12-bit sources (64×48
and 128×96). This is the mainstream no-alpha 4:4:4 path: a
registry-built encoder asked for PixelFormat::Yuv444P* with no 4th
alpha plane takes it, and it was previously only validated by
self-roundtrip — the existing 4:4:4 ffmpeg coverage was all on the
4444 + alpha entry point. The full-resolution 4:4:4 chroma (twice the
chroma macroblock grid of 4:2:2) exercises the §7.2 progressive block
scan with the deeper chroma slice payload 4:2:2 cannot reach; ffmpeg's
ap4h/ap4x no-alpha decode is 12-bit internally, so all cases compare
at 12-bit (8-bit ⟵ << 4, 10-bit ⟵ << 2, 12-bit as-is). See
tests/ffmpeg_cross_decode.rs.
Usage
[]
= "0.1"
= "0.1"
= "0.0"
Profile selection
The encoder picks a profile from the combination of pixel_format and
bit_rate:
pixel_format |
bit_rate hint (bps) |
Picked profile |
|---|---|---|
Yuv422P |
<= 70_000_000 |
Proxy |
Yuv422P |
<= 125_000_000 |
LT |
Yuv422P |
<= 180_000_000 or None |
Standard |
Yuv422P |
> 180_000_000 |
HQ |
Yuv444P |
>= 400_000_000 |
4444 XQ |
Yuv444P |
anything else | 4444 |
use CodecRegistry;
use ;
let mut reg = new;
register;
let mut params = video;
params.width = Some;
params.height = Some;
params.pixel_format = Some;
params.bit_rate = Some; // -> 422 HQ
let mut enc = reg.make_encoder?;
# Ok::
Codec id
- Codec:
"prores"; accepted pixel formatsYuv422P,Yuv444P. - Keyframe-only (all ProRes frames are intra).
Performance
A Criterion benchmark covers the decode hot path. Inputs are synthesized in-process via this crate's own encoder (no external fixtures); the encode cost is paid once at setup and excluded from the measured region. Each case decodes a single 128×96 frame:
| Case | Profile / format | median time |
|---|---|---|
apcn_422_8bit_128x96 |
422 Standard, 8-bit | ~68 µs |
ap4h_444_8bit_128x96 |
4444, 8-bit | ~106 µs |
apcn_422_10bit_128x96 |
422 Standard, 10-bit LE | ~69 µs |
Numbers are a single-machine baseline (Apple Silicon, --measurement-time 3); treat them as relative, not absolute. The 4:4:4 case is ~1.5× the
4:2:2 cost as expected (full-width chroma planes carry twice the
coefficient data). 10-bit tracks 8-bit closely since the extra work is
output sample packing, not entropy decode.
Run it:
A second Criterion benchmark (benches/encode.rs) covers the encode
hot path across all six RDD 36 profiles on a synthetic 128×96 gradient,
each at its default quantization_index, plus a 10-bit and an
interlaced (top-field-first) case:
| Case | Profile / format | median time |
|---|---|---|
apco_422_8bit_128x96 |
422 Proxy, 8-bit | ~63 µs |
apcs_422_8bit_128x96 |
422 LT, 8-bit | ~62 µs |
apcn_422_8bit_128x96 |
422 Standard, 8-bit | ~72 µs |
apch_422_8bit_128x96 |
422 HQ, 8-bit | ~85 µs |
ap4h_444_8bit_128x96 |
4444, 8-bit | ~111 µs |
ap4x_444_8bit_128x96 |
4444 XQ, 8-bit | ~136 µs |
apcn_422_10bit_128x96 |
422 Standard, 10-bit LE | ~80 µs |
apcn_422_8bit_interlaced_tff_128x96 |
422 Standard, interlaced | ~79 µs |
Encode cost climbs as the profile's default qi falls (Proxy qi 8 → 4444 XQ qi 1): finer quantisation leaves more non-zero coefficients for the run/level/sign coder to emit. The 4:4:4 profiles cost ~1.5–2× their 4:2:2 counterparts (full-width chroma planes). 10-bit tracks 8-bit closely; the interlaced case (two field pictures) matches the progressive Standard cost since total coefficient work is unchanged.
Fast paths in the DCT module
Both the decoder and the encoder probe the 8x8 block before invoking the textbook forward / inverse DCT and dispatch to a constant-time fast path on the relevant degenerate case:
| Path | Trigger condition | Cost vs general |
|---|---|---|
[dct::idct8x8_dc_only] |
dequantised block has all 63 AC = 0 | 1 multiply + 64 stores vs 64 × 16 mul-adds |
[dct::fdct8x8_constant] |
input block has all 64 samples bit-identical | 1 multiply + 63 stores vs 64 × 16 mul-adds |
The decode-side idct8x8_dc_only fires whenever the entropy coder's
endOfData() produced a smooth block — common at higher quantisation
indices on natural content. The encode-side fdct8x8_constant fires
on boundary-clamp pad blocks (RDD 36 §7.5.1 replicates the last
sample to fill a partial MB row), large flat areas, and the smooth
regions of synthetic gradients. Both paths are verified bit-exact
against the general DCT loops on every plausible input level
(dct::tests::idct_dc_only_matches_general_idct,
dct::tests::fdct_constant_matches_general_fdct) and end-to-end via
encoder::tests::constant_flat_frame_decodes_pixel_exact_at_hq, which
encodes a flat-value-v 64×48 frame at HQ (qi=2) and asserts every
Y-plane byte round-trips to v exactly.
Fuzzing
A cargo-fuzz harness lives under fuzz/ with three panic-free
targets that drive arbitrary attacker-controlled bytes through
ProRes's public decode entry points and header parsers:
decode_packet— feeds bytes into [decoder::decode_packet]. Covers the RDD 36 §5.1 frame() outer framing, §6.1.1 frame_header() (shall refuseclauses forbitstream_version > 1, reserved interlace_mode 3, v0 stream constraints on chroma_format / alpha_channel_type, out-of-range qmat entries), §6.3 picture_header() + slice_table(), the §5.3 + §7.1.1 run/level/sign coefficient coder, the §7.1.2 + Table 12-14 alpha run-length VLC for ap4h / ap4x, and the §5.1 ProRes RAW (aprn/aprh) refusal path.decode_packet_with_depth— same parse chain, but additionally exercises [decoder::decode_packet_with_depth]'s caller-supplied(BitDepth, ChromaFormat)output formatter (RDD 36 §7.5.1 level shifts forb = 8 / 10 / 12). The bit-depth and chroma tags are derived from the input's own first byte so libFuzzer steers mutations across all six (depth × chroma) combinations the registry route can reach.parse_headers— feeds independent input slices into all four public header parsers in [frame]: [frame::parse_frame] (§5.1 outer framing), [frame::parse_frame_header] (§6.1.1 frame_header- optional 64-byte luma + chroma quant matrices), [
frame::parse_picture_header] (§6.3 picture_header), and [frame::parse_slice_header] (§5.3 slice_header, both with-alpha and no-alpha shapes). These parsers are reachable from any application that bypasses the top-level decode path (e.g. a sample-bytes inspector) and are far cheaper to drive than the full decode chain, so libFuzzer can explore the header arithmetic and quant-matrix loading branches at a higher rate per second.
- optional 64-byte luma + chroma quant matrices), [
The two decode-pipeline harnesses peek at the wire-stream width /
height (bytes 16..18 and 18..20, BE u16) and bail out if width × height exceeds 65 536 pixels, so libFuzzer doesn't waste cycles on
inputs that the upstream OOM cap would reject anyway. A daily 30-minute
GitHub Actions run is scheduled under .github/workflows/fuzz.yml,
splitting the budget across the three targets.
# nightly toolchain required by libfuzzer-sys
&&
License
MIT — see LICENSE.