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

Eyevinn / hi264 / 28188681282

25 Jun 2026 05:31PM UTC coverage: 87.374% (-0.2%) from 87.601%
28188681282

push

github

tobbee
feat: fractional -fps and NTSC drop-frame timecode

Let -fps express fractional rates and add drop-frame timecode counting.

- hi264gen -fps now accepts an integer (25), a rational (30000/1001), or
  an NTSC decimal (29.97/59.94/23.976, snapped to the exact 1001 fraction).
  For mp4 the media timescale is the numerator and each sample lasts the
  denominator ticks, so fractional rates play at the correct speed
  (ffprobe reports r_frame_rate=30000/1001). Timecodes count at the
  nominal integer rate round(fps); -kbps->bpp uses the exact ratio.
- hi264gen -drop-frame: NTSC drop-frame counting, validated to 29.97/59.94.
  Drops the two (rate 30) / four (rate 60) lowest timecode labels at each
  minute except every tenth, and signals it in the pic_timing SEI via
  counting_type=4 and per-frame cnt_dropped_flag. Applies to both the
  burnt-in -text timecode and the SEI.
- yuv.FormatTextTC: text formatter variant taking the counting rate and
  drop-frame; FormatText is the integer/non-drop wrapper.
- encode.PicTimingConfig.DropFrame / PicTiming.Dropped carry the
  counting_type / cnt_dropped_flag into the SEI.
- Docs: cover pic-timing, start-frame, fractional fps, and drop-frame in the
  curated hi264gen usage header and the CLAUDE.md feature list / CLI examples
  (README and CHANGELOG updated alongside the relevant changes).

Verified: 30000/1001 mp4 timescale=30000 dur=1001; NDF rolls at frame 30;
drop-frame frames 1798..1802 -> 59:28, 59:29, 01:00:02, 01:00:03, 01:00:04
(labels ;00/;01 skipped), ffprobe renders ';'; -drop-frame rejected for 25
fps; ffmpeg decodes both; all 12 hi264gen golden checks still pass.

88 of 99 new or added lines in 3 files covered. (88.89%)

264 existing lines in 5 files now uncovered.

8124 of 9298 relevant lines covered (87.37%)

1.01 hits per line

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

92.62
/pkg/encode/frame_encode.go
1
package encode
2

3
import (
4
        "bytes"
5
        "fmt"
6

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

9
        "github.com/Eyevinn/hi264/internal/cabac"
10
        "github.com/Eyevinn/hi264/internal/context"
11
        "github.com/Eyevinn/hi264/pkg/yuv"
12
)
13

14
// FrameEncoder encodes a grid-based image as an H.264 IDR frame.
15
type FrameEncoder struct {
16
        Grid            *yuv.Grid
17
        Colors          yuv.ColorMap
18
        Plane           *yuv.PlaneGrid // alternative to Grid+Colors; takes priority if set
19
        QP              int
20
        DisableDeblock  int            // 0=enable, 1=disable
21
        CABAC           bool           // use CABAC entropy coding (Main profile) instead of CAVLC (Baseline)
22
        MaxNumRefFrames int            // max_num_ref_frames in SPS (0 for IDR-only, 1+ for P-frames)
23
        Width           int            // actual pixel width for SPS (0 = Grid.Width*16)
24
        Height          int            // actual pixel height for SPS (0 = Grid.Height*16)
25
        ColorSpace      yuv.ColorSpace // YCbCr matrix standard (default BT601)
26
        Range           yuv.Range      // sample value range (default LimitedRange)
27
        FPS             int            // frame rate for level selection (0 = ignore MBPS)
28
        Kbps            int            // bitrate in kbit/s for level selection (0 = ignore)
29
        // PicStructPresent sets vui pic_struct_present_flag in the SPS written by
30
        // EncodeSPSPPS. Required to attach pic_timing SEI clock timestamps.
31
        PicStructPresent bool
32

33
        // SPS/PPS, when non-nil, control IDR slice-header bit widths,
34
        // pic_order_cnt_type, pic_parameter_set_id, and slice_qp_delta
35
        // (compensating for pic_init_qp_minus26). This is what makes a
36
        // stand-alone IDR slice compatible with foreign parameter sets when
37
        // extending an existing stream. When both are nil, hi264's own
38
        // defaults are used (4 bits each for frame_num and POC LSB,
39
        // pps_id=0, pic_init_qp=26).
40
        SPS *avc.SPS
41
        PPS *avc.PPS
42
}
43

