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        Self {
39            profile_idc: cfg.profile.profile_idc(),
40            constraint_set1_flag: true, // constrained baseline
41            level_idc: cfg.level_idc,
42            seq_parameter_set_id: 0,
43            log2_max_frame_num_minus4: 0, // log2_max_frame_num = 4
44            pic_order_cnt_type: 0,
45            log2_max_pic_order_cnt_lsb_minus4: 0, // log2_max_poc_lsb = 4
46            max_num_ref_frames: cfg.num_ref_frames.max(1),
47            pic_width_in_mbs_minus1: (mb_w - 1) as u32,
48            pic_height_in_map_units_minus1: (mb_h - 1) as u32,
49            frame_crop_right: crop_right as u32,
50            frame_crop_bottom: crop_bottom as u32,
51        }
52    }
53
54    /// Writes the SPS RBSP (without NAL header) including trailing bits.
55    pub fn write_rbsp(&self, w: &mut BitWriter) {
56        w.write_bits(self.profile_idc as u32, 8);
57        // constraint_set0..5 flags + 2 reserved zero bits = u(8).
58        let mut constraints = 0u32;
59        if self.constraint_set1_flag {
60            constraints |= 1 << 6; // constraint_set1_flag is bit position 6 (MSB-first)
61        }
62        w.write_bits(constraints, 8);
63        w.write_bits(self.level_idc as u32, 8);
64        w.write_ue(self.seq_parameter_set_id);
65        // High-profile prefix (profile_idc >= 100, spec §7.3.2.1.1): chroma_format_idc,
66        // bit-depths, transform-bypass, scaling matrices. Baseline/Main (66/77) omit it.
67        if self.profile_idc >= 100 {
68            w.write_ue(1); // chroma_format_idc = 1 (4:2:0)
69            w.write_ue(0); // bit_depth_luma_minus8
70            w.write_ue(0); // bit_depth_chroma_minus8
71            w.write_bit(false); // qpprime_y_zero_transform_bypass_flag
72            w.write_bit(false); // seq_scaling_matrix_present_flag (flat dequant)
73        }
74        w.write_ue(self.log2_max_frame_num_minus4);
75        w.write_ue(self.pic_order_cnt_type);
76        if self.pic_order_cnt_type == 0 {
77            w.write_ue(self.log2_max_pic_order_cnt_lsb_minus4);
78        }
79        w.write_ue(self.max_num_ref_frames);
80        w.write_bit(false); // gaps_in_frame_num_value_allowed_flag
81        w.write_ue(self.pic_width_in_mbs_minus1);
82        w.write_ue(self.pic_height_in_map_units_minus1);
83        w.write_bit(true); // frame_mbs_only_flag = 1
84        w.write_bit(false); // direct_8x8_inference_flag
85        let cropping = self.frame_crop_right != 0 || self.frame_crop_bottom != 0;
86        w.write_bit(cropping); // frame_cropping_flag
87        if cropping {
88            w.write_ue(0); // frame_crop_left_offset
89            w.write_ue(self.frame_crop_right);
90            w.write_ue(0); // frame_crop_top_offset
91            w.write_ue(self.frame_crop_bottom);
92        }
93        w.write_bit(false); // vui_parameters_present_flag
94        w.rbsp_trailing_bits();
95    }
96
97    /// Builds the SPS as a complete NAL unit.
98    pub fn to_nal(&self) -> NalUnit {
99        let mut w = BitWriter::new();
100        self.write_rbsp(&mut w);
101        NalUnit::new(3, NalUnitType::Sps, w.into_bytes())
102    }
103}
104
105/// Picture parameter set for a CAVLC, single-slice-group CBP encoder.
106#[derive(Debug, Clone)]
107pub struct Pps {
108    pub pic_parameter_set_id: u32,
109    pub seq_parameter_set_id: u32,
110    pub num_ref_idx_l0_default_active_minus1: u32,
111    pub pic_init_qp_minus26: i32,
112    pub deblocking_filter_control_present_flag: bool,
113    /// `2` = IMPLICIT weighted bi-prediction (POC-distance weights) — needed so
114    /// unequal-distance B-frames (`bframes > 1`) blend correctly; `0` otherwise
115    /// (keeps the B-less PPS byte-identical, and `bframes == 1`'s equidistant B
116    /// gets 32:32 weights == the plain average anyway).
117    pub weighted_bipred_idc: u8,
118    /// CABAC (`1`) vs CAVLC (`0`). Set from [`EncoderConfig::cabac`].
119    pub entropy_coding_mode_flag: bool,
120    /// `transform_8x8_mode_flag` (High-profile PPS extension). Set from
121    /// [`EncoderConfig::transform_8x8`]; requires profile_idc 100.
122    pub transform_8x8_mode_flag: bool,
123}
124
125impl Pps {
126    /// Derives the PPS from an encoder configuration.
127    pub fn from_config(cfg: &EncoderConfig) -> Self {
128        Self {
129            pic_parameter_set_id: 0,
130            seq_parameter_set_id: 0,
131            num_ref_idx_l0_default_active_minus1: cfg.num_ref_frames.max(1) - 1,
132            pic_init_qp_minus26: cfg.qp as i32 - 26,
133            // We signal deblocking control in the slice so we can disable the
134            // in-loop filter (not yet implemented); this keeps our (non-filtered)
135            // reconstruction bit-identical to a reference decoder's.
136            deblocking_filter_control_present_flag: true,
137            weighted_bipred_idc: if cfg.bframes > 0 { 2 } else { 0 },
138            entropy_coding_mode_flag: cfg.cabac,
139            transform_8x8_mode_flag: cfg.transform_8x8,
140        }
141    }
142
143    /// Writes the PPS RBSP (without NAL header) including trailing bits.
144    pub fn write_rbsp(&self, w: &mut BitWriter) {
145        w.write_ue(self.pic_parameter_set_id);
146        w.write_ue(self.seq_parameter_set_id);
147        w.write_bit(self.entropy_coding_mode_flag); // entropy_coding_mode_flag (0=CAVLC, 1=CABAC)
148        w.write_bit(false); // bottom_field_pic_order_in_frame_present_flag
149        w.write_ue(0); // num_slice_groups_minus1
150        w.write_ue(self.num_ref_idx_l0_default_active_minus1);
151        w.write_ue(0); // num_ref_idx_l1_default_active_minus1
152        w.write_bit(false); // weighted_pred_flag
153        w.write_bits(self.weighted_bipred_idc as u32, 2); // weighted_bipred_idc
154        w.write_se(self.pic_init_qp_minus26);
155        w.write_se(0); // pic_init_qs_minus26
156        w.write_se(0); // chroma_qp_index_offset
157        w.write_bit(self.deblocking_filter_control_present_flag);
158        w.write_bit(false); // constrained_intra_pred_flag
159        w.write_bit(false); // redundant_pic_cnt_present_flag
160        // High-profile PPS extension (present iff more RBSP data). We only emit it to
161        // signal the 8×8 transform; no picture scaling matrices (flat dequant).
162        if self.transform_8x8_mode_flag {
163            w.write_bit(true); // transform_8x8_mode_flag
164            w.write_bit(false); // pic_scaling_matrix_present_flag
165            w.write_se(0); // second_chroma_qp_index_offset
166        }
167        w.rbsp_trailing_bits();
168    }
169
170    /// Builds the PPS as a complete NAL unit.
171    pub fn to_nal(&self) -> NalUnit {
172        let mut w = BitWriter::new();
173        self.write_rbsp(&mut w);
174        NalUnit::new(3, NalUnitType::Pps, w.into_bytes())
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use rusty_h264_common::{nal::emulation_unprevent, BitReader};
182
183    /// The shipped default is now Main + CABAC (U6: −9% BD for ~1.15× time). Assert it
184    /// reaches the bitstream, so a silent revert of the default is a test failure.
185    #[test]
186    fn default_config_signals_main_profile_and_cabac() {
187        let cfg = EncoderConfig::new(320, 240);
188        let sps = Sps::from_config(&cfg);
189        let rbsp = emulation_unprevent(&sps.to_nal().rbsp);
190        let mut r = BitReader::new(&rbsp);
191        assert_eq!(r.read_bits(8).unwrap(), 77, "profile_idc should be Main");
192
193        let pps = Pps::from_config(&cfg);
194        let rbsp = emulation_unprevent(&pps.to_nal().rbsp);
195        let mut r = BitReader::new(&rbsp);
196        assert_eq!(r.read_ue().unwrap(), 0); // pps id
197        assert_eq!(r.read_ue().unwrap(), 0); // sps id
198        assert!(r.read_bit().unwrap(), "entropy_coding_mode should be CABAC");
199    }
200
201    #[test]
202    fn sps_roundtrips_through_reader() {
203        // Pin the toolset this test is ABOUT (Baseline + CAVLC) rather than inheriting
204        // it from the default, which now ships Main + CABAC.
205        let mut cfg = EncoderConfig::new(1920, 1080); // 1080 not a multiple of 16 -> cropping
206        cfg.profile = rusty_h264_common::Profile::ConstrainedBaseline;
207        cfg.cabac = false;
208        let sps = Sps::from_config(&cfg);
209        let nal = sps.to_nal();
210
211        let rbsp = emulation_unprevent(&nal.rbsp);
212        let mut r = BitReader::new(&rbsp);
213        assert_eq!(r.read_bits(8).unwrap(), 66); // profile_idc
214        let constraints = r.read_bits(8).unwrap();
215        assert_eq!((constraints >> 6) & 1, 1); // constraint_set1_flag
216        assert_eq!(r.read_bits(8).unwrap(), 30); // level_idc
217        assert_eq!(r.read_ue().unwrap(), 0); // sps id
218        assert_eq!(r.read_ue().unwrap(), 0); // log2_max_frame_num_minus4
219        assert_eq!(r.read_ue().unwrap(), 0); // poc type
220        assert_eq!(r.read_ue().unwrap(), 0); // log2_max_poc_lsb_minus4
221        assert_eq!(r.read_ue().unwrap(), 1); // max_num_ref_frames
222        assert!(!r.read_bit().unwrap()); // gaps
223        assert_eq!(r.read_ue().unwrap(), 119); // 1920/16 - 1
224        assert_eq!(r.read_ue().unwrap(), 67); // ceil(1080/16)-1 = 68-1
225        assert!(r.read_bit().unwrap()); // frame_mbs_only
226        assert!(!r.read_bit().unwrap()); // direct_8x8
227        assert!(r.read_bit().unwrap()); // cropping present (1080)
228        assert_eq!(r.read_ue().unwrap(), 0); // crop left
229        assert_eq!(r.read_ue().unwrap(), 0); // crop right
230        assert_eq!(r.read_ue().unwrap(), 0); // crop top
231        assert_eq!(r.read_ue().unwrap(), 4); // crop bottom: (1088-1080)/2
232    }
233
234    #[test]
235    fn pps_roundtrips_through_reader() {
236        let mut cfg = EncoderConfig::new(640, 480);
237        cfg.profile = rusty_h264_common::Profile::ConstrainedBaseline;
238        cfg.cabac = false;
239        let pps = Pps::from_config(&cfg);
240        let nal = pps.to_nal();
241
242        let rbsp = emulation_unprevent(&nal.rbsp);
243        let mut r = BitReader::new(&rbsp);
244        assert_eq!(r.read_ue().unwrap(), 0); // pps id
245        assert_eq!(r.read_ue().unwrap(), 0); // sps id
246        assert!(!r.read_bit().unwrap()); // entropy_coding_mode (CAVLC)
247        assert!(!r.read_bit().unwrap()); // bottom_field
248        assert_eq!(r.read_ue().unwrap(), 0); // num_slice_groups_minus1
249        assert_eq!(r.read_ue().unwrap(), 0); // num_ref_idx_l0
250        assert_eq!(r.read_ue().unwrap(), 0); // num_ref_idx_l1
251        assert!(!r.read_bit().unwrap()); // weighted_pred
252        assert_eq!(r.read_bits(2).unwrap(), 0); // weighted_bipred_idc
253        assert_eq!(r.read_se().unwrap(), 0); // pic_init_qp_minus26 (qp 26)
254    }
255}