diff --git a/internal/generator/cpu.go b/internal/generator/cpu.go new file mode 100644 index 0000000..30973ed --- /dev/null +++ b/internal/generator/cpu.go @@ -0,0 +1,93 @@ +package generator + +import ( + "encoding/binary" + "fmt" +) + +type YCbCr10 struct { + Y uint32 + Cb uint32 + Cr uint32 +} + +type PixelSampler func(x, y, width, height, frameIndex int) YCbCr10 + +type CPUGenerator struct { + width int + height int + sampler PixelSampler +} + +func NewCPUGenerator( + width, height uint, + sampler PixelSampler, +) (*CPUGenerator, error) { + if width == 0 || height == 0 { + return nil, fmt.Errorf( + "width and height must be greater than 0, got: %dx%d", + width, + height, + ) + } + if width%6 != 0 { + return nil, fmt.Errorf( + "width must be divisible by 6, got: %d", + width, + ) + } + if sampler == nil { + return nil, fmt.Errorf("pixel sampler is nil") + } + + return &CPUGenerator{ + width: int(width), + height: int(height), + sampler: sampler, + }, nil +} + +func (g *CPUGenerator) GenerateFrame(dest []byte, frameIndex int) error { + frameSize := (g.width / 6) * g.height * 16 + if len(dest) < frameSize { + return fmt.Errorf( + "cpu: destination is too small: got %d bytes, need %d", + len(dest), + frameSize, + ) + } + + const componentMask uint32 = 0x3ff + offset := 0 + for y := 0; y < g.height; y++ { + for x := 0; x < g.width; x += 6 { + var pixels [6]YCbCr10 + for i := range pixels { + pixels[i] = g.sampler(x+i, y, g.width, g.height, frameIndex) + } + + word0 := pixels[0].Cb&componentMask | + (pixels[0].Y&componentMask)<<10 | + (pixels[0].Cr&componentMask)<<20 + word1 := pixels[1].Y&componentMask | + (pixels[2].Cb&componentMask)<<10 | + (pixels[2].Y&componentMask)<<20 + word2 := pixels[2].Cr&componentMask | + (pixels[3].Y&componentMask)<<10 | + (pixels[4].Cb&componentMask)<<20 + word3 := pixels[4].Y&componentMask | + (pixels[4].Cr&componentMask)<<10 | + (pixels[5].Y&componentMask)<<20 + + binary.LittleEndian.PutUint32(dest[offset:], word0) + binary.LittleEndian.PutUint32(dest[offset+4:], word1) + binary.LittleEndian.PutUint32(dest[offset+8:], word2) + binary.LittleEndian.PutUint32(dest[offset+12:], word3) + offset += 16 + } + } + + return nil +} + +func (g *CPUGenerator) Close() error { return nil } diff --git a/internal/generator/cpu_test.go b/internal/generator/cpu_test.go new file mode 100644 index 0000000..c17722e --- /dev/null +++ b/internal/generator/cpu_test.go @@ -0,0 +1,91 @@ +package generator + +import ( + "strings" + "testing" +) + +func TestNewCPUGeneratorValidation(t *testing.T) { + sampler := func(_, _, _, _, _ int) YCbCr10 { return YCbCr10{} } + tests := []struct { + name string + width uint + height uint + sampler PixelSampler + wantErrSub string + }{ + {name: "zero width", height: 1, sampler: sampler, wantErrSub: "greater than 0"}, + {name: "zero height", width: 6, sampler: sampler, wantErrSub: "greater than 0"}, + {name: "unaligned width", width: 7, height: 1, sampler: sampler, wantErrSub: "divisible by 6"}, + {name: "nil sampler", width: 6, height: 1, wantErrSub: "sampler is nil"}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + _, err := NewCPUGenerator(tc.width, tc.height, tc.sampler) + if err == nil || !strings.Contains(err.Error(), tc.wantErrSub) { + t.Fatalf("error = %v, want substring %q", err, tc.wantErrSub) + } + }) + } +} + +func TestCPUGeneratorGenerateFrame(t *testing.T) { + const width, height = 12, 2 + const frameIndex = 7 + g, err := NewCPUGenerator(width, height, func(x, y, _, _ int, tick int) YCbCr10 { + return YCbCr10{ + Y: uint32(100 + x + 10*y + tick), + Cb: uint32(200 + x + 10*y + tick), + Cr: uint32(300 + x + 10*y + tick), + } + }) + if err != nil { + t.Fatalf("NewCPUGenerator: %v", err) + } + + frame := make([]byte, width*height*8/3) + if err := g.GenerateFrame(frame, frameIndex); err != nil { + t.Fatalf("GenerateFrame: %v", err) + } + + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + gotY, gotCb, gotCr := sampleV210(frame, width, x, y) + chromaX := x - x%2 + wantY := uint32(100 + x + 10*y + frameIndex) + wantCb := uint32(200 + chromaX + 10*y + frameIndex) + wantCr := uint32(300 + chromaX + 10*y + frameIndex) + if gotY != wantY || gotCb != wantCb || gotCr != wantCr { + t.Fatalf("pixel (%d,%d): got %d/%d/%d, want %d/%d/%d", + x, y, gotY, gotCb, gotCr, wantY, wantCb, wantCr) + } + } + } +} + +func TestCPUGeneratorMasksComponentsToTenBits(t *testing.T) { + g, err := NewCPUGenerator(6, 1, func(_, _, _, _, _ int) YCbCr10 { + return YCbCr10{Y: 0xC01, Cb: 0xC02, Cr: 0xC03} + }) + if err != nil { + t.Fatalf("NewCPUGenerator: %v", err) + } + frame := make([]byte, 16) + if err := g.GenerateFrame(frame, 0); err != nil { + t.Fatalf("GenerateFrame: %v", err) + } + y, cb, cr := sampleV210(frame, 6, 0, 0) + if y != 1 || cb != 2 || cr != 3 { + t.Fatalf("masked components = %d/%d/%d, want 1/2/3", y, cb, cr) + } +} + +func TestCPUGeneratorRejectsSmallDestination(t *testing.T) { + g, err := NewCPUGenerator(6, 1, func(_, _, _, _, _ int) YCbCr10 { return YCbCr10{} }) + if err != nil { + t.Fatalf("NewCPUGenerator: %v", err) + } + if err := g.GenerateFrame(make([]byte, 15), 0); err == nil || !strings.Contains(err.Error(), "too small") { + t.Fatalf("error = %v, want destination size error", err) + } +}