• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Eyevinn / hi264 / 22096554696

17 Feb 2026 11:23AM UTC coverage: 87.085% (+0.2%) from 86.855%
22096554696

push

github

tobbee
feat: adaptive intra prediction mode selection for I_16x16 encoder

84 of 84 new or added lines in 1 file covered. (100.0%)

117 existing lines in 4 files now uncovered.

6244 of 7170 relevant lines covered (87.09%)

1.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.1
/pkg/decoder/decoder.go
1
// Package decoder implements the top-level H.264/AVC decoder orchestration.
2
package decoder
3

4
import (
5
        "encoding/binary"
6
        "fmt"
7

8
        "github.com/Eyevinn/mp4ff/avc"
9

10
        "github.com/Eyevinn/hi264/internal/cabac"
11
        "github.com/Eyevinn/hi264/internal/cavlc"
12
        "github.com/Eyevinn/hi264/internal/context"
13
        "github.com/Eyevinn/hi264/internal/pred"
14
        "github.com/Eyevinn/hi264/internal/slice"
15
        "github.com/Eyevinn/hi264/internal/transform"
16
        "github.com/Eyevinn/hi264/pkg/frame"
17
)
18

19
// Decoder is the H.264/AVC decoder.
20
type Decoder struct {
21
        spsMap      map[uint32]*avc.SPS
22
        ppsMap      map[uint32]*avc.PPS
23
        refFrame    *frame.Frame // most recently decoded reference frame (for P-slice)
24
        TraceMBCMP  bool         // emit MBCMP lines for FFmpeg comparison
25
        SkipDeblock bool         // skip deblocking filter for debugging
26
}
27

28
// ScalingMatrices holds effective scaling lists in raster order for dequantization.
29
// All lists are in raster scan order (row*width + col), converted from the
30
// zigzag scan order used in the bitstream.
31
type ScalingMatrices struct {
32
        IntraY4x4  [16]int32 // Intra luma 4x4 scaling list (SPS/PPS list 0)
33
        IntraCb4x4 [16]int32 // Intra chroma Cb 4x4 scaling list (SPS/PPS list 1)
34
        IntraCr4x4 [16]int32 // Intra chroma Cr 4x4 scaling list (SPS/PPS list 2)
35
        IntraY8x8  [64]int32 // Intra luma 8x8 scaling list (SPS/PPS list 6)
36
}
37

38
// Default 4x4 intra scaling list (Table 7-3) used when seq_scaling_list_present_flag[i]=0
39
// and the list would fall back to the default. Values are in zigzag scan order.
40
var defaultScalingList4x4Intra = [16]int{
41
        6, 13, 13, 20, 20, 20, 28, 28, 28, 28, 32, 32, 32, 37, 37, 42,
42
}
43

44
// Default 8x8 intra scaling list (Table 7-4) in raster scan order.
45
// Note: unlike the 4x4 default which is in zigzag scan order, the spec
46
// presents the 8x8 default as an 8x8 matrix (raster order).
47
var defaultScalingList8x8Intra = [64]int{
48
        6, 10, 13, 16, 18, 23, 25, 27,
49
        10, 11, 16, 18, 23, 25, 27, 29,
50
        13, 16, 18, 23, 25, 27, 29, 31,
51
        16, 18, 23, 25, 27, 29, 31, 33,
52
        18, 23, 25, 27, 29, 31, 33, 36,
53
        23, 25, 27, 29, 31, 33, 36, 38,
54
        25, 27, 29, 31, 33, 36, 38, 40,
55
        27, 29, 31, 33, 36, 38, 40, 42,
56
}
57

58
// buildScalingMatrices derives the effective scaling lists from SPS and PPS.
59
// Implements the fall-back rules from Table 7-2 of the H.264 spec:
60
//   - When seq/pic_scaling_list_present_flag[i]=0 (mp4ff returns nil),
61
//     fall back to the default table or copy from a previous list.
62
//   - Lists in the bitstream are in zigzag scan order; this function
63
//     converts them to raster scan order for use in dequantization.
64
func buildScalingMatrices(sps *avc.SPS, pps *avc.PPS) ScalingMatrices {
1✔
65
        sm := ScalingMatrices{}
1✔
66

1✔
67
        // Start with flat defaults (all 16s)
1✔
68
        for i := range sm.IntraY4x4 {
2✔
69
                sm.IntraY4x4[i] = 16
1✔
70
        }
1✔
71
        sm.IntraCb4x4 = sm.IntraY4x4
1✔
72
        sm.IntraCr4x4 = sm.IntraY4x4
1✔
73
        for i := range sm.IntraY8x8 {
2✔
74
                sm.IntraY8x8[i] = 16
1✔
75
        }
1✔
76

77
        // Apply SPS scaling lists with Table 7-2 fall-back
78
        if sps.SeqScalingMatrixPresentFlag {
1✔
79
                // List 0 (Intra Y 4x4): fall-back = Default_4x4_Intra
×
80
                if !applyScalingList4x4(&sm.IntraY4x4, sps.SeqScalingLists, 0) {
×
81
                        applyDefault4x4Intra(&sm.IntraY4x4)
×
82
                }
×
83
                // List 1 (Intra Cb 4x4): fall-back = ScalingList4x4[0]
84
                if !applyScalingList4x4(&sm.IntraCb4x4, sps.SeqScalingLists, 1) {
×
85
                        sm.IntraCb4x4 = sm.IntraY4x4
×
86
                }
×
87
                // List 2 (Intra Cr 4x4): fall-back = ScalingList4x4[1]
88
                if !applyScalingList4x4(&sm.IntraCr4x4, sps.SeqScalingLists, 2) {
×
89
                        sm.IntraCr4x4 = sm.IntraCb4x4
×
90
                }
×
91
                // List 6 (Intra Y 8x8): fall-back = Default_8x8_Intra
92
                if !applyScalingList8x8(&sm.IntraY8x8, sps.SeqScalingLists, 6) {
×
93
                        applyDefault8x8Intra(&sm.IntraY8x8)
×
94
                }
×
95
        }
96

97
        // PPS scaling lists override SPS with Table 7-2 fall-back
98
        if pps.PicScalingMatrixPresentFlag {
2✔
99
                // When pic_scaling_list_present_flag[i]=0:
1✔
100
                //   if seq_scaling_matrix_present_flag: keep SPS value (already in place)
1✔
101
                //   else: use default table / copy from previous list
1✔
102
                if sps.SeqScalingMatrixPresentFlag {
1✔
103
                        // SPS values already in sm — only override with explicit PPS lists
×
104
                        applyScalingList4x4(&sm.IntraY4x4, pps.PicScalingLists, 0)
×
105
                        applyScalingList4x4(&sm.IntraCb4x4, pps.PicScalingLists, 1)
×
106
                        applyScalingList4x4(&sm.IntraCr4x4, pps.PicScalingLists, 2)
×
107
                        applyScalingList8x8(&sm.IntraY8x8, pps.PicScalingLists, 6)
×
108
                } else {
1✔
109
                        // No SPS scaling — same fall-back as SPS
1✔
110
                        if !applyScalingList4x4(&sm.IntraY4x4, pps.PicScalingLists, 0) {
2✔
111
                                applyDefault4x4Intra(&sm.IntraY4x4)
1✔
112
                        }
1✔
113
                        if !applyScalingList4x4(&sm.IntraCb4x4, pps.PicScalingLists, 1) {
2✔
114
                                sm.IntraCb4x4 = sm.IntraY4x4
1✔
115
                        }
1✔
116
                        if !applyScalingList4x4(&sm.IntraCr4x4, pps.PicScalingLists, 2) {
2✔
117
                                sm.IntraCr4x4 = sm.IntraCb4x4
1✔
118
                        }
1✔
119
                        if !applyScalingList8x8(&sm.IntraY8x8, pps.PicScalingLists, 6) {
2✔
120
                                applyDefault8x8Intra(&sm.IntraY8x8)
1✔
121
                        }
1✔
122
                }
123
        }
124

125
        return sm
1✔
126
}
127

