oxideav-h264 0.1.9

Pure-Rust H.264 / AVC (ITU-T H.264 | ISO/IEC 14496-10) codec for oxideav — under spec-driven rewrite, no decode/encode functionality yet
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
//! §7.3.3 — slice_header emit (encoder side).
//!
//! Round-14: in-loop deblocking is enabled by default
//! (`disable_deblocking_filter_idc=0`). The PPS's
//! `deblocking_filter_control_present_flag=1` lets the slice header
//! drive the loop-filter state. Round-1..13 emitted
//! `disable_deblocking_filter_idc=1` to keep the encoder simple; we now
//! mirror what the decoder produces so the encoder's local recon stays
//! aligned with what downstream decoders see.
//!
//! Round-16: adds P-slice header writer
//! ([`write_p_slice_header`]) for non-IDR P-slices. Single-reference
//! L0-only profile: `num_ref_idx_active_override_flag = 0`,
//! `ref_pic_list_modification` empty, `dec_ref_pic_marking` non-IDR
//! "use defaults" path.

use crate::encoder::bitstream::BitWriter;

/// §7.3.3 — `field_pic_flag` / `bottom_field_flag` signalling for the
/// slice-header writers (round-416 PAFF support).
///
/// The pair of flags is only present in the bitstream when the active
/// SPS has `frame_mbs_only_flag == 0`; the writer trusts the caller to
/// pick the variant matching the SPS it emitted
/// ([`crate::encoder::sps::BaselineSpsConfig::interlaced_fields`]).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FieldPicSignal {
    /// SPS has `frame_mbs_only_flag = 1` — neither flag is coded.
    #[default]
    FrameMbsOnly,
    /// Interlaced SPS, frame picture: `field_pic_flag = 0`.
    FramePicture,
    /// Field picture: `field_pic_flag = 1`, `bottom_field_flag = 0`.
    TopField,
    /// Field picture: `field_pic_flag = 1`, `bottom_field_flag = 1`.
    BottomField,
}

impl FieldPicSignal {
    /// Emit the §7.3.3 `field_pic_flag` (+ conditional
    /// `bottom_field_flag`) bits at the post-`frame_num` position.
    fn write(self, w: &mut BitWriter) {
        match self {
            FieldPicSignal::FrameMbsOnly => {}
            FieldPicSignal::FramePicture => w.u(1, 0),
            FieldPicSignal::TopField => {
                w.u(1, 1);
                w.u(1, 0);
            }
            FieldPicSignal::BottomField => {
                w.u(1, 1);
                w.u(1, 1);
            }
        }
    }
}

/// Configuration for [`build_idr_i_slice_header_rbsp`]. Holds only the
/// slice-header fields. Caller appends `slice_data()` and trailing bits.
#[derive(Debug, Clone, Copy)]
pub struct IdrSliceHeaderConfig<'a> {
    pub first_mb_in_slice: u32,
    /// Raw `slice_type` value. For an "all-I, all slices same type"
    /// picture use 7 (`slice_type % 5 == 2` and `slice_type >= 5`).
    pub slice_type_raw: u32,
    pub pic_parameter_set_id: u32,
    /// §7.3.3 — `colour_plane_id` u(2), emitted between
    /// `pic_parameter_set_id` and `frame_num` when the active SPS has
    /// `separate_colour_plane_flag == 1` (round 448). `None` for
    /// ordinary streams.
    pub colour_plane_id: Option<u8>,
    /// `frame_num` width is `log2_max_frame_num_minus4 + 4`. We supply
    /// the value, the bit width is derived by the caller.
    pub frame_num: u32,
    pub frame_num_bits: u32,
    pub idr_pic_id: u32,
    /// `pic_order_cnt_lsb` width is `log2_max_pic_order_cnt_lsb_minus4 + 4`.
    pub pic_order_cnt_lsb: u32,
    pub poc_lsb_bits: u32,
    /// `slice_qp_delta` per §7.4.3.
    pub slice_qp_delta: i32,
    /// §7.4.3 — `disable_deblocking_filter_idc` ∈ {0, 1, 2}. 0 = filter
    /// across slice boundaries, 1 = no filtering, 2 = filter except on
    /// slice boundaries. Our single-slice encoder uses 0.
    pub disable_deblocking_filter_idc: u32,
    /// §7.4.3 — `slice_alpha_c0_offset_div2` ∈ -6..=6. Only emitted when
    /// `disable_deblocking_filter_idc != 1`.
    pub slice_alpha_c0_offset_div2: i32,
    /// §7.4.3 — `slice_beta_offset_div2` ∈ -6..=6. Only emitted when
    /// `disable_deblocking_filter_idc != 1`.
    pub slice_beta_offset_div2: i32,
    /// Round-416 — PAFF field signalling (see [`FieldPicSignal`]).
    pub field: FieldPicSignal,
    /// Round-416 — when `false`, the writer emits a **non-IDR I slice**
    /// header: `idr_pic_id` is omitted and `dec_ref_pic_marking()` uses
    /// the non-IDR path (`adaptive_ref_pic_marking_mode_flag = 0`,
    /// gated on `nal_ref_idc != 0`). Needed for the second I field of a
    /// PAFF IDR frame pair, which per §7.4.3 shares `frame_num = 0`
    /// with the IDR first field but is itself a non-IDR picture
    /// (`nal_unit_type = 1`).
    pub idr: bool,
    /// §7.3.3 — `nal_ref_idc` of the parent NAL; gates
    /// `dec_ref_pic_marking()` on the non-IDR path. Ignored when
    /// `idr == true` (IDR marking is always present).
    pub nal_ref_idc: u32,
    /// Round-436 — §7.4.3.3 `long_term_reference_flag` on the IDR
    /// path: `true` marks the IDR picture "used for long-term
    /// reference" with LongTermFrameIdx = 0 (and MaxLongTermFrameIdx =
    /// 0, unblocking later MMCO 3/6 without an MMCO 4).
    pub long_term_reference_flag: bool,
    /// Round-436 — §7.3.3.3 adaptive marking ops for the NON-IDR
    /// branch (e.g. the second I field of a PAFF IDR pair completing a
    /// long-term pair via MMCO 6). Empty = sliding window. Ignored
    /// when `idr == true`.
    pub mmco: &'a [EncMmcoOp],
    /// Round-453 — §7.3.3 `redundant_pic_cnt` (ue(v), coded when the
    /// PPS has `redundant_pic_cnt_present_flag = 1`; 0 = primary).
    pub redundant_pic_cnt: Option<u32>,
    /// Round-453 — §7.3.3 `slice_group_change_cycle` as
    /// `(value, bits)` for slice group map types 3..=5 (eq. 7-37
    /// bit width, computed by the caller).
    pub slice_group_change_cycle: Option<(u32, u32)>,
}