44
// idrSliceHeader returns the slice-header fields for an IDR slice using
45
// the encoder's SPS/PPS overrides if set, otherwise hi264's defaults.
46
func (e *FrameEncoder) idrSliceHeader(idrPicID uint32) IDRSliceHeader {
1✔
47
        h := IDRSliceHeader{
1✔
48
                QPDelta:                     int32(e.QP - 26),
1✔
49
                DisableDeblock:              e.DisableDeblock,
1✔
50
                IDRPicID:                    idrPicID,
1✔
51
                PPSID:                       0,
1✔
52
                Log2MaxFrameNumMinus4:       0,
1✔
53
                Log2MaxPicOrderCntLsbMinus4: 0,
1✔
54
                PicOrderCntType:             0,
1✔
55
                DeblockControlPresent:       true,
1✔
56
        }
1✔
57
        if e.SPS != nil {
2✔
58
                h.Log2MaxFrameNumMinus4 = uint32(e.SPS.Log2MaxFrameNumMinus4)
1✔
59
                h.Log2MaxPicOrderCntLsbMinus4 = uint32(e.SPS.Log2MaxPicOrderCntLsbMinus4)
1✔
60
                h.PicOrderCntType = uint8(e.SPS.PicOrderCntType)
1✔
61
        }
1✔
62
        if e.PPS != nil {
2✔
63
                h.PPSID = e.PPS.PicParameterSetID
1✔
64
                h.DeblockControlPresent = e.PPS.DeblockingFilterControlPresentFlag
1✔
65
                // slice_qp = pic_init_qp + slice_qp_delta. Solve for delta.
1✔
66
                h.QPDelta = int32(e.QP-26) - int32(e.PPS.PicInitQpMinus26)
1✔
67
        }
1✔
68
        return h
1✔
69
}
70

71
// plane returns the PlaneGrid to use for encoding, converting Grid+Colors if needed.
72
func (e *FrameEncoder) plane() (*yuv.PlaneGrid, error) {
1✔
73
        if e.Plane != nil {
2✔
74
                return e.Plane, nil
1✔
75
        }
1✔
76
        return yuv.GridToPlaneGrid(e.Grid, e.Colors)
1✔
77
}
78

79
// frameDimensions returns the frame width and height in pixels.
80
func (e *FrameEncoder) frameDimensions() (width, height int) {
1✔
81
        if e.Plane != nil {
2✔
82
                width = e.Plane.PixelWidth()
1✔
83
                height = e.Plane.PixelHeight()
1✔
84
        } else {
2✔
85
                width = e.Grid.Width * 16
1✔
86
                height = e.Grid.Height * 16
1✔
87
        }
1✔
88
        if e.Width > 0 {
2✔
89
                width = e.Width
1✔
90
        }
1✔
91
        if e.Height > 0 {
2✔
92
                height = e.Height
1✔
93
        }
1✔
94
        return width, height
1✔
95
}
96

97
// Encode produces an Annex-B bitstream containing SPS, PPS, and one IDR slice.
98
func (e *FrameEncoder) Encode() ([]byte, error) {
1✔
99
        var buf bytes.Buffer
1✔
100
        if err := e.EncodeSPSPPS(&buf); err != nil {
1✔
UNCOV
101
                return nil, err
×
102
        }
×
103
        slice, err := e.EncodeSlice(0)
1✔
104
        if err != nil {
1✔
UNCOV
105
                return nil, err
×
UNCOV
106
        }
×
107
        buf.Write(slice)
1✔
108
        return buf.Bytes(), nil
1✔
109
}
110