128
// applyScalingList4x4 converts a 4x4 scaling list from zigzag to raster order.
129
// Returns true if the list was present and applied, false if nil/missing.
130
func applyScalingList4x4(dst *[16]int32, lists []avc.ScalingList, idx int) bool {
1✔
131
        if idx >= len(lists) || lists[idx] == nil || len(lists[idx]) != 16 {
2✔
132
                return false
1✔
133
        }
1✔
134
        sl := lists[idx]
×
135
        for k := 0; k < 16; k++ {
×
136
                dst[zigzag4x4[k]] = int32(sl[k])
×
137
        }
×
138
        return true
×
139
}
140

141
// applyScalingList8x8 converts an 8x8 scaling list from zigzag to raster order.
142
// Returns true if the list was present and applied, false if nil/missing.
143
func applyScalingList8x8(dst *[64]int32, lists []avc.ScalingList, idx int) bool {
1✔
144
        if idx >= len(lists) || lists[idx] == nil || len(lists[idx]) != 64 {
2✔
145
                return false
1✔
146
        }
1✔
147
        sl := lists[idx]
×
148
        for k := 0; k < 64; k++ {
×
149
                dst[zigzag8x8[k]] = int32(sl[k])
×
150
        }
×
151
        return true
×
152
}
153

154
// applyDefault4x4Intra sets dst to the Default_4x4_Intra scaling list (Table 7-3),
155
// converted from zigzag to raster order.
156
func applyDefault4x4Intra(dst *[16]int32) {
1✔
157
        for k := 0; k < 16; k++ {
2✔
158
                dst[zigzag4x4[k]] = int32(defaultScalingList4x4Intra[k])
1✔
159
        }
1✔
160
}
161

162
// applyDefault8x8Intra sets dst to the Default_8x8_Intra scaling list (Table 7-4).
163
// The 8x8 default is already in raster order (unlike the 4x4 default which is in zigzag).
164
func applyDefault8x8Intra(dst *[64]int32) {
1✔
165
        for k := 0; k < 64; k++ {
2✔
166
                dst[k] = int32(defaultScalingList8x8Intra[k])
1✔
167
        }
1✔
168
}
169

170
// New creates a new H.264 decoder.
171
func New() *Decoder {
1✔
172
        return &Decoder{
1✔
173
                spsMap: make(map[uint32]*avc.SPS),
1✔
174
                ppsMap: make(map[uint32]*avc.PPS),
1✔
175
        }
1✔
176
}
1✔
177

178
// DecodeNALUs decodes a complete access unit (set of NALUs) and returns the reconstructed frame.
179
// For multi-frame streams, use DecodeAllFrames instead.
180
func (d *Decoder) DecodeNALUs(nalus [][]byte) (*frame.Frame, error) {
1✔
181
        for _, nalu := range nalus {
2✔
182
                if len(nalu) == 0 {
1✔
183
                        continue
×
184
                }
185
                naluType := avc.NaluType(nalu[0] & 0x1f)
1✔
186

1✔
187
                switch naluType {
1✔
188
                case avc.NALU_SPS:
1✔
189
                        sps, err := avc.ParseSPSNALUnit(nalu, true)
1✔
190
                        if err != nil {
1✔
191
                                return nil, fmt.Errorf("parse SPS: %w", err)
×
192
                        }
×
193
                        d.spsMap[sps.ParameterID] = sps
1✔
194
                case avc.NALU_PPS:
1✔
195
                        pps, err := avc.ParsePPSNALUnit(nalu, d.spsMap)
1✔
196
                        if err != nil {
1✔
197
                                return nil, fmt.Errorf("parse PPS: %w", err)
×
198
                        }
×
199
                        d.ppsMap[pps.PicParameterSetID] = pps
1✔
200
                case avc.NALU_IDR:
1✔
201
                        f, err := d.decodeIDR(nalu)
1✔
202
                        if err != nil {
1✔
203
                                return nil, err
×
204
                        }
×
205
                        d.refFrame = f
1✔
206
                        return f, nil
1✔
207
                }
208
        }
209
        return nil, fmt.Errorf("no IDR NALU found")
×
210
}
211

212
// DecodeAllFrames decodes all frames from a set of NALUs (IDR and non-IDR).
213
// Non-IDR slices are decoded as P_Skip (copy from reference); an error is
214
// returned if a non-IDR slice contains non-skip macroblocks.
215
// Returns frames in decode order.
216
func (d *Decoder) DecodeAllFrames(nalus [][]byte) ([]*frame.Frame, error) {
1✔
217
        return d.decodeFrames(nalus, true)
1✔
218
}
1✔
219

220
// DecodeIDRFrames decodes only IDR frames from a set of NALUs, skipping
221
// all non-IDR slices. Returns frames in decode order.
UNCOV
222
func (d *Decoder) DecodeIDRFrames(nalus [][]byte) ([]*frame.Frame, error) {
×
UNCOV
223
        return d.decodeFrames(nalus, false)
×
UNCOV
224
}
×
225

226
func (d *Decoder) decodeFrames(nalus [][]byte, includeNonIDR bool) ([]*frame.Frame, error) {
1✔
227
        var frames []*frame.Frame
1✔
228

1✔
229
        for _, nalu := range nalus {
2✔
230
                if len(nalu) == 0 {
1✔
UNCOV
231
                        continue
×
232
                }
233
                naluType := avc.NaluType(nalu[0] & 0x1f)
1✔
234

1✔
235
                switch naluType {
1✔
236
                case avc.NALU_SPS:
1✔
237
                        sps, err := avc.ParseSPSNALUnit(nalu, true)
1✔
238
                        if err != nil {
1✔
239
                                return frames, fmt.Errorf("parse SPS: %w", err)
×
240
                        }
×
241
                        d.spsMap[sps.ParameterID] = sps
1✔
242
                case avc.NALU_PPS:
1✔
243
                        pps, err := avc.ParsePPSNALUnit(nalu, d.spsMap)
1✔
244
                        if err != nil {
1✔
UNCOV
245
                                return frames, fmt.Errorf("parse PPS: %w", err)
×
246
                        }
×
247
                        d.ppsMap[pps.PicParameterSetID] = pps
1✔
248
                case avc.NALU_IDR:
1✔
249
                        f, err := d.decodeIDR(nalu)
1✔
250
                        if err != nil {
1✔
UNCOV
251
                                return frames, fmt.Errorf("IDR frame %d: %w", len(frames), err)
×
UNCOV
252
                        }
×
253
                        d.refFrame = f
1✔
254
                        frames = append(frames, f)
1✔
255
                case 1: // non-IDR coded slice
1✔
256
                        if !includeNonIDR {
1✔
UNCOV
257
                                continue
×
258
                        }
259
                        f, err := d.decodePSkip(nalu)
1✔
260
                        if err != nil {
2✔
261
                                return frames, fmt.Errorf("p frame %d: %w", len(frames), err)
1✔
262
                        }
1✔
263
                        d.refFrame = f
1✔
264
                        frames = append(frames, f)
1✔
265
                }
266
        }
267
        return frames, nil
1✔
268
}
269

