diff --git a/README.md b/README.md index d4d8362..0d1be0c 100644 --- a/README.md +++ b/README.md @@ -33,22 +33,22 @@ The minimum Rust version to build `dovi_tool` is 1.51.0. * Example: `dovi_tool generate --xml dolbyvision_metadata.xml -o RPU_from_xml.bin`   ##### From a generic profile 8.1 configuration JSON file - * See documentation: [generator.md](generator.md) or [example](assets/generator_example.json) - * Example: `dovi_tool generate -j assets/generator_example.json -o RPU_generated.bin` + * See documentation: [generator.md](docs/generator.md) or [examples](assets/generator_examples) + * Example: `dovi_tool generate -j assets/generator_examples/default_cmv40.json -o RPU_generated.bin`   ##### From an existing HDR10+ metadata JSON file The metadata is generated from a configuration JSON file, and the L1 metadata is derived from HDR10+ metadata. * The HDR10+ metadata has to contain scene information for proper scene cuts. - * Example: `dovi_tool generate -j assets/generator_example.json --hdr10plus-json hdr10plus_metadata.json -o RPU_from_hdr10plus.bin` + * Example: `dovi_tool generate -j assets/generator_examples/default_cmv40.json --hdr10plus-json hdr10plus_metadata.json -o RPU_from_hdr10plus.bin`   ##### From a madVR HDR measurement file The metadata is generated from a configuration JSON file, and the L1 metadata is derived from the madVR measurements. Supports using custom targets nits from Soulnight's madMeasureHDR Optimizer, with flag `--use-custom-targets`. - * Example: `dovi_tool generate -j assets/generator_example.json --madvr-file madmeasure-output.bin -o RPU_from_madVR.bin` + * Example: `dovi_tool generate -j assets/generator_examples/default_cmv40.json --madvr-file madmeasure-output.bin -o RPU_from_madVR.bin`   * #### editor Allows editing a binary RPU according to a JSON config. - See documentation: [editor.md](editor.md) or [examples](assets/editor_examples). + See documentation: [editor.md](docs/editor.md) or [examples](assets/editor_examples). All indices start at 0, and are inclusive. For example, using "0-39" edits the first 40 frames. * Example: `dovi_tool editor -i RPU.bin -j assets/editor_examples/mode.json -o RPU_mode2.bin`   diff --git a/assets/generator_example.json b/assets/generator_example.json deleted file mode 100644 index 1b6b3c2..0000000 --- a/assets/generator_example.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "cm_version": "V40", - "length": 100, - "level5": { - "active_area_left_offset": 0, - "active_area_right_offset": 0, - "active_area_top_offset": 0, - "active_area_bottom_offset": 0 - }, - "level6": { - "max_display_mastering_luminance": 1000, - "min_display_mastering_luminance": 1, - "max_content_light_level": 1000, - "max_frame_average_light_level": 400 - }, - "shots": [ - { - "start": 0, - "duration": 100, - "metadata_blocks": [ - { - "Level1": { - "min_pq": 0, - "max_pq": 2081, - "avg_pq": 819 - } - }, - { - "Level2": { - "target_max_pq": 3079, - "trim_slope": 2048, - "trim_offset": 2048, - "trim_power": 2048, - "trim_chroma_weight": 2048, - "trim_saturation_gain": 2048, - "ms_weight": 2048 - } - } - ] - } - ] -} diff --git a/assets/generator_examples/default_cmv29.json b/assets/generator_examples/default_cmv29.json new file mode 100644 index 0000000..9e36263 --- /dev/null +++ b/assets/generator_examples/default_cmv29.json @@ -0,0 +1,10 @@ +{ + "cm_version": "V29", + "length": 10, + "level6": { + "max_display_mastering_luminance": 1000, + "min_display_mastering_luminance": 1, + "max_content_light_level": 1000, + "max_frame_average_light_level": 400 + } +} diff --git a/assets/generator_examples/default_cmv40.json b/assets/generator_examples/default_cmv40.json new file mode 100644 index 0000000..ee1bcc6 --- /dev/null +++ b/assets/generator_examples/default_cmv40.json @@ -0,0 +1,10 @@ +{ + "cm_version": "V40", + "length": 10, + "level6": { + "max_display_mastering_luminance": 1000, + "min_display_mastering_luminance": 1, + "max_content_light_level": 1000, + "max_frame_average_light_level": 400 + } +} diff --git a/assets/generator_examples/full_example.json b/assets/generator_examples/full_example.json new file mode 100644 index 0000000..fbcc933 --- /dev/null +++ b/assets/generator_examples/full_example.json @@ -0,0 +1,80 @@ +{ + "cm_version": "V40", + "level5": { + "active_area_left_offset": 0, + "active_area_right_offset": 0, + "active_area_top_offset": 40, + "active_area_bottom_offset": 40 + }, + "level6": { + "max_display_mastering_luminance": 1000, + "min_display_mastering_luminance": 1, + "max_content_light_level": 1000, + "max_frame_average_light_level": 400 + }, + "default_metadata_blocks": [ + { + "Level2": { + "target_max_pq": 3079, + "trim_slope": 2048, + "trim_offset": 2048, + "trim_power": 2048, + "trim_chroma_weight": 2048, + "trim_saturation_gain": 2048, + "ms_weight": 2048 + } + }, + { + "Level9": { + "source_primary_index": 0 + } + }, + { + "Level11": { + "content_type": 4, + "whitepoint": 0, + "reference_mode_flag": true + } + } + ], + "shots": [ + { + "start": 0, + "duration": 10, + "metadata_blocks": [ + { + "Level1": { + "min_pq": 2, + "max_pq": 2938, + "avg_pq": 1456 + } + }, + { + "Level2": { + "target_max_pq": 2851, + "trim_slope": 2048, + "trim_offset": 2048, + "trim_power": 1800, + "trim_chroma_weight": 2048, + "trim_saturation_gain": 2048, + "ms_weight": 2048 + } + } + ], + "frame_edits": [ + { + "edit_offset": 5, + "metadata_blocks": [ + { + "Level1": { + "min_pq": 0, + "max_pq": 3079, + "avg_pq": 1229 + } + } + ] + } + ] + } + ] +} diff --git a/assets/generator_examples/no_duration.json b/assets/generator_examples/no_duration.json new file mode 100644 index 0000000..59f4f3d --- /dev/null +++ b/assets/generator_examples/no_duration.json @@ -0,0 +1,110 @@ +{ + "cm_version": "V40", + "level6": { + "max_display_mastering_luminance": 1000, + "min_display_mastering_luminance": 1, + "max_content_light_level": 1000, + "max_frame_average_light_level": 400 + }, + "default_metadata_blocks": [ + { + "Level1": { + "min_pq": 0, + "max_pq": 123, + "avg_pq": 39 + } + }, + { + "Level9": { + "source_primary_index": 0 + } + } + ], + "shots": [ + { + "start": 0, + "duration": 0, + "metadata_blocks": [ + { + "Level1": { + "min_pq": 0, + "max_pq": 123, + "avg_pq": 39 + } + }, + { + "Level2": { + "target_max_pq": 2851, + "trim_slope": 2048, + "trim_offset": 2048, + "trim_power": 1800, + "trim_chroma_weight": 2048, + "trim_saturation_gain": 2048, + "ms_weight": 2048 + } + } + ] + }, + { + "start": 0, + "duration": 0, + "metadata_blocks": [ + { + "Level2": { + "target_max_pq": 2851, + "trim_slope": 1400, + "trim_offset": 1234, + "trim_power": 1800, + "trim_chroma_weight": 2048, + "trim_saturation_gain": 2048, + "ms_weight": 2048 + } + }, + { + "Level5": { + "active_area_left_offset": 0, + "active_area_right_offset": 0, + "active_area_top_offset": 276, + "active_area_bottom_offset": 276 + } + } + ], + "frame_edits": [ + { + "edit_offset": 2, + "metadata_blocks": [ + { + "Level1": { + "min_pq": 0, + "max_pq": 3079, + "avg_pq": 1229 + } + }, + { + "Level2": { + "target_max_pq": 2851, + "trim_slope": 1999, + "trim_offset": 1999, + "trim_power": 1999, + "trim_chroma_weight": 2048, + "trim_saturation_gain": 2048, + "ms_weight": 2048 + } + }, + { + "Level2": { + "target_max_pq": 3079, + "trim_slope": 2048, + "trim_offset": 2048, + "trim_power": 2048, + "trim_chroma_weight": 2048, + "trim_saturation_gain": 2048, + "ms_weight": 2048 + } + } + ] + } + ] + } + ] +} diff --git a/assets/tests/hdr10plus_metadata.json b/assets/tests/hdr10plus_metadata.json new file mode 100644 index 0000000..39937bb --- /dev/null +++ b/assets/tests/hdr10plus_metadata.json @@ -0,0 +1,375 @@ +{ + "JSONInfo": { + "HDR10plusProfile": "A", + "Version": "1.0" + }, + "SceneInfo": [ + { + "LuminanceParameters": { + "AverageRGB": 1037, + "LuminanceDistributions": { + "DistributionIndex": [ + 1, + 5, + 10, + 25, + 50, + 75, + 90, + 95, + 99 + ], + "DistributionValues": [ + 3, + 14024, + 43, + 56, + 219, + 1036, + 2714, + 4668, + 14445 + ] + }, + "MaxScl": [ + 17830, + 16895, + 14252 + ] + }, + "NumberOfWindows": 1, + "TargetedSystemDisplayMaximumLuminance": 0, + "SceneFrameIndex": 0, + "SceneId": 0, + "SequenceFrameIndex": 0 + }, + { + "LuminanceParameters": { + "AverageRGB": 1037, + "LuminanceDistributions": { + "DistributionIndex": [ + 1, + 5, + 10, + 25, + 50, + 75, + 90, + 95, + 99 + ], + "DistributionValues": [ + 3, + 14024, + 43, + 56, + 219, + 1036, + 2714, + 4668, + 14445 + ] + }, + "MaxScl": [ + 17830, + 16895, + 14252 + ] + }, + "NumberOfWindows": 1, + "TargetedSystemDisplayMaximumLuminance": 0, + "SceneFrameIndex": 1, + "SceneId": 0, + "SequenceFrameIndex": 1 + }, + { + "LuminanceParameters": { + "AverageRGB": 1037, + "LuminanceDistributions": { + "DistributionIndex": [ + 1, + 5, + 10, + 25, + 50, + 75, + 90, + 95, + 99 + ], + "DistributionValues": [ + 3, + 14024, + 43, + 56, + 219, + 1036, + 2714, + 4668, + 14445 + ] + }, + "MaxScl": [ + 17830, + 16895, + 14252 + ] + }, + "NumberOfWindows": 1, + "TargetedSystemDisplayMaximumLuminance": 0, + "SceneFrameIndex": 2, + "SceneId": 0, + "SequenceFrameIndex": 2 + }, + { + "LuminanceParameters": { + "AverageRGB": 297, + "LuminanceDistributions": { + "DistributionIndex": [ + 1, + 5, + 10, + 25, + 50, + 75, + 90, + 95, + 99 + ], + "DistributionValues": [ + 6, + 2675, + 51, + 65, + 124, + 352, + 503, + 1158, + 3145 + ] + }, + "MaxScl": [ + 20487, + 20579, + 17047 + ] + }, + "NumberOfWindows": 1, + "TargetedSystemDisplayMaximumLuminance": 0, + "SceneFrameIndex": 0, + "SceneId": 1, + "SequenceFrameIndex": 3 + }, + { + "LuminanceParameters": { + "AverageRGB": 297, + "LuminanceDistributions": { + "DistributionIndex": [ + 1, + 5, + 10, + 25, + 50, + 75, + 90, + 95, + 99 + ], + "DistributionValues": [ + 6, + 2675, + 51, + 65, + 124, + 352, + 503, + 1158, + 3145 + ] + }, + "MaxScl": [ + 20487, + 20579, + 17047 + ] + }, + "NumberOfWindows": 1, + "TargetedSystemDisplayMaximumLuminance": 0, + "SceneFrameIndex": 1, + "SceneId": 1, + "SequenceFrameIndex": 4 + }, + { + "LuminanceParameters": { + "AverageRGB": 297, + "LuminanceDistributions": { + "DistributionIndex": [ + 1, + 5, + 10, + 25, + 50, + 75, + 90, + 95, + 99 + ], + "DistributionValues": [ + 6, + 2675, + 51, + 65, + 124, + 352, + 503, + 1158, + 3145 + ] + }, + "MaxScl": [ + 20487, + 20579, + 17047 + ] + }, + "NumberOfWindows": 1, + "TargetedSystemDisplayMaximumLuminance": 0, + "SceneFrameIndex": 2, + "SceneId": 1, + "SequenceFrameIndex": 5 + }, + { + "LuminanceParameters": { + "AverageRGB": 911, + "LuminanceDistributions": { + "DistributionIndex": [ + 1, + 5, + 10, + 25, + 50, + 75, + 90, + 95, + 99 + ], + "DistributionValues": [ + 3, + 11061, + 52, + 13, + 98, + 1556, + 2855, + 4055, + 11810 + ] + }, + "MaxScl": [ + 17513, + 16895, + 14316 + ] + }, + "NumberOfWindows": 1, + "TargetedSystemDisplayMaximumLuminance": 0, + "SceneFrameIndex": 0, + "SceneId": 2, + "SequenceFrameIndex": 6 + }, + { + "LuminanceParameters": { + "AverageRGB": 911, + "LuminanceDistributions": { + "DistributionIndex": [ + 1, + 5, + 10, + 25, + 50, + 75, + 90, + 95, + 99 + ], + "DistributionValues": [ + 3, + 11061, + 52, + 13, + 98, + 1556, + 2855, + 4055, + 11810 + ] + }, + "MaxScl": [ + 17513, + 16895, + 14316 + ] + }, + "NumberOfWindows": 1, + "TargetedSystemDisplayMaximumLuminance": 0, + "SceneFrameIndex": 1, + "SceneId": 2, + "SequenceFrameIndex": 7 + }, + { + "LuminanceParameters": { + "AverageRGB": 911, + "LuminanceDistributions": { + "DistributionIndex": [ + 1, + 5, + 10, + 25, + 50, + 75, + 90, + 95, + 99 + ], + "DistributionValues": [ + 3, + 11061, + 52, + 13, + 98, + 1556, + 2855, + 4055, + 11810 + ] + }, + "MaxScl": [ + 17513, + 16895, + 14316 + ] + }, + "NumberOfWindows": 1, + "TargetedSystemDisplayMaximumLuminance": 0, + "SceneFrameIndex": 2, + "SceneId": 2, + "SequenceFrameIndex": 8 + } + ], + "SceneInfoSummary": { + "SceneFirstFrameIndex": [ + 0, + 3, + 6 + ], + "SceneFrameNumbers": [ + 3, + 3, + 3 + ] + }, + "ToolInfo": { + "Tool": "hdr10plus_tool", + "Version": "1.2.1" + } +} diff --git a/editor.md b/docs/editor.md similarity index 98% rename from editor.md rename to docs/editor.md index 618d755..c8de2b8 100644 --- a/editor.md +++ b/docs/editor.md @@ -87,6 +87,8 @@ The editor expects a JSON config like the example below: // WP * 375 + 6504 // D65 = 0 "whitepoint": int, + + // Whether to force reference mode or not. "reference_mode_flag": boolean } } diff --git a/docs/generator.md b/docs/generator.md new file mode 100644 index 0000000..eda8a25 --- /dev/null +++ b/docs/generator.md @@ -0,0 +1,80 @@ +The generator can create a profile 8.1 RPU binary. +Any extension metadata can be added. + +A JSON config example: + +```json5 +{ + // CM version, either "V29" or "V40". + // Defaults to "V40". + "cm_version": string, + + // Number of metadata frames to generate. + // Optional if shots are specified, as well as for HDR10+ and madVR sourced generation. + "length": int, + + // Source min/max PQ values to override, optional. + // If not specified, derived from L6 metadata. + "source_min_pq": int, + "source_max_pq": int, + + // L5 metadata, optional. + // If not specified, L5 metadata is added with 0 offsets. + "level5": { + "active_area_left_offset": int, + "active_area_right_offset": int, + "active_area_top_offset": int, + "active_area_bottom_offset": int, + }, + + // L6 metadata, required for profile 8.1. + "level6": { + "max_display_mastering_luminance": int, + "min_display_mastering_luminance": int, + "max_content_light_level": int, + "max_frame_average_light_level": int, + }, + + // Metadata blocks that should be present in every RPU of the sequence. + // Does not accept L5, L6 and L254 metadata. + // Disallowed blocks are simply ignored. + // + // For HDR10+ or madVR generation, the default L1 metadata is replaced. + // + // Refer to assets/generator_examples/full_example.json + "default_metadata_blocks": Array, + + // Shots to specify metadata. + // Array of VideoShot objects. + // + // For HDR10+ or madVR generation: + // - The metadata is taken from the shots in the list order. + // This means that both start and duration can be 0. + // - It is expected that the source metadata has the same number of shots as this list. + // Missing or extra shots are ignored. + // + // Refer to generator examples. + "shots": [ + { + // Start frame. + "start": int, + // Shot frame length. + "duration": int, + + // List of metadata blocks to use for this shot. + "metadata_blocks": Array, + + // Metadata to use for specific frames in the shot. + "frame_edits": [ + { + // Frame offset to edit in the shot. + "edit_offset": int, + + // List of metadata blocks to use for the frame. + "metadata_blocks": Array, + } + ] + } + ], +} +``` diff --git a/profiles.md b/docs/profiles.md similarity index 100% rename from profiles.md rename to docs/profiles.md diff --git a/dolby_vision/CHANGELOG.md b/dolby_vision/CHANGELOG.md index 0e5600d..900aad3 100644 --- a/dolby_vision/CHANGELOG.md +++ b/dolby_vision/CHANGELOG.md @@ -1,6 +1,9 @@ -### ??? +### 1.6.0 - Fixed deserialize default value for `GenerateConfig`.`cm_version` field. +- Added `default_metadata_blocks` to `GenerateConfig` struct. +- Removed `target_nits` field from `GenerateConfig`. Use default blocks. +- ## 1.5.2 diff --git a/dolby_vision/src/rpu/dovi_rpu.rs b/dolby_vision/src/rpu/dovi_rpu.rs index b96e82d..f41eaf7 100644 --- a/dolby_vision/src/rpu/dovi_rpu.rs +++ b/dolby_vision/src/rpu/dovi_rpu.rs @@ -365,7 +365,7 @@ impl DoviRpu { header: RpuDataHeader::p8_default(), rpu_data_mapping: Some(RpuDataMapping::p8_default()), rpu_data_nlq: None, - vdr_dm_data: Some(VdrDmData::from_config(config)?), + vdr_dm_data: Some(VdrDmData::from_generate_config(config)?), last_byte: 0x80, ..Default::default() }) diff --git a/dolby_vision/src/rpu/generate.rs b/dolby_vision/src/rpu/generate.rs index a6a3710..2363932 100644 --- a/dolby_vision/src/rpu/generate.rs +++ b/dolby_vision/src/rpu/generate.rs @@ -4,7 +4,7 @@ use std::{ path::Path, }; -use anyhow::Result; +use anyhow::{ensure, Result}; #[cfg(feature = "serde_feature")] use serde::{Deserialize, Serialize}; @@ -16,53 +16,84 @@ use blocks::*; const OUT_NAL_HEADER: &[u8] = &[0, 0, 0, 1]; +/// Generic generation config struct. #[derive(Debug)] #[cfg_attr(feature = "serde_feature", derive(Deserialize, Serialize))] pub struct GenerateConfig { + /// Content mapping version + /// Optional, defaults to v4.0 #[cfg_attr(feature = "serde_feature", serde(default = "CmVersion::v40"))] pub cm_version: CmVersion, + /// Number of RPU frames to generate. + /// Required only when no shots are specified. + #[cfg_attr(feature = "serde_feature", serde(default))] pub length: usize, - /// Optional, specifies a L2 block for this target - pub target_nits: Option, - + /// Mastering display min luminance, as 12 bit PQ code. #[cfg_attr(feature = "serde_feature", serde(default))] pub source_min_pq: Option, - #[cfg_attr(feature = "serde_feature", serde(default))] - pub source_max_pq: Option, + /// Mastering display max luminance, as 12 bit PQ code. #[cfg_attr(feature = "serde_feature", serde(default))] - pub shots: Vec, + pub source_max_pq: Option, + /// Active area offsets. /// Defaults to zero offsets, should be present in RPU #[cfg_attr(feature = "serde_feature", serde(default))] pub level5: ExtMetadataBlockLevel5, + /// ST2086/HDR10 fallback metadata. + /// Required for deserialization. /// Defaults to 1000,0.0001 pub level6: ExtMetadataBlockLevel6, + + /// List of metadata blocks to use for every RPU generated. + /// + /// Per-shot or per-frame metadata replaces the default + /// metadata blocks if there are conflicts. + #[cfg_attr(feature = "serde_feature", serde(default))] + pub default_metadata_blocks: Vec, + + /// List of shots to generate. + #[cfg_attr(feature = "serde_feature", serde(default))] + pub shots: Vec, } +/// Struct defining a video shot. +/// A shot is a group of frames that share the same metadata. #[derive(Default, Debug, Clone)] #[cfg_attr(feature = "serde_feature", derive(Deserialize, Serialize))] pub struct VideoShot { + /// Optional (unused) ID of the shot. + /// Only XML generation provides this. #[cfg_attr(feature = "serde_feature", serde(default))] pub id: String, + /// Frame start offset of the shot. + /// Used as a sorting key for the shots. pub start: usize, + + /// Number of frames contained in the shot. pub duration: usize, + /// List of metadata blocks. #[cfg_attr(feature = "serde_feature", serde(default))] pub metadata_blocks: Vec, + /// List of per-frame metadata edits. #[cfg_attr(feature = "serde_feature", serde(default))] pub frame_edits: Vec, } +/// Struct to represent a list of metadata edits for a specific frame. #[derive(Default, Debug, Clone)] #[cfg_attr(feature = "serde_feature", derive(Deserialize, Serialize))] pub struct ShotFrameEdit { + /// Frame offset within the parent shot. pub edit_offset: usize, + + /// List of metadata blocks to use. pub metadata_blocks: Vec, } @@ -71,6 +102,16 @@ impl GenerateConfig { let rpu = DoviRpu::profile81_config(self)?; let mut list = Vec::with_capacity(self.length); + let shots_length: usize = self.shots.iter().map(|s| s.duration).sum(); + + ensure!( + self.length == shots_length, + format!( + "Config length is not the same as shots total duration. Config: {}, Shots: {}", + self.length, shots_length + ) + ); + for shot in &self.shots { let end = shot.duration; @@ -151,10 +192,9 @@ impl Default for GenerateConfig { Self { cm_version: CmVersion::V40, length: Default::default(), - target_nits: Default::default(), source_min_pq: Default::default(), source_max_pq: Default::default(), - shots: Default::default(), + default_metadata_blocks: Default::default(), level5: Default::default(), level6: ExtMetadataBlockLevel6 { max_display_mastering_luminance: 1000, @@ -162,7 +202,76 @@ impl Default for GenerateConfig { max_content_light_level: 0, max_frame_average_light_level: 0, }, + shots: Default::default(), + } + } +} + +impl VideoShot { + pub fn copy_metadata_from_shot( + &mut self, + other_shot: &VideoShot, + level_block_list: Option<&[u8]>, + ) { + // Add blocks to shot metadata + let new_shot_blocks: Vec = if let Some(block_list) = level_block_list { + other_shot + .metadata_blocks + .iter() + .filter(|b| !block_list.contains(&b.level())) + .cloned() + .collect() + } else { + other_shot.metadata_blocks.clone() + }; + + self.metadata_blocks.extend(new_shot_blocks); + + // Add blocks to existing frame edits for the same offsets + for frame_edit in &mut self.frame_edits { + let new_frame_edit = other_shot + .frame_edits + .iter() + .find(|e| e.edit_offset == frame_edit.edit_offset); + + if let Some(other_edit) = new_frame_edit { + let new_edit_blocks: Vec = + if let Some(block_list) = level_block_list { + other_edit + .metadata_blocks + .iter() + .filter(|b| !block_list.contains(&b.level())) + .cloned() + .collect() + } else { + other_edit.metadata_blocks.clone() + }; + + frame_edit.metadata_blocks.extend(new_edit_blocks); + } } + + // Add extra frame edits but don't replace + let existing_edit_offsets: Vec = + self.frame_edits.iter().map(|e| e.edit_offset).collect(); + + // Filter out unwanted blocks and add new edits + let added_frame_edits = other_shot + .frame_edits + .iter() + .filter(|e| !existing_edit_offsets.contains(&e.edit_offset)) + .cloned() + .map(|mut frame_edit| { + if let Some(block_list) = level_block_list { + frame_edit + .metadata_blocks + .retain(|b| !block_list.contains(&b.level())); + } + + frame_edit + }); + + self.frame_edits.extend(added_frame_edits); } } @@ -277,6 +386,7 @@ mod tests { assert_eq!(shot2_l2.trim_saturation_gain, 2048); assert_eq!(shot2_l2.ms_weight, 2048); } + if let ExtMetadataBlock::Level2(shot2_l2) = shot2_level2_iter.next().unwrap() { assert_eq!(shot2_l2.target_max_pq, 3079); assert_eq!(shot2_l2.trim_slope, 2049); diff --git a/dolby_vision/src/rpu/vdr_dm_data.rs b/dolby_vision/src/rpu/vdr_dm_data.rs index 647ade5..4994762 100644 --- a/dolby_vision/src/rpu/vdr_dm_data.rs +++ b/dolby_vision/src/rpu/vdr_dm_data.rs @@ -5,9 +5,7 @@ use bitvec_helpers::{bitvec_reader::BitVecReader, bitvec_writer::BitVecWriter}; use serde::{Deserialize, Serialize}; use super::dovi_rpu::DoviRpu; -use super::extension_metadata::blocks::{ - ExtMetadataBlock, ExtMetadataBlockLevel11, ExtMetadataBlockLevel2, -}; +use super::extension_metadata::blocks::{ExtMetadataBlock, ExtMetadataBlockLevel11}; use super::extension_metadata::*; use super::generate::GenerateConfig; use super::profiles::profile81::Profile81; @@ -329,7 +327,7 @@ impl VdrDmData { Ok(()) } else { - bail!("Did not find CM v2.9 DM data") + bail!("Cannot replace L2 metadata, no CM v4.0 DM data") } } ExtMetadataBlock::Level3(_) => self.replace_metadata_level(block), @@ -345,7 +343,7 @@ impl VdrDmData { Ok(()) } else { - bail!("Did not find CM v4.0 DM data") + bail!("Cannot replace L8 metadata, no CM v4.0 DM data") } } ExtMetadataBlock::Level9(_) => self.replace_metadata_level(block), @@ -358,7 +356,7 @@ impl VdrDmData { Ok(()) } else { - bail!("Did not find CM v4.0 DM data") + bail!("Cannot replace L10 metadata, no CM v4.0 DM data") } } ExtMetadataBlock::Level11(_) => self.replace_metadata_level(block), @@ -436,7 +434,7 @@ impl VdrDmData { } /// Sets static metadata (L5/L6/L11) and source levels - pub fn from_config(config: &GenerateConfig) -> Result { + pub fn from_generate_config(config: &GenerateConfig) -> Result { let mut vdr_dm_data = Profile81::dm_data(); match config.cm_version { @@ -452,13 +450,6 @@ impl VdrDmData { vdr_dm_data.set_static_metadata(config)?; vdr_dm_data.change_source_levels(config.source_min_pq, config.source_max_pq); - // Default L2 - if let Some(target_nits) = config.target_nits { - vdr_dm_data.add_metadata_block(ExtMetadataBlock::Level2( - ExtMetadataBlockLevel2::from_nits(target_nits), - ))?; - } - Ok(vdr_dm_data) } @@ -469,6 +460,19 @@ impl VdrDmData { ExtMetadataBlockLevel11::default_reference_cinema(), ))?; + if !config.default_metadata_blocks.is_empty() { + let level_block_list: &[u8] = &[5, 6, 254]; + + let allowed_default_blocks = config + .default_metadata_blocks + .iter() + .filter(|block| !level_block_list.contains(&block.level())); + + for block in allowed_default_blocks { + self.replace_metadata_block(block.clone())?; + } + } + Ok(()) } } diff --git a/dolby_vision/src/xml/parser.rs b/dolby_vision/src/xml/parser.rs index b5078fc..fa97e4e 100644 --- a/dolby_vision/src/xml/parser.rs +++ b/dolby_vision/src/xml/parser.rs @@ -80,10 +80,7 @@ impl CmXmlParser { parser.config.shots = parser.parse_shots(&video)?; parser.config.shots.sort_by_key(|s| s.start); - let first_shot = parser.config.shots.first().unwrap(); - let last_shot = parser.config.shots.last().unwrap(); - - parser.config.length = (last_shot.start + last_shot.duration) - first_shot.start; + parser.config.length = parser.config.shots.iter().map(|s| s.duration).sum(); } else { bail!("Could not find Video node"); } diff --git a/generator.md b/generator.md deleted file mode 100644 index 9e46a7b..0000000 --- a/generator.md +++ /dev/null @@ -1,59 +0,0 @@ -The generator can create a profile 8.1 RPU binary. -Any extension metadata can be added. - -A JSON config example: - -```json5 -{ - // CM version, either V29 or V40 - // Defaults to V40 - "cm_version": CmVersion, - - // Number of metadata frames to generate - "length": int, - - // Target nits for L2 metadata (0 to 10000). - // Usually 600, 1000, 2000 - // Optional if specific L2 targets are present - "target_nits": int, - - // Source min/max PQ values to override, optional - // If not specified, derived from L6 metadata - "source_min_pq": int, - "source_max_pq": int, - - // Shots to specify metadata for - // Ignored for HDR10+ and madVR generation - "shots": [ - { - // Start frame, defaults to 0 - "start": int, - // Shot frame length - "duration": int, - - // List of metadata blocks to use for this shot - // Refer to example or info JSON - "metadata_blocks": Array, - // Metadata to use for specific frames in the shot - "frame_edits": Array - } - ], - - // L5 metadata, optional - // If not specified, L5 metadata is added with 0 offsets - "level5": { - "active_area_left_offset": int, - "active_area_right_offset": int, - "active_area_top_offset": int, - "active_area_bottom_offset": int, - }, - - // L6 metadata, required for profile 8.1 - "level6": { - "max_display_mastering_luminance": int, - "min_display_mastering_luminance": int, - "max_content_light_level": int, - "max_frame_average_light_level": int, - } -} -``` diff --git a/src/dovi/generator.rs b/src/dovi/generator.rs index 5e792be..f994db8 100644 --- a/src/dovi/generator.rs +++ b/src/dovi/generator.rs @@ -1,4 +1,4 @@ -use anyhow::{bail, Result}; +use anyhow::{bail, ensure, Result}; use serde_json::Value; use std::fs::File; use std::io::{stdout, Read, Write}; @@ -12,6 +12,7 @@ use dolby_vision::rpu::generate::{GenerateConfig, ShotFrameEdit, VideoShot}; use dolby_vision::utils::nits_to_pq; use dolby_vision::xml::{CmXmlParser, XmlParserOpts}; +#[derive(Default)] pub struct Generator { json_path: Option, rpu_out: PathBuf, @@ -21,10 +22,12 @@ pub struct Generator { canvas_height: Option, madvr_path: Option, use_custom_targets: bool, + + pub config: Option, } impl Generator { - pub fn generate(cmd: Command) -> Result<()> { + pub fn from_command(cmd: Command) -> Result { if let Command::Generate { json_file, rpu_out, @@ -51,43 +54,68 @@ impl Generator { canvas_height, madvr_path: madvr_file, use_custom_targets, + config: None, }; - let config = if let Some(json_path) = &generator.json_path { - let json_file = File::open(json_path)?; - let mut config: GenerateConfig = serde_json::from_reader(&json_file)?; - - if let Some(hdr10plus_path) = &generator.hdr10plus_path { - parse_hdr10plus_for_l1(hdr10plus_path, &mut config)?; - } else if let Some(madvr_path) = &generator.madvr_path { - generate_metadata_from_madvr( - madvr_path, - generator.use_custom_targets, - &mut config, - )?; - } + Ok(generator) + } else { + bail!("Invalid command variant."); + } + } - config - } else if let Some(xml_path) = &generator.xml_path { - generator.config_from_xml(xml_path)? - } else { - bail!("Missing configuration or XML file!"); - }; + pub fn generate(&mut self) -> Result<()> { + let config = if let Some(json_path) = &self.json_path { + let json_file = File::open(json_path)?; + let mut config: GenerateConfig = serde_json::from_reader(&json_file)?; + + if let Some(hdr10plus_path) = &self.hdr10plus_path { + parse_hdr10plus_for_l1(hdr10plus_path, &mut config)?; + } else if let Some(madvr_path) = &self.madvr_path { + generate_metadata_from_madvr(madvr_path, self.use_custom_targets, &mut config)?; + } else if config.length == 0 && !config.shots.is_empty() { + // Set length from sum of shot durations + config.length = config.shots.iter().map(|s| s.duration).sum(); + } - generator.execute(&config)?; + ensure!( + config.length > 0 || !config.shots.is_empty(), + "Missing number of RPUs to generate, and no shots to derive it from" + ); + + // Create a single shot by default + if config.shots.is_empty() { + config.shots.push(VideoShot { + start: 0, + duration: config.length, + ..Default::default() + }) + } - println!("Done."); - } + config + } else if let Some(xml_path) = &self.xml_path { + self.config_from_xml(xml_path)? + } else { + bail!("Missing configuration or XML file!"); + }; + + self.config = Some(config); + self.execute()?; + + println!("Done."); Ok(()) } - fn execute(&self, config: &GenerateConfig) -> Result<()> { - println!("Generating metadata..."); + fn execute(&self) -> Result<()> { + if let Some(config) = &self.config { + println!("Generating metadata..."); - config.write_rpus(&self.rpu_out)?; + config.write_rpus(&self.rpu_out)?; - println!("Generated metadata for {} frames", config.length); + println!("Generated metadata for {} frames", config.length); + } else { + bail!("No generation config to execute!"); + } Ok(()) } @@ -110,8 +138,6 @@ fn parse_hdr10plus_for_l1(hdr10plus_path: &Path, config: &mut GenerateConfig) -> println!("Parsing HDR10+ JSON file..."); stdout().flush().ok(); - config.shots.clear(); - let mut s = String::new(); File::open(hdr10plus_path)?.read_to_string(&mut s)?; @@ -147,6 +173,8 @@ fn parse_hdr10plus_for_l1(hdr10plus_path: &Path, config: &mut GenerateConfig) -> let mut current_shot_id = 0; + let mut hdr10plus_shots = Vec::with_capacity(scene_first_frames.len()); + if let Some(scene_info) = json.get("SceneInfo") { if let Some(list) = scene_info.as_array() { frame_count = list.len(); @@ -173,7 +201,7 @@ fn parse_hdr10plus_for_l1(hdr10plus_path: &Path, config: &mut GenerateConfig) -> let avg_pq = (nits_to_pq((avg_rgb as f64 / 10.0).round() as u16) * 4095.0) .round() as u16; - let shot = VideoShot { + let mut shot = VideoShot { start: frame_no, duration: scene_frame_lengths[current_shot_id], metadata_blocks: vec![ExtMetadataBlock::Level1( @@ -182,11 +210,22 @@ fn parse_hdr10plus_for_l1(hdr10plus_path: &Path, config: &mut GenerateConfig) -> ..Default::default() }; - config.shots.push(shot); + let config_shot = config.shots.get(hdr10plus_shots.len()); + + if let Some(override_shot) = config_shot { + shot.copy_metadata_from_shot(override_shot, Some(&[1])) + } + + hdr10plus_shots.push(shot); + current_shot_id += 1; } } } + + // Now that the metadata was copied, we can replace the shots + config.shots.clear(); + config.shots.extend(hdr10plus_shots); } config.length = frame_count; @@ -202,8 +241,6 @@ pub fn generate_metadata_from_madvr( println!("Parsing madVR measurement file..."); stdout().flush().ok(); - config.shots.clear(); - let madvr_info = madvr_parse::MadVRMeasurements::parse_file(madvr_path)?; let level6_meta = ExtMetadataBlockLevel6 { @@ -213,43 +250,55 @@ pub fn generate_metadata_from_madvr( }; let frame_count = madvr_info.frames.len(); + let mut madvr_shots = Vec::with_capacity(madvr_info.scenes.len()); - for s in madvr_info.scenes.iter() { + for (i, scene) in madvr_info.scenes.iter().enumerate() { let min_pq = 0; - let max_pq = (s.max_pq * 4095.0).round() as u16; - let avg_pq = (s.avg_pq * 4095.0).round() as u16; + let max_pq = (scene.max_pq * 4095.0).round() as u16; + let avg_pq = (scene.avg_pq * 4095.0).round() as u16; let mut shot = VideoShot { - start: s.start as usize, - duration: s.length, + start: scene.start as usize, + duration: scene.length, metadata_blocks: vec![ExtMetadataBlock::Level1( ExtMetadataBlockLevel1::from_stats(min_pq, max_pq, avg_pq), )], ..Default::default() }; + let config_shot = config.shots.get(i); + if use_custom_targets && madvr_info.header.flags == 3 { // Use peak per frame, average from scene - let frames = s.get_frames(frame_count, &madvr_info.frames)?; + let frames = scene.get_frames(frame_count, &madvr_info.frames)?; frames.iter().enumerate().for_each(|(i, f)| { let min_pq = 0; let max_pq = (f.target_pq * 4095.0).round() as u16; - let avg_pq = (s.avg_pq * 4095.0).round() as u16; + let avg_pq = (scene.avg_pq * 4095.0).round() as u16; - shot.frame_edits.push(ShotFrameEdit { + let frame_edit = ShotFrameEdit { edit_offset: i, metadata_blocks: vec![ExtMetadataBlock::Level1( ExtMetadataBlockLevel1::from_stats(min_pq, max_pq, avg_pq), )], - }); + }; + + shot.frame_edits.push(frame_edit); }); - } else { - }; + } - config.shots.push(shot); + if let Some(override_shot) = config_shot { + shot.copy_metadata_from_shot(override_shot, Some(&[1])) + } + + madvr_shots.push(shot); } + // Now that the metadata was copied, we can replace the shots + config.shots.clear(); + config.shots.extend(madvr_shots); + // Set MaxCLL and MaxFALL if not set in config if config.level6.max_content_light_level == 0 { config.level6.max_content_light_level = level6_meta.max_content_light_level; diff --git a/src/dovi/tests.rs b/src/dovi/tests.rs index f442602..8539b6a 100644 --- a/src/dovi/tests.rs +++ b/src/dovi/tests.rs @@ -1,10 +1,14 @@ use anyhow::Result; +use dolby_vision::rpu::extension_metadata::blocks::ExtMetadataBlock; use std::fs::File; use std::{io::Read, path::PathBuf}; use dolby_vision::rpu::dovi_rpu::DoviRpu; use dolby_vision::rpu::generate::GenerateConfig; +use crate::commands::Command; +use crate::dovi::generator::Generator; + pub fn _parse_file(input: PathBuf) -> Result<(Vec, DoviRpu)> { let mut f = File::open(input)?; let metadata = f.metadata()?; @@ -270,7 +274,6 @@ fn generated_rpu() -> Result<()> { let config = GenerateConfig { length: 1000, - target_nits: Some(600), source_min_pq: None, source_max_pq: None, level5: ExtMetadataBlockLevel5::from_offsets(0, 0, 280, 280), @@ -280,6 +283,9 @@ fn generated_rpu() -> Result<()> { max_content_light_level: 1000, max_frame_average_light_level: 400, }, + default_metadata_blocks: vec![ExtMetadataBlock::Level2(ExtMetadataBlockLevel2::from_nits( + 600, + ))], ..Default::default() }; @@ -391,7 +397,6 @@ fn cmv40_full_rpu() -> Result<()> { let mut config = GenerateConfig { length: 10, - target_nits: Some(600), source_min_pq: None, source_max_pq: None, level5: ExtMetadataBlockLevel5::from_offsets(0, 0, 280, 280), @@ -401,6 +406,9 @@ fn cmv40_full_rpu() -> Result<()> { max_content_light_level: 1000, max_frame_average_light_level: 400, }, + default_metadata_blocks: vec![ExtMetadataBlock::Level2(ExtMetadataBlockLevel2::from_nits( + 600, + ))], ..Default::default() }; @@ -508,3 +516,339 @@ fn empty_dmv1_blocks() -> Result<()> { Ok(()) } + +#[cfg(target_os = "linux")] +#[test] +fn generate_default_cmv29() -> Result<()> { + let cmd = Command::Generate { + json_file: Some(PathBuf::from( + "./assets/generator_examples/default_cmv29.json", + )), + rpu_out: Some(PathBuf::from("/dev/null")), + hdr10plus_json: None, + xml: None, + canvas_width: None, + canvas_height: None, + madvr_file: None, + use_custom_targets: false, + }; + + let mut generator = Generator::from_command(cmd)?; + generator.generate()?; + + // Get updated config + let config = generator.config.unwrap(); + + let rpus = config.generate_rpu_list()?; + assert_eq!(rpus.len(), 10); + + let first_rpu = &rpus[0]; + let vdr_dm_data = first_rpu.vdr_dm_data.as_ref().unwrap(); + + assert_eq!(vdr_dm_data.scene_refresh_flag, 1); + + // Only L5 and L6 + assert_eq!(vdr_dm_data.metadata_blocks(1).unwrap().len(), 2); + // No CM v4.0 + assert!(vdr_dm_data.metadata_blocks(3).is_none()); + + if let ExtMetadataBlock::Level5(level5) = vdr_dm_data.get_block(5).unwrap() { + assert_eq!(level5.get_offsets(), (0, 0, 0, 0)); + } + + if let ExtMetadataBlock::Level6(level6) = vdr_dm_data.get_block(6).unwrap() { + assert_eq!(level6.min_display_mastering_luminance, 1); + assert_eq!(level6.max_display_mastering_luminance, 1000); + assert_eq!(level6.max_content_light_level, 1000); + assert_eq!(level6.max_frame_average_light_level, 400); + } + + Ok(()) +} + +#[cfg(target_os = "linux")] +#[test] +fn generate_default_cmv40() -> Result<()> { + let cmd = Command::Generate { + json_file: Some(PathBuf::from( + "./assets/generator_examples/default_cmv40.json", + )), + rpu_out: Some(PathBuf::from("/dev/null")), + hdr10plus_json: None, + xml: None, + canvas_width: None, + canvas_height: None, + madvr_file: None, + use_custom_targets: false, + }; + + let mut generator = Generator::from_command(cmd)?; + generator.generate()?; + + // Get updated config + let config = generator.config.unwrap(); + + let rpus = config.generate_rpu_list()?; + assert_eq!(rpus.len(), 10); + + let first_rpu = &rpus[0]; + let vdr_dm_data = first_rpu.vdr_dm_data.as_ref().unwrap(); + + assert_eq!(vdr_dm_data.scene_refresh_flag, 1); + + // Only L5 and L6 + assert_eq!(vdr_dm_data.metadata_blocks(1).unwrap().len(), 2); + // Only L11 and L254 + assert_eq!(vdr_dm_data.metadata_blocks(3).unwrap().len(), 2); + + if let ExtMetadataBlock::Level5(level5) = vdr_dm_data.get_block(5).unwrap() { + assert_eq!(level5.get_offsets(), (0, 0, 0, 0)); + } + + if let ExtMetadataBlock::Level6(level6) = vdr_dm_data.get_block(6).unwrap() { + assert_eq!(level6.min_display_mastering_luminance, 1); + assert_eq!(level6.max_display_mastering_luminance, 1000); + assert_eq!(level6.max_content_light_level, 1000); + assert_eq!(level6.max_frame_average_light_level, 400); + } + + if let ExtMetadataBlock::Level11(level11) = vdr_dm_data.get_block(11).unwrap() { + assert_eq!(level11.content_type, 1); + assert_eq!(level11.whitepoint, 0); + assert_eq!(level11.reference_mode_flag, true); + } + + Ok(()) +} + +#[cfg(target_os = "linux")] +#[test] +fn generate_full() -> Result<()> { + let cmd = Command::Generate { + json_file: Some(PathBuf::from( + "./assets/generator_examples/full_example.json", + )), + rpu_out: Some(PathBuf::from("/dev/null")), + hdr10plus_json: None, + xml: None, + canvas_width: None, + canvas_height: None, + madvr_file: None, + use_custom_targets: false, + }; + + let mut generator = Generator::from_command(cmd)?; + generator.generate()?; + + // Get updated config + let config = generator.config.unwrap(); + + let rpus = config.generate_rpu_list()?; + assert_eq!(rpus.len(), 10); + + let first_rpu = &rpus[0]; + let vdr_dm_data = first_rpu.vdr_dm_data.as_ref().unwrap(); + + assert_eq!(vdr_dm_data.scene_refresh_flag, 1); + + // L1, L2 * 2, L5, L6 + assert_eq!(vdr_dm_data.metadata_blocks(1).unwrap().len(), 5); + // Only L9, L11 and L254 + assert_eq!(vdr_dm_data.metadata_blocks(3).unwrap().len(), 3); + + if let ExtMetadataBlock::Level5(level5) = vdr_dm_data.get_block(5).unwrap() { + assert_eq!(level5.get_offsets(), (0, 0, 40, 40)); + } + + if let ExtMetadataBlock::Level6(level6) = vdr_dm_data.get_block(6).unwrap() { + assert_eq!(level6.min_display_mastering_luminance, 1); + assert_eq!(level6.max_display_mastering_luminance, 1000); + assert_eq!(level6.max_content_light_level, 1000); + assert_eq!(level6.max_frame_average_light_level, 400); + } + + // From default blocks + assert_eq!(vdr_dm_data.level_blocks_iter(2).count(), 2); + let mut shot_level2_iter = vdr_dm_data.level_blocks_iter(2); + + if let ExtMetadataBlock::Level2(level2) = shot_level2_iter.next().unwrap() { + assert_eq!(level2.target_max_pq, 2851); + assert_eq!(level2.trim_slope, 2048); + assert_eq!(level2.trim_offset, 2048); + assert_eq!(level2.trim_power, 1800); + assert_eq!(level2.trim_chroma_weight, 2048); + assert_eq!(level2.trim_saturation_gain, 2048); + assert_eq!(level2.ms_weight, 2048); + } + + if let ExtMetadataBlock::Level2(level2) = shot_level2_iter.next().unwrap() { + assert_eq!(level2.target_max_pq, 3079); + assert_eq!(level2.trim_slope, 2048); + assert_eq!(level2.trim_offset, 2048); + assert_eq!(level2.trim_power, 2048); + assert_eq!(level2.trim_chroma_weight, 2048); + assert_eq!(level2.trim_saturation_gain, 2048); + assert_eq!(level2.ms_weight, 2048); + } + + // From default blocks + if let ExtMetadataBlock::Level9(level9) = vdr_dm_data.get_block(9).unwrap() { + assert_eq!(level9.source_primary_index, 0); + } + + // Default block L11 overrides + if let ExtMetadataBlock::Level11(level11) = vdr_dm_data.get_block(11).unwrap() { + assert_eq!(level11.content_type, 4); + assert_eq!(level11.whitepoint, 0); + assert_eq!(level11.reference_mode_flag, true); + } + + Ok(()) +} + +#[cfg(target_os = "linux")] +#[test] +fn generate_full_hdr10plus() -> Result<()> { + let cmd = Command::Generate { + json_file: Some(PathBuf::from( + "./assets/generator_examples/no_duration.json", + )), + rpu_out: Some(PathBuf::from("/dev/null")), + hdr10plus_json: Some(PathBuf::from("./assets/tests/hdr10plus_metadata.json")), + xml: None, + canvas_width: None, + canvas_height: None, + madvr_file: None, + use_custom_targets: false, + }; + + let mut generator = Generator::from_command(cmd)?; + generator.generate()?; + + // Get updated config + let config = generator.config.unwrap(); + assert_eq!(config.shots.len(), 3); + + let rpus = config.generate_rpu_list()?; + assert_eq!(rpus.len(), 9); + + let shot1_rpu = &rpus[0]; + let shot1_vdr_dm_data = shot1_rpu.vdr_dm_data.as_ref().unwrap(); + + assert_eq!(shot1_vdr_dm_data.scene_refresh_flag, 1); + + // Only L1, L2 and L5 and L6 + assert_eq!(shot1_vdr_dm_data.metadata_blocks(1).unwrap().len(), 4); + // Only L9, L11 and L254 + assert_eq!(shot1_vdr_dm_data.metadata_blocks(3).unwrap().len(), 3); + + // Shot L1 is ignored, HDR10+ is used + if let ExtMetadataBlock::Level1(level1) = shot1_vdr_dm_data.get_block(1).unwrap() { + assert_eq!(level1.min_pq, 0); + assert_eq!(level1.max_pq, 3337); + assert_eq!(level1.avg_pq, 2097); + } + + // From shot blocks + assert_eq!(shot1_vdr_dm_data.level_blocks_iter(2).count(), 1); + let mut shot1_level2_iter = shot1_vdr_dm_data.level_blocks_iter(2); + + if let ExtMetadataBlock::Level2(level2) = shot1_level2_iter.next().unwrap() { + assert_eq!(level2.target_max_pq, 2851); + assert_eq!(level2.trim_slope, 2048); + assert_eq!(level2.trim_offset, 2048); + assert_eq!(level2.trim_power, 1800); + assert_eq!(level2.trim_chroma_weight, 2048); + assert_eq!(level2.trim_saturation_gain, 2048); + assert_eq!(level2.ms_weight, 2048); + } + + if let ExtMetadataBlock::Level5(level5) = shot1_vdr_dm_data.get_block(5).unwrap() { + assert_eq!(level5.get_offsets(), (0, 0, 0, 0)); + } + + let shot2_rpu = &rpus[3]; + let shot2_vdr_dm_data = shot2_rpu.vdr_dm_data.as_ref().unwrap(); + + assert_eq!(shot2_vdr_dm_data.scene_refresh_flag, 1); + + // Only L1, L5 and L6 + assert_eq!(shot2_vdr_dm_data.metadata_blocks(1).unwrap().len(), 4); + // Only L9, L11 and L254 + assert_eq!(shot2_vdr_dm_data.metadata_blocks(3).unwrap().len(), 3); + + if let ExtMetadataBlock::Level1(level1) = shot2_vdr_dm_data.get_block(1).unwrap() { + assert_eq!(level1.min_pq, 0); + assert_eq!(level1.max_pq, 3401); + assert_eq!(level1.avg_pq, 1609); + } + + // From shot blocks + assert_eq!(shot2_vdr_dm_data.level_blocks_iter(2).count(), 1); + let mut shot2_level2_iter = shot2_vdr_dm_data.level_blocks_iter(2); + + if let ExtMetadataBlock::Level2(level2) = shot2_level2_iter.next().unwrap() { + assert_eq!(level2.target_max_pq, 2851); + assert_eq!(level2.trim_slope, 1400); + assert_eq!(level2.trim_offset, 1234); + assert_eq!(level2.trim_power, 1800); + assert_eq!(level2.trim_chroma_weight, 2048); + assert_eq!(level2.trim_saturation_gain, 2048); + assert_eq!(level2.ms_weight, 2048); + } + + if let ExtMetadataBlock::Level5(level5) = shot2_vdr_dm_data.get_block(5).unwrap() { + assert_eq!(level5.get_offsets(), (0, 0, 276, 276)); + } + + if let ExtMetadataBlock::Level6(level6) = shot2_vdr_dm_data.get_block(6).unwrap() { + assert_eq!(level6.min_display_mastering_luminance, 1); + assert_eq!(level6.max_display_mastering_luminance, 1000); + assert_eq!(level6.max_content_light_level, 1000); + assert_eq!(level6.max_frame_average_light_level, 400); + } + + let frame_edit_rpu = &rpus[5]; + let edit_vdr_dm_data = frame_edit_rpu.vdr_dm_data.as_ref().unwrap(); + + assert_eq!(edit_vdr_dm_data.scene_refresh_flag, 0); + + // Only L1, L2 * 2, L5 and L6 + assert_eq!(edit_vdr_dm_data.metadata_blocks(1).unwrap().len(), 5); + // Only L9, L11 and L254 + assert_eq!(edit_vdr_dm_data.metadata_blocks(3).unwrap().len(), 3); + + // Also ignored L1 from edit + if let ExtMetadataBlock::Level1(level1) = edit_vdr_dm_data.get_block(1).unwrap() { + assert_eq!(level1.min_pq, 0); + assert_eq!(level1.max_pq, 3401); + assert_eq!(level1.avg_pq, 1609); + } + + // From edit blocks + assert_eq!(edit_vdr_dm_data.level_blocks_iter(2).count(), 2); + let mut edit_level2_iter = edit_vdr_dm_data.level_blocks_iter(2); + + // Replaced same target display trim + if let ExtMetadataBlock::Level2(level2) = edit_level2_iter.next().unwrap() { + assert_eq!(level2.target_max_pq, 2851); + assert_eq!(level2.trim_slope, 1999); + assert_eq!(level2.trim_offset, 1999); + assert_eq!(level2.trim_power, 1999); + assert_eq!(level2.trim_chroma_weight, 2048); + assert_eq!(level2.trim_saturation_gain, 2048); + assert_eq!(level2.ms_weight, 2048); + } + + if let ExtMetadataBlock::Level2(level2) = edit_level2_iter.next().unwrap() { + assert_eq!(level2.target_max_pq, 3079); + assert_eq!(level2.trim_slope, 2048); + assert_eq!(level2.trim_offset, 2048); + assert_eq!(level2.trim_power, 2048); + assert_eq!(level2.trim_chroma_weight, 2048); + assert_eq!(level2.trim_saturation_gain, 2048); + assert_eq!(level2.ms_weight, 2048); + } + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 4227eb8..6d76e1e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -88,7 +88,10 @@ fn main() -> Result<()> { output, } => RpuInjector::inject_rpu(input, rpu_in, output, cli_options), Command::Info { input, frame } => RpuInfo::info(input, frame), - Command::Generate { .. } => Generator::generate(opt.cmd), + Command::Generate { .. } => { + let mut generator = Generator::from_command(opt.cmd)?; + generator.generate() + } Command::Export { input, output } => Exporter::export(input, output), };