111
// EncodeSPSPPS writes SPS and PPS NALUs to buf (profile-aware).
112
func (e *FrameEncoder) EncodeSPSPPS(buf *bytes.Buffer) error {
1✔
113
        width, height := e.frameDimensions()
1✔
114

1✔
115
        level := ChooseLevel(width, height, e.FPS, e.Kbps, e.CABAC)
1✔
116
        if e.CABAC {
2✔
117
                spsRBSP := EncodeSPSMain(width, height, e.MaxNumRefFrames, level, e.ColorSpace, e.Range, e.PicStructPresent)
1✔
118
                if err := WriteNALU(buf, 7, 3, spsRBSP); err != nil {
1✔
UNCOV
119
                        return fmt.Errorf("write SPS: %w", err)
×
120
                }
×
121
                ppsRBSP := EncodePPSCABAC(e.DisableDeblock)
1✔
122
                if err := WriteNALU(buf, 8, 3, ppsRBSP); err != nil {
1✔
UNCOV
123
                        return fmt.Errorf("write PPS: %w", err)
×
UNCOV
124
                }
×
125
        } else {
1✔
126
                spsRBSP := EncodeSPS(width, height, e.MaxNumRefFrames, level, e.ColorSpace, e.Range, e.PicStructPresent)
1✔
127
                if err := WriteNALU(buf, 7, 3, spsRBSP); err != nil {
1✔
UNCOV
128
                        return fmt.Errorf("write SPS: %w", err)
×
129
                }
×
130
                ppsRBSP := EncodePPS(e.DisableDeblock)
1✔
131
                if err := WriteNALU(buf, 8, 3, ppsRBSP); err != nil {
1✔
UNCOV
132
                        return fmt.Errorf("write PPS: %w", err)
×
UNCOV
133
                }
×
134
        }
135
        return nil
1✔
136
}
137

138
// EncodeSlice encodes a single IDR slice NALU and returns it as Annex-B framed bytes.
139
// idrPicID is the idr_pic_id value (alternating 0/1 for consecutive IDR pictures).
140
func (e *FrameEncoder) EncodeSlice(idrPicID uint32) ([]byte, error) {
1✔
141
        if e.CABAC {
2✔
142
                return e.encodeSliceCABAC(idrPicID)
1✔
143
        }
1✔
144
        return e.encodeSliceCAVLC(idrPicID)
1✔
145
}
146

147
// EncodePSkipSlice encodes a P_Skip slice (all MBs skip, copying from reference).
148
// frameNum is the frame_num value for this slice.
149
// Returns an Annex-B framed non-IDR NALU (type=1, ref_idc=2).
150
func (e *FrameEncoder) EncodePSkipSlice(frameNum uint32) ([]byte, error) {
1✔
151
        width, height := e.frameDimensions()
1✔
152
        sps := &avc.SPS{
1✔
153
                Width:            uint(width),
1✔
154
                Height:           uint(height),
1✔
155
                PicOrderCntType:  0,
1✔
156
                FrameMbsOnlyFlag: true,
1✔
157
        }
1✔
158
        pps := &avc.PPS{
1✔
159
                DeblockingFilterControlPresentFlag: true,
1✔
160
                EntropyCodingModeFlag:              e.CABAC,
1✔
161
                PicInitQpMinus26:                   e.QP - 26,
1✔
162
        }
1✔
163
        // FrameEncoder controls both ends of its stream (IDR resets POC each
1✔
164
        // time), so picOrderCntLsb = 2*frame_num is correct here.
1✔
165
        return EncodePSkipSlice(sps, pps, frameNum, frameNum*2, e.DisableDeblock)
1✔
166
}
1✔
167

