PR #25436 opened by mrowlinson URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25436 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25436.patch
SEND NOTE — branch `mrowlinson:dovi-rpu-envelope` onto `master @ b5939983`, 13 commits, sealed tree `860c0358`. Gates: `git am` 13/13 clean on pristine b593998; full build warnings 0 in patched files; `make fate-list` identical base-vs-tip (3157 lines); fate-dovi-rpu-roundtrip 11/11, mux-copy 3/3, generate 5/5, p5-convert 9/9, p5-edit 19/19, p6-mux 60/60, p6-x264 16/16; oracle round-trip 705/705 MATCH Reviewer notes: Cover below was written for the 12-patch seal (base a881f0a9); this send is the comply re-cut: 13 patches (added 13/13 Changelog entry), base b5939983. FATE fragments in 10-12 are local-harness shape — take as RFC. --- Base: ffmpeg a881f0a9 (ffplay: only compile bprint_styles() with libass). Series: 12 patches, 18 files, +948/-50. No new deps (libx264 hook uses the existing optional-encoder pattern, same as libx265). Subject: [PATCH 0/12] Dolby Vision: opaque RPU ext passthrough + raw HEVC dvcC/dvh1 mux + mkv/ts mux + x264 hook + L11 byte-1 fix + FATE fragments + p5gap edit cases WHAT ---- Dolby Vision RPU carries extension blocks FFmpeg cannot parse (level 253, reserved levels, foreign-section blocks); those RPUs either fail to regenerate (EINVAL) or silently regroup across DM sections. And a raw HEVC+RPU stream copy to MP4 loses all DV signalling (hev1, no dvcC) because no demuxer attaches DOVI_CONF. v5 bodies are carried forward unchanged onto the fresh master tip (re-cut, zero drift — see below), including the p5gap editor cases as 12/12: 1/12 headers: opaque ext-block carriage (dovi_meta.h, dovi_rpu.h) 2/12 rpudec: carry unknown ext levels opaque, keep block order 3/12 rpuenc: re-emit opaque ext blocks byte-exactly 4/12 hevcdec: attach DOVI_CONF from first RPU in raw HEVC 5/12 movenc/isom_tags: write dvh1/dvhe when DOVI_CONF is present 6/12 matroska: carry profiles 11-19 as dvwC mapping (+demux accept) 7/12 mpegtsenc: write DOVI video stream descriptor (tag 0xB0) 8/12 libx264: Dolby Vision hook (profile-9 configure + T.35 SEI inject) 9/12 rpudec+rpuenc: fix L11 byte-1 layout ((ref<<4)|whitepoint) 10/12 fate/dovi-p6: P6 sweep FATE fragment + refs (mux matrix, x264) 11/12 fate/dovi: wire DOVI FATE fragments into tests/Makefile 12/12 fate/dovi-p5: cover p5gap editor cases (admd + transfer) 12 carries the p5gap edit-suite growth verbatim: ed_admd + ed_tx6 join P5_ED (19 cases), pinned by the p5gap oracle fixtures. The p5gap library-side behavior (st2094-superset reject, m5 editor parity, transfer/admd ops) lives in the local rpu_edit tool, not in libav, so it needs no libav patch. The FATE fragments are local-harness shape (absolute scratch paths, in-tree harness binaries); take 10-12 as RFC. The generate/edit tooling (tools/rpu_generate.c, tools/rpu_edit.c) is tools/, not libav, so it needs no liblibav patch; out of scope here beyond this note. WHY --- Parse -> generate round-trip is byte-exact on 705/705 oracle RPU units at tip; stream-copy keeps DV signalling on mp4 (dvh1/dvcC), mkv (dvwC for profiles 11-19) and mpeg-ts (0xB0 descriptor); x264 can now carry RPU the same way the x265 path does (T.35 SEI, profile 9); L11 metadata now parses to the oracle's own values on ref=0 and ref=1 units; the P6 sweep + x264 hook ship their FATE coverage in-tree; and the P5 edit suite now covers the transfer/admd ops (19/19 vs oracle). HOW TESTED ----------- - 12/12 patches `git am` clean in series order on 2 pristine clones of the fresh tip; tip trees byte-identical (tree 185f9b1079f6eab...); `git diff --check` clean over the series. - Full builds on base + both verify legs (`--enable-gpl --enable-libx264 --enable-libx265`); warnings identical base vs tip (28 lines, sorted diff empty, 0 in any patched/DOVI file); fate-list identical base vs tip (3156 lines, sorted diff empty), new targets fate-dovi-p6-mux + fate-dovi-p5-edit resolve (`-n` rc=0). - Both legs: fate-dovi-rpu-roundtrip 11/11, fate-dovi-mux-copy 3/3, fate-dovi-generate 5/5 MATCH, fate-dovi-p5-convert 9/9, fate-dovi-p5-edit 19/19, fate-dovi-p6-mux 60/60 + fate-dovi-p6-x264 16/16 GREEN (empty diffs vs tests/ref/fate throughout). - Libav delta of 10-12 is tests/ only, so the v3 oracle matrix 705/705 MATCH carries over unchanged. - Re-cut audit: submit-0001..0012 hunks are content-identical to the validated v5 set (only From SHAs, blob-id width, hunk offsets and the version footer differ) — zero upstream drift in the touched files between the v5 base and this tip. - L11 probes (black-box, dovi_tool 2.3.4): see FATE-MANIFEST-V3.md. - P4 G4-check: generate 5/5 byte-parity vs committed oracle bins holds on both legs (see DOVI-P4.md). From 70f7e4ae46a256571888e6f64bcbee2968db2dec Mon Sep 17 00:00:00 2001 From: Michael Rowlinson <[email protected]> Date: Tue, 20 Oct 2026 09:34:45 -0400 Subject: [PATCH 01/13] avutil/dovi_meta, avcodec/dovi_rpu: add opaque ext-block carriage Add the storage both sides of the RPU ext-block path need before any behavior changes: AVDOVIDmLevelRaw (opaque payload up to AV_DOVI_MAX_RAW_EXT_LEN bytes, sized to fit the AVDOVIDmData union without growing it) plus an opaque_ver selector on AVDOVIDmData (0 = natively parsed, else the DM section whose raw payload is kept). Also add a bitstream-order log to DOVIExt (push/drop_dynamic/ remap_static helpers) and an ff_dovi_rpu_ext_is_known() classifier mapping each level to its home section (CMv2.9 vs CMv4.0). No decoder or encoder behavior changes yet. Signed-off-by: Michael Rowlinson <[email protected]> --- libavcodec/dovi_rpu.h | 80 +++++++++++++++++++++++++++++++++++++++++-- libavutil/dovi_meta.h | 23 +++++++++++++ 2 files changed, 101 insertions(+), 2 deletions(-) diff --git a/libavcodec/dovi_rpu.h b/libavcodec/dovi_rpu.h index 1b74983205d..e89870c64b6 100644 --- a/libavcodec/dovi_rpu.h +++ b/libavcodec/dovi_rpu.h @@ -31,12 +31,19 @@ #include "codec_par.h" #define DOVI_MAX_DM_ID 15 +#define DOVI_MAX_STATIC_EXT 7 +#define DOVI_MAX_DYNAMIC_EXT 25 typedef struct DOVIExt { - AVDOVIDmData dm_static[7]; ///< static extension blocks - AVDOVIDmData dm_dynamic[25]; ///< dynamic extension blocks + AVDOVIDmData dm_static[DOVI_MAX_STATIC_EXT]; ///< static extension blocks + AVDOVIDmData dm_dynamic[DOVI_MAX_DYNAMIC_EXT]; ///< dynamic extension blocks int num_static; int num_dynamic; + struct { + uint8_t is_static; ///< 1: dm_static[idx], 0: dm_dynamic[idx] + uint8_t idx; ///< slot inside dm_static/dm_dynamic + } order[DOVI_MAX_STATIC_EXT + DOVI_MAX_DYNAMIC_EXT]; ///< bitstream order + int num_order; } DOVIExt; typedef struct DOVIContext { @@ -208,4 +215,73 @@ static inline int ff_dovi_rpu_extension_is_static(int level) } } +/** + * True if this level has a known payload layout in the given DM version + * section (1 = CMv2.9, 2 = CMv4.0). All other levels are carried opaque. + */ +static inline int ff_dovi_rpu_ext_is_known(int level, int ver) +{ + if (ver == 1) { + switch (level) { + case 1: + case 2: + case 4: + case 5: + case 6: + case 255: + return 1; + } + } else if (ver == 2) { + switch (level) { + case 3: + case 8: + case 9: + case 10: + case 11: + case 254: + return 1; + } + } + return 0; +} + +/* Record a stored block in bitstream order. Every block stored into + * dm_static/dm_dynamic pushes exactly one entry, so the log (32 slots + * for 7 + 25 blocks) cannot overflow. */ +static inline void ff_dovi_ext_push(DOVIExt *ext, int is_static, int idx) +{ + ext->order[ext->num_order].is_static = !!is_static; + ext->order[ext->num_order].idx = idx; + ext->num_order++; +} + +/* Drop dynamic entries from the order log (compressed updates replace + * the dynamic set). Static slots are untouched, surviving entries stay + * valid. */ +static inline void ff_dovi_ext_drop_dynamic(DOVIExt *ext) +{ + int n = 0; + for (int i = 0; i < ext->num_order; i++) { + if (ext->order[i].is_static) + ext->order[n++] = ext->order[i]; + } + ext->num_order = n; +} + +/* Remap the order log after swapping two dm_static slots, keeping every + * entry pointed at the same block. */ +static inline void ff_dovi_ext_remap_static(DOVIExt *ext, int a, int b) +{ + if (a == b) + return; + for (int i = 0; i < ext->num_order; i++) { + if (!ext->order[i].is_static) + continue; + if (ext->order[i].idx == a) + ext->order[i].idx = b; + else if (ext->order[i].idx == b) + ext->order[i].idx = a; + } +} + #endif /* AVCODEC_DOVI_RPU_H */ diff --git a/libavutil/dovi_meta.h b/libavutil/dovi_meta.h index c102bf384f4..e3a90b4e4da 100644 --- a/libavutil/dovi_meta.h +++ b/libavutil/dovi_meta.h @@ -308,6 +308,20 @@ typedef struct AVDOVIDmLevel255 { uint8_t dm_debug[4]; } AVDOVIDmLevel255; +/** + * Largest opaque payload kept for passthrough of unknown/reserved + * extension levels (e.g. level 253). Sized to fit the AVDOVIDmData + * union without growing it. + */ +#define AV_DOVI_MAX_RAW_EXT_LEN 64 + +typedef struct AVDOVIDmLevelRaw { + /* Opaque payload of a block carried for passthrough (see opaque_ver + * below), reproduced byte-exactly on re-encode. */ + uint8_t len; /* payload length in bytes */ + uint8_t data[AV_DOVI_MAX_RAW_EXT_LEN]; +} AVDOVIDmLevelRaw; + /** * Dolby Vision metadata extension block. Dynamic extension blocks may change * from frame to frame, while static blocks are constant throughout the entire @@ -317,6 +331,14 @@ typedef struct AVDOVIDmLevel255 { */ typedef struct AVDOVIDmData { uint8_t level; /* [1, 255] */ + /** + * Opaque carriage: 0 for a natively parsed block (union member selected + * by level), else the DM section (1 = CMv2.9, 2 = CMv4.0) whose raw + * payload is kept in `raw`. Set by the decoder for unknown/reserved + * levels and for known levels found in the other section; the encoder + * re-emits such blocks byte-exactly into their recorded section. + */ + uint8_t opaque_ver; union { AVDOVIDmLevel1 l1; /* dynamic */ AVDOVIDmLevel2 l2; /* dynamic, may appear multiple times */ @@ -331,6 +353,7 @@ typedef struct AVDOVIDmData { AVDOVIDmLevel11 l11; /* dynamic */ AVDOVIDmLevel254 l254; /* static */ AVDOVIDmLevel255 l255; /* static */ + AVDOVIDmLevelRaw raw; /* unknown/reserved levels, opaque */ }; } AVDOVIDmData; -- 2.54.0 From 0155b55eefc9e14525b392fc5e2dafe2735bc1ba Mon Sep 17 00:00:00 2001 From: Michael Rowlinson <[email protected]> Date: Tue, 20 Oct 2026 09:34:45 -0400 Subject: [PATCH 02/13] avcodec/dovi_rpudec: carry unknown ext levels opaque, keep block order Route ext blocks with no known layout in their DM section through a new opaque path instead of failing the RPU: the raw payload is kept with its section, and every stored block (static or dynamic) is logged in bitstream order so ff_dovi_get_metadata() reproduces the on-wire order instead of static-first. Oversized opaque blocks (past AV_DOVI_MAX_RAW_EXT_LEN) are dropped with a request_sample note rather than failing the whole unit, and compressed updates prune only the dynamic entries from the order log. Fixes L253 decode errors and foreign-section regrouping (e.g. a CMv2.9 level 3 inside a v1 section no longer moves into the v2 section). Signed-off-by: Michael Rowlinson <[email protected]> --- libavcodec/dovi_rpudec.c | 68 +++++++++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 12 deletions(-) diff --git a/libavcodec/dovi_rpudec.c b/libavcodec/dovi_rpudec.c index 79717f605f6..de34627d1d3 100644 --- a/libavcodec/dovi_rpudec.c +++ b/libavcodec/dovi_rpudec.c @@ -51,10 +51,21 @@ int ff_dovi_get_metadata(DOVIContext *s, AVDOVIMetadata **out_metadata) if (s->ext_blocks) { const DOVIExt *ext = s->ext_blocks; size_t ext_sz = FFMIN(sizeof(AVDOVIDmData), dovi->ext_block_size); - for (int i = 0; i < ext->num_static; i++) - memcpy(av_dovi_get_ext(dovi, dovi->num_ext_blocks++), &ext->dm_static[i], ext_sz); - for (int i = 0; i < ext->num_dynamic; i++) - memcpy(av_dovi_get_ext(dovi, dovi->num_ext_blocks++), &ext->dm_dynamic[i], ext_sz); + if (ext->num_order == ext->num_static + ext->num_dynamic) { + /* bitstream order */ + for (int i = 0; i < ext->num_order; i++) { + const AVDOVIDmData *dm = ext->order[i].is_static + ? &ext->dm_static[ext->order[i].idx] + : &ext->dm_dynamic[ext->order[i].idx]; + memcpy(av_dovi_get_ext(dovi, dovi->num_ext_blocks++), dm, ext_sz); + } + } else { + /* order log inconsistent; fall back to static-first */ + for (int i = 0; i < ext->num_static; i++) + memcpy(av_dovi_get_ext(dovi, dovi->num_ext_blocks++), &ext->dm_static[i], ext_sz); + for (int i = 0; i < ext->num_dynamic; i++) + memcpy(av_dovi_get_ext(dovi, dovi->num_ext_blocks++), &ext->dm_dynamic[i], ext_sz); + } } *out_metadata = dovi; @@ -147,7 +158,26 @@ static inline unsigned get_variable_bits(GetBitContext *gb, int n) } \ } while (0) -static int parse_ext_v1(DOVIContext *s, GetBitContext *gb, AVDOVIDmData *dm) +static void parse_ext_raw(DOVIContext *s, GetBitContext *gb, AVDOVIDmData *dm, + int ver, int ext_block_length) +{ + /* Unknown/reserved level: keep the payload opaque for passthrough. + * Oversized blocks never reach here (routed to a dummy above). */ + if (ff_dovi_rpu_ext_is_known(dm->level, 3 - ver)) { + av_log(s->logctx, AV_LOG_WARNING, "Level %u block in DM v%d " + "section, carrying it opaque\n", dm->level, ver); + } else { + avpriv_request_sample(s->logctx, "Dolby Vision DM v%d level %u", + ver, dm->level); + } + dm->opaque_ver = ver; + dm->raw.len = ext_block_length; + for (int i = 0; i < ext_block_length; i++) + dm->raw.data[i] = get_bits(gb, 8); +} + +static int parse_ext_v1(DOVIContext *s, GetBitContext *gb, AVDOVIDmData *dm, + int ver, int ext_block_length) { switch (dm->level) { case 1: @@ -188,7 +218,7 @@ static int parse_ext_v1(DOVIContext *s, GetBitContext *gb, AVDOVIDmData *dm) dm->l255.dm_debug[i] = get_bits(gb, 8); break; default: - avpriv_request_sample(s->logctx, "Dolby Vision DM v1 level %u", dm->level); + parse_ext_raw(s, gb, dm, ver, ext_block_length); } return 0; @@ -204,7 +234,7 @@ static AVCIExy get_cie_xy(GetBitContext *gb) } static int parse_ext_v2(DOVIContext *s, GetBitContext *gb, AVDOVIDmData *dm, - int ext_block_length) + int ver, int ext_block_length) { switch (dm->level) { case 3: @@ -269,7 +299,7 @@ static int parse_ext_v2(DOVIContext *s, GetBitContext *gb, AVDOVIDmData *dm, dm->l254.dm_version_index = get_bits(gb, 8); break; default: - avpriv_request_sample(s->logctx, "Dolby Vision DM v2 level %u", dm->level); + parse_ext_raw(s, gb, dm, ver, ext_block_length); } return 0; @@ -299,7 +329,15 @@ static int parse_ext_blocks(DOVIContext *s, GetBitContext *gb, int ver, level = get_bits(gb, 8); start_pos = get_bits_count(gb); - if (ff_dovi_rpu_extension_is_static(level)) { + if (!ff_dovi_rpu_ext_is_known(level, ver) && + ext_block_length > AV_DOVI_MAX_RAW_EXT_LEN) { + /* Too large to carry opaque; drop it rather than failing + * the whole RPU downstream. */ + avpriv_request_sample(s->logctx, "Dolby Vision DM v%d level %u " + "(%d bytes, exceeds passthrough limit)", + ver, level, ext_block_length); + dm = &dummy; + } else if (ff_dovi_rpu_extension_is_static(level)) { if (compression) { av_log(s->logctx, AV_LOG_WARNING, "Compressed DM RPU contains " "static extension block level %d\n", level); @@ -310,18 +348,20 @@ static int parse_ext_blocks(DOVIContext *s, GetBitContext *gb, int ver, if (ext->num_static >= FF_ARRAY_ELEMS(ext->dm_static)) return AVERROR_INVALIDDATA; dm = &ext->dm_static[ext->num_static++]; + ff_dovi_ext_push(ext, 1, ext->num_static - 1); } } else { if (ext->num_dynamic >= FF_ARRAY_ELEMS(ext->dm_dynamic)) return AVERROR_INVALIDDATA; dm = &ext->dm_dynamic[ext->num_dynamic++]; + ff_dovi_ext_push(ext, 0, ext->num_dynamic - 1); } memset(dm, 0, sizeof(*dm)); dm->level = level; switch (ver) { - case 1: ret = parse_ext_v1(s, gb, dm); break; - case 2: ret = parse_ext_v2(s, gb, dm, ext_block_length); break; + case 1: ret = parse_ext_v1(s, gb, dm, ver, ext_block_length); break; + case 2: ret = parse_ext_v2(s, gb, dm, ver, ext_block_length); break; default: avpriv_request_sample(s->logctx, "Dolby Vision DM v%d", ver); goto skip; @@ -704,8 +744,12 @@ int ff_dovi_rpu_parse(DOVIContext *s, const uint8_t *rpu, size_t rpu_size, /* Parse extension blocks */ if (s->ext_blocks) { DOVIExt *ext = s->ext_blocks; - if (!dm_compression) + if (!dm_compression) { ext->num_static = 0; + ext->num_order = 0; + } else { + ff_dovi_ext_drop_dynamic(ext); + } ext->num_dynamic = 0; } if ((ret = parse_ext_blocks(s, gb, 1, dm_compression, err_recognition)) < 0) { -- 2.54.0 From 731e679a65d26140a099c84a98c04fa1eaac7685 Mon Sep 17 00:00:00 2001 From: Michael Rowlinson <[email protected]> Date: Tue, 20 Oct 2026 09:34:45 -0400 Subject: [PATCH 03/13] avcodec/dovi_rpuenc: re-emit opaque ext blocks byte-exactly Teach the generator the opaque_ver carriage from the headers patch: blocks flagged opaque are written back with their recorded length, level, payload bytes and section, and their bytes are reserved in the buffer-size estimate. Block counting moves from a level switch to the section-aware classifier so foreign-section blocks land in their recorded section, and the ext-block cache keeps the same order log the parser maintains (including the swap remap and compressed-update pruning). Together with the parser patch this makes parse -> generate round-trip byte-exact on RPUs carrying unknown/reserved levels or foreign-section blocks. Signed-off-by: Michael Rowlinson <[email protected]> --- libavcodec/dovi_rpuenc.c | 112 ++++++++++++++++++++++++++++++--------- 1 file changed, 88 insertions(+), 24 deletions(-) diff --git a/libavcodec/dovi_rpuenc.c b/libavcodec/dovi_rpuenc.c index 4bd87f496fe..30252693ffd 100644 --- a/libavcodec/dovi_rpuenc.c +++ b/libavcodec/dovi_rpuenc.c @@ -313,8 +313,10 @@ static int try_reuse_ext(DOVIExt *ext, const AVDOVIMetadata *metadata) /* Find the first matching ext block and move it to [idx] */ for (j = idx; j < ext->num_static; j++) { if (!memcmp(&ext->dm_static[j], dm, sizeof(*dm))) { - if (j != idx) + if (j != idx) { FFSWAP(AVDOVIDmData, ext->dm_static[j], ext->dm_static[idx]); + ff_dovi_ext_remap_static(ext, j, idx); + } idx++; break; } @@ -456,10 +458,24 @@ static int av_q2den(AVRational q, int den) return (q.num + (q.den >> 1)) / q.den; } +static void generate_ext_raw(PutBitContext *pb, const AVDOVIDmData *dm) +{ + set_ue_golomb(pb, dm->raw.len); + put_bits(pb, 8, dm->level); + for (int i = 0; i < dm->raw.len; i++) + put_bits(pb, 8, dm->raw.data[i]); +} + static void generate_ext_v1(PutBitContext *pb, const AVDOVIDmData *dm) { int ext_block_length, start_pos, pad_bits; + if (dm->opaque_ver) { + if (dm->opaque_ver == 1) + generate_ext_raw(pb, dm); + return; + } + switch (dm->level) { case 1: ext_block_length = 5; break; case 2: ext_block_length = 11; break; @@ -534,6 +550,12 @@ static void generate_ext_v2(PutBitContext *pb, const AVDOVIDmData *dm) { int ext_block_length, start_pos, pad_bits; + if (dm->opaque_ver) { + if (dm->opaque_ver == 2) + generate_ext_raw(pb, dm); + return; + } + switch (dm->level) { case 3: ext_block_length = 5; break; case 8: @@ -650,7 +672,9 @@ int ff_dovi_rpu_generate(DOVIContext *s, const AVDOVIMetadata *metadata, const AVDOVIColorMetadata *color; int vdr_dm_metadata_present, vdr_rpu_id, use_prev_vdr_rpu, profile, buffer_size, rpu_size, pad, zero_run, dm_compression; + int buffer_size_raw = 0; int num_ext_blocks_v1, num_ext_blocks_v2; + int n_static = 0, n_dynamic = 0; int dv_md_compression = s->cfg.dv_md_compression; uint32_t crc; uint8_t *dst; @@ -748,33 +772,66 @@ int ff_dovi_rpu_generate(DOVIContext *s, const AVDOVIMetadata *metadata, if (dm_compression && ff_dovi_rpu_extension_is_static(dm->level)) continue; - switch (dm->level) { - case 1: - case 2: - case 4: - case 5: - case 6: - case 255: + /* Storage below holds DOVI_MAX_STATIC_EXT + DOVI_MAX_DYNAMIC_EXT. */ + if (ff_dovi_rpu_extension_is_static(dm->level)) { + if (++n_static > DOVI_MAX_STATIC_EXT) { + av_log(s->logctx, AV_LOG_ERROR, "Too many static ext blocks\n"); + return AVERROR_INVALIDDATA; + } + } else { + if (++n_dynamic > DOVI_MAX_DYNAMIC_EXT) { + av_log(s->logctx, AV_LOG_ERROR, "Too many dynamic ext blocks\n"); + return AVERROR_INVALIDDATA; + } + } + + if (dm->opaque_ver == 1) { num_ext_blocks_v1++; - break; - case 3: - case 8: - case 9: - case 10: - case 11: - case 254: + } else if (dm->opaque_ver == 2) { num_ext_blocks_v2++; - break; - default: - av_log(s->logctx, AV_LOG_ERROR, "Invalid ext block level %d\n", - dm->level); + } else if (!dm->opaque_ver) { + switch (dm->level) { + case 1: + case 2: + case 4: + case 5: + case 6: + case 255: + num_ext_blocks_v1++; + break; + case 3: + case 8: + case 9: + case 10: + case 11: + case 254: + num_ext_blocks_v2++; + break; + default: + av_log(s->logctx, AV_LOG_ERROR, "Invalid ext block level %d\n", + dm->level); + return AVERROR_INVALIDDATA; + } + continue; + } else { + av_log(s->logctx, AV_LOG_ERROR, "Invalid opaque DM version %d\n", + dm->opaque_ver); return AVERROR_INVALIDDATA; } + + /* Opaque block: validate carried payload, reserve its bytes. */ + if (dm->raw.len > AV_DOVI_MAX_RAW_EXT_LEN) { + av_log(s->logctx, AV_LOG_ERROR, "Ext block level %d payload " + "too large (%u bytes)\n", dm->level, dm->raw.len); + return AVERROR_INVALIDDATA; + } + buffer_size_raw += dm->raw.len; } buffer_size = 12 /* vdr seq info */ + 5 /* CRC32 + terminator */; buffer_size += num_ext_blocks_v1 * 13; buffer_size += num_ext_blocks_v2 * 28; + buffer_size += buffer_size_raw; if (!use_prev_vdr_rpu) { buffer_size += 160; for (int c = 0; c < 3; c++) { @@ -943,14 +1000,21 @@ int ff_dovi_rpu_generate(DOVIContext *s, const AVDOVIMetadata *metadata, if (ext) { size_t ext_sz = FFMIN(sizeof(AVDOVIDmData), metadata->ext_block_size); ext->num_dynamic = 0; - if (!dm_compression) + if (!dm_compression) { ext->num_static = 0; + ext->num_order = 0; + } else { + ff_dovi_ext_drop_dynamic(ext); + } for (int i = 0; i < metadata->num_ext_blocks; i++) { const AVDOVIDmData *dm = av_dovi_get_ext(metadata, i); - if (!ff_dovi_rpu_extension_is_static(dm->level)) - memcpy(&ext->dm_dynamic[ext->num_dynamic++], dm, ext_sz); - else if (!dm_compression) - memcpy(&ext->dm_static[ext->num_static++], dm, ext_sz); + if (!ff_dovi_rpu_extension_is_static(dm->level)) { + memcpy(&ext->dm_dynamic[ext->num_dynamic], dm, ext_sz); + ff_dovi_ext_push(ext, 0, ext->num_dynamic++); + } else if (!dm_compression) { + memcpy(&ext->dm_static[ext->num_static], dm, ext_sz); + ff_dovi_ext_push(ext, 1, ext->num_static++); + } } } } else { -- 2.54.0 From 7116af1c89767aa3383f01bfcd803f3ae72d7828 Mon Sep 17 00:00:00 2001 From: Michael Rowlinson <[email protected]> Date: Tue, 20 Oct 2026 09:34:45 -0400 Subject: [PATCH 04/13] avformat/hevcdec: attach DOVI_CONF from first RPU in raw HEVC The raw HEVC demuxer never attached Dolby Vision configuration, so an HEVC+RPU stream copy lost all DV signalling (hev1, no dvcC). Hunt the first type-62 NAL: peek at it in read_header for seekable inputs (1 MB cap; the mux setup snapshots codecpar before any packet flows, so a packet-time attach would arrive too late), with a packet-path fallback (16 MB cap, chunked growth, silent give-up at EOS) covering pipes and late RPUs. The found RPU is unescaped, parsed with ff_dovi_rpu_parse, and attached as DOVI_CONF (profile from the RPU guess, level from the RPU header, compat 0 for profile 5 / 1 for 8.x); profiles 4/7 are skipped like the encoder does. Signed-off-by: Michael Rowlinson <[email protected]> --- libavformat/hevcdec.c | 328 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 327 insertions(+), 1 deletion(-) diff --git a/libavformat/hevcdec.c b/libavformat/hevcdec.c index 4fc4068cd59..9035efb1a89 100644 --- a/libavformat/hevcdec.c +++ b/libavformat/hevcdec.c @@ -19,9 +19,13 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ +#include "libavcodec/dovi_rpu.h" #include "libavcodec/hevc/hevc.h" +#include "libavutil/dovi_meta.h" +#include "libavutil/mem.h" #include "avformat.h" +#include "demux.h" #include "rawdec.h" static int hevc_probe(const AVProbeData *p) @@ -61,4 +65,326 @@ static int hevc_probe(const AVProbeData *p) return 0; } -FF_DEF_RAWVIDEO_DEMUXER(hevc, "raw HEVC video", hevc_probe, "hevc,h265,265", AV_CODEC_ID_HEVC) +/* Scan cap for the first-RPU hunt below: a Dolby Vision RPU rides as the + * last NAL of an AU, so one AU worth of bytes is plenty; beyond this we + * give up (verbose note, silent at EOS) rather than buffering a stream. + * The buffer grows in 1 MB steps and is freed as soon as the hunt ends. */ +#define HEVC_DOVI_SCAN_CAP (16 * 1024 * 1024) +#define HEVC_DOVI_SCAN_STEP (1024 * 1024) + +typedef struct HEVCDemuxerContext { + FFRawVideoDemuxerContext raw; /* must be first: raw options/class */ + uint8_t *scan_buf; + int scan_len; + int scan_cap; + int scan_pos; /* resume offset: bytes before it are settled */ + int64_t scan_next_pos; /* -1 until the first packet */ + int dovi_done; /* attach attempted (success or give-up) */ +} HEVCDemuxerContext; + +/* Annex-B start-code length at pos (0, 3 or 4), 0 when no start code. */ +static int hevc_sc_len(const uint8_t *buf, int len, int pos) +{ + if (pos + 3 <= len && !buf[pos] && !buf[pos + 1] && buf[pos + 2] == 1) + return 3; + if (pos + 4 <= len && !buf[pos] && !buf[pos + 1] && !buf[pos + 2] && + buf[pos + 3] == 1) + return 4; + return 0; +} + +/* Remove emulation-prevention bytes (00 00 03 -> 00 00). Returns new len. */ +static int hevc_unescape(uint8_t *dst, const uint8_t *src, int len) +{ + int i = 0, o = 0; + while (i < len) { + if (i + 2 < len && !src[i] && !src[i + 1] && src[i + 2] == 3) { + dst[o++] = 0; + dst[o++] = 0; + i += 3; + } else { + dst[o++] = src[i++]; + } + } + return o; +} + +/* Build a DOVI configuration record from one unescaped RPU (NAL prefix 25 + * included, NAL header excluded) and attach it as stream coded_side_data. + * Profiles 4/7 (enhancement layer) are skipped, mirroring the encoder. */ +static void hevc_attach_dovi_conf(AVFormatContext *s, AVStream *st, + const uint8_t *rpu, int rpu_len) +{ + DOVIContext dovi = { 0 }; + AVDOVIMetadata *md = NULL; + const AVDOVIRpuDataHeader *hdr; + AVDOVIDecoderConfigurationRecord *cfg; + size_t cfg_size; + int profile, compat_id, ret; + + if (av_packet_side_data_get(st->codecpar->coded_side_data, + st->codecpar->nb_coded_side_data, + AV_PKT_DATA_DOVI_CONF)) + return; /* already attached */ + + if ((ret = ff_dovi_rpu_parse(&dovi, rpu, rpu_len, 0)) < 0) + goto out; + if (ff_dovi_get_metadata(&dovi, &md) <= 0) + goto out; + hdr = av_dovi_get_header(md); + profile = ff_dovi_guess_profile_hevc(hdr); + if (profile != 5 && profile != 8) { + av_log(s, AV_LOG_INFO, "HEVC Dolby Vision RPU with unsupported " + "profile %d, not signalling dvcC.\n", profile); + goto out; + } + + /* Compatibility id follows the encoder convention (dovi_rpuenc.c): + * 5 is always 0; 8 needs BL colors the raw demuxer never has, so it + * defaults to 1 (8.1, by far the most common) and says so. */ + compat_id = profile == 5 ? 0 : 1; + if (profile == 8) + av_log(s, AV_LOG_INFO, "HEVC profile 8 RPU, assuming BL compatibility " + "id 1 (8.1); re-mux from a container for exact signalling.\n"); + + cfg = av_dovi_alloc(&cfg_size); + if (!cfg) + goto out; + cfg->dv_version_major = 1; + cfg->dv_version_minor = 0; + cfg->dv_profile = profile; + cfg->dv_level = hdr->vdr_rpu_level; + cfg->rpu_present_flag = 1; + cfg->el_present_flag = 0; + cfg->bl_present_flag = 1; + cfg->dv_bl_signal_compatibility_id = compat_id; + cfg->dv_md_compression = AV_DOVI_COMPRESSION_NONE; + + if (!av_packet_side_data_add(&st->codecpar->coded_side_data, + &st->codecpar->nb_coded_side_data, + AV_PKT_DATA_DOVI_CONF, + (uint8_t *) cfg, cfg_size, 0)) { + av_free(cfg); + goto out; + } + av_log(s, AV_LOG_INFO, "HEVC Dolby Vision RPU detected: profile %d, " + "level %d, dvcC will be written on MP4 mux.\n", + profile, cfg->dv_level); + +out: + av_free(md); + ff_dovi_ctx_unref(&dovi); +} + +/* Hunt scan_buf (from scan_pos) for a complete type-62 NAL and attach + * DOVI_CONF from the first one that parses. final marks an EOS-terminated + * buffer (short last packet), so a trailing NAL with no start code after + * it still counts. Returns 1 once the hunt is over. */ +static int hevc_scan_dovi(AVFormatContext *s, AVStream *st, + HEVCDemuxerContext *c, int final) +{ + while (c->scan_pos < c->scan_len) { + int pos = c->scan_pos; + int sc = hevc_sc_len(c->scan_buf, c->scan_len, pos); + int nal, hdr, end, payload; + + if (!sc) { + c->scan_pos++; + continue; + } + nal = pos + sc; + if (nal + 2 > c->scan_len) + return 0; /* NAL header split across reads, need more */ + if (((c->scan_buf[nal] >> 1) & 0x3f) != HEVC_NAL_UNSPEC62) { + c->scan_pos = nal; /* settled: skip code, rescan payload */ + continue; + } + /* Delimit: next start code, else EOS (trailing zeros are padding). */ + end = -1; + for (int q = nal + 2; q < c->scan_len; q++) { + if (hevc_sc_len(c->scan_buf, c->scan_len, q)) { + end = q; + break; + } + } + hdr = nal + 2; /* skip the 2-byte HEVC NAL header */ + if (end < 0) { + if (!final) + return 0; /* NAL may continue in the next read */ + end = c->scan_len; + while (end > hdr && !c->scan_buf[end - 1]) + end--; /* NAL padding */ + } + payload = end - hdr; + /* 25 prefix + terminator + CRC32 is the smallest plausible RPU. */ + if (payload > 6 && c->scan_buf[hdr] == 25) { + uint8_t *unesc = av_malloc(payload); + if (unesc) { + int ulen = hevc_unescape(unesc, c->scan_buf + hdr, payload); + hevc_attach_dovi_conf(s, st, unesc, ulen); + av_free(unesc); + } + } + c->scan_pos = end; + if (av_packet_side_data_get(st->codecpar->coded_side_data, + st->codecpar->nb_coded_side_data, + AV_PKT_DATA_DOVI_CONF)) + return 1; /* attached */ + /* Unparsable or wrong profile: keep hunting later RPUs. */ + } + return 0; +} + +/* Peek at the head of a seekable stream for a first-AU RPU and attach + * DOVI_CONF before any packet flows. This is the path that matters for + * muxing: output stream parameters are snapshotted from demuxer codecpar + * before the first read_packet, so packet-time attaches arrive too late. + * Falls back to the read_packet hunt (pipes, RPUs past the peek window). */ +#define HEVC_DOVI_PEEK_CAP (1024 * 1024) + +static int hevc_peek_dovi(AVFormatContext *s, AVStream *st, + HEVCDemuxerContext *c) +{ + int64_t pos0 = avio_tell(s->pb); + int final = 0, ret = 0; + + while (c->scan_len < HEVC_DOVI_PEEK_CAP) { + int want = HEVC_DOVI_PEEK_CAP - c->scan_len; + uint8_t *grown; + int n; + + if (c->scan_len + want > c->scan_cap) { + grown = av_realloc(c->scan_buf, c->scan_len + want); + if (!grown) + break; + c->scan_buf = grown; + c->scan_cap = c->scan_len + want; + } + n = avio_read(s->pb, c->scan_buf + c->scan_len, want); + if (n <= 0) { + final = 1; + break; + } + c->scan_len += n; + if (n < want) { + final = 1; /* short read: EOS */ + break; + } + /* A complete first AU (trailing start code present) is enough. */ + if (hevc_scan_dovi(s, st, c, 0)) { + c->dovi_done = 1; + break; + } + } + if (!c->dovi_done && hevc_scan_dovi(s, st, c, final)) + c->dovi_done = 1; + + if (avio_seek(s->pb, pos0, SEEK_SET) < 0) + av_log(s, AV_LOG_WARNING, "HEVC Dolby Vision peek could not seek " + "back, stream may be misread.\n"); + /* Drop the peek bytes; read_packet re-hunts if still needed. */ + c->scan_len = c->scan_pos = 0; + if (c->dovi_done) { + av_free(c->scan_buf); + c->scan_buf = NULL; + c->scan_cap = 0; + } + return ret; +} + +static int hevc_read_header(AVFormatContext *s) +{ + HEVCDemuxerContext *c = s->priv_data; + int ret = ff_raw_video_read_header(s); + AVStream *st = s->streams[0]; + + if (ret < 0) + return ret; + c->scan_next_pos = -1; + if ((s->pb->seekable & AVIO_SEEKABLE_NORMAL) && !c->dovi_done) + hevc_peek_dovi(s, st, c); + return 0; +} + +static int hevc_read_packet(AVFormatContext *s, AVPacket *pkt) +{ + HEVCDemuxerContext *c = s->priv_data; + AVStream *st = s->streams[0]; + int ret = ff_raw_read_partial_packet(s, pkt); + int need; + uint8_t *grown; + + if (c->dovi_done) + return ret; + if (ret < 0) { + /* EOS: one last scan with a delimited tail, then silent done. + * A fully scanned stream without RPU is simply not Dolby Vision. */ + if (c->scan_len) + hevc_scan_dovi(s, st, c, 1); + c->dovi_done = 1; + goto done; + } + + /* Seek (or any discontinuity) invalidates the reassembled prefix. */ + if (c->scan_next_pos >= 0 && pkt->pos != c->scan_next_pos) { + c->scan_len = 0; + c->scan_pos = 0; + } + c->scan_next_pos = pkt->pos + pkt->size; + + need = c->scan_len + pkt->size; + if (need > HEVC_DOVI_SCAN_CAP) { + av_log(s, AV_LOG_VERBOSE, "No Dolby Vision RPU in the first %d bytes, " + "dvcC will not be written.\n", HEVC_DOVI_SCAN_CAP); + c->dovi_done = 1; + goto done; + } + if (need > c->scan_cap) { + int cap = FFMIN(((need + HEVC_DOVI_SCAN_STEP - 1) / HEVC_DOVI_SCAN_STEP) * + HEVC_DOVI_SCAN_STEP, HEVC_DOVI_SCAN_CAP); + grown = av_realloc(c->scan_buf, cap); + if (!grown) { + av_log(s, AV_LOG_WARNING, "Out of memory scanning for Dolby " + "Vision RPU, dvcC will not be written.\n"); + c->dovi_done = 1; + goto done; + } + c->scan_buf = grown; + c->scan_cap = cap; + } + memcpy(c->scan_buf + c->scan_len, pkt->data, pkt->size); + c->scan_len += pkt->size; + if (hevc_scan_dovi(s, st, c, 0)) + c->dovi_done = 1; + +done: + if (c->dovi_done) { + av_free(c->scan_buf); + c->scan_buf = NULL; + c->scan_len = c->scan_cap = c->scan_pos = 0; + } + return ret; +} + +static int hevc_read_close(AVFormatContext *s) +{ + HEVCDemuxerContext *c = s->priv_data; + av_free(c->scan_buf); + c->scan_buf = NULL; + c->scan_len = c->scan_cap = c->scan_pos = 0; + return 0; +} + +const FFInputFormat ff_hevc_demuxer = { + .p.name = "hevc", + .p.long_name = NULL_IF_CONFIG_SMALL("raw HEVC video"), + .p.extensions = "hevc,h265,265", + .p.flags = AVFMT_GENERIC_INDEX | AVFMT_NOTIMESTAMPS, + .p.priv_class = &ff_rawvideo_demuxer_class, + .read_probe = hevc_probe, + .read_header = hevc_read_header, + .read_packet = hevc_read_packet, + .read_close = hevc_read_close, + .raw_codec_id = AV_CODEC_ID_HEVC, + .priv_data_size = sizeof(HEVCDemuxerContext), +}; -- 2.54.0 From c9305097e7de403ad70fbdd22fb40f4568e8c6fe Mon Sep 17 00:00:00 2001 From: Michael Rowlinson <[email protected]> Date: Tue, 20 Oct 2026 09:34:45 -0400 Subject: [PATCH 05/13] avformat/movenc, isom_tags: write dvh1/dvhe when DOVI_CONF is present Upgrade the default hev1/hvc1 codec tag to dvh1 (el = 0) or dvhe (el = 1) when DOVI_CONF side data is attached, gated exactly like the dvcC box writer (MP4 mode, -strict unofficial) so the tag never promises an unwritten box; explicit user tags are untouched. Also add the missing dvh1 HEVC entry to the ISObmff tag table (mux validation plus demux id), which the old "handled within mov.c" comment had left out. Signed-off-by: Michael Rowlinson <[email protected]> --- libavformat/isom_tags.c | 2 +- libavformat/movenc.c | 38 +++++++++++++++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/libavformat/isom_tags.c b/libavformat/isom_tags.c index 06166c0fa88..e136dce86e6 100644 --- a/libavformat/isom_tags.c +++ b/libavformat/isom_tags.c @@ -124,7 +124,7 @@ const AVCodecTag ff_codec_movvideo_tags[] = { { AV_CODEC_ID_HEVC, MKTAG('h', 'e', 'v', '1') }, /* HEVC/H.265 which indicates parameter sets may be in ES */ { AV_CODEC_ID_HEVC, MKTAG('h', 'v', 'c', '1') }, /* HEVC/H.265 which indicates parameter sets shall not be in ES */ { AV_CODEC_ID_HEVC, MKTAG('d', 'v', 'h', 'e') }, /* HEVC-based Dolby Vision derived from hev1 */ - /* dvh1 is handled within mov.c */ + { AV_CODEC_ID_HEVC, MKTAG('d', 'v', 'h', '1') }, /* HEVC-based Dolby Vision derived from hev1 */ { AV_CODEC_ID_H264, MKTAG('a', 'v', 'c', '1') }, /* AVC-1/H.264 */ { AV_CODEC_ID_H264, MKTAG('a', 'v', 'c', '2') }, diff --git a/libavformat/movenc.c b/libavformat/movenc.c index debe1acb7e0..c8376956e52 100644 --- a/libavformat/movenc.c +++ b/libavformat/movenc.c @@ -2183,6 +2183,33 @@ static int mov_get_rawvideo_codec_tag(AVFormatContext *s, MOVTrack *track) return tag; } +/* Upgrade a default HEVC sample entry to Dolby Vision signalling when the + * track carries a DOVI configuration: single-layer (no EL) becomes dvh1, + * EL-carrying becomes dvhe. Gated exactly like the dvcc/dvvC box writer + * (MP4 mode, -strict unofficial) so the tag never promises a box that is + * not written. */ +static unsigned int mov_get_hevc_dv_tag(AVFormatContext *s, MOVTrack *track, + unsigned int tag) +{ + const AVPacketSideData *sd; + const AVDOVIDecoderConfigurationRecord *dovi; + + if (tag != MKTAG('h', 'e', 'v', '1') && tag != MKTAG('h', 'v', 'c', '1')) + return tag; + if (track->mode != MODE_MP4) + return tag; + if (s->strict_std_compliance > FF_COMPLIANCE_UNOFFICIAL) + return tag; + sd = av_packet_side_data_get(track->st->codecpar->coded_side_data, + track->st->codecpar->nb_coded_side_data, + AV_PKT_DATA_DOVI_CONF); + if (!sd) + return tag; + dovi = (const AVDOVIDecoderConfigurationRecord *) sd->data; + return dovi->el_present_flag ? MKTAG('d', 'v', 'h', 'e') + : MKTAG('d', 'v', 'h', '1'); +} + static unsigned int mov_get_codec_tag(AVFormatContext *s, MOVTrack *track) { unsigned int tag = track->par->codec_tag; @@ -2279,9 +2306,14 @@ static unsigned int mov_find_codec_tag(AVFormatContext *s, MOVTrack *track) if (track->mode == MODE_MOV) { return mov_get_codec_tag(s, track); - } else - return validate_codec_tag(s->oformat->codec_tag, track->par->codec_tag, - track->par->codec_id); + } else { + unsigned int tag = validate_codec_tag(s->oformat->codec_tag, + track->par->codec_tag, + track->par->codec_id); + if (track->par->codec_id == AV_CODEC_ID_HEVC) + tag = mov_get_hevc_dv_tag(s, track, tag); + return tag; + } } /** Write uuid atom. -- 2.54.0 From c910f471d92608c6773bfa64f0d0ef5c2e2b4343 Mon Sep 17 00:00:00 2001 From: Michael Rowlinson <[email protected]> Date: Tue, 20 Oct 2026 12:06:53 -0400 Subject: [PATCH 06/13] matroska: carry Dolby Vision profiles 11-19 as dvwC mapping Demux already parses any 7-bit profile; the muxer dropped the BlockAdditionMapping for dv_profile > 10 while movenc already writes dvwC there. Follow the public Cellar codec spec mapping: dvcC for profiles 0-7, dvvC for 8-10 and 20, dvwC (0x64767743) otherwise; demux accepts DVWC through the same dvcc/dvvc path. Values are carried opaquely either way. Signed-off-by: Michael Rowlinson <[email protected]> --- libavformat/matroska.h | 1 + libavformat/matroskadec.c | 1 + libavformat/matroskaenc.c | 10 +++++++--- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/libavformat/matroska.h b/libavformat/matroska.h index c6986cfd1db..1748456b65a 100644 --- a/libavformat/matroska.h +++ b/libavformat/matroska.h @@ -363,6 +363,7 @@ typedef enum { MATROSKA_BLOCK_ADD_ID_TYPE_ITU_T_T35 = 4, MATROSKA_BLOCK_ADD_ID_TYPE_DVCC = 0x64766343, // MKBETAG('d','v','c','C') MATROSKA_BLOCK_ADD_ID_TYPE_DVVC = 0x64767643, // MKBETAG('d','v','v','C') + MATROSKA_BLOCK_ADD_ID_TYPE_DVWC = 0x64767743, // MKBETAG('d','v','w','C') MATROSKA_BLOCK_ADD_ID_TYPE_HVCE = 0x68766345, // MKBETAG('h','v','c','E') } MatroskaBlockAddIDType; diff --git a/libavformat/matroskadec.c b/libavformat/matroskadec.c index b793f67fa07..54d7d256bfa 100644 --- a/libavformat/matroskadec.c +++ b/libavformat/matroskadec.c @@ -2577,6 +2577,7 @@ static int mkv_parse_block_addition_mappings(AVFormatContext *s, AVStream *st, M break; case MATROSKA_BLOCK_ADD_ID_TYPE_DVCC: case MATROSKA_BLOCK_ADD_ID_TYPE_DVVC: + case MATROSKA_BLOCK_ADD_ID_TYPE_DVWC: if ((ret = mkv_parse_dvcc_dvvc(s, st, track, &mapping->extradata)) < 0) return ret; diff --git a/libavformat/matroskaenc.c b/libavformat/matroskaenc.c index a8408da8c1a..4d8bd60ba24 100644 --- a/libavformat/matroskaenc.c +++ b/libavformat/matroskaenc.c @@ -1760,7 +1760,7 @@ static void mkv_write_blockadditionmapping(AVFormatContext *s, const MatroskaMux const AVDOVIDecoderConfigurationRecord *dovi = (const AVDOVIDecoderConfigurationRecord *)sd->data; - if (dovi->dv_profile <= 10) { + { ebml_master mapping; uint8_t buf[ISOM_DVCC_DVVC_SIZE]; uint32_t type; @@ -1768,10 +1768,14 @@ static void mkv_write_blockadditionmapping(AVFormatContext *s, const MatroskaMux uint64_t expected_size = (2 + 1 + (sizeof(DVCC_DVVC_BLOCK_TYPE_NAME) - 1)) + (2 + 1 + 4) + (2 + 1 + ISOM_DVCC_DVVC_SIZE); - if (dovi->dv_profile > 7) { + /* Cellar codec spec: dvcC for profiles 0-7, dvvC for 8-10 and 20, + * dvwC for 11-19 (and any other value, carried opaquely). */ + if (dovi->dv_profile <= 7) { + type = MATROSKA_BLOCK_ADD_ID_TYPE_DVCC; + } else if (dovi->dv_profile <= 10 || dovi->dv_profile == 20) { type = MATROSKA_BLOCK_ADD_ID_TYPE_DVVC; } else { - type = MATROSKA_BLOCK_ADD_ID_TYPE_DVCC; + type = MATROSKA_BLOCK_ADD_ID_TYPE_DVWC; } ff_isom_put_dvcc_dvvc(s, buf, dovi); -- 2.54.0 From fb2e714f1c0d8756a5fdb8e3da03b3fb67094268 Mon Sep 17 00:00:00 2001 From: dovi-p6 <dovi-p6@local> Date: Tue, 20 Oct 2026 11:53:58 -0400 Subject: [PATCH 07/13] mpegtsenc: write DOVI video stream descriptor from DOVI_CONF Signed-off-by: Michael Rowlinson <[email protected]> --- libavformat/mpegtsenc.c | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/libavformat/mpegtsenc.c b/libavformat/mpegtsenc.c index e57889cb2d3..8779984f715 100644 --- a/libavformat/mpegtsenc.c +++ b/libavformat/mpegtsenc.c @@ -23,6 +23,7 @@ #include "libavutil/bswap.h" #include "libavutil/crc.h" #include "libavutil/dict.h" +#include "libavutil/dovi_meta.h" #include "libavutil/intreadwrite.h" #include "libavutil/mathematics.h" #include "libavutil/mem.h" @@ -851,6 +852,32 @@ static int mpegts_write_pmt(AVFormatContext *s, MpegTSService *service) } } } + /* Dolby Vision video stream descriptor (tag 0xB0), mirroring the + * layout parsed by the demuxer: version, profile/level/flags, + * dependency_pid when no BL is present, compatibility id and + * metadata compression. Written whenever DOVI_CONF is present. */ + { + const AVPacketSideData *sd = av_packet_side_data_get(st->codecpar->coded_side_data, + st->codecpar->nb_coded_side_data, AV_PKT_DATA_DOVI_CONF); + if (sd && sd->size >= sizeof(AVDOVIDecoderConfigurationRecord)) { + const AVDOVIDecoderConfigurationRecord *dovi = + (const AVDOVIDecoderConfigurationRecord *)sd->data; + uint8_t len = 4 + 1 + (dovi->bl_present_flag ? 0 : 2); + *q++ = DOVI_VIDEO_STREAM_DESCRIPTOR; + *q++ = len; + *q++ = dovi->dv_version_major; + *q++ = dovi->dv_version_minor; + put16(&q, ((dovi->dv_profile & 0x7f) << 9) | + ((dovi->dv_level & 0x3f) << 3) | + (!!dovi->rpu_present_flag << 2) | + (!!dovi->el_present_flag << 1) | + !!dovi->bl_present_flag); + if (!dovi->bl_present_flag) + put16(&q, 0xffff); /* dependency_pid unknown: null PID */ + *q++ = ((dovi->dv_bl_signal_compatibility_id & 0x0f) << 4) | + ((dovi->dv_md_compression & 0x03) << 2) | 0x03; + } + } break; case AVMEDIA_TYPE_DATA: if (codec_id == AV_CODEC_ID_SMPTE_KLV) { -- 2.54.0 From 5c61a0f352cf70f71b6a20782126ceeab117d24c Mon Sep 17 00:00:00 2001 From: dovi-p6 <dovi-p6@local> Date: Tue, 20 Oct 2026 11:53:58 -0400 Subject: [PATCH 08/13] libx264: Dolby Vision hook (P9 configure + T.35 SEI inject) Signed-off-by: Michael Rowlinson <[email protected]> --- libavcodec/dovi_rpuenc.c | 8 ++++++++ libavcodec/libx264.c | 44 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/libavcodec/dovi_rpuenc.c b/libavcodec/dovi_rpuenc.c index 30252693ffd..2de2fb32d45 100644 --- a/libavcodec/dovi_rpuenc.c +++ b/libavcodec/dovi_rpuenc.c @@ -151,6 +151,14 @@ static av_cold int dovi_configure_ext(DOVIContext *s, enum AVCodecID codec_id, color_trc == AVCOL_TRC_BT709) { bl_compat_id = 2; } + break; + case 9: /* AVC SDR */ + if (color_space == AVCOL_SPC_BT709 && + color_primaries == AVCOL_PRI_BT709 && + color_trc == AVCOL_TRC_BT709) { + bl_compat_id = 2; + } + break; } if (!dv_profile || bl_compat_id < 0) { diff --git a/libavcodec/libx264.c b/libavcodec/libx264.c index a432c0b50e5..143e1eefdbb 100644 --- a/libavcodec/libx264.c +++ b/libavcodec/libx264.c @@ -32,6 +32,7 @@ #include "libavutil/video_hint.h" #include "avcodec.h" #include "codec_internal.h" +#include "dovi_rpu.h" #include "encode.h" #include "internal.h" #include "packet_internal.h" @@ -113,6 +114,7 @@ typedef struct X264Context { int scenechange_threshold; int noise_reduction; int udu_sei; + DOVIContext dovi; AVDictionary *x264_params; @@ -607,6 +609,40 @@ static int setup_frame(AVCodecContext *ctx, const AVFrame *frame, } } + sd = av_frame_get_side_data(frame, AV_FRAME_DATA_DOVI_METADATA); + if (x4->dovi.cfg.dv_profile && sd) { + const AVDOVIMetadata *metadata = (const AVDOVIMetadata *)sd->data; + x264_sei_payload_t *sei_payload; + uint8_t *rpu; + int rpu_size; + void *tmp; + + ret = ff_dovi_rpu_generate(&x4->dovi, metadata, FF_DOVI_WRAP_T35, + &rpu, &rpu_size); + if (ret < 0) + goto fail; + + tmp = av_fast_realloc(sei->payloads, &sei_data_size, + (sei->num_payloads + 1) * sizeof(*sei_payload)); + if (!tmp) { + av_free(rpu); + ret = AVERROR(ENOMEM); + goto fail; + } + sei->payloads = tmp; + sei->sei_free = av_free; + sei_payload = &sei->payloads[sei->num_payloads]; + sei_payload->payload = rpu; + sei_payload->payload_size = rpu_size; + sei_payload->payload_type = SEI_TYPE_USER_DATA_REGISTERED_ITU_T_T35; + sei->num_payloads++; + } else if (x4->dovi.cfg.dv_profile) { + av_log(ctx, AV_LOG_ERROR, "Dolby Vision enabled, but received frame " + "without AV_FRAME_DATA_DOVI_METADATA\n"); + ret = AVERROR_INVALIDDATA; + goto fail; + } + *ppic = pic; return 0; @@ -786,6 +822,8 @@ static av_cold int X264_close(AVCodecContext *avctx) av_freep(&x4->sei); + ff_dovi_ctx_unref(&x4->dovi); + for (int i = 0; i < x4->nb_reordered_opaque; i++) opaque_uninit(&x4->reordered_opaque[i]); av_freep(&x4->reordered_opaque); @@ -1426,6 +1464,10 @@ static av_cold int X264_init(AVCodecContext *avctx) avctx->bit_rate = x4->params.rc.i_bitrate*1000LL; + x4->dovi.logctx = avctx; + if ((ret = ff_dovi_configure(&x4->dovi, avctx)) < 0) + return ret; + x4->enc = x264_encoder_open(&x4->params); if (!x4->enc) return AVERROR_EXTERNAL; @@ -1592,6 +1634,8 @@ static const AVOption options[] = { { "sc_threshold", "Scene change threshold", OFFSET(scenechange_threshold), AV_OPT_TYPE_INT, { .i64 = -1 }, INT_MIN, INT_MAX, VE }, { "noise_reduction", "Noise reduction", OFFSET(noise_reduction), AV_OPT_TYPE_INT, { .i64 = -1 }, INT_MIN, INT_MAX, VE }, { "udu_sei", "Use user data unregistered SEI if available", OFFSET(udu_sei), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, VE }, + { "dolbyvision", "Enable Dolby Vision RPU coding", OFFSET(dovi.enable), AV_OPT_TYPE_BOOL, {.i64 = FF_DOVI_AUTOMATIC }, -1, 1, VE, .unit = "dovi" }, + { "auto", NULL, 0, AV_OPT_TYPE_CONST, {.i64 = FF_DOVI_AUTOMATIC}, .flags = VE, .unit = "dovi" }, { "x264-params", "Override the x264 configuration using a :-separated list of key=value parameters", OFFSET(x264_params), AV_OPT_TYPE_DICT, { 0 }, 0, 0, VE }, { "mb_info", "Set mb_info data through AVSideData, only useful when used from the API", OFFSET(mb_info), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, VE }, { NULL }, -- 2.54.0 From c482711f467a8c34d6bd02709592d00429d2dde6 Mon Sep 17 00:00:00 2001 From: Michael Rowlinson <[email protected]> Date: Tue, 20 Oct 2026 12:27:08 -0400 Subject: [PATCH 09/13] avcodec/dovi_rpudec, dovi_rpuenc: fix L11 byte-1 layout ((ref<<4)|whitepoint, oracle parity) Signed-off-by: Michael Rowlinson <[email protected]> --- libavcodec/dovi_rpudec.c | 4 ++-- libavcodec/dovi_rpuenc.c | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libavcodec/dovi_rpudec.c b/libavcodec/dovi_rpudec.c index de34627d1d3..5df067cbbbd 100644 --- a/libavcodec/dovi_rpudec.c +++ b/libavcodec/dovi_rpudec.c @@ -288,9 +288,9 @@ static int parse_ext_v2(DOVIContext *s, GetBitContext *gb, AVDOVIDmData *dm, break; case 11: dm->l11.content_type = get_bits(gb, 8); - dm->l11.whitepoint = get_bits(gb, 4); - dm->l11.reference_mode_flag = get_bits1(gb); skip_bits(gb, 3); /* reserved */ + dm->l11.reference_mode_flag = get_bits1(gb); + dm->l11.whitepoint = get_bits(gb, 4); skip_bits(gb, 8); /* reserved */ skip_bits(gb, 8); /* reserved */ break; diff --git a/libavcodec/dovi_rpuenc.c b/libavcodec/dovi_rpuenc.c index 2de2fb32d45..56f664438d5 100644 --- a/libavcodec/dovi_rpuenc.c +++ b/libavcodec/dovi_rpuenc.c @@ -654,9 +654,9 @@ static void generate_ext_v2(PutBitContext *pb, const AVDOVIDmData *dm) break; case 11: put_bits(pb, 8, dm->l11.content_type); - put_bits(pb, 4, dm->l11.whitepoint); - put_bits(pb, 1, dm->l11.reference_mode_flag); put_bits(pb, 3, 0); /* reserved */ + put_bits(pb, 1, dm->l11.reference_mode_flag); + put_bits(pb, 4, dm->l11.whitepoint); put_bits(pb, 8, 0); /* reserved */ put_bits(pb, 8, 0); /* reserved */ break; -- 2.54.0 From e64f38598f6c228a5cd79f9317441c2b99204c59 Mon Sep 17 00:00:00 2001 From: Michael Rowlinson <[email protected]> Date: Wed, 21 Oct 2026 01:53:55 -0400 Subject: [PATCH 10/13] fate/dovi-p6: P6 sweep FATE fragment + refs (mux matrix + x264 hook) Drop-in tests/fate fragment from the p6 lane with oracle-pinned refs. Harness runs tools/dovi_p6_sweep (mux: 20 profiles x mkv/mkv-rt/ts-rt; x264: open/configure/SEI-inject/skip) and diffs reports vs refs. Signed-off-by: Michael Rowlinson <[email protected]> --- tests/fate/dovi-p6.mak | 22 +++++++++++++ tests/ref/fate/dovi-p6-mux | 61 +++++++++++++++++++++++++++++++++++++ tests/ref/fate/dovi-p6-x264 | 17 +++++++++++ 3 files changed, 100 insertions(+) create mode 100644 tests/fate/dovi-p6.mak create mode 100644 tests/ref/fate/dovi-p6-mux create mode 100644 tests/ref/fate/dovi-p6-x264 diff --git a/tests/fate/dovi-p6.mak b/tests/fate/dovi-p6.mak new file mode 100644 index 00000000000..f109b53c025 --- /dev/null +++ b/tests/fate/dovi-p6.mak @@ -0,0 +1,22 @@ +# FATE fragment: Dolby Vision P6 sweep (mux matrix + x264 hook). +# Drop-in shape for ffmpeg tests/fate/. Runs the dovi_p6_sweep harness +# (built against an in-tree ffmpeg) and diffs its report against refs. +FATE_DOVI += fate-dovi-p6-mux fate-dovi-p6-x264 + +P6_SWEEP ?= /Volumes/NVME/dovi-ffmpeg-ws/scratch/p6/build/dovi_p6_sweep +P6_BUILD ?= /Volumes/NVME/dovi-ffmpeg-ws/scratch/p6/build +P6_OUT ?= /Volumes/NVME/dovi-ffmpeg-ws/scratch/p6/fate +FF_SRC ?= /Volumes/NVME/dovi-ffmpeg-ws/scratch/p6/ffmpeg-p6 + +fate-dovi-p6-mux: $(P6_SWEEP) + mkdir -p $(P6_OUT) + $(P6_SWEEP) mux $(CURDIR)/tests/vectors/mux_dovi.hevc $(P6_OUT) > $(P6_OUT)/mux.got 2> $(P6_OUT)/mux.err + diff -u $(CURDIR)/tests/ref/fate/dovi-p6-mux $(P6_OUT)/mux.got + +fate-dovi-p6-x264: $(P6_SWEEP) + mkdir -p $(P6_OUT) + cd tests/vectors && $(P6_SWEEP) x264 . > $(P6_OUT)/x264.got 2> $(P6_OUT)/x264.err + diff -u $(CURDIR)/tests/ref/fate/dovi-p6-x264 $(P6_OUT)/x264.got + +$(P6_SWEEP): + $(MAKE) -C tools FF_SRC=$(FF_SRC) BUILD_DIR=$(P6_BUILD) p6 diff --git a/tests/ref/fate/dovi-p6-mux b/tests/ref/fate/dovi-p6-mux new file mode 100644 index 00000000000..d02ccd07285 --- /dev/null +++ b/tests/ref/fate/dovi-p6-mux @@ -0,0 +1,61 @@ +PASS mkv p=0 map=dvcC(n=1) want=dvcC +PASS mkv p=0 rt=match +PASS ts p=0 rt=match +PASS mkv p=1 map=dvcC(n=1) want=dvcC +PASS mkv p=1 rt=match +PASS ts p=1 rt=match +PASS mkv p=2 map=dvcC(n=1) want=dvcC +PASS mkv p=2 rt=match +PASS ts p=2 rt=match +PASS mkv p=3 map=dvcC(n=1) want=dvcC +PASS mkv p=3 rt=match +PASS ts p=3 rt=match +PASS mkv p=4 map=dvcC(n=1) want=dvcC +PASS mkv p=4 rt=match +PASS ts p=4 rt=match +PASS mkv p=5 map=dvcC(n=1) want=dvcC +PASS mkv p=5 rt=match +PASS ts p=5 rt=match +PASS mkv p=6 map=dvcC(n=1) want=dvcC +PASS mkv p=6 rt=match +PASS ts p=6 rt=match +PASS mkv p=7 map=dvcC(n=1) want=dvcC +PASS mkv p=7 rt=match +PASS ts p=7 rt=match +PASS mkv p=8 map=dvvC(n=1) want=dvvC +PASS mkv p=8 rt=match +PASS ts p=8 rt=match +PASS mkv p=9 map=dvvC(n=1) want=dvvC +PASS mkv p=9 rt=match +PASS ts p=9 rt=match +PASS mkv p=10 map=dvvC(n=1) want=dvvC +PASS mkv p=10 rt=match +PASS ts p=10 rt=match +PASS mkv p=11 map=dvwC(n=1) want=dvwC +PASS mkv p=11 rt=match +PASS ts p=11 rt=match +PASS mkv p=12 map=dvwC(n=1) want=dvwC +PASS mkv p=12 rt=match +PASS ts p=12 rt=match +PASS mkv p=13 map=dvwC(n=1) want=dvwC +PASS mkv p=13 rt=match +PASS ts p=13 rt=match +PASS mkv p=19 map=dvwC(n=1) want=dvwC +PASS mkv p=19 rt=match +PASS ts p=19 rt=match +PASS mkv p=20 map=dvvC(n=1) want=dvvC +PASS mkv p=20 rt=match +PASS ts p=20 rt=match +PASS mkv p=21 map=dvwC(n=1) want=dvwC +PASS mkv p=21 rt=match +PASS ts p=21 rt=match +PASS mkv p=33 map=dvwC(n=1) want=dvwC +PASS mkv p=33 rt=match +PASS ts p=33 rt=match +PASS mkv p=100 map=dvwC(n=1) want=dvwC +PASS mkv p=100 rt=match +PASS ts p=100 rt=match +PASS mkv p=127 map=dvwC(n=1) want=dvwC +PASS mkv p=127 rt=match +PASS ts p=127 rt=match +MUX SWEEP profiles=20 done diff --git a/tests/ref/fate/dovi-p6-x264 b/tests/ref/fate/dovi-p6-x264 new file mode 100644 index 00000000000..0ab91e1edde --- /dev/null +++ b/tests/ref/fate/dovi-p6-x264 @@ -0,0 +1,17 @@ +PASS x264 vec profile5.bin readable +PASS x264 vec first RPU ok len=149 +PASS x264 RPU parse +PASS x264 forced open +PASS x264 conf profile=9 compat=2 level=1 +PASS x264 frame alloc +PASS x264 frame metadata attached +PASS x264 expected T.35 len=162 +PASS x264 encode bytes=1043 +PASS x264 SEI T.35 byte-exact n=3 len=162 +PASS x264 auto open +PASS x264 auto skip noconf +PASS x264 auto encode +PASS x264 auto no SEI-4 +PASS x264 forced open2 +PASS x264 nometa EINVAL +X264 HOOK done -- 2.54.0 From 646aea78f7a07a803f253b8012fd1248f553c7a0 Mon Sep 17 00:00:00 2001 From: Michael Rowlinson <[email protected]> Date: Wed, 21 Oct 2026 01:54:16 -0400 Subject: [PATCH 11/13] fate/dovi: wire DOVI FATE fragments into tests/Makefile Chain: tests/Makefile -> tests/fate/dovi.mak -> dovi-p5.mak + dovi-p6.mak (own P6_FF_SRC tree var; ?= keeps first default seen). Lane-root Makefile aggregate not folded: ffmpeg already ships a root Makefile at that path. dovi-p5.mak ships here as chain dependency. Signed-off-by: Michael Rowlinson <[email protected]> --- tests/Makefile | 1 + tests/fate/dovi-p5.mak | 58 ++++++++++++++++++++++++++++++ tests/fate/dovi-p6.mak | 8 +++-- tests/fate/dovi.mak | 82 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 tests/fate/dovi-p5.mak create mode 100644 tests/fate/dovi.mak diff --git a/tests/Makefile b/tests/Makefile index 7d1597bdf76..dcb08ab286c 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -192,6 +192,7 @@ include $(SRC_PATH)/tests/fate/dca.mak include $(SRC_PATH)/tests/fate/demux.mak include $(SRC_PATH)/tests/fate/dfa.mak include $(SRC_PATH)/tests/fate/dnxhd.mak +include $(SRC_PATH)/tests/fate/dovi.mak include $(SRC_PATH)/tests/fate/dpcm.mak include $(SRC_PATH)/tests/fate/dvvideo.mak include $(SRC_PATH)/tests/fate/ea.mak diff --git a/tests/fate/dovi-p5.mak b/tests/fate/dovi-p5.mak new file mode 100644 index 00000000000..a0fe1a70a48 --- /dev/null +++ b/tests/fate/dovi-p5.mak @@ -0,0 +1,58 @@ +# FATE fragment: Dolby Vision P5 profile-convert + RPU editor. +# Drop-in shape for ffmpeg tests/fate/. Compares rpu_edit output +# byte-exact against oracle (dovi_tool CLI) fixtures in tests/vectors/p5/. +FATE_DOVI += fate-dovi-p5-convert fate-dovi-p5-edit + +RPU_EDIT ?= /Volumes/NVME/dovi-ffmpeg-ws/scratch/p5/build/rpu_edit +P5_BUILD ?= /Volumes/NVME/dovi-ffmpeg-ws/scratch/p5/build +P5_OUT ?= /Volumes/NVME/dovi-ffmpeg-ws/scratch/p5/fate +FF_SRC ?= /Volumes/NVME/dovi-ffmpeg-ws/wt-ffmpeg-gen2 + +P5_CV = cv_fel_m1:fel_orig.bin cv_fel_m2:fel_orig.bin \ + cv_fel_m5:fel_orig.bin cv_mel_m4:mel_orig.bin \ + cv_p4_m4:profile4.bin cv_p502_m3:profile5-02.bin \ + cv_c4_m1:cmv40_full_rpu.bin +P5_ED = ed_crop_fel:fel_orig.bin ed_crop_mel:mel_orig.bin \ + ed_dl5:fel_orig.bin ed_dl5z:profile5.bin \ + ed_rmap:fel_orig.bin ed_rmc4:cmv40_full_rpu.bin \ + ed_l6rep:fel_orig.bin ed_l6new:profile4.bin \ + ed_l9:cmv40_full_rpu.bin ed_l11:cmv40_full_rpu.bin \ + ed_l255:fel_orig.bin ed_minmax:fel_orig.bin \ + mix_rm:p5/mix3.bin mix_dup:p5/mix3.bin \ + mix_pre:p5/mix3.bin mix_scut:p5/mix3.bin + +fate-dovi-p5-convert: $(RPU_EDIT) + mkdir -p $(P5_OUT) + cd tests/vectors && for c in $(P5_CV); do \ + n=$${c%%:*}; i=$${c##*:}; \ + $(RPU_EDIT) -i $$i -o $(P5_OUT)/$$n.bin -j p5/cfg_$$n.json \ + 2>/dev/null && \ + cmp -s $(P5_OUT)/$$n.bin p5/exp_$$n.bin && \ + echo "$$n PASS"; \ + done > $(P5_OUT)/cv.got + cd tests/vectors && $(RPU_EDIT) -i profile5.bin \ + -o $(P5_OUT)/cv_p5_m3.bin -j p5/cfg_cv_p502_m3.json \ + 2>/dev/null && cmp -s $(P5_OUT)/cv_p5_m3.bin profile8.bin && \ + echo "cv_p5_m3 PASS" >> $(P5_OUT)/cv.got + cd tests/vectors && ! $(RPU_EDIT) -i profile5.bin \ + -o $(P5_OUT)/cv_p5_m1.bin -j p5/cfg_cv_p5_m1.json \ + 2>/dev/null && echo "cv_p5_m1_err PASS" >> $(P5_OUT)/cv.got + diff -u $(CURDIR)/tests/ref/fate/dovi-p5-convert $(P5_OUT)/cv.got + +fate-dovi-p5-edit: $(RPU_EDIT) + mkdir -p $(P5_OUT) + cd tests/vectors && for c in $(P5_ED); do \ + n=$${c%%:*}; i=$${c##*:}; \ + $(RPU_EDIT) -i $$i -o $(P5_OUT)/$$n.bin -j p5/cfg_$$n.json \ + 2>/dev/null && \ + cmp -s $(P5_OUT)/$$n.bin p5/exp_$$n.bin && \ + echo "$$n PASS"; \ + done > $(P5_OUT)/ed.got + cd tests/vectors && $(RPU_EDIT) -i cmv40_full_rpu.bin \ + -o $(P5_OUT)/ed_wp16.bin -j p5/cfg_ed_wp16.json \ + 2>/dev/null && test ! -s $(P5_OUT)/ed_wp16.bin && \ + echo "ed_wp16 PASS" >> $(P5_OUT)/ed.got + diff -u $(CURDIR)/tests/ref/fate/dovi-p5-edit $(P5_OUT)/ed.got + +$(RPU_EDIT): + $(MAKE) -C tools FF_SRC=$(FF_SRC) BUILD_DIR=$(P5_BUILD) diff --git a/tests/fate/dovi-p6.mak b/tests/fate/dovi-p6.mak index f109b53c025..20986aba9ab 100644 --- a/tests/fate/dovi-p6.mak +++ b/tests/fate/dovi-p6.mak @@ -3,10 +3,14 @@ # (built against an in-tree ffmpeg) and diffs its report against refs. FATE_DOVI += fate-dovi-p6-mux fate-dovi-p6-x264 +# Own tree var (not FF_SRC): the chain shares one FF_SRC default for the +# RT/P5 sections, and `?=` keeps the first default seen, so a shared name +# here would silently inherit the wrong tree. Same default as before, +# still overridable (`make fate-dovi-p6-mux P6_FF_SRC=<tree>`). P6_SWEEP ?= /Volumes/NVME/dovi-ffmpeg-ws/scratch/p6/build/dovi_p6_sweep P6_BUILD ?= /Volumes/NVME/dovi-ffmpeg-ws/scratch/p6/build P6_OUT ?= /Volumes/NVME/dovi-ffmpeg-ws/scratch/p6/fate -FF_SRC ?= /Volumes/NVME/dovi-ffmpeg-ws/scratch/p6/ffmpeg-p6 +P6_FF_SRC ?= /Volumes/NVME/dovi-ffmpeg-ws/scratch/p6/ffmpeg-p6 fate-dovi-p6-mux: $(P6_SWEEP) mkdir -p $(P6_OUT) @@ -19,4 +23,4 @@ fate-dovi-p6-x264: $(P6_SWEEP) diff -u $(CURDIR)/tests/ref/fate/dovi-p6-x264 $(P6_OUT)/x264.got $(P6_SWEEP): - $(MAKE) -C tools FF_SRC=$(FF_SRC) BUILD_DIR=$(P6_BUILD) p6 + $(MAKE) -C tools FF_SRC=$(P6_FF_SRC) BUILD_DIR=$(P6_BUILD) p6 diff --git a/tests/fate/dovi.mak b/tests/fate/dovi.mak new file mode 100644 index 00000000000..98e12db961e --- /dev/null +++ b/tests/fate/dovi.mak @@ -0,0 +1,82 @@ +# FATE fragment: Dolby Vision RPU round-trip. Drop-in shape for ffmpeg +# tests/fate/. Runs from repo root after `make -C tools` (binary lives in +# NVME scratch per project rules; override below if needed). +FATE_DOVI += fate-dovi-rpu-roundtrip + +RPU_RT ?= /Volumes/NVME/dovi-ffmpeg-ws/scratch/gen2/build/rpu_roundtrip +# FF_SRC selects the ffmpeg tree the harness links against (see tools/Makefile). +# Green requires a tree with opaque ext-block passthrough (ffmpeg-gen2@9c5dacc+). +FF_SRC ?= /Volumes/NVME/dovi-ffmpeg-ws/wt-ffmpeg-gen2 +RPU_VECTORS = profile5.bin profile8.bin mel_rpu.bin cmv40_full_rpu.bin \ + fel_rpu.bin p8_001_end_crc32.bin unordered_l8_blocks.bin \ + level253-ext-metadata.bin st2094_10_level3.bin + +fate-dovi-rpu-roundtrip: $(RPU_RT) + cd tests/vectors && $(RPU_RT) $(RPU_VECTORS) 2>/dev/null | \ + diff -u $(CURDIR)/tests/ref/fate/dovi-rpu-roundtrip - + +$(RPU_RT): + $(MAKE) -C tools FF_SRC=$(FF_SRC) + +# Mux gap: HEVC+RPU stream copy must keep Dolby Vision signalling. +# Needs an ffmpeg built from a tree with the P3 demux/mux changes +# (patches/ffmpeg-p3-hevc-dvcc-dvh1.patch); output lives in NVME scratch. +FATE_DOVI += fate-dovi-mux-copy + +FFMPEG ?= /Volumes/NVME/dovi-ffmpeg-ws/ffmpeg/ffmpeg +MUX_OUT ?= /Volumes/NVME/dovi-ffmpeg-ws/scratch/gen2/fate-mux + +fate-dovi-mux-copy: + mkdir -p $(MUX_OUT) + cd tests/vectors && $(FFMPEG) -y -v error -i mux_dovi.hevc \ + -c copy -strict unofficial $(MUX_OUT)/p8.mp4 && \ + $(FFMPEG) -y -v error -i mux_p5.hevc \ + -c copy -strict unofficial $(MUX_OUT)/p5.mp4 && \ + $(FFMPEG) -y -v error -i mux_base.hevc \ + -c copy -strict unofficial $(MUX_OUT)/base.mp4 + python3 tests/dvprobe.py p8=$(MUX_OUT)/p8.mp4 p5=$(MUX_OUT)/p5.mp4 \ + base=$(MUX_OUT)/base.mp4 | \ + diff -u $(CURDIR)/tests/ref/fate/dovi-mux-copy - + +# Generate gap: XML metadata -> RPU must match the oracle byte-exactly. +# Needs only a built tree (no ffmpeg patches); output lives in NVME scratch. +FATE_DOVI += fate-dovi-generate + +GEN_FF_SRC ?= /Volumes/NVME/dovi-ffmpeg-ws/scratch/p4gen/ffmpeg +GEN_BUILD_DIR ?= /Volumes/NVME/dovi-ffmpeg-ws/scratch/p4gen/build +RPU_GEN ?= $(GEN_BUILD_DIR)/rpu_generate +GEN_OUT ?= /Volumes/NVME/dovi-ffmpeg-ws/scratch/p4gen/fate-gen + +fate-dovi-generate: $(RPU_GEN) + mkdir -p $(GEN_OUT) + cd tests/vectors && { \ + $(RPU_GEN) -x cmv2_9.xml --canvas-width 3840 --canvas-height 2160 \ + -o $(GEN_OUT)/29.bin >/dev/null && \ + cmp -s $(GEN_OUT)/29.bin cmv2_9_expected.bin && \ + echo 'cmv2_9: frames=108 MATCH'; \ + $(RPU_GEN) -x cmv4_0_2.xml -o $(GEN_OUT)/40.bin >/dev/null && \ + cmp -s $(GEN_OUT)/40.bin cmv4_0_2_expected.bin && \ + echo 'cmv4_0_2: frames=259 MATCH'; \ + $(RPU_GEN) -x cmv4_0_2_custom_displays.xml \ + -o $(GEN_OUT)/custom.bin >/dev/null && \ + cmp -s $(GEN_OUT)/custom.bin cmv4_0_2_custom_expected.bin && \ + echo 'cmv4_0_2_custom: frames=24 MATCH'; \ + $(RPU_GEN) -x cmv4_2_xml_510.xml -o $(GEN_OUT)/510.bin >/dev/null && \ + cmp -s $(GEN_OUT)/510.bin cmv4_2_510_expected.bin && \ + echo 'cmv4_2_510: frames=24 MATCH'; \ + $(RPU_GEN) -x cmv4_0_2.xml --canvas-width 3840 \ + --canvas-height 2160 -o $(GEN_OUT)/l5.bin >/dev/null && \ + cmp -s $(GEN_OUT)/l5.bin cmv4_0_2_l5_expected.bin && \ + echo 'cmv4_0_2_l5: frames=259 MATCH'; \ + } | diff -u $(CURDIR)/tests/ref/fate/dovi-generate - + +$(RPU_GEN): + $(MAKE) -C tools FF_SRC=$(GEN_FF_SRC) BUILD_DIR=$(GEN_BUILD_DIR) \ + rpu_generate + +# Fragment chain: P5 convert/edit + P6 mux/x264 gates. Included here so +# this file stays the single in-tree entry (root Makefile includes only +# this file). Included LAST: `?=` keeps the first default seen, and the +# P5/RT sections above share one FF_SRC default, so order is safe. +include tests/fate/dovi-p5.mak +include tests/fate/dovi-p6.mak -- 2.54.0 From 9f4cd4b914180cc84bc046ff6d6b84ccc6497a05 Mon Sep 17 00:00:00 2001 From: Michael Rowlinson <[email protected]> Date: Wed, 21 Oct 2026 10:06:07 -0400 Subject: [PATCH 12/13] fate/dovi-p5: cover p5gap editor cases (admd + transfer) Signed-off-by: Michael Rowlinson <[email protected]> --- tests/fate/dovi-p5.mak | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/fate/dovi-p5.mak b/tests/fate/dovi-p5.mak index a0fe1a70a48..1b3331bf9af 100644 --- a/tests/fate/dovi-p5.mak +++ b/tests/fate/dovi-p5.mak @@ -18,6 +18,7 @@ P5_ED = ed_crop_fel:fel_orig.bin ed_crop_mel:mel_orig.bin \ ed_l6rep:fel_orig.bin ed_l6new:profile4.bin \ ed_l9:cmv40_full_rpu.bin ed_l11:cmv40_full_rpu.bin \ ed_l255:fel_orig.bin ed_minmax:fel_orig.bin \ + ed_admd:fel_orig.bin ed_tx6:fel_orig.bin \ mix_rm:p5/mix3.bin mix_dup:p5/mix3.bin \ mix_pre:p5/mix3.bin mix_scut:p5/mix3.bin -- 2.54.0 From 00ddd1b226065999e17ead2927bc6f705da78e37 Mon Sep 17 00:00:00 2001 From: Michael Rowlinson <[email protected]> Date: Sun, 25 Oct 2026 01:30:40 -0400 Subject: [PATCH 13/13] Changelog: Dolby Vision RPU envelope passthrough and mux signalling Covers the 0001-0009 envelope series (opaque ext-block carriage, hevc DOVI_CONF attach, mov/mkv/ts signalling, libx264 hook, L11 fix). Signed-off-by: Michael Rowlinson <[email protected]> --- Changelog | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Changelog b/Changelog index f4b1dba7e89..a38ba2672ac 100644 --- a/Changelog +++ b/Changelog @@ -2,6 +2,8 @@ Entries are sorted chronologically from oldest to youngest within each release, releases are sorted from youngest to oldest. version <next>: +- Dolby Vision RPU envelope passthrough (opaque extension blocks) with mux + signalling (dvh1/dvhe, dvwC, MPEG-TS descriptor) and libx264 injection version 9.1: - ASTC image encoding and decoding via libastcenc, with .astc and KTX 1.0 muxers and demuxers -- 2.54.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
