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:
Davi Andrei 2026-10-08 22:23:25 -03:00
parent 29b454ac17
commit a8f91e3ffe
4 changed files with 89 additions and 2 deletions

View File

@ -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

View File

@ -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();

View File

@ -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()?;

View File

@ -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);