168
func (e *FrameEncoder) encodeSliceCAVLC(idrPicID uint32) ([]byte, error) {
1✔
169
        pg, err := e.plane()
1✔
170
        if err != nil {
1✔
UNCOV
171
                return nil, err
×
UNCOV
172
        }
×
173
        mbWidth := pg.MBWidth()
1✔
174
        mbHeight := pg.MBHeight()
1✔
175
        width := mbWidth * 16
1✔
176
        height := mbHeight * 16
1✔
177

1✔
178
        // Build slice (header + MB data) in a single BitWriter to maintain bit alignment
1✔
179
        sliceW := NewBitWriter()
1✔
180
        WriteSliceHeader(sliceW, e.idrSliceHeader(idrPicID))
1✔
181

1✔
182
        // Encode macroblocks
1✔
183
        // Track nC (number of non-zero coefficients) per 4x4 block for context
1✔
184
        // nC is computed from neighbors A (left) and B (above)
1✔
185
        nCLuma := make([][]int, mbHeight*4)
1✔
186
        for i := range nCLuma {
2✔
187
                nCLuma[i] = make([]int, mbWidth*4)
1✔
188
        }
1✔
189
        nCCb := make([][]int, mbHeight*2)
1✔
190
        for i := range nCCb {
2✔
191
                nCCb[i] = make([]int, mbWidth*2)
1✔
192
        }
1✔
193
        nCCr := make([][]int, mbHeight*2)
1✔
194
        for i := range nCCr {
2✔
195
                nCCr[i] = make([]int, mbWidth*2)
1✔
196
        }
1✔
197

198
        // Reconstructed pixel values (needed for DC prediction of neighbors)
199
        // Use flat slices with stride to minimize allocations.
200
        strideY := width
1✔
201
        strideC := width / 2
1✔
202
        reconY := make([]uint8, height*strideY)
1✔
203
        reconCb := make([]uint8, (height/2)*strideC)
1✔
204
        reconCr := make([]uint8, (height/2)*strideC)
1✔
205

1✔
206
        for mbY := range mbHeight {
2✔
207
                for mbX := range mbWidth {
2✔
208
                        lumaVals := pg.MBLumaValues(mbX, mbY)
1✔
209
                        cbVals, crVals := pg.MBChromaSub(mbX, mbY)
1✔
210

1✔
211
                        err := e.encodeMBPlane(sliceW, mbX, mbY, lumaVals, cbVals, crVals,
1✔
212
                                nCLuma, nCCb, nCCr, reconY, strideY, reconCb, reconCr, strideC)
1✔
213
                        if err != nil {
1✔
UNCOV
214
                                return nil, fmt.Errorf("encode MB (%d,%d): %w", mbX, mbY, err)
×
UNCOV
215
                        }
×
216
                }
217
        }
218

219
        // RBSP trailing bits
220
        sliceW.WriteBit(1) // rbsp_stop_one_bit
1✔
221
        sliceW.AlignToByte()
1✔
222

1✔
223
        var buf bytes.Buffer
1✔
224
        sliceRBSP := sliceW.Bytes()
1✔
225
        if err := WriteNALU(&buf, 5, 3, sliceRBSP); err != nil {
1✔
UNCOV
226
                return nil, fmt.Errorf("write IDR: %w", err)
×
UNCOV
227
        }
×
228

229
        return buf.Bytes(), nil
1✔
230
}
231

232
// encMBState tracks encoder-side MB state for CABAC context derivation.
233
type encMBState struct {
234
        mbType              int
235
        intraChromaPredMode int
236
        cbpChroma           int
237
        codedBlockFlag      [6][16]uint8
238
}
239

