Skip to main content

rusty_h264_encoder/
params.rs

1//! Sequence and picture parameter sets (SPS / PPS) generation.
2//!
3//! Follows the H.264 spec syntax (§7.3.2.1.1 / §7.3.2.2) restricted to the
4//! Constrained Baseline feature set: `frame_mbs_only_flag = 1`, no scaling
5//! matrices, CAVLC entropy coding, no chroma/luma bit-depth extensions.
6
7use crate::config::EncoderConfig;
8use rusty_h264_common::{BitWriter, NalUnit, NalUnitType};
9
10/// Sequence parameter set, carrying only the fields a CBP encoder emits.
11#[derive(Debug, Clone)]
12pub struct Sps {
13    pub profile_idc: u8,
14    pub constraint_set1_flag: bool,
15    pub level_idc: u8,
16    pub seq_parameter_set_id: u32,
17    pub log2_max_frame_num_minus4: u32,
18    pub pic_order_cnt_type: u32,
19    pub log2_max_pic_order_cnt_lsb_minus4: u32,
20    pub max_num_ref_frames: u32,
21    pub pic_width_in_mbs_minus1: u32,
22    pub pic_height_in_map_units_minus1: u32,
23    /// Cropping in chroma-sample units (right, bottom) when the coded MB grid
24    /// overshoots the requested luma resolution.
25    pub frame_crop_right: u32,
26    pub frame_crop_bottom: u32,
27}
28
29impl Sps {
30    /// Derives the SPS from an encoder configuration.
31    pub fn from_config(cfg: &EncoderConfig) -> Self {
32        let mb_w = cfg.mb_width();
33        let mb_h = cfg.mb_height();
34        // Crop offsets are expressed in units of CropUnitX/Y. For 4:2:0 and
35        // frame_mbs_only_flag=1, CropUnitX=2, CropUnitY=2.
36        let crop_right = (mb_w * 16 - cfg.width) / 2;
37        let crop_bottom = (mb_h * 16 - cfg.height) / 2;
38        // LEVEL FLOOR from what the stream actually signals (Table A-1):
39        // the frame must fit the level's MaxFS, and `max_num_ref_frames`
40        // frames must fit its MaxDpbMbs — or the stream nominally violates
41        // its own level_idc. Both clauses have real instances here: 720p at
42        // refs 3 exceeds 3.0's DPB (needs 3.1, exactly what x264 signals),
43        // and 1080p exceeds 3.0's MaxFS at ANY ref count (needs 4.0) — the
44        // fixed `level_idc: 30` default had been signalling that violation
45        // on every 1080p encode, surfaced the day this floor was tested.
46        // Only ever RAISES the caller's level, so every already-conformant
47        // configuration is byte-identical.
48        let frame_mbs = (mb_w * mb_h) as u32;
49        let dpb_mbs = frame_mbs * cfg.num_ref_frames.max(1);
50        const LEVEL_CAPS: [(u8, u32, u32); 16] = [
51            // (level_idc, MaxFS, MaxDpbMbs)
52            (10, 99, 396),
53            (11, 396, 900),
54            (12, 396, 2376),
55            (13, 396, 2376),
56            (20, 396, 2376),
57            (21, 792, 4752),
58            (22, 1620, 8100),
59            (30, 1620, 8100),
60            (31, 3600, 18000),
61            (32, 5120, 20480),
62            (40, 8192, 32768),
63            (41, 8192, 32768),
64            (42, 8704, 34816),
65            (50, 22080, 110400),
66            (51, 36864, 184320),
67            (52, 36864, 184320),
68        ];
69        let level_floor = LEVEL_CAPS
70            .iter()
71            .find(|&&(_, max_fs, max_dpb)| max_fs >= frame_mbs && max_dpb >= dpb_mbs)
72            .map(|&(l, _, _)| l)
73            .unwrap_or(52);
74        Self {
75            profile_idc: cfg.profile.profile_idc(),
76            constraint_set1_flag: true, // constrained baseline
77            level_idc: cfg.level_idc.max(level_floor),
78            seq_parameter_set_id: 0,
79            log2_max_frame_num_minus4: 0, // log2_max_frame_num = 4
80            pic_order_cnt_type: 0,
81            log2_max_pic_order_cnt_lsb_minus4: 4, // log2_max_poc_lsb = 8 (256): b-pyramid interleaves reference POCs in coding order, and a 16-value lsb put consecutive-reference steps past the §8.2.1 half-range — ffmpeg (correctly) lost the msb and every list ordering after it
82            max_num_ref_frames: cfg.num_ref_frames.max(1),
83            pic_width_in_mbs_minus1: (mb_w - 1) as u32,
84            pic_height_in_map_units_minus1: (mb_h - 1) as u32,
85            frame_crop_right: crop_right as u32,
86            frame_crop_bottom: crop_bottom as u32,
87        }
88    }
89
90    /// Writes the SPS RBSP (without NAL header) including trailing bits.
91    pub fn write_rbsp(&self, w: &mut BitWriter) {
92        w.write_bits(self.profile_idc as u32, 8);
93        // constraint_set0..5 flags + 2 reserved zero bits = u(8).
94        let mut constraints = 0u32;
95        if self.constraint_set1_flag {
96            constraints |= 1 << 6; // constraint_set1_flag is bit position 6 (MSB-first)
97        }
98        w.write_bits(constraints, 8);
99        w.write_bits(self.level_idc as u32, 8);
100        w.write_ue(self.seq_parameter_set_id);
101        // High-profile prefix (profile_idc >= 100, spec §7.3.2.1.1): chroma_format_idc,
102        // bit-depths, transform-bypass, scaling matrices. Baseline/Main (66/77) omit it.
103        if self.profile_idc >= 100 {
104            w.write_ue(1); // chroma_format_idc = 1 (4:2:0)
105            w.write_ue(0); // bit_depth_luma_minus8
106            w.write_ue(0); // bit_depth_chroma_minus8
107            w.write_bit(false); // qpprime_y_zero_transform_bypass_flag
108            w.write_bit(false); // seq_scaling_matrix_present_flag (flat dequant)
109        }
110        w.write_ue(self.log2_max_frame_num_minus4);
111        w.write_ue(self.pic_order_cnt_type);
112        if self.pic_order_cnt_type == 0 {
113            w.write_ue(self.log2_max_pic_order_cnt_lsb_minus4);
114        }
115        w.write_ue(self.max_num_ref_frames);
116        w.write_bit(false); // gaps_in_frame_num_value_allowed_flag
117        w.write_ue(self.pic_width_in_mbs_minus1);
118        w.write_ue(self.pic_height_in_map_units_minus1);
119        w.write_bit(true); // frame_mbs_only_flag = 1
120                           // direct_8x8_inference_flag = 1: REQUIRED by the spec for level_idc >= 30
121                           // (every 720p+ stream), and the only value x264/ffmpeg ever emit. The
122                           // encoder's direct derivation (b_direct corner colZero) and the
123                           // transform_size_8x8_flag conditions (allow_t8 / allow8) are keyed to
124                           // this value IN LOCKSTEP — flipping it back without them desyncs B+8x8.
125        w.write_bit(true); // direct_8x8_inference_flag
126        let cropping = self.frame_crop_right != 0 || self.frame_crop_bottom != 0;
127        w.write_bit(cropping); // frame_cropping_flag
128        if cropping {
129            w.write_ue(0); // frame_crop_left_offset
130            w.write_ue(self.frame_crop_right);
131            w.write_ue(0); // frame_crop_top_offset
132            w.write_ue(self.frame_crop_bottom);
133        }
134        w.write_bit(false); // vui_parameters_present_flag
135        w.rbsp_trailing_bits();
136    }
137
138    /// Builds the SPS as a complete NAL unit.
139    pub fn to_nal(&self) -> NalUnit {
140        let mut w = BitWriter::new();
141        self.write_rbsp(&mut w);
142        NalUnit::new(3, NalUnitType::Sps, w.into_bytes())
143    }
144}
145
146/// Picture parameter set for a CAVLC, single-slice-group CBP encoder.
147#[derive(Debug, Clone)]
148pub struct Pps {
149    pub pic_parameter_set_id: u32,
150    pub seq_parameter_set_id: u32,
151    pub num_ref_idx_l0_default_active_minus1: u32,
152    pub pic_init_qp_minus26: i32,
153    pub deblocking_filter_control_present_flag: bool,
154    /// `2` = IMPLICIT weighted bi-prediction (POC-distance weights) — needed so
155    /// unequal-distance B-frames (`bframes > 1`) blend correctly; `0` otherwise
156    /// (keeps the B-less PPS byte-identical, and `bframes == 1`'s equidistant B
157    /// gets 32:32 weights == the plain average anyway).
158    pub weighted_bipred_idc: u8,
159    /// Explicit weighted prediction for P slices (x264 parity — its `weightp`
160    /// default is on). The DECODER side has supported this for months
161    /// (validated against x264 weightp streams); the encoder emits identity
162    /// weights except where the per-slice fade estimator finds a real gain.
163    pub weighted_pred_flag: bool,
164    /// CABAC (`1`) vs CAVLC (`0`). Set from [`EncoderConfig::cabac`].
165    pub entropy_coding_mode_flag: bool,
166    /// `transform_8x8_mode_flag` (High-profile PPS extension). Set from
167    /// [`EncoderConfig::transform_8x8`]; requires profile_idc 100.
168    pub transform_8x8_mode_flag: bool,
169}
170
171impl Pps {
172    /// Derives the PPS from an encoder configuration.
173    pub fn from_config(cfg: &EncoderConfig) -> Self {
174        Self {
175            pic_parameter_set_id: 0,
176            seq_parameter_set_id: 0,
177            num_ref_idx_l0_default_active_minus1: cfg.num_ref_frames.max(1) - 1,
178            pic_init_qp_minus26: cfg.qp as i32 - 26,
179            // We signal deblocking control in the slice so we can disable the
180            // in-loop filter (not yet implemented); this keeps our (non-filtered)
181            // reconstruction bit-identical to a reference decoder's.
182            deblocking_filter_control_present_flag: true,
183            weighted_bipred_idc: if cfg.bframes > 0 { 2 } else { 0 },
184            weighted_pred_flag: cfg.weightp,
185            entropy_coding_mode_flag: cfg.cabac,
186            transform_8x8_mode_flag: cfg.transform_8x8,
187        }
188    }
189
190    /// Writes the PPS RBSP (without NAL header) including trailing bits.
191    pub fn write_rbsp(&self, w: &mut BitWriter) {
192        w.write_ue(self.pic_parameter_set_id);
193        w.write_ue(self.seq_parameter_set_id);
194        w.write_bit(self.entropy_coding_mode_flag); // entropy_coding_mode_flag (0=CAVLC, 1=CABAC)
195        w.write_bit(false); // bottom_field_pic_order_in_frame_present_flag
196        w.write_ue(0); // num_slice_groups_minus1
197        w.write_ue(self.num_ref_idx_l0_default_active_minus1);
198        w.write_ue(0); // num_ref_idx_l1_default_active_minus1
199        w.write_bit(self.weighted_pred_flag); // weighted_pred_flag (explicit P WP)
200        w.write_bits(self.weighted_bipred_idc as u32, 2); // weighted_bipred_idc
201        w.write_se(self.pic_init_qp_minus26);
202        w.write_se(0); // pic_init_qs_minus26
203        w.write_se(0); // chroma_qp_index_offset
204        w.write_bit(self.deblocking_filter_control_present_flag);
205        w.write_bit(false); // constrained_intra_pred_flag
206        w.write_bit(false); // redundant_pic_cnt_present_flag
207                            // High-profile PPS extension (present iff more RBSP data). We only emit it to
208                            // signal the 8×8 transform; no picture scaling matrices (flat dequant).
209        if self.transform_8x8_mode_flag {
210            w.write_bit(true); // transform_8x8_mode_flag
211            w.write_bit(false); // pic_scaling_matrix_present_flag
212            w.write_se(0); // second_chroma_qp_index_offset
213        }
214        w.rbsp_trailing_bits();
215    }
216
217    /// Builds the PPS as a complete NAL unit.
218    pub fn to_nal(&self) -> NalUnit {
219        let mut w = BitWriter::new();
220        self.write_rbsp(&mut w);
221        NalUnit::new(3, NalUnitType::Pps, w.into_bytes())
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    #[allow(unused_imports)]
229    use alloc::{
230        boxed::Box,
231        format,
232        string::{String, ToString},
233        vec,
234        vec::Vec,
235    };
236    #[allow(unused_imports)]
237    use rusty_h264_common::once::OnceLock;
238    use rusty_h264_common::{nal::emulation_unprevent, BitReader};
239
240    /// The shipped default is now HIGH + CABAC + the 8x8 transform (R6, 2026-08-08;
241    /// previously Main + CABAC). Assert all three reach the BITSTREAM, not merely the
242    /// config: `transform_8x8` is inert unless the PPS actually carries
243    /// `transform_8x8_mode_flag`, so checking the struct field would pin nothing.
244    #[test]
245    fn default_config_signals_high_profile_cabac_and_8x8() {
246        let cfg = EncoderConfig::new(320, 240);
247        let sps = Sps::from_config(&cfg);
248        let nal = sps.to_nal();
249        let rbsp = emulation_unprevent(&nal.rbsp);
250        let mut r = BitReader::new(&rbsp);
251        assert_eq!(r.read_bits(8).unwrap(), 100, "profile_idc should be High");
252
253        let pps = Pps::from_config(&cfg);
254        let nal = pps.to_nal();
255        let rbsp = emulation_unprevent(&nal.rbsp);
256        let mut r = BitReader::new(&rbsp);
257        assert_eq!(r.read_ue().unwrap(), 0); // pic_parameter_set_id
258        assert_eq!(r.read_ue().unwrap(), 0); // seq_parameter_set_id
259        assert!(r.read_bit().unwrap(), "entropy_coding_mode should be CABAC");
260        // Walk the rest of the base PPS to reach the extension (spec 7.3.2.2).
261        r.read_bit().unwrap(); // bottom_field_pic_order_in_frame_present_flag
262        assert_eq!(r.read_ue().unwrap(), 0, "num_slice_groups_minus1"); // no slice groups
263        r.read_ue().unwrap(); // num_ref_idx_l0_default_active_minus1
264        r.read_ue().unwrap(); // num_ref_idx_l1_default_active_minus1
265        r.read_bit().unwrap(); // weighted_pred_flag
266        r.read_bits(2).unwrap(); // weighted_bipred_idc
267        r.read_se().unwrap(); // pic_init_qp_minus26
268        r.read_se().unwrap(); // pic_init_qs_minus26
269        r.read_se().unwrap(); // chroma_qp_index_offset
270        r.read_bit().unwrap(); // deblocking_filter_control_present_flag
271        r.read_bit().unwrap(); // constrained_intra_pred_flag
272        r.read_bit().unwrap(); // redundant_pic_cnt_present_flag
273        assert!(
274            r.read_bit().unwrap(),
275            "transform_8x8_mode_flag should be set — without it in the PPS the 8x8              transform cannot be signalled at all and the default is inert"
276        );
277    }
278
279    #[test]
280    fn sps_roundtrips_through_reader() {
281        // Pin the toolset this test is ABOUT (Baseline + CAVLC) rather than inheriting
282        // it from the default, which now ships Main + CABAC.
283        let mut cfg = EncoderConfig::new(1920, 1080); // 1080 not a multiple of 16 -> cropping
284        cfg.profile = rusty_h264_common::Profile::ConstrainedBaseline;
285        cfg.cabac = false;
286        cfg.num_ref_frames = 1; // pinned like profile/cabac: the default is now 3, and at 1080p that (correctly) raises the level floor this test isn't about
287        let sps = Sps::from_config(&cfg);
288        let nal = sps.to_nal();
289
290        let rbsp = emulation_unprevent(&nal.rbsp);
291        let mut r = BitReader::new(&rbsp);
292        assert_eq!(r.read_bits(8).unwrap(), 66); // profile_idc
293        let constraints = r.read_bits(8).unwrap();
294        assert_eq!((constraints >> 6) & 1, 1); // constraint_set1_flag
295                                               // 40, not the config's 30: 1080p is 8160 MBs and level 3.0's MaxFS is
296                                               // 1620 — the old fixed level was a latent spec violation on every
297                                               // 1080p stream, surfaced (and fixed) by the Table A-1 level floor.
298        assert_eq!(r.read_bits(8).unwrap(), 40); // level_idc
299        assert_eq!(r.read_ue().unwrap(), 0); // sps id
300        assert_eq!(r.read_ue().unwrap(), 0); // log2_max_frame_num_minus4
301        assert_eq!(r.read_ue().unwrap(), 0); // poc type
302        assert_eq!(r.read_ue().unwrap(), 4); // log2_max_poc_lsb_minus4 (256-value lsb — the b-pyramid POC fix)
303        assert_eq!(r.read_ue().unwrap(), 1); // max_num_ref_frames
304        assert!(!r.read_bit().unwrap()); // gaps
305        assert_eq!(r.read_ue().unwrap(), 119); // 1920/16 - 1
306        assert_eq!(r.read_ue().unwrap(), 67); // ceil(1080/16)-1 = 68-1
307        assert!(r.read_bit().unwrap()); // frame_mbs_only
308        assert!(r.read_bit().unwrap()); // direct_8x8_inference_flag = 1 (level >= 3.0 requirement)
309        assert!(r.read_bit().unwrap()); // cropping present (1080)
310        assert_eq!(r.read_ue().unwrap(), 0); // crop left
311        assert_eq!(r.read_ue().unwrap(), 0); // crop right
312        assert_eq!(r.read_ue().unwrap(), 0); // crop top
313        assert_eq!(r.read_ue().unwrap(), 4); // crop bottom: (1088-1080)/2
314    }
315
316    /// The DPB-derived level floor (Table A-1 MaxDpbMbs): raises `level_idc`
317    /// exactly when the signalled reference count cannot fit the caller's
318    /// level, and never otherwise.
319    #[test]
320    fn level_floor_tracks_dpb() {
321        let lvl = |w: usize, h: usize, refs: u32| {
322            let mut cfg = EncoderConfig::new(w, h);
323            cfg.num_ref_frames = refs;
324            Sps::from_config(&cfg).level_idc
325        };
326        assert_eq!(lvl(352, 288, 1), 30); // CIF: caller's 3.0 stands
327        assert_eq!(lvl(352, 288, 3), 30); // CIF x3 = 1188 MBs, still fits 3.0
328        assert_eq!(lvl(1280, 720, 1), 31); // 720p = 3600 MBs > 3.0's MaxFS 1620 (latent; floor fixes it)
329        assert_eq!(lvl(1280, 720, 3), 31); // 720p x3 = 10800 fits 3.1's DPB 18000
330        assert_eq!(lvl(1920, 1080, 1), 40); // 1080p = 8160 MBs > 3.2's MaxFS (the latent 1080p violation)
331        assert_eq!(lvl(1920, 1080, 3), 40); // and x3 = 24480 fits 4.0's DPB 32768
332        assert_eq!(lvl(1920, 1080, 16), 51); // stress: 16-ref 1080p
333    }
334
335    #[test]
336    fn pps_roundtrips_through_reader() {
337        let mut cfg = EncoderConfig::new(640, 480);
338        cfg.profile = rusty_h264_common::Profile::ConstrainedBaseline;
339        cfg.cabac = false;
340        cfg.num_ref_frames = 1; // pinned: this test is about PPS syntax, not the refs default (now 3)
341        cfg.weightp = false; // pinned likewise: weightp defaults ON since the x264-parity landing
342        let pps = Pps::from_config(&cfg);
343        let nal = pps.to_nal();
344
345        let rbsp = emulation_unprevent(&nal.rbsp);
346        let mut r = BitReader::new(&rbsp);
347        assert_eq!(r.read_ue().unwrap(), 0); // pps id
348        assert_eq!(r.read_ue().unwrap(), 0); // sps id
349        assert!(!r.read_bit().unwrap()); // entropy_coding_mode (CAVLC)
350        assert!(!r.read_bit().unwrap()); // bottom_field
351        assert_eq!(r.read_ue().unwrap(), 0); // num_slice_groups_minus1
352        assert_eq!(r.read_ue().unwrap(), 0); // num_ref_idx_l0
353        assert_eq!(r.read_ue().unwrap(), 0); // num_ref_idx_l1
354        assert!(!r.read_bit().unwrap()); // weighted_pred
355        assert_eq!(r.read_bits(2).unwrap(), 0); // weighted_bipred_idc
356        assert_eq!(r.read_se().unwrap(), 0); // pic_init_qp_minus26 (qp 26)
357    }
358}