270
// DecodeAnnexB decodes the first IDR frame from an Annex-B byte stream.
271
func (d *Decoder) DecodeAnnexB(data []byte) (*frame.Frame, error) {
1✔
272
        nalus := avc.ExtractNalusFromByteStream(data)
1✔
273
        return d.DecodeNALUs(nalus)
1✔
274
}
1✔
275

276
// DecodeAllAnnexB decodes all frames (IDR + P_Skip) from an Annex-B byte stream.
277
func (d *Decoder) DecodeAllAnnexB(data []byte) ([]*frame.Frame, error) {
1✔
278
        nalus := avc.ExtractNalusFromByteStream(data)
1✔
279
        return d.DecodeAllFrames(nalus)
1✔
280
}
1✔
281

282
// DecodeIDRAnnexB decodes only IDR frames from an Annex-B byte stream.
283
func (d *Decoder) DecodeIDRAnnexB(data []byte) ([]*frame.Frame, error) {
×
UNCOV
284
        nalus := avc.ExtractNalusFromByteStream(data)
×
UNCOV
285
        return d.DecodeIDRFrames(nalus)
×
UNCOV
286
}
×
287

288
// DecodeAVC decodes the first IDR frame from AVC-format data
289
// (each NALU preceded by a 4-byte big-endian length field).
290
func (d *Decoder) DecodeAVC(data []byte) (*frame.Frame, error) {
1✔
291
        nalus, err := extractAVCNalus(data)
1✔
292
        if err != nil {
1✔
UNCOV
293
                return nil, err
×
UNCOV
294
        }
×
295
        return d.DecodeNALUs(nalus)
1✔
296
}
297

298
// DecodeAllAVC decodes all frames from AVC-format data
299
// (each NALU preceded by a 4-byte big-endian length field).
300
func (d *Decoder) DecodeAllAVC(data []byte) ([]*frame.Frame, error) {
1✔
301
        nalus, err := extractAVCNalus(data)
1✔
302
        if err != nil {
1✔
UNCOV
303
                return nil, err
×
UNCOV
304
        }
×
305
        return d.DecodeAllFrames(nalus)
1✔
306
}
307

308
// DecodeIDRAVC decodes only IDR frames from AVC-format data
309
// (each NALU preceded by a 4-byte big-endian length field).
UNCOV
310
func (d *Decoder) DecodeIDRAVC(data []byte) ([]*frame.Frame, error) {
×
UNCOV
311
        nalus, err := extractAVCNalus(data)
×
UNCOV
312
        if err != nil {
×
UNCOV
313
                return nil, err
×
UNCOV
314
        }
×
315
        return d.DecodeIDRFrames(nalus)
×
316
}
317

318
// extractAVCNalus extracts NALUs from AVC-format data where each NALU
319
// is preceded by a 4-byte big-endian length field.
320
func extractAVCNalus(data []byte) ([][]byte, error) {
1✔
321
        var nalus [][]byte
1✔
322
        for len(data) >= 4 {
2✔
323
                length := int(binary.BigEndian.Uint32(data[:4]))
1✔
324
                data = data[4:]
1✔
325
                if length < 0 || length > len(data) {
1✔
UNCOV
326
                        return nil, fmt.Errorf("AVC NALU length %d exceeds remaining data %d", length, len(data))
×
UNCOV
327
                }
×
328
                nalus = append(nalus, data[:length])
1✔
329
                data = data[length:]
1✔
330
        }
331
        return nalus, nil
1✔
332
}
333

334
// cloneFrame creates a deep copy of a frame.
335
func cloneFrame(src *frame.Frame) *frame.Frame {
1✔
336
        dst := frame.NewFrame(src.Width, src.Height)
1✔
337
        copy(dst.Y, src.Y)
1✔
338
        copy(dst.Cb, src.Cb)
1✔
339
        copy(dst.Cr, src.Cr)
1✔
340
        return dst
1✔
341
}
1✔
342

343
// decodePSkip decodes a P-slice where all MBs are P_Skip (copy from reference).
344
func (d *Decoder) decodePSkip(nalu []byte) (*frame.Frame, error) {
1✔
345
        if d.refFrame == nil {
1✔
UNCOV
346
                return nil, fmt.Errorf("P_Skip: no reference frame available")
×
347
        }
×
348

349
        // Parse slice header to validate, but for P_Skip we just copy the reference
350
        sh, err := avc.ParseSliceHeader(nalu, d.spsMap, d.ppsMap)
1✔
351
        if err != nil {
1✔
UNCOV
352
                return nil, fmt.Errorf("parse P-slice header: %w", err)
×
353
        }
×
354

355
        pps := d.ppsMap[sh.PicParamID]
1✔
356
        sps := d.spsMap[pps.SeqParameterSetID]
1✔
357

1✔
358
        width := int(sps.Width)
1✔
359
        height := int(sps.Height)
1✔
360

1✔
361
        // For P_Skip, verify all MBs are skip by reading mb_skip_run via CAVLC
1✔
362
        if !pps.EntropyCodingModeFlag {
2✔
363
                // CAVLC path: skip header at bit level, read mb_skip_run
1✔
364
                fullData := removeEBSPPrevention(nalu)
1✔
365
                br := cavlc.NewBitReader(fullData)
1✔
366
                nalRefIdc := (nalu[0] >> 5) & 0x3
1✔
367

1✔
368
                err = br.SkipSliceHeaderP(cavlc.SliceHeaderParams{
1✔
369
                        FrameMbsOnly:                          sps.FrameMbsOnlyFlag,
1✔
370
                        Log2MaxFrameNumMinus4:                 uint(sps.Log2MaxFrameNumMinus4),
1✔
371
                        PicOrderCntType:                       uint(sps.PicOrderCntType),
1✔
372
                        Log2MaxPicOrderCntLsbMinus4:           uint(sps.Log2MaxPicOrderCntLsbMinus4),
1✔
373
                        BottomFieldPicOrderInFramePresentFlag: pps.BottomFieldPicOrderInFramePresentFlag,
1✔
374
                        DeblockingFilterControlPresent:        pps.DeblockingFilterControlPresentFlag,
1✔
375
                        RedundantPicCntPresentFlag:            pps.RedundantPicCntPresentFlag,
1✔
376
                }, nalRefIdc)
1✔
377
                if err != nil {
1✔
378
                        return nil, fmt.Errorf("skip P-slice header: %w", err)
×
UNCOV
379
                }
×
380

381
                // Read mb_skip_run
382
                mbSkipRun, err := br.ReadUE()
1✔
383
                if err != nil {
1✔
UNCOV
384
                        return nil, fmt.Errorf("read mb_skip_run: %w", err)
×
385
                }
×
386

387
                totalMBs := ((width + 15) / 16) * ((height + 15) / 16)
1✔
388
                if int(mbSkipRun) != totalMBs {
1✔
UNCOV
389
                        return nil, fmt.Errorf("P_Skip: mb_skip_run=%d, expected %d (non-skip P-frames not yet supported)",
×
UNCOV
390
                                mbSkipRun, totalMBs)
×
UNCOV
391
                }
×
392
        } else {
1✔
393
                // CABAC path: use parsed header to find CABAC data start
1✔
394
                sliceData := removeEBSPPrevention(nalu[sh.Size:])
1✔
395
                sliceQPY := 26 + int(pps.PicInitQpMinus26) + int(sh.SliceQPDelta)
1✔
396
                dec, err2 := cabac.NewDecoder(sliceData)
1✔
397
                if err2 != nil {
1✔
UNCOV
398
                        return nil, fmt.Errorf("CABAC P_Skip: init decoder: %w", err2)
×
UNCOV
399
                }
×
400
                models := context.InitModels(sliceQPY, 0, int(sh.CabacInitIDC))
1✔
401
                ctx := models[:]
1✔
402

1✔
403
                totalMBs := ((width + 15) / 16) * ((height + 15) / 16)
1✔
404
                for mbIdx := range totalMBs {
2✔
405
                        mbSkipFlag := dec.DecodeDecision(&ctx[11]) // ctxIdxInc=0 always for all-skip
1✔
406
                        if mbSkipFlag != 1 {
2✔
407
                                return nil, fmt.Errorf("CABAC P-frame: non-skip MB %d not supported (mb_skip_flag=%d)",
1✔
408
                                        mbIdx, mbSkipFlag)
1✔
409
                        }
1✔
410
                        isLast := mbIdx == totalMBs-1
1✔
411
                        term := dec.DecodeTerminate()
1✔
412
                        if isLast && term != 1 {
1✔
UNCOV
413
                                return nil, fmt.Errorf("CABAC P_Skip: expected end_of_slice at last MB %d", mbIdx)
×
UNCOV
414
                        }
×
415
                        if !isLast && term != 0 {
1✔
UNCOV
416
                                return nil, fmt.Errorf("CABAC P_Skip: unexpected end_of_slice at MB %d", mbIdx)
×
UNCOV
417
                        }
×
418
                }
419
        }
420

421
        // P_Skip: all macroblocks copy from reference with zero motion vector
422
        // No deblocking needed (bS=0 for all-skip)
423
        return cloneFrame(d.refFrame), nil
1✔
424
}
425