240
func (e *FrameEncoder) encodeSliceCABAC(idrPicID uint32) ([]byte, error) {
1✔
241
        pg, err := e.plane()
1✔
242
        if err != nil {
1✔
UNCOV
243
                return nil, err
×
UNCOV
244
        }
×
245
        mbWidth := pg.MBWidth()
1✔
246
        mbHeight := pg.MBHeight()
1✔
247
        width := mbWidth * 16
1✔
248
        height := mbHeight * 16
1✔
249

1✔
250
        // Build slice header with CABAC alignment
1✔
251
        headerW := NewBitWriter()
1✔
252
        WriteSliceHeaderCABAC(headerW, e.idrSliceHeader(idrPicID))
1✔
253
        headerBytes := headerW.Bytes()
1✔
254

1✔
255
        // Initialize CABAC encoder and context models
1✔
256
        enc := cabac.NewEncoder()
1✔
257
        models := context.InitModels(e.QP, 2, 0) // sliceType=2 (I-slice)
1✔
258
        ctx := models[:]
1✔
259

1✔
260
        // MB state for neighbor context derivation
1✔
261
        mbStates := make([]encMBState, mbWidth*mbHeight)
1✔
262

1✔
263
        // Reconstructed pixel values (needed for DC prediction of neighbors)
1✔
264
        // Use flat slices with stride to minimize allocations.
1✔
265
        strideY := width
1✔
266
        strideC := width / 2
1✔
267
        reconY := make([]uint8, height*strideY)
1✔
268
        reconCb := make([]uint8, (height/2)*strideC)
1✔
269
        reconCr := make([]uint8, (height/2)*strideC)
1✔
270

1✔
271
        for mbY := range mbHeight {
2✔
272
                for mbX := range mbWidth {
2✔
273
                        lumaVals := pg.MBLumaValues(mbX, mbY)
1✔
274
                        cbVals, crVals := pg.MBChromaSub(mbX, mbY)
1✔
275

1✔
276
                        mbIdx := mbY*mbWidth + mbX
1✔
277
                        err := e.encodeMBCABACPlane(enc, ctx, mbStates, mbIdx, mbX, mbY,
1✔
278
                                lumaVals, cbVals, crVals,
1✔
279
                                mbWidth, mbHeight, reconY, strideY, reconCb, reconCr, strideC)
1✔
280
                        if err != nil {
1✔
UNCOV
281
                                return nil, fmt.Errorf("encode MB (%d,%d): %w", mbX, mbY, err)
×
UNCOV
282
                        }
×
283

284
                        // end_of_slice: terminate(1) for last MB, terminate(0) otherwise
285
                        isLast := mbY == mbHeight-1 && mbX == mbWidth-1
1✔
286
                        if isLast {
2✔
287
                                enc.EncodeTerminate(1)
1✔
288
                        } else {
2✔
289
                                enc.EncodeTerminate(0)
1✔
290
                        }
1✔
291
                }
292
        }
293

294
        cabacBytes := enc.Flush()
1✔
295

1✔
296
        // Concatenate header + CABAC data as RBSP
1✔
297
        sliceRBSP := make([]byte, 0, len(headerBytes)+len(cabacBytes))
1✔
298
        sliceRBSP = append(sliceRBSP, headerBytes...)
1✔
299
        sliceRBSP = append(sliceRBSP, cabacBytes...)
1✔
300

1✔
301
        var buf bytes.Buffer
1✔
302
        if err := WriteNALU(&buf, 5, 3, sliceRBSP); err != nil {
1✔
UNCOV
303
                return nil, fmt.Errorf("write IDR: %w", err)
×
UNCOV
304
        }
×
305

306
        return buf.Bytes(), nil
1✔
307
}
308

309
// deriveDCCBFCtx derives the coded_block_flag context for Intra16x16DC.
310
// condTermFlagA/B default to 1 when neighbor is not available.
311
func deriveDCCBFCtx(left, top *encMBState) int {
1✔
312
        condA := 1
1✔
313
        condB := 1
1✔
314
        if left != nil {
2✔
315
                condA = int(left.codedBlockFlag[encCtxBlockCatIntra16x16DC][0])
1✔
316
        }
1✔
317
        if top != nil {
2✔
318
                condB = int(top.codedBlockFlag[encCtxBlockCatIntra16x16DC][0])
1✔
319
        }
1✔
320
        return condA + 2*condB
1✔
321
}
322

323
// deriveChromaDCCBFCtx derives the coded_block_flag context for ChromaDC.
324
// iCbCr: 0 for Cb, 1 for Cr.
325
func deriveChromaDCCBFCtx(left, top *encMBState, iCbCr int) int {
1✔
326
        condA := 1
1✔
327
        condB := 1
1✔
328
        if left != nil {
2✔
329
                condA = int(left.codedBlockFlag[encCtxBlockCatChromaDC][iCbCr])
1✔
330
        }
1✔
331
        if top != nil {
2✔
332
                condB = int(top.codedBlockFlag[encCtxBlockCatChromaDC][iCbCr])
1✔
333
        }
1✔
334
        return condA + 2*condB
1✔
335
}
336

337
func computeLumaDCPred(reconY []uint8, strideY, mbX, mbY int) uint8 {
1✔
338
        hasTop := mbY > 0
1✔
339
        hasLeft := mbX > 0
1✔
340

1✔
341
        if !hasTop && !hasLeft {
2✔
342
                return 128
1✔
343
        }
1✔
344

345
        sum := 0
1✔
346
        count := 0
1✔
347

1✔
348
        if hasTop {
2✔
349
                off := (mbY*16-1)*strideY + mbX*16
1✔
350
                for x := range 16 {
2✔
351
                        sum += int(reconY[off+x])
1✔
352
                }
1✔
353
                count += 16
1✔
354
        }
355

356
        if hasLeft {
2✔
357
                off := mbY*16*strideY + mbX*16 - 1
1✔
358
                for y := range 16 {
2✔
359
                        sum += int(reconY[off+y*strideY])
1✔
360
                }
1✔
361
                count += 16
1✔
362
        }
363

364
        if count == 32 {
2✔
365
                return uint8((sum + 16) >> 5)
1✔
366
        }
1✔
367
        return uint8((sum + 8) >> 4)
1✔
368
}
369

