diff mbox series

[FFmpeg-devel,07/10] avcodec/dovi_rpu: add ff_dovi_rpu_generate()

Message ID 20240403154330.71585-7-ffmpeg@haasn.xyz
State New
Headers show
Series [FFmpeg-devel,01/10] avcodec: add dolbyvision option | expand

Checks

Context Check Description
andriy/make_x86 success Make finished
andriy/make_fate_x86 success Make fate finished

Commit Message

Niklas Haas April 3, 2024, 3:43 p.m. UTC
From: Niklas Haas <git@haasn.dev>

This function takes a decoded AVDOVIMetadata struct and turns it back
into a binary RPU. Verified using existing tools, and matches the
bitstream in official reference files.

I decided to just roll the EMDF and NAL encapsulation into this function
because the end user will need to do it otherwise anyways.
---
 libavcodec/dovi_rpu.c | 542 ++++++++++++++++++++++++++++++++++++++++++
 libavcodec/dovi_rpu.h |  20 +-
 2 files changed, 560 insertions(+), 2 deletions(-)
diff mbox series

Patch

diff --git a/libavcodec/dovi_rpu.c b/libavcodec/dovi_rpu.c
index b4e8d0cdea4..89353ccd1e4 100644
--- a/libavcodec/dovi_rpu.c
+++ b/libavcodec/dovi_rpu.c
@@ -29,6 +29,9 @@ 
 #include "dovi_rpu.h"
 #include "golomb.h"
 #include "get_bits.h"