426
// decodeIDR decodes an IDR frame.
427
func (d *Decoder) decodeIDR(nalu []byte) (*frame.Frame, error) {
1✔
428
        // Parse slice header
1✔
429
        sh, err := avc.ParseSliceHeader(nalu, d.spsMap, d.ppsMap)
1✔
430
        if err != nil {
1✔
UNCOV
431
                return nil, fmt.Errorf("parse slice header: %w", err)
×
UNCOV
432
        }
×
433

434
        pps := d.ppsMap[sh.PicParamID]
1✔
435
        sps := d.spsMap[pps.SeqParameterSetID]
1✔
436

1✔
437
        // Calculate dimensions
1✔
438
        width := int(sps.Width)
1✔
439
        height := int(sps.Height)
1✔
440
        mbWidth := (width + 15) / 16
1✔
441
        mbHeight := (height + 15) / 16
1✔
442

1✔
443
        // Calculate slice QP
1✔
444
        sliceQPY := 26 + int(pps.PicInitQpMinus26) + int(sh.SliceQPDelta)
1✔
445

1✔
446
        // Determine chroma array type
1✔
447
        chromaArrayType := 1 // 4:2:0 default
1✔
448
        if sps.ChromaFormatIDC == 0 {
1✔
UNCOV
449
                chromaArrayType = 0 // monochrome
×
450
        }
×
451

452
        bitDepthY := 8 + int(sps.BitDepthLumaMinus8)
1✔
453
        bitDepthC := 8 + int(sps.BitDepthChromaMinus8)
1✔
454

1✔
455
        var sc *slice.SliceContext
1✔
456
        if pps.EntropyCodingModeFlag {
2✔
457
                // CABAC path
1✔
458
                sliceData := removeEBSPPrevention(nalu[sh.Size:])
1✔
459
                var err2 error
1✔
460
                sc, err2 = slice.DecodeSliceData(sliceData, sliceQPY, mbWidth, mbHeight,
1✔
461
                        pps.Transform8x8ModeFlag, chromaArrayType, bitDepthY, bitDepthC,
1✔
462
                        int(pps.ChromaQpIndexOffset), d.TraceMBCMP)
1✔
463
                if err2 != nil {
1✔
UNCOV
464
                        return nil, fmt.Errorf("decode slice data (CABAC): %w", err2)
×
UNCOV
465
                }
×
466
        } else {
1✔
467
                // CAVLC path: operate on full EBSP-decoded NALU, skip header at bit level
1✔
468
                fullData := removeEBSPPrevention(nalu)
1✔
469
                br := cavlc.NewBitReader(fullData)
1✔
470

1✔
471
                err = br.SkipSliceHeaderIDR(cavlc.SliceHeaderParams{
1✔
472
                        FrameMbsOnly:                          sps.FrameMbsOnlyFlag,
1✔
473
                        Log2MaxFrameNumMinus4:                 uint(sps.Log2MaxFrameNumMinus4),
1✔
474
                        PicOrderCntType:                       uint(sps.PicOrderCntType),
1✔
475
                        Log2MaxPicOrderCntLsbMinus4:           uint(sps.Log2MaxPicOrderCntLsbMinus4),
1✔
476
                        BottomFieldPicOrderInFramePresentFlag: pps.BottomFieldPicOrderInFramePresentFlag,
1✔
477
                        DeblockingFilterControlPresent:        pps.DeblockingFilterControlPresentFlag,
1✔
478
                        RedundantPicCntPresentFlag:            pps.RedundantPicCntPresentFlag,
1✔
479
                })
1✔
480
                if err != nil {
1✔
UNCOV
481
                        return nil, fmt.Errorf("skip slice header (CAVLC): %w", err)
×
UNCOV
482
                }
×
483

484
                var err2 error
1✔
485
                sc, err2 = slice.DecodeSliceDataCAVLC(br, sliceQPY, mbWidth, mbHeight,
1✔
486
                        pps.Transform8x8ModeFlag, chromaArrayType, bitDepthY, bitDepthC,
1✔
487
                        int(pps.ChromaQpIndexOffset), d.TraceMBCMP)
1✔
488
                if err2 != nil {
1✔
UNCOV
489
                        return nil, fmt.Errorf("decode slice data (CAVLC): %w", err2)
×
UNCOV
490
                }
×
491
        }
492

493
        // Reconstruct frame
494
        f := frame.NewFrame(width, height)
1✔
495

1✔
496
        // Extract color space metadata from VUI if present
1✔
497
        if sps.VUI != nil {
2✔
498
                if sps.VUI.ColourDescriptionFlag {
2✔
499
                        f.ColorDescriptionValid = true
1✔
500
                        f.MatrixCoefficients = sps.VUI.MatrixCoefficients
1✔
501
                }
1✔
502
                f.VideoFullRangeFlag = sps.VUI.VideoFullRangeFlag
1✔
503
        }
504

505
        err = reconstructFrame(sc, f, sps, pps)
1✔
506
        if err != nil {
1✔
UNCOV
507
                return nil, fmt.Errorf("reconstruct frame: %w", err)
×
UNCOV
508
        }
×
509

510
        // Apply deblocking filter
511
        if sh.DisableDeblockingFilterIDC != 1 && !d.SkipDeblock {
2✔
512
                frame.Deblock(f, sc,
1✔
513
                        int(sh.SliceAlphaC0OffsetDiv2)*2,
1✔
514
                        int(sh.SliceBetaOffsetDiv2)*2)
1✔
515
        }
1✔
516

517
        return f, nil
1✔
518
}
519