/// Round-436 — §7.3.3.3 / Table 7-9 `memory_management_control_operation`
/// for the encoder's `dec_ref_pic_marking()` writer. Each variant
/// carries the operation's trailing syntax element(s); the writer emits
/// `adaptive_ref_pic_marking_mode_flag = 1`, the ops in order, then the
/// op-0 terminator.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EncMmcoOp {
    /// op 1 — `difference_of_pic_nums_minus1`.
    MarkShortTermUnused(u32),
    /// op 2 — `long_term_pic_num`.
    MarkLongTermUnused(u32),
    /// op 3 — `(difference_of_pic_nums_minus1, long_term_frame_idx)`.
    AssignLongTerm(u32, u32),
    /// op 4 — `max_long_term_frame_idx_plus1`.
    SetMaxLongTermIdx(u32),
    /// op 5 — mark all reference pictures unused.
    MarkAllUnused,
    /// op 6 — `long_term_frame_idx` for the current picture.
    AssignCurrentLongTerm(u32),
}

/// Round-436 — emit the §7.3.3.3 non-IDR `dec_ref_pic_marking()` body:
/// sliding window when `mmco` is empty, else the adaptive op list with
/// its op-0 terminator.
fn write_dec_ref_pic_marking_non_idr(w: &mut BitWriter, mmco: &[EncMmcoOp]) {
    if mmco.is_empty() {
        w.u(1, 0); // adaptive_ref_pic_marking_mode_flag = 0
        return;
    }
    w.u(1, 1); // adaptive_ref_pic_marking_mode_flag = 1
    for op in mmco {
        match *op {
            EncMmcoOp::MarkShortTermUnused(diff) => {
                w.ue(1);
                w.ue(diff);
            }
            EncMmcoOp::MarkLongTermUnused(ltpn) => {
                w.ue(2);
                w.ue(ltpn);
            }
            EncMmcoOp::AssignLongTerm(diff, ltfi) => {
                w.ue(3);
                w.ue(diff);
                w.ue(ltfi);
            }
            EncMmcoOp::SetMaxLongTermIdx(max_plus1) => {
                w.ue(4);
                w.ue(max_plus1);
            }
            EncMmcoOp::MarkAllUnused => w.ue(5),
            EncMmcoOp::AssignCurrentLongTerm(ltfi) => {
                w.ue(6);
                w.ue(ltfi);
            }
        }
    }
    w.ue(0); // memory_management_control_operation = 0 (end)
}

/// Round-436 — §7.3.3.1 / Table 7-7 `modification_of_pic_nums_idc` for
/// the encoder's `ref_pic_list_modification()` writer.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EncRplmOp {
    /// idc 0 — `abs_diff_pic_num_minus1`, subtract from picNumLXPred.
    Subtract(u32),
    /// idc 1 — `abs_diff_pic_num_minus1`, add to picNumLXPred.
    Add(u32),
    /// idc 2 — `long_term_pic_num` of the reference to splice in.
    LongTerm(u32),
}

/// Round-436 — emit one list's §7.3.3.1 `ref_pic_list_modification()`
/// body: flag 0 when `ops` is empty, else flag 1 + the ops + the idc-3
/// terminator.
fn write_rplm_list(w: &mut BitWriter, ops: &[EncRplmOp]) {
    if ops.is_empty() {
        w.u(1, 0);
        return;
    }
    w.u(1, 1); // ref_pic_list_modification_flag_lX = 1
    for op in ops {
        match *op {
            EncRplmOp::Subtract(v) => {
                w.ue(0);
                w.ue(v);
            }
            EncRplmOp::Add(v) => {
                w.ue(1);
                w.ue(v);
            }
            EncRplmOp::LongTerm(v) => {
                w.ue(2);
                w.ue(v);
            }
        }
    }
    w.ue(3); // modification_of_pic_nums_idc = 3 (end)
}

/// Round-30 — extra parameters for the CABAC-enabled slice writers.
#[derive(Debug, Clone, Copy)]
pub struct CabacSliceParams {
    /// §7.3.3 — `cabac_init_idc` ∈ 0..=2. Only emitted on non-I/SI
    /// slices when CABAC is enabled. The decoder uses it to pick the
    /// per-element initialisation column (Tables 9-12..9-33).
    pub cabac_init_idc: u32,
}

/// Emit the bits of a single IDR I-slice header into the supplied
/// `BitWriter`. The writer is left at the position where `slice_data()`
/// should begin.
pub fn write_idr_i_slice_header(w: &mut BitWriter, cfg: &IdrSliceHeaderConfig<'_>) {
    w.ue(cfg.first_mb_in_slice);
    w.ue(cfg.slice_type_raw);
    w.ue(cfg.pic_parameter_set_id);
    // §7.3.3 — colour_plane_id u(2) (separate_colour_plane_flag == 1).
    if let Some(plane) = cfg.colour_plane_id {
        debug_assert!(plane <= 2, "colour_plane_id must be 0..=2 (§7.4.3)");
        w.u(2, plane as u32);
    }
    // No `colour_plane_id` (separate_colour_plane_flag == 0 in our SPS).
    w.u(cfg.frame_num_bits, cfg.frame_num);
    // §7.3.3 — field_pic_flag / bottom_field_flag when the SPS has
    // frame_mbs_only_flag == 0 (round-416 PAFF).
    cfg.field.write(w);
    // idr_pic_id only on IDR pictures (§7.3.3).
    if cfg.idr {
        w.ue(cfg.idr_pic_id);
    }
    // POC type 0 branch: pic_order_cnt_lsb. No delta_pic_order_cnt_bottom
    // (bottom_field_pic_order_in_frame_present_flag == 0 in our PPS).
    w.u(cfg.poc_lsb_bits, cfg.pic_order_cnt_lsb);
    // §7.3.3 — redundant_pic_cnt when the PPS enables it (round-453).
    if let Some(rpc) = cfg.redundant_pic_cnt {
        w.ue(rpc);
    }
    // I-slice: no num_ref_idx_active_override_flag,
    // no ref_pic_list_modification, no pred_weight_table.

    // §7.3.3.3 — dec_ref_pic_marking().
    if cfg.idr {
        // IDR path (nal_ref_idc != 0 always for IDR):
        // no_output_of_prior_pics_flag = 0; long_term_reference_flag
        // per config (round-436 long-term IDR fields).
        w.u(1, 0);
        w.u(1, u32::from(cfg.long_term_reference_flag));
    } else if cfg.nal_ref_idc != 0 {
        // Non-IDR reference I picture (e.g. the second field of a PAFF
        // IDR pair): sliding window, or the round-436 adaptive MMCO
        // list.
        write_dec_ref_pic_marking_non_idr(w, cfg.mmco);
    }

    // No cabac_init_idc — entropy_coding_mode_flag == 0 (CAVLC).
    w.se(cfg.slice_qp_delta);
    // No sp_for_switch_flag, slice_qs_delta — not SP/SI.

    // §7.3.3 — deblocking_filter_control_present_flag == 1 in our PPS,
    // so we always emit `disable_deblocking_filter_idc`. When the IDC
    // is not 1 (i.e. some form of filtering is enabled), the alpha-c0
    // and beta offsets follow.
    debug_assert!(cfg.disable_deblocking_filter_idc <= 2);
    debug_assert!((-6..=6).contains(&cfg.slice_alpha_c0_offset_div2));
    debug_assert!((-6..=6).contains(&cfg.slice_beta_offset_div2));
    w.ue(cfg.disable_deblocking_filter_idc);
    if cfg.disable_deblocking_filter_idc != 1 {
        w.se(cfg.slice_alpha_c0_offset_div2);
        w.se(cfg.slice_beta_offset_div2);
    }
    // §7.3.3 — slice_group_change_cycle u(v) for slice group map
    // types 3..=5 (round-453 FMO).
    if let Some((value, bits)) = cfg.slice_group_change_cycle {
        w.u(bits, value);
    }
}