+#include "itut35.h"
+#include "put_bits.h"
+#include "put_golomb.h"
 #include "refstruct.h"
 
 enum {
@@ -358,6 +361,42 @@  static inline int64_t get_se_coef(GetBitContext *gb, const AVDOVIRpuDataHeader *
     return 0; /* unreachable */
 }
 
+static inline void put_ue_coef(PutBitContext *pb, const AVDOVIRpuDataHeader *hdr,
+                               uint64_t coef)
+{
+    union { uint32_t u32; float f32; } fpart;
+
+    switch (hdr->coef_data_type) {
+    case RPU_COEFF_FIXED:
+        set_ue_golomb(pb, coef >> hdr->coef_log2_denom);
+        put_bits64(pb, hdr->coef_log2_denom,
+                   coef & ((1LL << hdr->coef_log2_denom) - 1));
+        break;
+    case RPU_COEFF_FLOAT:
+        fpart.f32 = coef / (float) (1LL << hdr->coef_log2_denom);
+        put_bits64(pb, hdr->coef_log2_denom, fpart.u32);
+        break;
+    }
+}
+
+static inline void put_se_coef(PutBitContext *pb, const AVDOVIRpuDataHeader *hdr,
+                               uint64_t coef)
+{
+    union { uint32_t u32; float f32; } fpart;
+
+    switch (hdr->coef_data_type) {
+    case RPU_COEFF_FIXED:
+        set_se_golomb(pb, coef >> hdr->coef_log2_denom);
+        put_bits64(pb, hdr->coef_log2_denom,
+                   coef & ((1LL << hdr->coef_log2_denom) - 1));
+        break;
+    case RPU_COEFF_FLOAT:
+        fpart.f32 = coef / (float) (1LL << hdr->coef_log2_denom);
+        put_bits64(pb, hdr->coef_log2_denom, fpart.u32);
+        break;
+    }
+}
+
 static inline unsigned get_variable_bits(GetBitContext *gb, int n)
 {
     unsigned int value = get_bits(gb, n);
@@ -885,3 +924,506 @@  fail:
     ff_dovi_ctx_unref(s); /* don't leak potentially invalid state */
     return AVERROR_INVALIDDATA;
 }
+
+static int av_q2den(AVRational q, int den)
+{
+    if (q.den == den)
+        return q.num;
+    q = av_mul_q(q, av_make_q(den, 1));
+    return (q.num + (q.den >> 1)) / q.den;
+}
+
+static void generate_ext_v1(PutBitContext *pb, const AVDOVIDmData *dm)
+{
+    int ext_block_length, start_pos, pad_bits;
+
+    switch (dm->level) {
+    case 1:   ext_block_length = 5;  break;
+    case 2:   ext_block_length = 11; break;
+    case 4:   ext_block_length = 3;  break;
+    case 5:   ext_block_length = 7;  break;
+    case 6:   ext_block_length = 8;  break;
+    case 255: ext_block_length = 6;  break;
+    default: return;
+    }
+
+    set_ue_golomb(pb, ext_block_length);
+    put_bits(pb, 8, dm->level);
+    start_pos = put_bits_count(pb);
+
+    switch (dm->level) {
+    case 1:
+        put_bits(pb, 12, dm->l1.min_pq);
+        put_bits(pb, 12, dm->l1.max_pq);
+        put_bits(pb, 12, dm->l1.avg_pq);
+        break;
+    case 2:
+        put_bits(pb, 12, dm->l2.target_max_pq);
+        put_bits(pb, 12, dm->l2.trim_slope);
+        put_bits(pb, 12, dm->l2.trim_offset);
+        put_bits(pb, 12, dm->l2.trim_power);
+        put_bits(pb, 12, dm->l2.trim_chroma_weight);
+        put_bits(pb, 12, dm->l2.trim_saturation_gain);
+        put_bits(pb, 13, dm->l2.ms_weight + 8192);
+        break;
+    case 4:
+        put_bits(pb, 12, dm->l4.anchor_pq);
+        put_bits(pb, 12, dm->l4.anchor_power);
+        break;
+    case 5:
+        put_bits(pb, 13, dm->l5.left_offset);
+        put_bits(pb, 13, dm->l5.right_offset);
+        put_bits(pb, 13, dm->l5.top_offset);
+        put_bits(pb, 13, dm->l5.bottom_offset);
+        break;
+    case 6:
+        put_bits(pb, 16, dm->l6.max_luminance);
+        put_bits(pb, 16, dm->l6.min_luminance);
+        put_bits(pb, 16, dm->l6.max_cll);
+        put_bits(pb, 16, dm->l6.max_fall);
+        break;
+    case 255:
+        put_bits(pb, 8, dm->l255.dm_run_mode);
+        put_bits(pb, 8, dm->l255.dm_run_version);
+        for (int i = 0; i < 4; i++)
+            put_bits(pb, 8, dm->l255.dm_debug[i]);
+        break;
+    }
+
+    pad_bits = ext_block_length * 8 - (put_bits_count(pb) - start_pos);
+    av_assert1(pad_bits >= 0);
+    put_bits(pb, pad_bits, 0);
+}
+
+static void put_cie_xy(PutBitContext *pb, AVCIExy xy)
+{
+    const int denom = 32767;
+    put_sbits(pb, 16, av_q2den(xy.x, denom));
+    put_sbits(pb, 16, av_q2den(xy.y, denom));
+}
+
+#define ANY6(arr) (arr[0] || arr[1] || arr[2] || arr[3] || arr[4] || arr[5])
+#define ANY_XY(xy) (xy.x.num || xy.y.num)
+#define ANY_CSP(csp) (ANY_XY(csp.prim.r) || ANY_XY(csp.prim.g) || \
+                      ANY_XY(csp.prim.b) || ANY_XY(csp.wp))
+
+static void generate_ext_v2(PutBitContext *pb, const AVDOVIDmData *dm)
+{
+    int ext_block_length, start_pos, pad_bits;
+
+    switch (dm->level) {
+    case 3: ext_block_length = 5; break;
+    case 8:
+        if (ANY6(dm->l8.hue_vector_field)) {
+            ext_block_length = 25;
+        } else if (ANY6(dm->l8.saturation_vector_field)) {
+            ext_block_length = 19;
+        } else if (dm->l8.clip_trim) {
+            ext_block_length = 13;
+        } else if (dm->l8.target_mid_contrast) {
+            ext_block_length = 12;
+        } else {
+            ext_block_length = 10;
+        }
+        break;
+    case 9:
+        if (ANY_CSP(dm->l9.source_display_primaries)) {
+            ext_block_length = 17;
+        } else {
+            ext_block_length = 1;
+        }
+        break;
+    case 10:
+        if (ANY_CSP(dm->l10.target_display_primaries)) {
+            ext_block_length = 21;
+        } else {
+            ext_block_length = 5;
+        }
+        break;
+    case 11:  ext_block_length = 5; break;
+    case 254: ext_block_length = 2; break;
+    default: return;
+    }
+
+    set_ue_golomb(pb, ext_block_length);
+    put_bits(pb, 8, dm->level);
+    start_pos = put_bits_count(pb);
+
+    switch (dm->level) {
+    case 3:
+        put_bits(pb, 12, dm->l3.min_pq_offset);
+        put_bits(pb, 12, dm->l3.max_pq_offset);
+        put_bits(pb, 12, dm->l3.avg_pq_offset);
+        break;
+    case 8:
+        put_bits(pb, 8, dm->l8.target_display_index);
+        put_bits(pb, 12, dm->l8.trim_slope);
+        put_bits(pb, 12, dm->l8.trim_offset);
+        put_bits(pb, 12, dm->l8.trim_power);
+        put_bits(pb, 12, dm->l8.trim_chroma_weight);
+        put_bits(pb, 12, dm->l8.trim_saturation_gain);
+        put_bits(pb, 12, dm->l8.ms_weight + 8192);
+        if (ext_block_length < 12)
+            break;
+        put_bits(pb, 12, dm->l8.target_mid_contrast);
+        if (ext_block_length < 13)
+            break;
+        put_bits(pb, 12, dm->l8.clip_trim);
+        if (ext_block_length < 19)
+            break;
+        for (int i = 0; i < 6; i++)
+            put_bits(pb, 8, dm->l8.saturation_vector_field[i]);
+        if (ext_block_length < 25)
+            break;
+        for (int i = 0; i < 6; i++)
+            put_bits(pb, 8, dm->l8.hue_vector_field[i]);
+        break;
+    case 9:
+        put_bits(pb, 8, dm->l9.source_primary_index);
+        if (ext_block_length < 17)
+            break;
+        put_cie_xy(pb, dm->l9.source_display_primaries.prim.r);
+        put_cie_xy(pb, dm->l9.source_display_primaries.prim.g);
+        put_cie_xy(pb, dm->l9.source_display_primaries.prim.b);
+        put_cie_xy(pb, dm->l9.source_display_primaries.wp);
+        break;
+    case 10:
+        put_bits(pb, 8, dm->l10.target_display_index);
+        put_bits(pb, 12, dm->l10.target_max_pq);
+        put_bits(pb, 12, dm->l10.target_min_pq);
+        put_bits(pb, 8, dm->l10.target_primary_index);
+        if (ext_block_length < 21)
+            break;
+        put_cie_xy(pb, dm->l10.target_display_primaries.prim.r);
+        put_cie_xy(pb, dm->l10.target_display_primaries.prim.g);
+        put_cie_xy(pb, dm->l10.target_display_primaries.prim.b);
+        put_cie_xy(pb, dm->l10.target_display_primaries.wp);
+        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, 2, dm->l11.sharpness);
+        put_bits(pb, 2, dm->l11.noise_reduction);
+        put_bits(pb, 2, dm->l11.mpeg_noise_reduction);
+        put_bits(pb, 2, dm->l11.frame_rate_conversion);
+        put_bits(pb, 2, dm->l11.brightness);
+        put_bits(pb, 2, dm->l11.color);
+        break;
+    case 254:
+        put_bits(pb, 8, dm->l254.dm_mode);
+        put_bits(pb, 8, dm->l254.dm_version_index);
+        break;
+    }
+
+    pad_bits = ext_block_length * 8 - (put_bits_count(pb) - start_pos);
+    av_assert1(pad_bits >= 0);
+    put_bits(pb, pad_bits, 0);
+}
+
+int ff_dovi_rpu_generate(DOVIContext *s, const AVDOVIMetadata *metadata,
+                         uint8_t **out_rpu, int *out_size)
+{
+    PutBitContext *pb = &(PutBitContext){0};
+    const AVDOVIRpuDataHeader *hdr;
+    const AVDOVIDataMapping *mapping;
+    const AVDOVIColorMetadata *color;
+    int vdr_dm_metadata_changed, vdr_rpu_id, use_prev_vdr_rpu, profile,
+        buffer_size, rpu_size, pad, zero_run;
+    int num_ext_blocks_v1, num_ext_blocks_v2;
+    uint32_t crc;
+    uint8_t *dst;
+    if (!metadata) {
+        *out_rpu = NULL;
+        *out_size = 0;
+        return 0;
+    }
+
+    hdr = av_dovi_get_header(metadata);
+    mapping = av_dovi_get_mapping(metadata);
+    color = av_dovi_get_color(metadata);
+    av_assert0(s->cfg.dv_profile);
+
+    if (hdr->rpu_type != 2) {
+        av_log(s->logctx, AV_LOG_ERROR, "Unhandled RPU type %"PRIu8"\n",
+               hdr->rpu_type);
+        return AVERROR_INVALIDDATA;
+    }
+
+    vdr_rpu_id = -1;
+    for (int i = 0; i <= DOVI_MAX_DM_ID; i++) {
+        if (s->vdr[i] && !memcmp(&s->vdr[i]->mapping, mapping, sizeof(*mapping))) {
+            vdr_rpu_id = i;
+            break;
+        } else if (vdr_rpu_id < 0 && (!s->vdr[i] || i == DOVI_MAX_DM_ID)) {
+            vdr_rpu_id = i;
+        }
+    }
+
+    if (!s->vdr[vdr_rpu_id]) {
+        s->vdr[vdr_rpu_id] = ff_refstruct_allocz(sizeof(DOVIVdr));
+        if (!s->vdr[vdr_rpu_id])
+            return AVERROR(ENOMEM);
+    }
+
+    if (!s->vdr[color->dm_metadata_id]) {
+        s->vdr[color->dm_metadata_id] = ff_refstruct_allocz(sizeof(DOVIVdr));
+        if (!s->vdr[color->dm_metadata_id])
+            return AVERROR(ENOMEM);
+    }
+
+    num_ext_blocks_v1 = num_ext_blocks_v2 = 0;
+    for (int i = 0; i < metadata->num_ext_blocks; i++) {
+        const AVDOVIDmData *dm = av_dovi_get_ext(metadata, i);
+        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;
+        }
+    }
+
+    vdr_dm_metadata_changed = !s->color || memcmp(s->color, color, sizeof(*color));
+    use_prev_vdr_rpu = !memcmp(&s->vdr[vdr_rpu_id]->mapping, mapping, sizeof(*mapping));
+
+    buffer_size = 12 /* vdr seq info */ + 5 /* CRC32 + terminator */;
+    buffer_size += num_ext_blocks_v1 * 13;
+    buffer_size += num_ext_blocks_v2 * 28;
+    if (!use_prev_vdr_rpu) {
+        buffer_size += 160;
+        for (int c = 0; c < 3; c++) {
+            for (int i = 0; i < mapping->curves[c].num_pivots - 1; i++) {
+                switch (mapping->curves[c].mapping_idc[i]) {
+                case AV_DOVI_MAPPING_POLYNOMIAL: buffer_size += 26;  break;
+                case AV_DOVI_MAPPING_MMR:        buffer_size += 177; break;
+                }
+            }
+        }
+    }
+    if (vdr_dm_metadata_changed)
+        buffer_size += 67;
+
+    av_fast_padded_malloc(&s->rpu_buf, &s->rpu_buf_sz, buffer_size);
+    if (!s->rpu_buf)
+        return AVERROR(ENOMEM);
+    init_put_bits(pb, s->rpu_buf, s->rpu_buf_sz);
+
+    /* RPU header */
+    put_bits(pb, 6, hdr->rpu_type);
+    put_bits(pb, 11, hdr->rpu_format);
+    put_bits(pb, 4, hdr->vdr_rpu_profile);
+    put_bits(pb, 4, hdr->vdr_rpu_level);
+    put_bits(pb, 1, 1); /* vdr_seq_info_present */
+    put_bits(pb, 1, hdr->chroma_resampling_explicit_filter_flag);
+    put_bits(pb, 2, hdr->coef_data_type);
+    if (hdr->coef_data_type == RPU_COEFF_FIXED)
+        set_ue_golomb(pb, hdr->coef_log2_denom);
+    put_bits(pb, 2, hdr->vdr_rpu_normalized_idc);
+    put_bits(pb, 1, hdr->bl_video_full_range_flag);
+    if ((hdr->rpu_format & 0x700) == 0) {
+        set_ue_golomb(pb, hdr->bl_bit_depth - 8);
+        set_ue_golomb(pb, hdr->el_bit_depth - 8);
+        set_ue_golomb(pb, hdr->vdr_bit_depth - 8);
+        put_bits(pb, 1, hdr->spatial_resampling_filter_flag);
+        put_bits(pb, 3, 0); /* reserved_zero_3bits */
+        put_bits(pb, 1, hdr->el_spatial_resampling_filter_flag);
+        put_bits(pb, 1, hdr->disable_residual_flag);
+    }
+    s->header = *hdr;
+
+    put_bits(pb, 1, vdr_dm_metadata_changed);
+    put_bits(pb, 1, use_prev_vdr_rpu);
+    set_ue_golomb(pb, vdr_rpu_id);
+    s->mapping = &s->vdr[vdr_rpu_id]->mapping;
+
+    if (!use_prev_vdr_rpu) {
+        set_ue_golomb(pb, mapping->mapping_color_space);
+        set_ue_golomb(pb, mapping->mapping_chroma_format_idc);
+        for (int c = 0; c < 3; c++) {
+            const AVDOVIReshapingCurve *curve = &mapping->curves[c];
+            int prev = 0;
+            set_ue_golomb(pb, curve->num_pivots - 2);
+            for (int i = 0; i < curve->num_pivots; i++) {
+                put_bits(pb, hdr->bl_bit_depth, curve->pivots[i] - prev);
+                prev = curve->pivots[i];
+            }
+        }
+
+        if (mapping->nlq_method_idc != AV_DOVI_NLQ_NONE) {
+            put_bits(pb, 3, mapping->nlq_method_idc);
+            put_bits(pb, hdr->bl_bit_depth, mapping->nlq_pivots[0]);
+            put_bits(pb, hdr->bl_bit_depth, mapping->nlq_pivots[1] - mapping->nlq_pivots[0]);
+        }
+
+        set_ue_golomb(pb, mapping->num_x_partitions - 1);
+        set_ue_golomb(pb, mapping->num_y_partitions - 1);
+
+        for (int c = 0; c < 3; c++) {
+            const AVDOVIReshapingCurve *curve = &mapping->curves[c];
+            for (int i = 0; i < curve->num_pivots - 1; i++) {
+                set_ue_golomb(pb, curve->mapping_idc[i]);
+                switch (curve->mapping_idc[i]) {
+                case AV_DOVI_MAPPING_POLYNOMIAL: {
+                    set_ue_golomb(pb, curve->poly_order[i] - 1);
+                    if (curve->poly_order[i] == 1)
+                        put_bits(pb, 1, 0); /* linear_interp_flag */
+                    for (int k = 0; k <= curve->poly_order[i]; k++)
+                        put_se_coef(pb, hdr, curve->poly_coef[i][k]);
+                    break;
+                }
+                case AV_DOVI_MAPPING_MMR: {
+                    put_bits(pb, 2, curve->mmr_order[i] - 1);
+                    put_se_coef(pb, hdr, curve->mmr_constant[i]);
+                    for (int j = 0; j < curve->mmr_order[i]; j++) {
+                        for (int k = 0; k < 7; k++)
+                            put_se_coef(pb, hdr, curve->mmr_coef[i][j][k]);
+                    }
+                    break;
+                }
+                }
+            }
+        }
+
+        if (mapping->nlq_method_idc != AV_DOVI_NLQ_NONE) {
+            for (int c = 0; c < 3; c++) {
+                const AVDOVINLQParams *nlq = &mapping->nlq[c];
+                put_bits(pb, hdr->el_bit_depth, nlq->nlq_offset);
+                put_ue_coef(pb, hdr, nlq->vdr_in_max);
+                switch (mapping->nlq_method_idc) {
+                case AV_DOVI_NLQ_LINEAR_DZ:
+                    put_ue_coef(pb, hdr, nlq->linear_deadzone_slope);
+                    put_ue_coef(pb, hdr, nlq->linear_deadzone_threshold);
+                    break;
+                }
+            }
+        }
+
+        memcpy(&s->vdr[vdr_rpu_id]->mapping, mapping, sizeof(*mapping));
+    }
+
+    if (vdr_dm_metadata_changed) {
+        const int denom = profile == 4 ? (1 << 30) : (1 << 28);
+        set_ue_golomb(pb, color->dm_metadata_id); /* affected_dm_id */
+        set_ue_golomb(pb, color->dm_metadata_id); /* current_dm_id */
+        set_ue_golomb(pb, color->scene_refresh_flag);
+        for (int i = 0; i < 9; i++)
+            put_sbits(pb, 16, av_q2den(color->ycc_to_rgb_matrix[i], 1 << 13));
+        for (int i = 0; i < 3; i++)
+            put_bits32(pb, av_q2den(color->ycc_to_rgb_offset[i], denom));
+        for (int i = 0; i < 9; i++)
+            put_sbits(pb, 16, av_q2den(color->rgb_to_lms_matrix[i], 1 << 14));
+        put_bits(pb, 16, color->signal_eotf);
+        put_bits(pb, 16, color->signal_eotf_param0);
+        put_bits(pb, 16, color->signal_eotf_param1);
+        put_bits32(pb, color->signal_eotf_param2);
+        put_bits(pb, 5, color->signal_bit_depth);
+        put_bits(pb, 2, color->signal_color_space);
+        put_bits(pb, 2, color->signal_chroma_format);
+        put_bits(pb, 2, color->signal_full_range_flag);
+        put_bits(pb, 12, color->source_min_pq);
+        put_bits(pb, 12, color->source_max_pq);
+        put_bits(pb, 10, color->source_diagonal);
+
+        memcpy(&s->vdr[color->dm_metadata_id]->color, color, sizeof(*color));
+        s->color = &s->vdr[color->dm_metadata_id]->color;
+    }
+
+    set_ue_golomb(pb, num_ext_blocks_v1);
+    align_put_bits(pb);
+    for (int i = 0; i < metadata->num_ext_blocks; i++)
+        generate_ext_v1(pb, av_dovi_get_ext(metadata, i));
+
+    if (num_ext_blocks_v2) {
+        set_ue_golomb(pb, num_ext_blocks_v2);
+        align_put_bits(pb);
+        for (int i = 0; i < metadata->num_ext_blocks; i++)
+            generate_ext_v2(pb, av_dovi_get_ext(metadata, i));
+    }
+
+    flush_put_bits(pb);
+    crc = av_bswap32(av_crc(av_crc_get_table(AV_CRC_32_IEEE), -1,
+                            s->rpu_buf, put_bytes_output(pb)));
+    put_bits32(pb, crc);
+    put_bits(pb, 8, 0x80); /* terminator */
+    flush_put_bits(pb);
+
+    rpu_size = put_bytes_output(pb);
+    switch (s->cfg.dv_profile) {
+    case 10:
+        /* AV1 uses T.35 OBU with EMDF header */
+        *out_rpu = av_malloc(rpu_size + 15);
+        if (!*out_rpu)
+            return AVERROR(ENOMEM);
+        init_put_bits(pb, *out_rpu, rpu_size + 15);
+        put_bits(pb,  8, ITU_T_T35_COUNTRY_CODE_US);
+        put_bits(pb, 16, ITU_T_T35_PROVIDER_CODE_DOLBY);
+        put_bits32(pb, 0x800); /* provider_oriented_code */
+        put_bits(pb, 27, 0x01be6841u); /* fixed EMDF header, see above */
+        if (rpu_size > 0xFF) {
+            av_assert2(rpu_size <= 0x10000);
+            put_bits(pb, 8, (rpu_size >> 8) - 1);
+            put_bits(pb, 1, 1); /* read_more */
+            put_bits(pb, 8, rpu_size & 0xFF);
+            put_bits(pb, 1, 0);
+        } else {
+            put_bits(pb, 8, rpu_size);
+            put_bits(pb, 1, 0);
+        }
+        ff_copy_bits(pb, s->rpu_buf, rpu_size * 8);
+        put_bits(pb, 17, 0x400); /* emdf payload id + emdf_protection */
+
+        pad = pb->bit_left & 7;
+        put_bits(pb, pad, (1 << pad) - 1); /* pad to next byte with 1 bits */
+        flush_put_bits(pb);
+        *out_size = put_bytes_output(pb);
+        return 0;
+
+    case 5:
+    case 8:
+        *out_rpu = dst = av_malloc(1 + rpu_size * 3 / 2); /* worst case */
+        if (!*out_rpu)
+            return AVERROR(ENOMEM);
+        *dst++ = 25; /* NAL prefix */
+        zero_run = 0;
+        for (int i = 0; i < rpu_size; i++) {
+            if (zero_run < 2) {
+                if (s->rpu_buf[i] == 0) {
+                    zero_run++;
+                } else {
+                    zero_run = 0;
+                }
+            } else {
+                if ((s->rpu_buf[i] & ~3) == 0) {
+                    /* emulation prevention */
+                    *dst++ = 3;
+                }
+                zero_run = s->rpu_buf[i] == 0;
+            }
+            *dst++ = s->rpu_buf[i];
+        }
+        *out_size = dst - *out_rpu;
+        return 0;
+
+    default:
+        /* Should be unreachable */
+        av_assert0(0);
+        return AVERROR_BUG;
+    }
+}
diff --git a/libavcodec/dovi_rpu.h b/libavcodec/dovi_rpu.h
index 33e19dd037c..64433b73e65 100644
--- a/libavcodec/dovi_rpu.h
+++ b/libavcodec/dovi_rpu.h
@@ -44,20 +44,22 @@  typedef struct DOVIContext {
     AVDOVIDecoderConfigurationRecord cfg;
 
     /**
-     * Currently active RPU data header, updates on every dovi_rpu_parse().
+     * Currently active RPU data header, updates on every ff_dovi_rpu_parse()
+     * or ff_dovi_rpu_generate().
      */
     AVDOVIRpuDataHeader header;
 
     /**
      * Currently active data mappings, or NULL. Points into memory owned by the
      * corresponding rpu/vdr_ref, which becomes invalid on the next call to
-     * dovi_rpu_parse.
+     * ff_dovi_rpu_parse() or ff_dovi_rpu_generate().
      */
     const AVDOVIDataMapping *mapping;
     const AVDOVIColorMetadata *color;
 
     /**
      * Currently active extension blocks, updates on every ff_dovi_rpu_parse()
+     * or ff_dovi_rpu_generate().
      */
     AVDOVIDmData *ext_blocks;
     int num_ext_blocks;
@@ -108,4 +110,18 @@  int ff_dovi_attach_side_data(DOVIContext *s, AVFrame *frame);
  */
 int ff_dovi_configure(DOVIContext *s, AVCodecContext *avctx);
 
+/**
+ * Synthesize a Dolby Vision RPU reflecting the current state. Note that this
+ * assumes all previous calls to `ff_dovi_rpu_generate` have been appropriately
+ * signalled, i.e. it will not re-send already transmitted redundant data.
+ *
+ * Mutates the internal state of DOVIContext to reflect the change.
+ * Returns 0 or a negative error code.
+ *
+ * This generates a fully formed RPU ready for inclusion in the bitstream,
+ * including the EMDF header (profile 10) or NAL encapsulation (otherwise).
+ */
+int ff_dovi_rpu_generate(DOVIContext *s, const AVDOVIMetadata *metadata,
+                         uint8_t **out_rpu, int *out_size);
+
 #endif /* AVCODEC_DOVI_RPU_H */