520
// removeEBSPPrevention removes emulation prevention bytes (0x03 after 0x00 0x00).
521
func removeEBSPPrevention(data []byte) []byte {
1✔
522
        result := make([]byte, 0, len(data))
1✔
523
        i := 0
1✔
524
        for i < len(data) {
2✔
525
                if i+2 < len(data) && data[i] == 0 && data[i+1] == 0 && data[i+2] == 3 {
2✔
526
                        result = append(result, 0, 0)
1✔
527
                        i += 3 // skip the 0x03
1✔
528
                } else {
2✔
529
                        result = append(result, data[i])
1✔
530
                        i++
1✔
531
                }
1✔
532
        }
533
        return result
1✔
534
}
535

536
// reconstructFrame reconstructs the decoded picture from the parsed macroblock data.
537
func reconstructFrame(sc *slice.SliceContext, f *frame.Frame, sps *avc.SPS, pps *avc.PPS) error {
1✔
538
        sm := buildScalingMatrices(sps, pps)
1✔
539

1✔
540
        for mbIdx := 0; mbIdx < sc.TotalMBs; mbIdx++ {
2✔
541
                mbX := mbIdx % sc.MBWidth
1✔
542
                mbY := mbIdx / sc.MBWidth
1✔
543
                mb := &sc.MBs[mbIdx]
1✔
544

1✔
545
                if mb.MBType >= 1 && mb.MBType <= 24 {
2✔
546
                        // I_16x16
1✔
547
                        err := reconstructI16x16(sc, f, mbIdx, mbX, mbY, mb, &sm)
1✔
548
                        if err != nil {
1✔
UNCOV
549
                                return fmt.Errorf("reconstruct I_16x16 mb %d: %w", mbIdx, err)
×
UNCOV
550
                        }
×
551
                } else if mb.MBType == slice.MBTypeINxN {
2✔
552
                        if mb.TransformSize8x8 {
2✔
553
                                err := reconstructI8x8(sc, f, mbIdx, mbX, mbY, mb, &sm)
1✔
554
                                if err != nil {
1✔
UNCOV
555
                                        return fmt.Errorf("reconstruct I_8x8 mb %d: %w", mbIdx, err)
×
UNCOV
556
                                }
×
557
                        } else {
1✔
558
                                err := reconstructI4x4(sc, f, mbIdx, mbX, mbY, mb, &sm)
1✔
559
                                if err != nil {
1✔
UNCOV
560
                                        return fmt.Errorf("reconstruct I_4x4 mb %d: %w", mbIdx, err)
×
UNCOV
561
                                }
×
562
                        }
563
                }
564

565
                // Reconstruct chroma
566
                if sc.ChromaArrayType != 0 {
2✔
567
                        reconstructChroma(sc, f, mbIdx, mbX, mbY, mb, &sm)
1✔
568
                }
1✔
569
        }
570

571
        return nil
1✔
572
}
573

574
// reconstructI16x16 reconstructs an I_16x16 macroblock.
575
func reconstructI16x16(sc *slice.SliceContext, f *frame.Frame,
576
        mbIdx, mbX, mbY int, mb *slice.MBData, sm *ScalingMatrices) error {
1✔
577
        // 1. Get prediction block
1✔
578
        top, left, topLeft, hasTop, hasLeft := getLuma16x16Neighbors(f, mbX, mbY)
1✔
579
        var topSlice, leftSlice []uint8
1✔
580
        if hasTop {
2✔
581
                topSlice = top[:]
1✔
582
        }
1✔
583
        if hasLeft {
2✔
584
                leftSlice = left[:]
1✔
585
        }
1✔
586
        predBlock := pred.Predict16x16(mb.IntraPredMode16x16, topSlice, leftSlice, topLeft)
1✔
587

1✔
588
        // 2. Inverse transform DC coefficients
1✔
589
        // DC coefficients from CABAC are in zigzag scan order; Hadamard expects raster (row-major)
1✔
590
        var dcMatrix [16]int32
1✔
591
        for k := 0; k < 16; k++ {
2✔
592
                dcMatrix[zigzag4x4[k]] = mb.Intra16x16DCLevel[k]
1✔
593
        }
1✔
594
        dcTransformed := transform.InverseHadamard4x4(dcMatrix)
1✔
595
        dcScaledRaster := transform.DequantDC4x4(dcTransformed, mb.QPY, sm.IntraY4x4[0])
1✔
596
        // Remap scaled DC from raster to z-scan order for block assignment
1✔
597
        var dcScaled [16]int32
1✔
598
        for i := 0; i < 16; i++ {
2✔
599
                dcScaled[i] = dcScaledRaster[zScanToRaster[i]]
1✔
600
        }
1✔
601

602
        // 3. For each 4x4 block, apply inverse transform and add prediction
603
        sl := &sm.IntraY4x4
1✔
604
        var lumaBlock [16][16]uint8
1✔
605
        for i := 0; i < 16; i++ {
2✔
606
                // Map 4x4 block index to position
1✔
607
                bx := inverseRasterX4x4[i]
1✔
608
                by := inverseRasterY4x4[i]
1✔
609

1✔
610
                // Build coefficient block: DC from Hadamard, AC from residual
1✔
611
                // AC coefficients need zigzag scan conversion from CABAC order to matrix order
1✔
612
                var block4x4 [16]int32
1✔
613
                block4x4[0] = dcScaled[i]
1✔
614
                for j := 0; j < 15; j++ {
2✔
615
                        block4x4[zigzag4x4AC[j]] = mb.Intra16x16ACLevel[i][j]
1✔
616
                }
1✔
617

618
                // Dequantize AC coefficients (DC already dequantized)
619
                dequantBlock := transform.Dequant4x4(block4x4, mb.QPY, sl)
1✔
620
                // Restore the already-scaled DC
1✔
621
                dequantBlock[0] = dcScaled[i]
1✔
622

1✔
623
                // Inverse transform
1✔
624
                residual := transform.InverseTransform4x4(dequantBlock)
1✔
625

1✔
626
                // Add prediction + residual, clip to [0,255]
1✔
627
                for y := 0; y < 4; y++ {
2✔
628
                        for x := 0; x < 4; x++ {
2✔
629
                                val := int32(predBlock[by+y][bx+x]) + residual[y*4+x]
1✔
630
                                lumaBlock[by+y][bx+x] = clip8(val)
1✔
631
                        }
1✔
632
                }
633
        }
634

635
        f.SetLuma16x16(mbX, mbY, lumaBlock)
1✔
636
        return nil
1✔
637
}
638

