diff --git a/assets/generator_examples/l1_cmv29_override_avg_cmv40.json b/assets/generator_examples/l1_cmv29_override_avg_cmv40.json new file mode 100644 index 0000000..560aa02 --- /dev/null +++ b/assets/generator_examples/l1_cmv29_override_avg_cmv40.json @@ -0,0 +1,38 @@ +{ + "cm_version": "V29", + "l1_avg_pq_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 + }, + "shots": [ + { + "start": 0, + "duration": 1, + "metadata_blocks": [ + { + "Level1": { + "min_pq": 0, + "max_pq": 1678, + "avg_pq": 948 + } + } + ] + }, + { + "start": 1, + "duration": 1, + "metadata_blocks": [ + { + "Level1": { + "min_pq": 0, + "max_pq": 3074, + "avg_pq": 1450 + } + } + ] + } + ] +} diff --git a/docs/generator.md b/docs/generator.md index f422746..9c90a66 100644 --- a/docs/generator.md +++ b/docs/generator.md @@ -27,6 +27,9 @@ A JSON config example: "source_min_pq": int, "source_max_pq": int, + // CM version to override the minimum L1 `avg_pq` + "l1_avg_pq_cm_version": string, + // L5 metadata, optional. // If not specified, L5 metadata is added with 0 offsets. "level5": { diff --git a/dolby_vision/CHANGELOG.md b/dolby_vision/CHANGELOG.md index a307326..9f925b9 100644 --- a/dolby_vision/CHANGELOG.md +++ b/dolby_vision/CHANGELOG.md @@ -2,6 +2,9 @@ ## 2.0.1 - Added `replace_levels_from_rpu` function to `DoviRpu`. +- Added `l1_avg_pq_cm_version` to `GenerateConfig`. + - Allows overriding the minimum L1 `avg_pq` CM version. + - Example use case: Some grades are done in `CM v4.0` but distributed as `CM v2.9` RPU. ## 2.0.0 - Modified `extension_metadata::blocks` parsing functions to return a `Result`. diff --git a/dolby_vision/src/rpu/generate.rs b/dolby_vision/src/rpu/generate.rs index b2153c4..c38bca8 100644 --- a/dolby_vision/src/rpu/generate.rs +++ b/dolby_vision/src/rpu/generate.rs @@ -49,6 +49,10 @@ pub struct GenerateConfig { #[cfg_attr(feature = "serde_feature", serde(default))] pub source_max_pq: Option, + /// CM version to override the minimum L1 `avg_pq` + #[cfg_attr(feature = "serde_feature", serde(default))] + pub l1_avg_pq_cm_version: Option, + /// Active area offsets. /// Defaults to zero offsets, should be present in RPU #[cfg_attr(feature = "serde_feature", serde(default))] @@ -222,7 +226,7 @@ impl GenerateConfig { pub fn fixup_l1(&mut self) { let clamp_l1 = |block: &mut ExtMetadataBlock| { if let ExtMetadataBlock::Level1(l1) = block { - l1.clamp_values_cm_version(self.cm_version); + l1.clamp_values_cm_version(self.l1_avg_pq_cm_version.unwrap_or(self.cm_version)); } }; @@ -246,6 +250,7 @@ impl Default for GenerateConfig { long_play_mode: Default::default(), source_min_pq: Default::default(), source_max_pq: Default::default(), + l1_avg_pq_cm_version: Default::default(), default_metadata_blocks: Default::default(), level5: Default::default(), level6: Some(ExtMetadataBlockLevel6 { diff --git a/src/dovi/generator.rs b/src/dovi/generator.rs index 33c2a52..3524cdb 100644 --- a/src/dovi/generator.rs +++ b/src/dovi/generator.rs @@ -85,9 +85,12 @@ impl Generator { let mut config = if let Some(json_path) = &self.json_path { let json_file = File::open(json_path)?; - println!("Reading generator config file..."); + println!("Reading generate config file..."); let mut config: GenerateConfig = serde_json::from_reader(&json_file)?; + // Set default to the config's CM version if it wasn't specified + config.l1_avg_pq_cm_version.get_or_insert(config.cm_version); + 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 { @@ -241,7 +244,7 @@ fn parse_hdr10plus_for_l1>( min_pq, max_pq, avg_pq, - config.cm_version, + config.l1_avg_pq_cm_version.unwrap(), ), )], ..Default::default() @@ -302,7 +305,7 @@ pub fn generate_metadata_from_madvr>( min_pq, max_pq, avg_pq, - config.cm_version, + config.l1_avg_pq_cm_version.unwrap(), ), )], ..Default::default() @@ -326,7 +329,7 @@ pub fn generate_metadata_from_madvr>( min_pq, max_pq, avg_pq, - config.cm_version, + config.l1_avg_pq_cm_version.unwrap(), ), )], }; diff --git a/tests/rpu/generate.rs b/tests/rpu/generate.rs index a22c695..c4ed447 100644 --- a/tests/rpu/generate.rs +++ b/tests/rpu/generate.rs @@ -609,9 +609,75 @@ fn generate_l1_cmv40() -> Result<()> { assert_eq!(shot2_vdr_dm_data.scene_refresh_flag, 1); - // Only L5 and L6 + // Only L1, L5 and L6 + assert_eq!(shot2_vdr_dm_data.metadata_blocks(1).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, 3074); + assert_eq!(level1.avg_pq, 1450); + } + + Ok(()) +} + +#[test] +fn l1_cmv29_override_avg_cmv40() -> Result<()> { + let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?; + let temp = assert_fs::TempDir::new().unwrap(); + + let generate_config = Path::new("assets/generator_examples/l1_cmv29_override_avg_cmv40.json"); + let output_rpu = temp.child("RPU.bin"); + + let assert = cmd + .arg(SUBCOMMAND) + .arg("--json") + .arg(generate_config) + .arg("--rpu-out") + .arg(output_rpu.as_ref()) + .assert(); + + println!( + "{:?}", + std::str::from_utf8(assert.get_output().stdout.as_ref()) + ); + assert.success().stderr(predicate::str::is_empty()); + + output_rpu.assert(predicate::path::is_file()); + + let rpus = dolby_vision::rpu::utils::parse_rpu_file(output_rpu.as_ref())?; + assert_eq!(rpus.len(), 2); + + 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, L5 and L6 + assert_eq!(shot1_vdr_dm_data.metadata_blocks(1).unwrap().len(), 3); + + // No CM v4.0 + assert!(shot1_vdr_dm_data.metadata_blocks(3).is_none()); + + if let ExtMetadataBlock::Level1(level1) = shot1_vdr_dm_data.get_block(1).unwrap() { + assert_eq!(level1.min_pq, 0); + // Clamped to 2081 + assert_eq!(level1.max_pq, 2081); + // Clamped to 1229 + assert_eq!(level1.avg_pq, 1229); + } + + let shot2_rpu = &rpus[1]; + 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(), 3); + // No CM v4.0 + assert!(shot2_vdr_dm_data.metadata_blocks(3).is_none()); + if let ExtMetadataBlock::Level1(level1) = shot2_vdr_dm_data.get_block(1).unwrap() { assert_eq!(level1.min_pq, 0); assert_eq!(level1.max_pq, 3074);