// ---------------------------------------------------------------------------
// Round-16 — P-slice header (single L0 ref, no list modification).
// ---------------------------------------------------------------------------

/// Configuration for [`write_p_slice_header`]. Holds only the slice-header
/// fields. Caller appends `slice_data()` and trailing bits.
///
/// This writer covers the **simple non-IDR P-slice** path:
///
/// * `nal_unit_type = 1` (Slice non-IDR)
/// * `slice_type` = 5 (P, all slices same type) per Table 7-6
/// * `num_ref_idx_active_override_flag = 0` (use PPS default; for our
///   PPS that's `num_ref_idx_l0_default_active_minus1 = 0` ⇒ one L0 ref)
/// * `ref_pic_list_modification`: list-0 modification flag = 0
///   (no modifications). No list-1 (P-slice).
/// * `dec_ref_pic_marking` with `adaptive_ref_pic_marking_mode_flag = 0`
///   (sliding-window default — works for our 1-ref short-term setup).
/// * No `pred_weight_table` (PPS `weighted_pred_flag = 0`).
#[derive(Debug, Clone, Copy)]
pub struct PSliceHeaderConfig<'a> {
    pub first_mb_in_slice: u32,
    /// Raw `slice_type` per Table 7-6. Use 5 for "all slices in picture
    /// are P" (slice_type % 5 == 0 and >= 5).
    pub slice_type_raw: u32,
    pub pic_parameter_set_id: u32,
    /// §7.3.3 — `colour_plane_id` u(2), emitted between
    /// `pic_parameter_set_id` and `frame_num` when the active SPS has
    /// `separate_colour_plane_flag == 1` (round 448). `None` for
    /// ordinary streams.
    pub colour_plane_id: Option<u8>,
    pub frame_num: u32,
    pub frame_num_bits: u32,
    pub pic_order_cnt_lsb: u32,
    pub poc_lsb_bits: u32,
    pub slice_qp_delta: i32,
    pub disable_deblocking_filter_idc: u32,
    pub slice_alpha_c0_offset_div2: i32,
    pub slice_beta_offset_div2: i32,
    /// `nal_ref_idc` of the parent NAL — used to gate
    /// `dec_ref_pic_marking()` per §7.3.3 last branch.
    pub nal_ref_idc: u32,
    /// Round-30 — when `Some`, the writer emits `cabac_init_idc` ue(v)
    /// at the §7.3.3 position (between `dec_ref_pic_marking` and
    /// `slice_qp_delta`). Required when the PPS has
    /// `entropy_coding_mode_flag = 1` and the slice is non-I/SI.
    pub cabac: Option<CabacSliceParams>,
    /// Round-416 — PAFF field signalling (see [`FieldPicSignal`]).
    pub field: FieldPicSignal,
    /// Round-436 — §7.3.3.1 list-0 modification ops (e.g. idc 2
    /// splicing a long-term field to `ref_idx` 0). Empty = flag 0.
    pub rplm_l0: &'a [EncRplmOp],
    /// Round-436 — §7.3.3.3 adaptive marking ops (Table 7-9). Empty =
    /// sliding window.
    pub mmco: &'a [EncMmcoOp],
    /// Round-453 — §7.3.3.2 `pred_weight_table()` for P slices. MUST
    /// be `Some` when the PPS has `weighted_pred_flag = 1` (the table
    /// is mandatory in every P slice header then) and `None`
    /// otherwise.
    pub pred_weight_table: Option<ExplicitPredWeightTableL0>,
    /// Round-453 — §7.3.3 `num_ref_idx_active_override_flag`: `Some(n)`
    /// codes the override with `num_ref_idx_l0_active_minus1 = n`;
    /// `None` keeps the PPS default (flag 0).
    pub num_ref_idx_l0_active_minus1: Option<u32>,
    /// Round-453 — §7.3.3 `redundant_pic_cnt` (see
    /// [`IdrSliceHeaderConfig::redundant_pic_cnt`]).
    pub redundant_pic_cnt: Option<u32>,
    /// Round-453 — §7.3.3 `slice_group_change_cycle` `(value, bits)`.
    pub slice_group_change_cycle: Option<(u32, u32)>,
}

/// Round-453 — §7.3.3.2 explicit weighted-prediction table for a
/// single-L0-ref P slice (`weighted_pred_flag = 1`).
///
/// Luma only: `luma_weight_l0_flag = 1` with the coded (weight,
/// offset) pair when `luma` is `Some`, else `luma_weight_l0_flag = 0`
/// (the decoder infers `2^luma_log2_weight_denom` / 0 per §7.4.3.2 —
/// an identity weighting, the cheapest way to satisfy the mandatory
/// table on frames where no fade was detected). Chroma weights are
/// never coded (`chroma_weight_l0_flag = 0`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ExplicitPredWeightTableL0 {
    /// §7.4.3.2 `luma_log2_weight_denom`, range 0..=7.
    pub luma_log2_weight_denom: u32,
    /// `(luma_weight_l0[0], luma_offset_l0[0])`, each in -128..=127.
    pub luma: Option<(i32, i32)>,
}