639
// reconstructI4x4 reconstructs an I_4x4 macroblock.
640
func reconstructI4x4(sc *slice.SliceContext, f *frame.Frame,
641
        mbIdx, mbX, mbY int, mb *slice.MBData, sm *ScalingMatrices) error {
1✔
642
        x0 := mbX * 16
1✔
643
        y0 := mbY * 16
1✔
644
        sl := &sm.IntraY4x4
1✔
645

1✔
646
        for i := 0; i < 16; i++ {
2✔
647
                bx := inverseRasterX4x4[i]
1✔
648
                by := inverseRasterY4x4[i]
1✔
649

1✔
650
                // Get reference samples for this 4x4 block
1✔
651
                ref := getLuma4x4Neighbors(f, x0+bx, y0+by, i, mbX, mbY, sc.MBWidth, sc.MBHeight)
1✔
652

1✔
653
                // Predict
1✔
654
                leftAvail := x0+bx > 0
1✔
655
                topAvail := y0+by > 0
1✔
656
                predBlock := pred.Predict4x4(mb.Intra4x4PredMode[i], ref, leftAvail, topAvail)
1✔
657

1✔
658
                // Apply zigzag scan to convert from CABAC scan order to matrix order
1✔
659
                var coeffs [16]int32
1✔
660
                for j := 0; j < 16; j++ {
2✔
661
                        coeffs[zigzag4x4[j]] = mb.LumaLevel4x4[i][j]
1✔
662
                }
1✔
663
                // Dequantize + inverse transform
664
                dequant := transform.Dequant4x4(coeffs, mb.QPY, sl)
1✔
665
                residual := transform.InverseTransform4x4(dequant)
1✔
666

1✔
667
                // Reconstruct
1✔
668
                for y := 0; y < 4; y++ {
2✔
669
                        for x := 0; x < 4; x++ {
2✔
670
                                val := int32(predBlock[y][x]) + residual[y*4+x]
1✔
671
                                f.SetLumaPixel(x0+bx+x, y0+by+y, clip8(val))
1✔
672
                        }
1✔
673
                }
674
        }
675

676
        return nil
1✔
677
}
678

679
// reconstructI8x8 reconstructs an I_8x8 macroblock.
680
func reconstructI8x8(sc *slice.SliceContext, f *frame.Frame,
681
        mbIdx, mbX, mbY int, mb *slice.MBData, sm *ScalingMatrices) error {
1✔
682
        x0 := mbX * 16
1✔
683
        y0 := mbY * 16
1✔
684
        frameW := sc.MBWidth * 16
1✔
685
        frameH := sc.MBHeight * 16
1✔
686
        sl := &sm.IntraY8x8
1✔
687

1✔
688
        for i := 0; i < 4; i++ {
2✔
689
                bx := (i % 2) * 8
1✔
690
                by := (i / 2) * 8
1✔
691

1✔
692
                // 1. Get filtered reference samples
1✔
693
                ref := getLuma8x8Neighbors(f, x0+bx, y0+by, i, frameW, frameH)
1✔
694

1✔
695
                // 2. Predict
1✔
696
                leftAvail := x0+bx > 0
1✔
697
                topAvail := y0+by > 0
1✔
698
                predBlock := pred.Predict8x8(mb.Intra8x8PredMode[i], ref, leftAvail, topAvail)
1✔
699

1✔
700
                // 3. Scan order conversion → matrix raster order
1✔
701
                var coeffs [64]int32
1✔
702
                if sc.IsCAVLC {
2✔
703
                        // CAVLC: already stored in raster order by decode
1✔
704
                        coeffs = mb.LumaLevel8x8[i]
1✔
705
                } else {
2✔
706
                        // CABAC: convert from zigzag scan order to raster
1✔
707
                        for j := 0; j < 64; j++ {
2✔
708
                                coeffs[zigzag8x8[j]] = mb.LumaLevel8x8[i][j]
1✔
709
                        }
1✔
710
                }
711

712
                // 4. Dequantize
713
                dequant := transform.Dequant8x8(coeffs, mb.QPY, sl)
1✔
714

1✔
715
                // 5. Inverse transform
1✔
716
                residual := transform.InverseTransform8x8(dequant)
1✔
717

1✔
718
                // 6. Reconstruct: prediction + residual, clip to [0,255]
1✔
719
                for y := 0; y < 8; y++ {
2✔
720
                        for x := 0; x < 8; x++ {
2✔
721
                                val := int32(predBlock[y][x]) + residual[y*8+x]
1✔
722
                                f.SetLumaPixel(x0+bx+x, y0+by+y, clip8(val))
1✔
723
                        }
1✔
724
                }
725

726
        }
727

728
        return nil
1✔
729
}
730

731
// getLuma8x8Neighbors returns 25 filtered reference samples for 8x8 intra prediction.
732
// Layout: [L7, L6, L5, L4, L3, L2, L1, L0, TL, T0..T7, T8..T15]
733
// Handles availability, substitution (8.3.2.2.2), and filtering (8.3.2.2.3).
734
func getLuma8x8Neighbors(f *frame.Frame, blkX, blkY int, i8x8 int, frameW, frameH int) [25]uint8 {
1✔
735
        var ref [25]uint8
1✔
736
        var avail [25]bool
1✔
737

1✔
738
        // Left: ref[0]=L7(bottom)..ref[7]=L0(top) = p[-1,7]..p[-1,0]
1✔
739
        for i := 0; i < 8; i++ {
2✔
740
                px, py := blkX-1, blkY+7-i
1✔
741
                if px >= 0 && py >= 0 && py < frameH {
2✔
742
                        ref[i] = f.GetLumaPixel(px, py)
1✔
743
                        avail[i] = true
1✔
744
                }
1✔
745
        }
746

747
        // Top-left: ref[8] = p[-1,-1]
748
        if blkX > 0 && blkY > 0 {
2✔
749
                ref[8] = f.GetLumaPixel(blkX-1, blkY-1)
1✔
750
                avail[8] = true
1✔
751
        }
1✔
752

753
        // Top: ref[9]=T0..ref[16]=T7 = p[0,-1]..p[7,-1]
754
        for i := 0; i < 8; i++ {
2✔
755
                px, py := blkX+i, blkY-1
1✔
756
                if py >= 0 && px >= 0 && px < frameW {
2✔
757
                        ref[9+i] = f.GetLumaPixel(px, py)
1✔
758
                        avail[9+i] = true
1✔
759
                }
1✔
760
        }
761

762
        // Top-right: ref[17]=T8..ref[24]=T15 = p[8,-1]..p[15,-1]
763
        // Not available for 8x8 block index 3 (bottom-right in MB)
764
        topRightOK := i8x8 != 3
1✔
765
        for i := 0; i < 8; i++ {
2✔
766
                px, py := blkX+8+i, blkY-1
1✔
767
                if topRightOK && py >= 0 && px >= 0 && px < frameW {
2✔
768
                        ref[17+i] = f.GetLumaPixel(px, py)
1✔
769
                        avail[17+i] = true
1✔
770
                }
1✔
771
        }
772

773
        // Substitution (section 8.3.2.2.2)
774
        // Scan order: ref[0] (bottom-left) to ref[24] (top-right)
775
        firstAvailIdx := -1
1✔
776
        for i := 0; i < 25; i++ {
2✔
777
                if avail[i] {
2✔
778
                        firstAvailIdx = i
1✔
779
                        break
1✔
780
                }
781
        }
782
        if firstAvailIdx == -1 {
2✔
783
                // No available samples - use DC value
1✔
784
                for i := range ref {
2✔
785
                        ref[i] = 128
1✔
786
                }
1✔
787
        } else {
1✔
788
                // Fill all positions before firstAvailIdx with its value
1✔
789
                for i := 0; i < firstAvailIdx; i++ {
2✔
790
                        ref[i] = ref[firstAvailIdx]
1✔
791
                }
1✔
792
                // Fill forward: unavailable positions get previous value
793
                for i := firstAvailIdx + 1; i < 25; i++ {
2✔
794
                        if !avail[i] {
2✔
795
                                ref[i] = ref[i-1]
1✔
796
                        }
1✔
797
                }
798
        }
799

800
        // Filtering (section 8.3.2.2.3) - 1-2-1 low-pass filter
801
        return filterRefSamples8x8(ref)
1✔
802
}
803

