M2: H.265 stays in the Main tier (pick the level)
Left to choose, NVENC answered a peak bitrate above its level's Main tier limit with the High tier: 1080p30 at 12/18 Mbit/s was written as level 4 High tier, 2160p30/60 at 40/60 Mbit/s as 5.0/5.1 High tier, which many hardware decoders refuse. The level is now the lowest whose Main tier limits hold the picture size, sample rate and peak bitrate (H.265 Table A.8), e.g. 1080p30 at 18 Mbit/s is level 4.1 Main tier. Unit test of the table; hardware test that the hvcC says Main tier at the expected level. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
29b454ac17
commit
a8f91e3ffe
|
|
@ -142,8 +142,10 @@ level and flags are the encoder's own. If it is missing the export stops with th
|
|||
One codec enum (`nvenc::Codec`, chosen by `Profile::HevcMain` the way `VtProfile::HevcMain` does it
|
||||
for VideoToolbox) parameterises the NVENC session: the codec and profile GUIDs, the capability
|
||||
query, the `hevcConfig` member of the codec-configuration union, how NAL units are told apart
|
||||
(`(b0 >> 1) & 0x3f`) and the level numbering (`level × 30`, so 4.1 is 123; the export leaves the
|
||||
level to the encoder). The H.264 path is unchanged: the same export is the same file, byte for byte.
|
||||
(`(b0 >> 1) & 0x3f`) and the level numbering (`level × 30`, so 4.1 is 123). The level is the lowest
|
||||
whose **Main tier** limits hold the picture size, sample rate and peak bitrate (H.265 Table A.8,
|
||||
`nvenc::hevc::main_tier_level`): left to choose, NVENC answered 1080p30 at an 18 Mbit/s peak with
|
||||
level 4 High tier, which many hardware decoders refuse; it is now level 4.1 Main tier. The H.264 path is unchanged: the same export is the same file, byte for byte.
|
||||
|
||||
- **Choosing the format is the opt-in,** as on macOS: Export ▸ Format ▸ H.265 (HEVC) or
|
||||
`"format": "hevc"`. `register()` hands `nvenc::hevc_available` to
|
||||
|
|
|
|||
|
|
@ -20,6 +20,43 @@ pub fn level_code(level_x10: u8) -> Option<u8> {
|
|||
matches!(level_x10, 10 | 20 | 21 | 30 | 31 | 40 | 41 | 50 | 51 | 52 | 60 | 61 | 62).then(|| level_x10.saturating_mul(3))
|
||||
}
|
||||
|
||||
/// The lowest HEVC level (× 10) whose **Main tier** limits take `width × height` pictures at `fps`
|
||||
/// and a peak bitrate (and, with the one-second VBV buffer, a CPB) of `max_kbps` (H.265 Table A.8,
|
||||
/// Main / Main 10 profiles: `MaxLumaPs`, `MaxLumaSr`, `MaxBR`, and pictures no wider or taller than
|
||||
/// `sqrt(8 × MaxLumaPs)`). `None` when no Main tier level does (the encoder then chooses).
|
||||
pub fn main_tier_level(width: u32, height: u32, fps: (u32, u32), max_kbps: u32) -> Option<u8> {
|
||||
// (level × 10, MaxLumaPs, MaxLumaSr, Main tier MaxBR = MaxCPB in kbit/s)
|
||||
const LEVELS: [(u8, u64, u64, u64); 13] = [
|
||||
(10, 36_864, 552_960, 128),
|
||||
(20, 122_880, 3_686_400, 1_500),
|
||||
(21, 245_760, 7_372_800, 3_000),
|
||||
(30, 552_960, 16_588_800, 6_000),
|
||||
(31, 983_040, 33_177_600, 10_000),
|
||||
(40, 2_228_224, 66_846_720, 12_000),
|
||||
(41, 2_228_224, 133_693_440, 20_000),
|
||||
(50, 8_912_896, 267_386_880, 25_000),
|
||||
(51, 8_912_896, 534_773_760, 40_000),
|
||||
(52, 8_912_896, 1_069_547_520, 60_000),
|
||||
(60, 35_651_584, 1_069_547_520, 60_000),
|
||||
(61, 35_651_584, 2_139_095_040, 120_000),
|
||||
(62, 35_651_584, 4_278_190_080, 240_000),
|
||||
];
|
||||
if fps.1 == 0 {
|
||||
return None;
|
||||
}
|
||||
let ps = u64::from(width).checked_mul(u64::from(height))?;
|
||||
// luma samples per second, rounded up
|
||||
let sr = ps.checked_mul(u64::from(fps.0))?.div_ceil(u64::from(fps.1));
|
||||
let side = u64::from(width.max(height));
|
||||
LEVELS
|
||||
.iter()
|
||||
.find(|(_, max_ps, max_sr, max_br)| {
|
||||
// side² ≤ 8 × MaxLumaPs: the dimension limit without a square root
|
||||
ps <= *max_ps && sr <= *max_sr && u64::from(max_kbps) <= *max_br && side.saturating_mul(side) <= max_ps.saturating_mul(8)
|
||||
})
|
||||
.map(|(l, ..)| *l)
|
||||
}
|
||||
|
||||
/// Bytes of the SPS RBSP before the general profile / tier / level record: `sps_video_parameter_set_id`,
|
||||
/// `sps_max_sub_layers_minus1` and `sps_temporal_id_nesting_flag` share the first one.
|
||||
const PTL_START: usize = 1;
|
||||
|
|
@ -117,6 +154,31 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_level_is_the_lowest_main_tier_one_that_fits() {
|
||||
// size and sample rate alone
|
||||
assert_eq!(main_tier_level(1280, 720, (30, 1), 5_000), Some(31));
|
||||
assert_eq!(main_tier_level(1920, 1080, (30, 1), 8_000), Some(40));
|
||||
// the bitrate moves 1080p30 up a level instead of to the High tier (what NVENC picked by itself)
|
||||
assert_eq!(main_tier_level(1920, 1080, (30, 1), 18_000), Some(41));
|
||||
assert_eq!(main_tier_level(1920, 1080, (60, 1), 12_000), Some(41));
|
||||
assert_eq!(main_tier_level(3840, 2160, (30, 1), 22_500), Some(50));
|
||||
assert_eq!(main_tier_level(3840, 2160, (30, 1), 60_000), Some(52));
|
||||
assert_eq!(main_tier_level(3840, 2160, (60, 1), 60_000), Some(52));
|
||||
assert_eq!(main_tier_level(7680, 4320, (30, 1), 60_000), Some(60));
|
||||
// 29.97 rounds the sample rate up
|
||||
assert_eq!(main_tier_level(1920, 1080, (30_000, 1_001), 8_000), Some(40));
|
||||
// a long thin picture is held by the dimension limit, not the area
|
||||
assert_eq!(main_tier_level(8192, 64, (30, 1), 1_000), Some(50));
|
||||
// beyond every Main tier level, or nonsense: the encoder chooses
|
||||
assert_eq!(main_tier_level(1920, 1080, (30, 1), 300_000), None);
|
||||
assert_eq!(main_tier_level(16_384, 16_384, (30, 1), 1_000), None);
|
||||
assert_eq!(main_tier_level(1920, 1080, (30, 0), 1_000), None);
|
||||
assert_eq!(main_tier_level(u32::MAX, u32::MAX, (u32::MAX, 1), u32::MAX), None);
|
||||
// every answer is a level the encoder takes
|
||||
assert!(main_tier_level(1920, 1080, (30, 1), 18_000).and_then(level_code).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_record_is_the_sps_it_came_from() {
|
||||
let c = record();
|
||||
|
|
|
|||
|
|
@ -181,6 +181,12 @@ impl Nvenc {
|
|||
let codec = cfg.profile.codec();
|
||||
let level = match (codec, cfg.level) {
|
||||
(Codec::Hevc, Some(l)) => Some(hevc::level_code(l).ok_or_else(|| format!("{}.{} is not an HEVC level", l / 10, l % 10))?),
|
||||
// left to itself NVENC answers a bitrate above its chosen level's Main tier limit with the
|
||||
// High tier, which many hardware decoders refuse: pick the lowest Main tier level instead
|
||||
(Codec::Hevc, None) => {
|
||||
let max_kbps = if cfg.cbr { cfg.bitrate_kbps } else { cfg.max_bitrate_kbps.max(cfg.bitrate_kbps) };
|
||||
hevc::main_tier_level(w, h, cfg.fps, max_kbps).and_then(hevc::level_code)
|
||||
}
|
||||
(_, l) => l,
|
||||
};
|
||||
let mut session = Session::open()?;
|
||||
|
|
|
|||
|
|
@ -283,6 +283,23 @@ fn an_encoder_dropped_mid_stream_releases_everything() {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_bitrates_move_up_a_level_and_stay_in_the_main_tier() {
|
||||
let _one = SERIAL.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
// 1080p30 at a 12 / 18 Mbit/s peak: above level 4's Main tier (12), where NVENC's own choice was
|
||||
// level 4 High tier, which many hardware decoders refuse
|
||||
for (w, h, fps, kbps, max_kbps, level_idc) in
|
||||
[(1920u32, 1080u32, 30u32, 12_000u32, 18_000u32, 123u8), (1920, 1080, 30, 4_000, 6_000, 120), (1280, 720, 30, 5_000, 7_500, 93)]
|
||||
{
|
||||
let cfg = Config { fps: (fps, 1), bitrate_kbps: kbps, max_bitrate_kbps: max_kbps, keyint: 30, ..config(w, h) };
|
||||
let Some((enc, packets, _)) = encode_all(&cfg, 4) else { return };
|
||||
assert_eq!(packets.len(), 4);
|
||||
let record = enc.hevc_config().expect("an hvcC record");
|
||||
assert!(!record.general_tier_flag, "{w}x{h} at {max_kbps} kbit/s: Main tier");
|
||||
assert_eq!(record.general_level_idc, level_idc, "{w}x{h} at {max_kbps} kbit/s");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_keyframe_every_one_or_two_pictures_is_written_without_b_frames() {
|
||||
let _one = SERIAL.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
|
|
|
|||
Loading…
Reference in New Issue