impl ExplicitPredWeightTableL0 {
    /// The effective §8.4.2.3.2 `(w0, o0)` after §7.4.3.2 inference.
    pub fn effective(&self) -> (i32, i32) {
        self.luma
            .unwrap_or((1i32 << self.luma_log2_weight_denom, 0))
    }
}

/// Emit the bits of a single P-slice header (non-IDR) into the supplied
/// `BitWriter`. The writer is left at the position where `slice_data()`
/// should begin.
pub fn write_p_slice_header(w: &mut BitWriter, cfg: &PSliceHeaderConfig<'_>) {
    debug_assert!(cfg.slice_type_raw % 5 == 0); // P-slice
    w.ue(cfg.first_mb_in_slice);
    w.ue(cfg.slice_type_raw);
    w.ue(cfg.pic_parameter_set_id);
    // §7.3.3 — colour_plane_id u(2) (separate_colour_plane_flag == 1).
    if let Some(plane) = cfg.colour_plane_id {
        debug_assert!(plane <= 2, "colour_plane_id must be 0..=2 (§7.4.3)");
        w.u(2, plane as u32);
    }
    // No `colour_plane_id` (separate_colour_plane_flag == 0).
    w.u(cfg.frame_num_bits, cfg.frame_num);
    // §7.3.3 — field_pic_flag / bottom_field_flag when the SPS has
    // frame_mbs_only_flag == 0 (round-416 PAFF).
    cfg.field.write(w);
    // No idr_pic_id — non-IDR.
    // POC type 0 branch.
    w.u(cfg.poc_lsb_bits, cfg.pic_order_cnt_lsb);
    // No delta_pic_order_cnt_bottom (PPS bottom_field_pic_order_in_frame_present_flag == 0).
    // §7.3.3 — redundant_pic_cnt when the PPS enables it (round-453).
    if let Some(rpc) = cfg.redundant_pic_cnt {
        w.ue(rpc);
    }
    // No direct_spatial_mv_pred_flag — P-slice (B only).

    // §7.3.3 — num_ref_idx_active_override_flag for P/SP/B slices:
    // PPS default (num_ref_idx_l0_default_active_minus1 = 0) unless the
    // caller overrides (round-453 multi-reference P).
    match cfg.num_ref_idx_l0_active_minus1 {
        Some(n) => {
            debug_assert!(n <= 31);
            w.u(1, 1);
            w.ue(n);
        }
        None => w.u(1, 0),
    }

    // §7.3.3.1 — ref_pic_list_modification(): list-0 only for P-slice.
    write_rplm_list(w, cfg.rplm_l0);
    // No list-1 loop for P-slice.

    // §7.3.3.2 — pred_weight_table(), present iff the PPS has
    // weighted_pred_flag == 1 (round-453). Single L0 ref, luma only:
    //   luma_log2_weight_denom     ue(v)
    //   chroma_log2_weight_denom   ue(v)   (ChromaArrayType != 0)
    //   luma_weight_l0_flag        u(1)
    //   [luma_weight_l0[0] se(v), luma_offset_l0[0] se(v)]
    //   chroma_weight_l0_flag      u(1) = 0
    if let Some(pwt) = cfg.pred_weight_table {
        debug_assert!(pwt.luma_log2_weight_denom <= 7);
        w.ue(pwt.luma_log2_weight_denom);
        // Our P writers run at ChromaArrayType 1/2/3 (never 0):
        // chroma_log2_weight_denom mirrors luma's.
        w.ue(pwt.luma_log2_weight_denom);
        match pwt.luma {
            Some((wt, off)) => {
                debug_assert!((-128..=127).contains(&wt));
                debug_assert!((-128..=127).contains(&off));
                w.u(1, 1);
                w.se(wt);
                w.se(off);
            }
            None => w.u(1, 0),
        }
        w.u(1, 0); // chroma_weight_l0_flag = 0
    }

    // §7.3.3.3 — dec_ref_pic_marking() when nal_ref_idc != 0: sliding
    // window, or the round-436 adaptive MMCO list.
    if cfg.nal_ref_idc != 0 {
        write_dec_ref_pic_marking_non_idr(w, cfg.mmco);
    }

    // §7.3.3 — cabac_init_idc when CABAC is enabled and slice is non-I/SI.
    if let Some(c) = cfg.cabac {
        debug_assert!(c.cabac_init_idc <= 2);
        w.ue(c.cabac_init_idc);
    }
    w.se(cfg.slice_qp_delta);
    // No SP/SI fields — slice_type is P.

    // §7.3.3 — deblocking_filter_control_present_flag == 1 in our PPS.
    debug_assert!(cfg.disable_deblocking_filter_idc <= 2);
    debug_assert!((-6..=6).contains(&cfg.slice_alpha_c0_offset_div2));
    debug_assert!((-6..=6).contains(&cfg.slice_beta_offset_div2));
    w.ue(cfg.disable_deblocking_filter_idc);
    if cfg.disable_deblocking_filter_idc != 1 {
        w.se(cfg.slice_alpha_c0_offset_div2);
        w.se(cfg.slice_beta_offset_div2);
    }
    // §7.3.3 — slice_group_change_cycle u(v) for slice group map
    // types 3..=5 (round-453 FMO).
    if let Some((value, bits)) = cfg.slice_group_change_cycle {
        w.u(bits, value);
    }
}

// ---------------------------------------------------------------------------
// Round-20 — B-slice header (single L0 + single L1 ref, no list mod, spatial
// direct MV pred, non-reference B by default).
// ---------------------------------------------------------------------------

/// Configuration for [`write_b_slice_header`]. Holds only the slice-header
/// fields. Caller appends `slice_data()` and trailing bits.
///
/// This writer covers the **simple non-IDR B-slice** path:
///
/// * `nal_unit_type = 1` (Slice non-IDR)
/// * `slice_type` = 6 (B, all slices same type) per Table 7-6
/// * `direct_spatial_mv_pred_flag = 1` — round 20 only emits explicit
///   B_L0_16x16 / B_L1_16x16 / B_Bi_16x16 macroblocks (no B_Skip /
///   B_Direct yet) so the flag is irrelevant in practice; we set it to
///   1 (spatial direct, the simpler mode) for consistency with §8.4.1.2.2.
/// * `num_ref_idx_active_override_flag = 0` (use PPS defaults; for our
///   round-20 PPS that's `num_ref_idx_l0_default_active_minus1 = 0` and
///   `num_ref_idx_l1_default_active_minus1 = 0` ⇒ one L0 ref + one L1 ref)
/// * `ref_pic_list_modification`: list-0 and list-1 modification flags = 0
/// * `dec_ref_pic_marking` only when `nal_ref_idc != 0` (we emit B as
///   non-reference, so absent)
/// * No `pred_weight_table` (PPS `weighted_bipred_idc = 0` ⇒ default
///   weighted prediction per §8.4.2.3.1, i.e. `(L0 + L1 + 1) >> 1` for
///   bipred and identity for L0-only / L1-only)
///
/// Round-26 — when `pred_weight_table` is `Some`, the writer emits the
/// §7.3.3.2 sub-structure between `ref_pic_list_modification` and
/// `dec_ref_pic_marking`. The caller is responsible for setting the
/// PPS's `weighted_bipred_idc = 1` (or `weighted_pred_flag = 1` for
/// P-slices) so the decoder pulls the table.
#[derive(Debug, Clone, Copy)]
pub struct BSliceHeaderConfig {
    pub first_mb_in_slice: u32,
    /// Raw `slice_type` per Table 7-6. Use 6 for "all slices in picture
    /// are B" (slice_type % 5 == 1 and >= 5).
    pub slice_type_raw: u32,
    pub pic_parameter_set_id: u32,
    /// §7.3.3 — `colour_plane_id` u(2), emitted between
    /// `pic_parameter_set_id` and `frame_num` when the active SPS has
    /// `separate_colour_plane_flag == 1` (round 448). `None` for
    /// ordinary streams.
    pub colour_plane_id: Option<u8>,
    pub frame_num: u32,
    pub frame_num_bits: u32,
    pub pic_order_cnt_lsb: u32,
    pub poc_lsb_bits: u32,
    /// §7.4.3 / §8.4.1.2.2 — `direct_spatial_mv_pred_flag`. For our
    /// round-20 explicit-only B path we set 1 (spatial). The value
    /// only matters if the bitstream contains a B_Skip / B_Direct MB.
    pub direct_spatial_mv_pred_flag: bool,
    pub slice_qp_delta: i32,
    pub disable_deblocking_filter_idc: u32,
    pub slice_alpha_c0_offset_div2: i32,
    pub slice_beta_offset_div2: i32,
    /// `nal_ref_idc` of the parent NAL — used to gate
    /// `dec_ref_pic_marking()` per §7.3.3 last branch. For non-reference
    /// B-frames pass 0; the writer omits dec_ref_pic_marking().
    pub nal_ref_idc: u32,
    /// Round-26 — explicit weighted-prediction parameters. `Some` when
    /// the PPS has `weighted_bipred_idc == 1` (and the encoder picked
    /// non-trivial weights for this slice); `None` when the slice
    /// should fall back to the §8.4.2.3.1 default merge.
    ///
    /// Carries one (weight, offset) entry per ref index in each list.
    /// Round-26 always uses a single L0 + L1 ref (matches the round-20
    /// PPS defaults), so each list has exactly one luma entry. Chroma
    /// entries are absent (`luma_weight_lN_flag = 1`,
    /// `chroma_weight_lN_flag = 0`) to keep the syntax minimal.
    pub pred_weight_table: Option<ExplicitBipredWeightTable>,
    /// Round-31 — when `Some`, the writer emits `cabac_init_idc` ue(v)
    /// at the §7.3.3 position (between `dec_ref_pic_marking` and
    /// `slice_qp_delta`). Required when the PPS has
    /// `entropy_coding_mode_flag = 1` and the slice is non-I/SI.
    pub cabac: Option<CabacSliceParams>,
    /// Round-436 — PAFF field signalling (see [`FieldPicSignal`]).
    /// `FrameMbsOnly` (the default) matches the historical writer
    /// (SPS `frame_mbs_only_flag = 1`, no field bits coded).
    pub field: FieldPicSignal,
}

/// Round-26 — minimal explicit weighted-prediction table for a
/// single-L0-ref + single-L1-ref B slice.
///
/// Mirrors §7.3.3.2 syntax fields. We only emit luma weights and skip
/// chroma (chroma_weight_lN_flag = 0 for every ref → decoder infers the
/// default chroma weights per §7.4.3.2). Each list has a single entry.
#[derive(Debug, Clone, Copy)]
pub struct ExplicitBipredWeightTable {
    /// §7.4.3.2 `luma_log2_weight_denom`, range 0..=7.
    pub luma_log2_weight_denom: u32,
    /// L0 luma weight (`luma_weight_l0[0]`), range -128..=127.
    pub luma_weight_l0: i32,
    /// L0 luma offset (`luma_offset_l0[0]`), range -128..=127.
    pub luma_offset_l0: i32,
    /// L1 luma weight (`luma_weight_l1[0]`), range -128..=127.
    pub luma_weight_l1: i32,
    /// L1 luma offset (`luma_offset_l1[0]`), range -128..=127.
    pub luma_offset_l1: i32,
}

/// Emit the bits of a single B-slice header (non-IDR) into the supplied
/// `BitWriter`. The writer is left at the position where `slice_data()`
/// should begin.
pub fn write_b_slice_header(w: &mut BitWriter, cfg: &BSliceHeaderConfig) {
    debug_assert!(cfg.slice_type_raw % 5 == 1); // B-slice
    w.ue(cfg.first_mb_in_slice);
    w.ue(cfg.slice_type_raw);
    w.ue(cfg.pic_parameter_set_id);
    // §7.3.3 — colour_plane_id u(2) (separate_colour_plane_flag == 1).
    if let Some(plane) = cfg.colour_plane_id {
        debug_assert!(plane <= 2, "colour_plane_id must be 0..=2 (§7.4.3)");
        w.u(2, plane as u32);
    }
    // No `colour_plane_id` (separate_colour_plane_flag == 0).
    w.u(cfg.frame_num_bits, cfg.frame_num);
    // §7.3.3 — field_pic_flag / bottom_field_flag when the SPS has
    // frame_mbs_only_flag == 0 (round-436 PAFF B fields).
    cfg.field.write(w);
    // No idr_pic_id — non-IDR.
    // POC type 0 branch.
    w.u(cfg.poc_lsb_bits, cfg.pic_order_cnt_lsb);
    // No delta_pic_order_cnt_bottom (PPS bottom_field_pic_order_in_frame_present_flag == 0).
    // No redundant_pic_cnt (PPS redundant_pic_cnt_present_flag == 0).

    // §7.3.3 — direct_spatial_mv_pred_flag for B slices (always emitted
    // even when no Direct MB is present, per §7.3.3 syntax structure).
    w.u(
        1,
        if cfg.direct_spatial_mv_pred_flag {
            1
        } else {
            0
        },
    );

    // §7.3.3 — num_ref_idx_active_override_flag. We use the PPS default
    // (num_ref_idx_l0_default_active_minus1 = 0, ditto l1), so the
    // override is 0 and the `_minus1` fields are absent.
    w.u(1, 0);

    // §7.3.3.1 — ref_pic_list_modification(): both lists for B-slice.
    // ref_pic_list_modification_flag_l0 = 0, ref_pic_list_modification_flag_l1 = 0.
    w.u(1, 0);
    w.u(1, 0);

    // §7.3.3.2 — pred_weight_table(). Round-26: emitted when
    // `pred_weight_table` is `Some` (PPS `weighted_bipred_idc == 1`).
    // Layout for our single-L0-ref + single-L1-ref B slice:
    //   luma_log2_weight_denom        ue(v)
    //   chroma_log2_weight_denom      ue(v)   (ChromaArrayType=1 in our SPS)
    //   for L0:
    //     luma_weight_l0_flag         u(1) = 1
    //     luma_weight_l0[0]           se(v)
    //     luma_offset_l0[0]           se(v)
    //     chroma_weight_l0_flag       u(1) = 0  (use defaults)
    //   for L1: symmetric.
    if let Some(pwt) = cfg.pred_weight_table {
        debug_assert!(pwt.luma_log2_weight_denom <= 7);
        debug_assert!((-128..=127).contains(&pwt.luma_weight_l0));
        debug_assert!((-128..=127).contains(&pwt.luma_offset_l0));
        debug_assert!((-128..=127).contains(&pwt.luma_weight_l1));
        debug_assert!((-128..=127).contains(&pwt.luma_offset_l1));
        w.ue(pwt.luma_log2_weight_denom);
        // ChromaArrayType=1 (4:2:0) in our SPS; chroma_log2_weight_denom
        // is required even though we don't ship per-ref chroma weights.
        // The decoder uses it to infer the default chroma weights per
        // §7.4.3.2. We mirror luma_log2_weight_denom for simplicity.
        w.ue(pwt.luma_log2_weight_denom);
        // L0 entry (single ref).
        w.u(1, 1); // luma_weight_l0_flag = 1
        w.se(pwt.luma_weight_l0);
        w.se(pwt.luma_offset_l0);
        w.u(1, 0); // chroma_weight_l0_flag = 0 (use default chroma weight)
                   // L1 entry (single ref).
        w.u(1, 1); // luma_weight_l1_flag = 1
        w.se(pwt.luma_weight_l1);
        w.se(pwt.luma_offset_l1);
        w.u(1, 0); // chroma_weight_l1_flag = 0
    }

    // §7.3.3.3 — dec_ref_pic_marking() when nal_ref_idc != 0. For non-
    // reference B-frames (nal_ref_idc == 0) the field is absent.
    if cfg.nal_ref_idc != 0 {
        w.u(1, 0); // adaptive_ref_pic_marking_mode_flag = 0 (sliding window)
    }

    // §7.3.3 — cabac_init_idc when CABAC is enabled and slice is non-I/SI.
    if let Some(c) = cfg.cabac {
        debug_assert!(c.cabac_init_idc <= 2);
        w.ue(c.cabac_init_idc);
    }
    w.se(cfg.slice_qp_delta);
    // No SP/SI fields — slice_type is B.

    // §7.3.3 — deblocking_filter_control_present_flag == 1 in our PPS.
    debug_assert!(cfg.disable_deblocking_filter_idc <= 2);
    debug_assert!((-6..=6).contains(&cfg.slice_alpha_c0_offset_div2));
    debug_assert!((-6..=6).contains(&cfg.slice_beta_offset_div2));
    w.ue(cfg.disable_deblocking_filter_idc);
    if cfg.disable_deblocking_filter_idc != 1 {
        w.se(cfg.slice_alpha_c0_offset_div2);
        w.se(cfg.slice_beta_offset_div2);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::encoder::nal::build_nal_unit;
    use crate::encoder::pps::{build_baseline_pps_rbsp, BaselinePpsConfig};
    use crate::encoder::sps::{build_baseline_sps_rbsp, BaselineSpsConfig};
    use crate::nal::{parse_nal_unit, AnnexBSplitter, NalHeader, NalUnitType};
    use crate::pps::Pps;
    use crate::slice_header::{SliceHeader, SliceType};
    use crate::sps::Sps;

    #[test]
    fn idr_slice_header_round_trips_through_decoder_parser() {
        let sps_rbsp = build_baseline_sps_rbsp(&BaselineSpsConfig {
            bit_depth_luma_minus8: 0,
            bit_depth_chroma_minus8: 0,
            seq_scaling_lists: None,
            interlaced_fields: false,
            mbaff: false,
            vui: None,
            seq_parameter_set_id: 0,
            level_idc: 30,
            width_in_mbs: 4,
            height_in_mbs: 4,
            log2_max_frame_num_minus4: 4,
            log2_max_poc_lsb_minus4: 4,
            max_num_ref_frames: 1,
            profile_idc: 66,
            chroma_format_idc: 1,
            separate_colour_plane: false,
        });
        let sps = Sps::parse(&sps_rbsp).unwrap();
        let pps_rbsp = build_baseline_pps_rbsp(&BaselinePpsConfig::default());
        let pps = Pps::parse(&pps_rbsp).unwrap();

        let mut w = BitWriter::new();
        write_idr_i_slice_header(
            &mut w,
            &IdrSliceHeaderConfig {
                first_mb_in_slice: 0,
                slice_type_raw: 7, // I, all-same-type
                pic_parameter_set_id: 0,
                colour_plane_id: None,
                frame_num: 0,
                frame_num_bits: sps.log2_max_frame_num_minus4 + 4,
                idr_pic_id: 0,
                pic_order_cnt_lsb: 0,
                poc_lsb_bits: sps.log2_max_pic_order_cnt_lsb_minus4 + 4,
                slice_qp_delta: 0,
                disable_deblocking_filter_idc: 1,
                slice_alpha_c0_offset_div2: 0,
                slice_beta_offset_div2: 0,
                field: FieldPicSignal::FrameMbsOnly,
                idr: true,
                nal_ref_idc: 3,
                long_term_reference_flag: false,
                mmco: &[],
                redundant_pic_cnt: None,
                slice_group_change_cycle: None,
            },
        );
        // Append a trivial slice_data placeholder + rbsp_trailing_bits so
        // the parser doesn't blow up on EOF mid-header.
        w.u(1, 1); // dummy bit so trailing alignment doesn't degenerate
        w.rbsp_trailing_bits();
        let rbsp = w.into_bytes();

        // Build the IDR NAL unit and feed it back through the existing
        // decoder primitives.
        let nal = build_nal_unit(3, NalUnitType::SliceIdr, &rbsp);
        let mut split = AnnexBSplitter::new(&nal);
        let nal_bytes = split.next().unwrap();
        let nu = parse_nal_unit(nal_bytes).unwrap();
        let header = NalHeader {
            forbidden_zero_bit: 0,
            nal_ref_idc: 3,
            nal_unit_type: NalUnitType::SliceIdr,
        };
        let parsed = SliceHeader::parse(&nu.rbsp, &sps, &pps, &header).expect("parse slice header");
        assert_eq!(parsed.first_mb_in_slice, 0);
        assert_eq!(parsed.slice_type, SliceType::I);
        assert!(parsed.all_slices_same_type);
        assert_eq!(parsed.pic_parameter_set_id, 0);
        assert_eq!(parsed.frame_num, 0);
        assert_eq!(parsed.idr_pic_id, 0);
        assert_eq!(parsed.pic_order_cnt_lsb, 0);
        assert_eq!(parsed.slice_qp_delta, 0);
        assert_eq!(parsed.disable_deblocking_filter_idc, 1);
    }

    #[test]
    fn p_slice_header_round_trips_through_decoder_parser() {
        let sps_rbsp = build_baseline_sps_rbsp(&BaselineSpsConfig {
            bit_depth_luma_minus8: 0,
            bit_depth_chroma_minus8: 0,
            seq_scaling_lists: None,
            interlaced_fields: false,
            mbaff: false,
            vui: None,
            seq_parameter_set_id: 0,
            level_idc: 30,
            width_in_mbs: 4,
            height_in_mbs: 4,
            log2_max_frame_num_minus4: 4,
            log2_max_poc_lsb_minus4: 4,
            max_num_ref_frames: 1,
            profile_idc: 66,
            chroma_format_idc: 1,
            separate_colour_plane: false,
        });
        let sps = Sps::parse(&sps_rbsp).unwrap();
        let pps_rbsp = build_baseline_pps_rbsp(&BaselinePpsConfig::default());
        let pps = Pps::parse(&pps_rbsp).unwrap();

        let mut w = BitWriter::new();
        write_p_slice_header(
            &mut w,
            &PSliceHeaderConfig {
                first_mb_in_slice: 0,
                slice_type_raw: 5, // P, all-same-type
                pic_parameter_set_id: 0,
                colour_plane_id: None,
                frame_num: 1,
                frame_num_bits: sps.log2_max_frame_num_minus4 + 4,
                pic_order_cnt_lsb: 2,
                poc_lsb_bits: sps.log2_max_pic_order_cnt_lsb_minus4 + 4,
                slice_qp_delta: 0,
                disable_deblocking_filter_idc: 0,
                slice_alpha_c0_offset_div2: 0,
                slice_beta_offset_div2: 0,
                nal_ref_idc: 2,
                cabac: None,
                field: FieldPicSignal::FrameMbsOnly,
                rplm_l0: &[],
                mmco: &[],
                pred_weight_table: None,
                num_ref_idx_l0_active_minus1: None,
                redundant_pic_cnt: None,
                slice_group_change_cycle: None,
            },
        );
        // Append a dummy bit + trailing so the parser doesn't blow up.
        w.u(1, 1);
        w.rbsp_trailing_bits();
        let rbsp = w.into_bytes();

        let nal = build_nal_unit(2, NalUnitType::SliceNonIdr, &rbsp);
        let mut split = AnnexBSplitter::new(&nal);
        let nal_bytes = split.next().unwrap();
        let nu = parse_nal_unit(nal_bytes).unwrap();
        let header = NalHeader {
            forbidden_zero_bit: 0,
            nal_ref_idc: 2,
            nal_unit_type: NalUnitType::SliceNonIdr,
        };
        let parsed =
            SliceHeader::parse(&nu.rbsp, &sps, &pps, &header).expect("parse P slice header");
        assert_eq!(parsed.first_mb_in_slice, 0);
        assert_eq!(parsed.slice_type, SliceType::P);
        assert!(parsed.all_slices_same_type);
        assert_eq!(parsed.frame_num, 1);
        assert_eq!(parsed.pic_order_cnt_lsb, 2);
        assert_eq!(parsed.disable_deblocking_filter_idc, 0);
        assert_eq!(parsed.slice_alpha_c0_offset_div2, 0);
        assert_eq!(parsed.slice_beta_offset_div2, 0);
        assert!(!parsed.num_ref_idx_active_override_flag);
        assert!(parsed.ref_pic_list_modification.modifications_l0.is_empty());
        let drm = parsed.dec_ref_pic_marking.as_ref().unwrap();
        assert!(drm.adaptive_marking.is_none());
    }

    #[test]
    fn b_slice_header_round_trips_through_decoder_parser() {
        // Round-20 — non-reference B slice in a Main-profile stream
        // (B-slices are forbidden under Baseline §A.2.2).
        let sps_rbsp = build_baseline_sps_rbsp(&BaselineSpsConfig {
            bit_depth_luma_minus8: 0,
            bit_depth_chroma_minus8: 0,
            seq_scaling_lists: None,
            interlaced_fields: false,
            mbaff: false,
            vui: None,
            seq_parameter_set_id: 0,
            level_idc: 30,
            width_in_mbs: 4,
            height_in_mbs: 4,
            log2_max_frame_num_minus4: 4,
            log2_max_poc_lsb_minus4: 4,
            max_num_ref_frames: 2,
            profile_idc: 77,
            chroma_format_idc: 1,
            separate_colour_plane: false,
        });
        let sps = Sps::parse(&sps_rbsp).unwrap();
        let pps_rbsp = build_baseline_pps_rbsp(&BaselinePpsConfig::default());
        let pps = Pps::parse(&pps_rbsp).unwrap();

        let mut w = BitWriter::new();
        write_b_slice_header(
            &mut w,
            &BSliceHeaderConfig {
                first_mb_in_slice: 0,
                slice_type_raw: 6, // B, all-same-type
                pic_parameter_set_id: 0,
                colour_plane_id: None,
                frame_num: 2,
                frame_num_bits: sps.log2_max_frame_num_minus4 + 4,
                pic_order_cnt_lsb: 1,
                poc_lsb_bits: sps.log2_max_pic_order_cnt_lsb_minus4 + 4,
                direct_spatial_mv_pred_flag: true,
                slice_qp_delta: 0,
                disable_deblocking_filter_idc: 0,
                slice_alpha_c0_offset_div2: 0,
                slice_beta_offset_div2: 0,
                nal_ref_idc: 0, // non-reference B
                pred_weight_table: None,
                cabac: None,
                field: FieldPicSignal::FrameMbsOnly,
            },
        );
        // Dummy bit + trailing so the parser doesn't blow up on EOF.
        w.u(1, 1);
        w.rbsp_trailing_bits();
        let rbsp = w.into_bytes();

        let nal = build_nal_unit(0, NalUnitType::SliceNonIdr, &rbsp);
        let mut split = AnnexBSplitter::new(&nal);
        let nal_bytes = split.next().unwrap();
        let nu = parse_nal_unit(nal_bytes).unwrap();
        let header = NalHeader {
            forbidden_zero_bit: 0,
            nal_ref_idc: 0,
            nal_unit_type: NalUnitType::SliceNonIdr,
        };
        let parsed =
            SliceHeader::parse(&nu.rbsp, &sps, &pps, &header).expect("parse B slice header");
        assert_eq!(parsed.first_mb_in_slice, 0);
        assert_eq!(parsed.slice_type, SliceType::B);
        assert!(parsed.all_slices_same_type);
        assert_eq!(parsed.frame_num, 2);
        assert_eq!(parsed.pic_order_cnt_lsb, 1);
        assert!(parsed.direct_spatial_mv_pred_flag);
        assert!(!parsed.num_ref_idx_active_override_flag);
        assert!(parsed.ref_pic_list_modification.modifications_l0.is_empty());
        assert!(parsed.ref_pic_list_modification.modifications_l1.is_empty());
        // nal_ref_idc == 0 → dec_ref_pic_marking absent.
        assert!(parsed.dec_ref_pic_marking.is_none());
        assert_eq!(parsed.disable_deblocking_filter_idc, 0);
    }

    #[test]
    fn b_slice_header_with_pred_weight_table_round_trips() {
        // Round-26 — explicit weighted bipred. PPS signals
        // `weighted_bipred_idc = 1`, slice header carries one
        // pred_weight_table entry per list at log2_wd = 5.
        let sps_rbsp = build_baseline_sps_rbsp(&BaselineSpsConfig {
            bit_depth_luma_minus8: 0,
            bit_depth_chroma_minus8: 0,
            seq_scaling_lists: None,
            interlaced_fields: false,
            mbaff: false,
            vui: None,
            seq_parameter_set_id: 0,
            level_idc: 30,
            width_in_mbs: 4,
            height_in_mbs: 4,
            log2_max_frame_num_minus4: 4,
            log2_max_poc_lsb_minus4: 4,
            max_num_ref_frames: 2,
            profile_idc: 77,
            chroma_format_idc: 1,
            separate_colour_plane: false,
        });
        let sps = Sps::parse(&sps_rbsp).unwrap();
        let pps_rbsp = build_baseline_pps_rbsp(&BaselinePpsConfig {
            redundant_pic_cnt_present_flag: false,
            slice_groups: None,
            constrained_intra_pred_flag: false,
            pic_scaling_lists: None,
            chroma_format_idc: 1,
            pic_parameter_set_id: 0,
            seq_parameter_set_id: 0,
            pic_init_qp_minus26: 0,
            chroma_qp_index_offset: 0,
            weighted_pred_flag: false,
            weighted_bipred_idc: 1,
            entropy_coding_mode_flag: false,
            transform_8x8_mode_flag: false,
        });
        let pps = Pps::parse(&pps_rbsp).unwrap();
        assert_eq!(pps.weighted_bipred_idc, 1);

        let pwt = ExplicitBipredWeightTable {
            luma_log2_weight_denom: 5,
            luma_weight_l0: 45,
            luma_offset_l0: 2,
            luma_weight_l1: 19,
            luma_offset_l1: -1,
        };
        let mut w = BitWriter::new();
        write_b_slice_header(
            &mut w,
            &BSliceHeaderConfig {
                first_mb_in_slice: 0,
                slice_type_raw: 6,
                pic_parameter_set_id: 0,
                colour_plane_id: None,
                frame_num: 2,
                frame_num_bits: sps.log2_max_frame_num_minus4 + 4,
                pic_order_cnt_lsb: 1,
                poc_lsb_bits: sps.log2_max_pic_order_cnt_lsb_minus4 + 4,
                direct_spatial_mv_pred_flag: true,
                slice_qp_delta: 0,
                disable_deblocking_filter_idc: 0,
                slice_alpha_c0_offset_div2: 0,
                slice_beta_offset_div2: 0,
                nal_ref_idc: 0,
                pred_weight_table: Some(pwt),
                cabac: None,
                field: FieldPicSignal::FrameMbsOnly,
            },
        );
        // Dummy bit + trailing so the parser doesn't blow up on EOF.
        w.u(1, 1);
        w.rbsp_trailing_bits();
        let rbsp = w.into_bytes();

        let nal = build_nal_unit(0, NalUnitType::SliceNonIdr, &rbsp);
        let mut split = AnnexBSplitter::new(&nal);
        let nal_bytes = split.next().unwrap();
        let nu = parse_nal_unit(nal_bytes).unwrap();
        let header = NalHeader {
            forbidden_zero_bit: 0,
            nal_ref_idc: 0,
            nal_unit_type: NalUnitType::SliceNonIdr,
        };
        let parsed =
            SliceHeader::parse(&nu.rbsp, &sps, &pps, &header).expect("parse B slice header");

        // The decoder must surface our pred_weight_table verbatim.
        let parsed_pwt = parsed.pred_weight_table.as_ref().expect("pwt present");
        assert_eq!(parsed_pwt.luma_log2_weight_denom, 5);
        assert_eq!(parsed_pwt.chroma_log2_weight_denom, 5);
        // Single L0 entry, present.
        assert_eq!(parsed_pwt.luma_weights_l0, vec![Some((45, 2))]);
        // Chroma L0 absent (chroma_weight_l0_flag = 0).
        assert_eq!(parsed_pwt.chroma_weights_l0, vec![None]);
        // Single L1 entry, present.
        assert_eq!(parsed_pwt.luma_weights_l1, vec![Some((19, -1))]);
        assert_eq!(parsed_pwt.chroma_weights_l1, vec![None]);
    }
}