370
// lumaPredict16x16 computes the per-pixel I_16x16 prediction array for the given mode.
371
// This matches the decoder's prediction: V mode uses the full top row, H mode uses
372
// the full left column, DC mode uses a uniform value.
373
func lumaPredict16x16(reconY []uint8, strideY, mbX, mbY, mode int) [256]uint8 {
1✔
374
        var pred [256]uint8
1✔
375
        switch mode {
1✔
376
        case 0: // Vertical — each column gets top row pixel
1✔
377
                off := (mbY*16-1)*strideY + mbX*16
1✔
378
                for x := range 16 {
2✔
379
                        topVal := reconY[off+x]
1✔
380
                        for y := range 16 {
2✔
381
                                pred[y*16+x] = topVal
1✔
382
                        }
1✔
383
                }
384
        case 1: // Horizontal — each row gets left column pixel
1✔
385
                for y := range 16 {
2✔
386
                        leftVal := reconY[(mbY*16+y)*strideY+mbX*16-1]
1✔
387
                        for x := range 16 {
2✔
388
                                pred[y*16+x] = leftVal
1✔
389
                        }
1✔
390
                }
391
        case 2: // DC — uniform value
1✔
392
                dcVal := computeLumaDCPred(reconY, strideY, mbX, mbY)
1✔
393
                for i := range pred {
2✔
394
                        pred[i] = dcVal
1✔
395
                }
1✔
396
        }
397
        return pred
1✔
398
}
399

400
// chromaPredict8x8 computes the per-pixel chroma prediction array for the given mode.
401
// This matches the decoder's chroma prediction: DC gives per-sub-block averages,
402
// V copies the top row, H copies the left column.
403
func chromaPredict8x8(recon []uint8, strideC, mbX, mbY, mode int) [64]uint8 {
1✔
404
        var pred [64]uint8
1✔
405
        switch mode {
1✔
406
        case 0: // DC — per-sub-block DC prediction
1✔
407
                dcPreds := computeChromaDCPreds4x4(recon, strideC, mbX, mbY)
1✔
408
                for blk := range 4 {
2✔
409
                        x0 := (blk % 2) * 4
1✔
410
                        y0 := (blk / 2) * 4
1✔
411
                        for y := range 4 {
2✔
412
                                for x := range 4 {
2✔
413
                                        pred[(y0+y)*8+x0+x] = dcPreds[blk]
1✔
414
                                }
1✔
415
                        }
416
                }
417
        case 1: // Horizontal — each row gets left column pixel
1✔
418
                for y := range 8 {
2✔
419
                        leftVal := recon[(mbY*8+y)*strideC+mbX*8-1]
1✔
420
                        for x := range 8 {
2✔
421
                                pred[y*8+x] = leftVal
1✔
422
                        }
1✔
423
                }
424
        case 2: // Vertical — each column gets top row pixel
1✔
425
                off := (mbY*8-1)*strideC + mbX*8
1✔
426
                for x := range 8 {
2✔
427
                        topVal := recon[off+x]
1✔
428
                        for y := range 8 {
2✔
429
                                pred[y*8+x] = topVal
1✔
430
                        }
1✔
431
                }
432
        }
433
        return pred
1✔
434
}
435

436
func absInt(v int) int {
1✔
437
        if v < 0 {
2✔
438
                return -v
1✔
439
        }
1✔
440
        return v
1✔
441
}
442