804
// filterRefSamples8x8 applies the 1-2-1 low-pass filter to 8x8 reference samples.
805
func filterRefSamples8x8(ref [25]uint8) [25]uint8 {
1✔
806
        var f [25]uint8
1✔
807
        // Bottom-left edge: p'[-1,7] = (p[-1,6] + 3*p[-1,7] + 2) >> 2
1✔
808
        f[0] = uint8((int(ref[1]) + 3*int(ref[0]) + 2) >> 2)
1✔
809
        // Interior samples (left column, TL, top row)
1✔
810
        for i := 1; i < 24; i++ {
2✔
811
                f[i] = uint8((int(ref[i-1]) + 2*int(ref[i]) + int(ref[i+1]) + 2) >> 2)
1✔
812
        }
1✔
813
        // Top-right edge: p'[15,-1] = (p[14,-1] + 3*p[15,-1] + 2) >> 2
814
        f[24] = uint8((int(ref[23]) + 3*int(ref[24]) + 2) >> 2)
1✔
815
        return f
1✔
816
}
817

818
// reconstructChroma reconstructs both chroma components for a macroblock.
819
func reconstructChroma(sc *slice.SliceContext, f *frame.Frame,
820
        mbIdx, mbX, mbY int, mb *slice.MBData, sm *ScalingMatrices) {
1✔
821
        chromaSL := [2]*[16]int32{&sm.IntraCb4x4, &sm.IntraCr4x4}
1✔
822

1✔
823
        for iCbCr := 0; iCbCr < 2; iCbCr++ {
2✔
824
                sl := chromaSL[iCbCr]
1✔
825

1✔
826
                // Get chroma prediction
1✔
827
                top, left, topLeft, hasTop, hasLeft := getChromaNeighbors(f, iCbCr, mbX, mbY)
1✔
828
                var topSlice, leftSlice []uint8
1✔
829
                if hasTop {
2✔
830
                        topSlice = top[:]
1✔
831
                }
1✔
832
                if hasLeft {
2✔
833
                        leftSlice = left[:]
1✔
834
                }
1✔
835
                predBlock := pred.PredictChroma(mb.IntraChromaPredMode, topSlice, leftSlice, topLeft, 8)
1✔
836

1✔
837
                // Inverse Hadamard on DC coefficients
1✔
838
                var dcCoeffs [4]int32
1✔
839
                copy(dcCoeffs[:], mb.ChromaDCLevel[iCbCr][:])
1✔
840
                dcTransformed := transform.InverseHadamard2x2(dcCoeffs)
1✔
841

1✔
842
                // Calculate chroma QP
1✔
843
                qpc := chromaQP(mb.QPY + sc.ChromaQpIndexOffset)
1✔
844
                dcScaled := transform.DequantChromaDC2x2(dcTransformed, qpc, sl[0])
1✔
845

1✔
846
                // For each 4x4 chroma block
1✔
847
                var chromaBlock [8][8]uint8
1✔
848
                for blk := 0; blk < 4; blk++ {
2✔
849
                        bx := (blk % 2) * 4
1✔
850
                        by := (blk / 2) * 4
1✔
851

1✔
852
                        var block4x4 [16]int32
1✔
853
                        block4x4[0] = dcScaled[blk]
1✔
854
                        if mb.CBPChroma > 1 {
2✔
855
                                // Apply zigzag scan conversion for AC coefficients
1✔
856
                                for j := 0; j < 15; j++ {
2✔
857
                                        block4x4[zigzag4x4AC[j]] = mb.ChromaACLevel[iCbCr][blk][j]
1✔
858
                                }
1✔
859
                        }
860

861
                        dequant := transform.Dequant4x4(block4x4, qpc, sl)
1✔
862
                        dequant[0] = dcScaled[blk]
1✔
863
                        residual := transform.InverseTransform4x4(dequant)
1✔
864

1✔
865
                        for y := 0; y < 4; y++ {
2✔
866
                                for x := 0; x < 4; x++ {
2✔
867
                                        val := int32(predBlock[by+y][bx+x]) + residual[y*4+x]
1✔
868
                                        chromaBlock[by+y][bx+x] = clip8(val)
1✔
869
                                }
1✔
870
                        }
871
                }
872

873
                f.SetChroma8x8(iCbCr, mbX, mbY, chromaBlock)
1✔
874
        }
875
}
876

877
// getLuma16x16Neighbors returns the reference samples for 16x16 luma prediction.
878
func getLuma16x16Neighbors(f *frame.Frame, mbX, mbY int) (
879
        top [16]uint8, left [16]uint8, topLeft uint8, hasTop, hasLeft bool) {
1✔
880
        x0 := mbX * 16
1✔
881
        y0 := mbY * 16
1✔
882

1✔
883
        if mbY > 0 {
2✔
884
                hasTop = true
1✔
885
                for x := 0; x < 16; x++ {
2✔
886
                        top[x] = f.GetLumaPixel(x0+x, y0-1)
1✔
887
                }
1✔
888
        }
889

890
        if mbX > 0 {
2✔
891
                hasLeft = true
1✔
892
                for y := 0; y < 16; y++ {
2✔
893
                        left[y] = f.GetLumaPixel(x0-1, y0+y)
1✔
894
                }
1✔
895
        }
896

897
        if mbX > 0 && mbY > 0 {
2✔
898
                topLeft = f.GetLumaPixel(x0-1, y0-1)
1✔
899
        }
1✔
900

901
        return
1✔
902
}
903

904
// getLuma4x4Neighbors returns the 13 reference samples for 4x4 prediction.
905
// topRightNotAvail4x4 lists 4x4 block indices where the upper-right 4 samples
906
// are never available (the containing block has a higher z-scan index or is in
907
// the not-yet-decoded right MB).
908
var topRightNotAvail4x4 = [16]bool{
909
        false, false, false, true, // blocks 0-3: block 3's TR is in undecoded block 6
910
        false, false, false, true, // blocks 4-7: block 7's TR is in right MB
911
        false, false, false, true, // blocks 8-11: block 11's TR is in undecoded block 12
912
        false, false, false, true, // blocks 12-15: block 15's TR is in right MB
913
}
914