443
// computeChromaDCPreds4x4 computes per-sub-block chroma DC predictions,
444
// matching the decoder's predictChromaDC8x8 (spec section 8.3.4.1).
445
// Returns [4]uint8 for sub-blocks: TL(0), TR(1), BL(2), BR(3).
446
func computeChromaDCPreds4x4(recon []uint8, strideC, mbX, mbY int) [4]uint8 {
1✔
447
        hasTop := mbY > 0
1✔
448
        hasLeft := mbX > 0
1✔
449

1✔
450
        var top [8]int
1✔
451
        var left [8]int
1✔
452
        if hasTop {
2✔
453
                off := (mbY*8-1)*strideC + mbX*8
1✔
454
                for x := range 8 {
2✔
455
                        top[x] = int(recon[off+x])
1✔
456
                }
1✔
457
        }
458
        if hasLeft {
2✔
459
                off := mbY*8*strideC + mbX*8 - 1
1✔
460
                for y := range 8 {
2✔
461
                        left[y] = int(recon[off+y*strideC])
1✔
462
                }
1✔
463
        }
464

465
        var preds [4]uint8
1✔
466
        for blk := range 4 {
2✔
467
                x0 := (blk % 2) * 4
1✔
468
                y0 := (blk / 2) * 4
1✔
469

1✔
470
                sum := 0
1✔
471
                count := 0
1✔
472

1✔
473
                isTopRow := blk < 2
1✔
474
                isLeftCol := blk%2 == 0
1✔
475

1✔
476
                useTop := false
1✔
477
                useLeft := false
1✔
478

1✔
479
                switch {
1✔
480
                case isTopRow && isLeftCol: // TL: use both if available
1✔
481
                        useTop = hasTop
1✔
482
                        useLeft = hasLeft
1✔
483
                case isTopRow && !isLeftCol: // TR: prefer top, fallback to left
1✔
484
                        if hasTop {
2✔
485
                                useTop = true
1✔
486
                        } else if hasLeft {
3✔
487
                                useLeft = true
1✔
488
                        }
1✔
489
                case !isTopRow && isLeftCol: // BL: prefer left, fallback to top
1✔
490
                        if hasLeft {
2✔
491
                                useLeft = true
1✔
492
                        } else if hasTop {
3✔
493
                                useTop = true
1✔
494
                        }
1✔
495
                default: // BR: use both if available
1✔
496
                        useTop = hasTop
1✔
497
                        useLeft = hasLeft
1✔
498
                }
499

500
                if useTop {
2✔
501
                        for x := x0; x < x0+4; x++ {
2✔
502
                                sum += top[x]
1✔
503
                                count++
1✔
504
                        }
1✔
505
                }
506
                if useLeft {
2✔
507
                        for y := y0; y < y0+4; y++ {
2✔
508
                                sum += left[y]
1✔
509
                                count++
1✔
510
                        }
1✔
511
                }
512

513
                if count > 0 {
2✔
514
                        preds[blk] = uint8((sum + count/2) / count)
1✔
515
                } else {
2✔
516
                        preds[blk] = 128
1✔
517
                }
1✔
518
        }
519
        return preds
1✔
520
}
521

522
func computeNC4x4(nCLuma [][]int, blkX, blkY int) int {
1✔
523
        hasA := blkX > 0
1✔
524
        hasB := blkY > 0
1✔
525

1✔
526
        nA, nB := 0, 0
1✔
527
        if hasA {
2✔
528
                nA = nCLuma[blkY][blkX-1]
1✔
529
        }
1✔
530
        if hasB {
2✔
531
                nB = nCLuma[blkY-1][blkX]
1✔
532
        }
1✔
533

534
        if hasA && hasB {
2✔
535
                return (nA + nB + 1) / 2
1✔
536
        }
1✔
537
        if hasA {
2✔
538
                return nA
1✔
539
        }
1✔
540
        if hasB {
2✔
541
                return nB
1✔
542
        }
1✔
543
        return 0
1✔
544
}
545

546
func clipU8(v int) uint8 {
1✔
547
        if v < 0 {
1✔
548
                return 0
×
549
        }
×
550
        if v > 255 {
1✔
UNCOV
551
                return 255
×
UNCOV
552
        }
×
553
        return uint8(v)
1✔
554
}
555

556
// Inlined inverse transforms to avoid import cycle with transform package
557
func dequantDC4x4(coeffs [16]int32, qp int, weightScaleDC int32) [16]int32 {
1✔
558
        var result [16]int32
1✔
559
        qpPer := qp / 6
1✔
560
        qpRem := qp % 6
1✔
561
        levelScale := levelScale4x4[qpRem][0] * weightScaleDC
1✔
562
        if qpPer >= 6 {
1✔
UNCOV
563
                for i := range 16 {
×
UNCOV
564
                        result[i] = coeffs[i] * levelScale << uint(qpPer-6)
×
UNCOV
565
                }
×
566
        } else {
1✔
567
                for i := range 16 {
2✔
568
                        result[i] = (coeffs[i]*levelScale + (1 << uint(5-qpPer))) >> uint(6-qpPer)
1✔
569
                }
1✔
570
        }
571
        return result
1✔
572
}
573

574
func dequantChromaDC2x2(coeffs [4]int32, qpc int, weightScaleDC int32) [4]int32 {
1✔
575
        var result [4]int32
1✔
576
        qpPer := qpc / 6
1✔
577
        qpRem := qpc % 6
1✔
578
        levelScale := levelScale4x4[qpRem][0] * weightScaleDC
1✔
579
        if qpPer >= 5 {
1✔
UNCOV
580
                for i := range 4 {
×
UNCOV
581
                        result[i] = coeffs[i] * levelScale << uint(qpPer-5)
×
UNCOV
582
                }
×
583
        } else {
1✔
584
                for i := range 4 {
2✔
585
                        result[i] = (coeffs[i] * levelScale) >> uint(5-qpPer)
1✔
586
                }
1✔
587
        }
588
        return result
1✔
589
}
590

591
func inverseHadamard4x4(coeffs [16]int32) [16]int32 {
1✔
592
        var temp [16]int32
1✔
593
        for i := range 4 {
2✔
594
                s0, s1, s2, s3 := coeffs[i*4], coeffs[i*4+1], coeffs[i*4+2], coeffs[i*4+3]
1✔
595
                temp[i*4+0] = s0 + s1 + s2 + s3
1✔
596
                temp[i*4+1] = s0 + s1 - s2 - s3
1✔
597
                temp[i*4+2] = s0 - s1 - s2 + s3
1✔
598
                temp[i*4+3] = s0 - s1 + s2 - s3
1✔
599
        }
1✔
600
        var result [16]int32
1✔
601
        for j := range 4 {
2✔
602
                f0, f1, f2, f3 := temp[j], temp[4+j], temp[8+j], temp[12+j]
1✔
603
                result[j] = f0 + f1 + f2 + f3
1✔
604
                result[4+j] = f0 + f1 - f2 - f3
1✔
605
                result[8+j] = f0 - f1 - f2 + f3
1✔
606
                result[12+j] = f0 - f1 + f2 - f3
1✔
607
        }
1✔
608
        return result
1✔
609
}
610

611
func inverseHadamard2x2(coeffs [4]int32) [4]int32 {
1✔
612
        return [4]int32{
1✔
613
                coeffs[0] + coeffs[1] + coeffs[2] + coeffs[3],
1✔
614
                coeffs[0] - coeffs[1] + coeffs[2] - coeffs[3],
1✔
615
                coeffs[0] + coeffs[1] - coeffs[2] - coeffs[3],
1✔
616
                coeffs[0] - coeffs[1] - coeffs[2] + coeffs[3],
1✔
617
        }
1✔
618
}
1✔
619

620
var levelScale4x4 = [6][3]int32{
621
        {10, 13, 16},
622
        {11, 14, 18},
623
        {13, 16, 20},
624
        {14, 18, 23},
625
        {16, 20, 25},
626
        {18, 23, 29},
627
}
628

629
// inverseRasterX4x4 maps 4x4 block index (0-15) to x position within MB.
630
var inverseRasterX4x4 = [16]int{
631
        0, 4, 0, 4, 8, 12, 8, 12,
632
        0, 4, 0, 4, 8, 12, 8, 12,
633
}
634

635
// inverseRasterY4x4 maps 4x4 block index (0-15) to y position within MB.
636
var inverseRasterY4x4 = [16]int{
637
        0, 0, 4, 4, 0, 0, 4, 4,
638
        8, 8, 12, 12, 8, 8, 12, 12,
639
}
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