915
func getLuma4x4Neighbors(f *frame.Frame, x0, y0 int, blkIdx int, mbX, mbY int, mbW, mbH int) [13]uint8 {
1✔
916
        var ref [13]uint8
1✔
917
        frameW := mbW * 16
1✔
918
        frameH := mbH * 16
1✔
919
        // ref[0..3] = L3,L2,L1,L0 (left column, bottom to top)
1✔
920
        // ref[4]    = TL (top-left)
1✔
921
        // ref[5..12]= T0..T7 (top row + upper-right)
1✔
922

1✔
923
        for i := 0; i < 4; i++ {
2✔
924
                if x0 > 0 && y0+3-i >= 0 && y0+3-i < frameH {
2✔
925
                        ref[i] = f.GetLumaPixel(x0-1, y0+3-i)
1✔
926
                } else {
2✔
927
                        ref[i] = 128
1✔
928
                }
1✔
929
        }
930

931
        if x0 > 0 && y0 > 0 {
2✔
932
                ref[4] = f.GetLumaPixel(x0-1, y0-1)
1✔
933
        } else {
2✔
934
                ref[4] = 128
1✔
935
        }
1✔
936

937
        // Top samples T0..T3
938
        for i := 0; i < 4; i++ {
2✔
939
                if y0 > 0 && x0+i >= 0 && x0+i < frameW {
2✔
940
                        ref[5+i] = f.GetLumaPixel(x0+i, y0-1)
1✔
941
                } else {
2✔
942
                        ref[5+i] = 128
1✔
943
                }
1✔
944
        }
945

946
        // Top-right samples T4..T7: check availability
947
        trAvail := true
1✔
948
        if topRightNotAvail4x4[blkIdx] {
2✔
949
                trAvail = false
1✔
950
        } else if blkIdx == 5 && mbX >= mbW-1 {
3✔
951
                // Block 5 (12,0): TR is in MB above-right, not available at right edge
1✔
952
                trAvail = false
1✔
953
        } else if blkIdx == 13 {
3✔
954
                // Block 13 (12,8): TR is in right MB (not decoded) — already handled by table
1✔
955
                trAvail = false
1✔
956
        }
1✔
957

958
        if trAvail {
2✔
959
                for i := 4; i < 8; i++ {
2✔
960
                        if y0 > 0 && x0+i >= 0 && x0+i < frameW {
2✔
961
                                ref[5+i] = f.GetLumaPixel(x0+i, y0-1)
1✔
962
                        } else {
2✔
963
                                ref[5+i] = 128
1✔
964
                        }
1✔
965
                }
966
        } else {
1✔
967
                // When upper-right is not available, fill T4..T7 with T3
1✔
968
                for i := 4; i < 8; i++ {
2✔
969
                        ref[5+i] = ref[8] // ref[8] = T3
1✔
970
                }
1✔
971
        }
972

973
        return ref
1✔
974
}
975

976
// getChromaNeighbors returns reference samples for chroma prediction.
977
func getChromaNeighbors(f *frame.Frame, comp int, mbX, mbY int) (
978
        top [8]uint8, left [8]uint8, topLeft uint8, hasTop, hasLeft bool) {
1✔
979
        x0 := mbX * 8
1✔
980
        y0 := mbY * 8
1✔
981

1✔
982
        if mbY > 0 {
2✔
983
                hasTop = true
1✔
984
                for x := 0; x < 8; x++ {
2✔
985
                        top[x] = f.GetChromaPixel(comp, x0+x, y0-1)
1✔
986
                }
1✔
987
        }
988

989
        if mbX > 0 {
2✔
990
                hasLeft = true
1✔
991
                for y := 0; y < 8; y++ {
2✔
992
                        left[y] = f.GetChromaPixel(comp, x0-1, y0+y)
1✔
993
                }
1✔
994
        }
995

996
        if mbX > 0 && mbY > 0 {
2✔
997
                topLeft = f.GetChromaPixel(comp, x0-1, y0-1)
1✔
998
        }
1✔
999

1000
        return
1✔
1001
}
1002

1003
// inverseRasterX4x4 maps 4x4 block index (0-15) to x position.
1004
// Uses H.264 spec hierarchical scan (Table 6-2 / equations 6-17, 6-18):
1005
// Outer level: 8x8 blocks in raster scan; Inner level: 4x4 blocks within each 8x8.
1006
var inverseRasterX4x4 = [16]int{
1007
        0, 4, 0, 4, 8, 12, 8, 12,
1008
        0, 4, 0, 4, 8, 12, 8, 12,
1009
}
1010

1011
// inverseRasterY4x4 maps 4x4 block index (0-15) to y position.
1012
var inverseRasterY4x4 = [16]int{
1013
        0, 0, 4, 4, 0, 0, 4, 4,
1014
        8, 8, 12, 12, 8, 8, 12, 12,
1015
}
1016

1017
// zScanToRaster maps z-scan 4x4 block index to raster index (row*4+col).
1018
// This is an involution (self-inverse permutation).
1019
var zScanToRaster = [16]int{0, 1, 4, 5, 2, 3, 6, 7, 8, 9, 12, 13, 10, 11, 14, 15}
1020

1021
// zigzag4x4 maps CABAC scan position to 4x4 matrix position (Table 8-13 of the spec).
1022
var zigzag4x4 = [16]int{
1023
        0, 1, 4, 8, 5, 2, 3, 6,
1024
        9, 12, 13, 10, 7, 11, 14, 15,
1025
}
1026

1027
// zigzag4x4AC maps CABAC scan position to matrix position for AC coefficients
1028
// (positions 1-15, skipping DC at position 0).
1029
var zigzag4x4AC = [15]int{
1030
        1, 4, 8, 5, 2, 3, 6,
1031
        9, 12, 13, 10, 7, 11, 14, 15,
1032
}
1033

1034
// zigzag8x8 maps CABAC scan position to 8x8 matrix position (Table 8-14).
1035
var zigzag8x8 = [64]int{
1036
        0, 1, 8, 16, 9, 2, 3, 10,
1037
        17, 24, 32, 25, 18, 11, 4, 5,
1038
        12, 19, 26, 33, 40, 48, 41, 34,
1039
        27, 20, 13, 6, 7, 14, 21, 28,
1040
        35, 42, 49, 56, 57, 50, 43, 36,
1041
        29, 22, 15, 23, 30, 37, 44, 51,
1042
        58, 59, 52, 45, 38, 31, 39, 46,
1043
        53, 60, 61, 54, 47, 55, 62, 63,
1044
}
1045

1046
// chromaQP maps luma QP to chroma QP (Table 8-15).
1047
func chromaQP(qpY int) int {
1✔
1048
        if qpY < 0 {
1✔
UNCOV
1049
                qpY = 0
×
UNCOV
1050
        }
×
1051
        if qpY < 30 {
2✔
1052
                return qpY
1✔
1053
        }
1✔
1054
        if qpY > 51 {
1✔
UNCOV
1055
                return 51
×
UNCOV
1056
        }
×
1057
        // QP mapping table for indices 30-51
1058
        qpcTable := []int{
1✔
1059
                29, 30, 31, 32, 32, 33, 34, 34,
1✔
1060
                35, 35, 36, 36, 37, 37, 37, 38,
1✔
1061
                38, 38, 39, 39, 39, 39,
1✔
1062
        }
1✔
1063
        return qpcTable[qpY-30]
1✔
1064
}
1065

1066
func clip8(val int32) uint8 {
1✔
1067
        if val < 0 {
2✔
1068
                return 0
1✔
1069
        }
1✔
1070
        if val > 255 {
2✔
1071
                return 255
1✔
1072
        }
1✔
1073
        return uint8(val)
1✔
1074
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc