CPU patterns fallback
This commit is contained in:
+39
-48
@@ -11,79 +11,57 @@ type YCbCr10 struct {
|
|||||||
Cr uint32
|
Cr uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
type PixelSampler func(x, y, width, height, frameIndex int) YCbCr10
|
// FrameRenderer writes a complete frame or patches part of an existing frame.
|
||||||
|
type FrameRenderer func(dst []byte, width, height, frameIndex int) error
|
||||||
|
|
||||||
type CPUGenerator struct {
|
type CPUGenerator struct {
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
sampler PixelSampler
|
base []byte
|
||||||
|
patch FrameRenderer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCPUGenerator(
|
func NewCPUGenerator(
|
||||||
width, height uint,
|
width, height uint,
|
||||||
sampler PixelSampler,
|
baseRenderer FrameRenderer,
|
||||||
|
patch FrameRenderer,
|
||||||
) (*CPUGenerator, error) {
|
) (*CPUGenerator, error) {
|
||||||
if width == 0 || height == 0 {
|
if width == 0 || height == 0 {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf("cpu: width and height must be greater than zero, got %dx%d", width, height)
|
||||||
"width and height must be greater than 0, got: %dx%d",
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if width%6 != 0 {
|
if width%6 != 0 {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf("cpu: width must be divisible by 6, got %d", width)
|
||||||
"width must be divisible by 6, got: %d",
|
|
||||||
width,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if sampler == nil {
|
if baseRenderer == nil {
|
||||||
return nil, fmt.Errorf("pixel sampler is nil")
|
return nil, fmt.Errorf("cpu: base renderer is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &CPUGenerator{
|
g := &CPUGenerator{
|
||||||
width: int(width),
|
width: int(width),
|
||||||
height: int(height),
|
height: int(height),
|
||||||
sampler: sampler,
|
base: make([]byte, int(width/6*height)*16),
|
||||||
}, nil
|
patch: patch,
|
||||||
|
}
|
||||||
|
if err := baseRenderer(g.base, g.width, g.height, 0); err != nil {
|
||||||
|
return nil, fmt.Errorf("cpu: render base frame: %w", err)
|
||||||
|
}
|
||||||
|
return g, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *CPUGenerator) GenerateFrame(dest []byte, frameIndex int) error {
|
func (g *CPUGenerator) GenerateFrame(dst []byte, frameIndex int) error {
|
||||||
frameSize := (g.width / 6) * g.height * 16
|
if len(dst) < len(g.base) {
|
||||||
if len(dest) < frameSize {
|
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"cpu: destination is too small: got %d bytes, need %d",
|
"cpu: destination is too small: got %d bytes, need %d",
|
||||||
len(dest),
|
len(dst),
|
||||||
frameSize,
|
len(g.base),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const componentMask uint32 = 0x3ff
|
copy(dst, g.base)
|
||||||
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 |
|
if g.patch != nil {
|
||||||
(pixels[0].Y&componentMask)<<10 |
|
if err := g.patch(dst[:len(g.base)], g.width, g.height, frameIndex); err != nil {
|
||||||
(pixels[0].Cr&componentMask)<<20
|
return fmt.Errorf("cpu: patch frame %d: %w", frameIndex, err)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,3 +69,16 @@ func (g *CPUGenerator) GenerateFrame(dest []byte, frameIndex int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *CPUGenerator) Close() error { return nil }
|
func (g *CPUGenerator) Close() error { return nil }
|
||||||
|
|
||||||
|
func packV210Block(dst []byte, pixels [6]YCbCr10) {
|
||||||
|
const mask uint32 = 0x3ff
|
||||||
|
word0 := pixels[0].Cb&mask | (pixels[0].Y&mask)<<10 | (pixels[0].Cr&mask)<<20
|
||||||
|
word1 := pixels[1].Y&mask | (pixels[2].Cb&mask)<<10 | (pixels[2].Y&mask)<<20
|
||||||
|
word2 := pixels[2].Cr&mask | (pixels[3].Y&mask)<<10 | (pixels[4].Cb&mask)<<20
|
||||||
|
word3 := pixels[4].Y&mask | (pixels[4].Cr&mask)<<10 | (pixels[5].Y&mask)<<20
|
||||||
|
|
||||||
|
binary.LittleEndian.PutUint32(dst, word0)
|
||||||
|
binary.LittleEndian.PutUint32(dst[4:], word1)
|
||||||
|
binary.LittleEndian.PutUint32(dst[8:], word2)
|
||||||
|
binary.LittleEndian.PutUint32(dst[12:], word3)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package generator
|
||||||
|
|
||||||
|
import "math"
|
||||||
|
|
||||||
|
const movingSquareSize = 150
|
||||||
|
|
||||||
|
type baseColorFunc func(x, y, width, height int) YCbCr10
|
||||||
|
|
||||||
|
type squareBounds struct {
|
||||||
|
minX float64
|
||||||
|
maxX float64
|
||||||
|
minY float64
|
||||||
|
maxY float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func movingSquareBounds(width, height, frameIndex int) squareBounds {
|
||||||
|
const half = float64(movingSquareSize) / 2
|
||||||
|
centerX := float64(width) / 2
|
||||||
|
centerY := float64(height) / 2
|
||||||
|
time := float64(frameIndex) / 25.0
|
||||||
|
offset := math.Sin(time*0.5) * (centerX - half)
|
||||||
|
return squareBounds{
|
||||||
|
minX: centerX - half + offset,
|
||||||
|
maxX: centerX + half + offset,
|
||||||
|
minY: centerY - half,
|
||||||
|
maxY: centerY + half,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b squareBounds) contains(x, y int) bool {
|
||||||
|
return float64(x) >= b.minX && float64(x) < b.maxX &&
|
||||||
|
float64(y) >= b.minY && float64(y) < b.maxY
|
||||||
|
}
|
||||||
|
|
||||||
|
func patchMovingSquare(
|
||||||
|
dst []byte,
|
||||||
|
width, height, frameIndex int,
|
||||||
|
baseColor baseColorFunc,
|
||||||
|
) error {
|
||||||
|
bounds := movingSquareBounds(width, height, frameIndex)
|
||||||
|
|
||||||
|
firstPixelX := max(0, int(math.Floor(bounds.minX)))
|
||||||
|
lastPixelX := min(width, int(math.Ceil(bounds.maxX)))
|
||||||
|
firstBlockX := firstPixelX / 6 * 6
|
||||||
|
lastBlockX := min(width, (lastPixelX+5)/6*6)
|
||||||
|
firstY := max(0, int(math.Floor(bounds.minY)))
|
||||||
|
lastY := min(height, int(math.Ceil(bounds.maxY)))
|
||||||
|
|
||||||
|
for y := firstY; y < lastY; y++ {
|
||||||
|
for x := firstBlockX; x < lastBlockX; x += 6 {
|
||||||
|
var pixels [6]YCbCr10
|
||||||
|
for i := range pixels {
|
||||||
|
px := x + i
|
||||||
|
color := baseColor(px, y, width, height)
|
||||||
|
if bounds.contains(px, y) {
|
||||||
|
color = invertStudioRange(color)
|
||||||
|
}
|
||||||
|
pixels[i] = color
|
||||||
|
}
|
||||||
|
offset := (y*width + x) / 6 * 16
|
||||||
|
packV210Block(dst[offset:offset+16], pixels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func invertStudioRange(color YCbCr10) YCbCr10 {
|
||||||
|
return YCbCr10{
|
||||||
|
Y: 1004 - color.Y,
|
||||||
|
Cb: 1024 - color.Cb,
|
||||||
|
Cr: 1024 - color.Cr,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package generator
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
var ebu75Colors = [...]YCbCr10{
|
||||||
|
{Y: 721, Cb: 512, Cr: 512},
|
||||||
|
{Y: 674, Cb: 176, Cr: 543},
|
||||||
|
{Y: 581, Cb: 589, Cr: 176},
|
||||||
|
{Y: 534, Cb: 253, Cr: 207},
|
||||||
|
{Y: 251, Cb: 771, Cr: 817},
|
||||||
|
{Y: 204, Cb: 435, Cr: 848},
|
||||||
|
{Y: 111, Cb: 848, Cr: 481},
|
||||||
|
{Y: 64, Cb: 512, Cr: 512},
|
||||||
|
}
|
||||||
|
|
||||||
|
var ebu100Colors = [...]YCbCr10{
|
||||||
|
{Y: 940, Cb: 512, Cr: 512},
|
||||||
|
{Y: 877, Cb: 64, Cr: 553},
|
||||||
|
{Y: 754, Cb: 615, Cr: 64},
|
||||||
|
{Y: 691, Cb: 167, Cr: 105},
|
||||||
|
{Y: 313, Cb: 857, Cr: 919},
|
||||||
|
{Y: 250, Cb: 409, Cr: 960},
|
||||||
|
{Y: 127, Cb: 960, Cr: 471},
|
||||||
|
{Y: 64, Cb: 512, Cr: 512},
|
||||||
|
}
|
||||||
|
|
||||||
|
var grayBarsColors = [...]YCbCr10{
|
||||||
|
{Y: 64, Cb: 512, Cr: 512},
|
||||||
|
{Y: 137, Cb: 512, Cr: 512},
|
||||||
|
{Y: 210, Cb: 512, Cr: 512},
|
||||||
|
{Y: 283, Cb: 512, Cr: 512},
|
||||||
|
{Y: 356, Cb: 512, Cr: 512},
|
||||||
|
{Y: 429, Cb: 512, Cr: 512},
|
||||||
|
{Y: 502, Cb: 512, Cr: 512},
|
||||||
|
{Y: 575, Cb: 512, Cr: 512},
|
||||||
|
{Y: 648, Cb: 512, Cr: 512},
|
||||||
|
{Y: 721, Cb: 512, Cr: 512},
|
||||||
|
{Y: 794, Cb: 512, Cr: 512},
|
||||||
|
{Y: 867, Cb: 512, Cr: 512},
|
||||||
|
{Y: 940, Cb: 512, Cr: 512},
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ebu75BaseColor = colorBars(ebu75Colors[:])
|
||||||
|
ebu100BaseColor = colorBars(ebu100Colors[:])
|
||||||
|
grayBarsBaseColor = colorBars(grayBarsColors[:])
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewCPUPatternGenerator(width, height uint, pattern string) (*CPUGenerator, error) {
|
||||||
|
var baseColor baseColorFunc
|
||||||
|
var dynamic bool
|
||||||
|
switch pattern {
|
||||||
|
case "ebu75":
|
||||||
|
baseColor = ebu75BaseColor
|
||||||
|
case "ebu75-move":
|
||||||
|
baseColor, dynamic = ebu75BaseColor, true
|
||||||
|
case "ebu100":
|
||||||
|
baseColor = ebu100BaseColor
|
||||||
|
case "ebu100-move":
|
||||||
|
baseColor, dynamic = ebu100BaseColor, true
|
||||||
|
case "gray-bars":
|
||||||
|
baseColor = grayBarsBaseColor
|
||||||
|
case "gray-bars-move":
|
||||||
|
baseColor, dynamic = grayBarsBaseColor, true
|
||||||
|
case "gray-ramp":
|
||||||
|
baseColor = grayRampBaseColor
|
||||||
|
case "gray-ramp-move":
|
||||||
|
baseColor, dynamic = grayRampBaseColor, true
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("cpu pattern %q is not implemented", pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
var patch FrameRenderer
|
||||||
|
if dynamic {
|
||||||
|
patch = movingSquarePatch(baseColor)
|
||||||
|
}
|
||||||
|
return NewCPUGenerator(width, height, baseRenderer(baseColor), patch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func grayRampBaseColor(x, _, width, _ int) YCbCr10 {
|
||||||
|
return YCbCr10{
|
||||||
|
Y: uint32(64 + (x*876)/width),
|
||||||
|
Cb: 512,
|
||||||
|
Cr: 512,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func colorBars(colors []YCbCr10) baseColorFunc {
|
||||||
|
return func(x, _, width, _ int) YCbCr10 {
|
||||||
|
bar := min(x*len(colors)/width, len(colors)-1)
|
||||||
|
return colors[bar]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func baseRenderer(baseColor baseColorFunc) FrameRenderer {
|
||||||
|
return func(dst []byte, width, height, _ int) error {
|
||||||
|
return renderBasePattern(dst, width, height, baseColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func movingSquarePatch(baseColor baseColorFunc) FrameRenderer {
|
||||||
|
return func(dst []byte, width, height, frameIndex int) error {
|
||||||
|
return patchMovingSquare(dst, width, height, frameIndex, baseColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderBasePattern(dst []byte, width, height int, baseColor baseColorFunc) error {
|
||||||
|
for y := 0; y < height; y++ {
|
||||||
|
for x := 0; x < width; x += 6 {
|
||||||
|
var pixels [6]YCbCr10
|
||||||
|
for i := range pixels {
|
||||||
|
pixels[i] = baseColor(x+i, y, width, height)
|
||||||
|
}
|
||||||
|
offset := (y*width + x) / 6 * 16
|
||||||
|
packV210Block(dst[offset:offset+16], pixels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package generator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCPUEBU75Static(t *testing.T) {
|
||||||
|
const width, height = 1920, 1080
|
||||||
|
g, err := NewCPUPatternGenerator(width, height, "ebu75")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewCPUPatternGenerator: %v", err)
|
||||||
|
}
|
||||||
|
frame := make([]byte, width*height*8/3)
|
||||||
|
if err := g.GenerateFrame(frame, 99); err != nil {
|
||||||
|
t.Fatalf("GenerateFrame: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
x int
|
||||||
|
wantY, wantCb, wantCr uint32
|
||||||
|
}{
|
||||||
|
{x: 0, wantY: 721, wantCb: 512, wantCr: 512},
|
||||||
|
{x: 300, wantY: 674, wantCb: 176, wantCr: 543},
|
||||||
|
{x: 960, wantY: 251, wantCb: 771, wantCr: 817},
|
||||||
|
{x: 1800, wantY: 64, wantCb: 512, wantCr: 512},
|
||||||
|
} {
|
||||||
|
y, cb, cr := sampleV210(frame, width, tc.x, 100)
|
||||||
|
if y != tc.wantY || cb != tc.wantCb || cr != tc.wantCr {
|
||||||
|
t.Fatalf("x=%d: got %d/%d/%d, want %d/%d/%d",
|
||||||
|
tc.x, y, cb, cr, tc.wantY, tc.wantCb, tc.wantCr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCPUEBU75MovingSquare(t *testing.T) {
|
||||||
|
const width, height = 1920, 1080
|
||||||
|
g, err := NewCPUPatternGenerator(width, height, "ebu75-move")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewCPUPatternGenerator: %v", err)
|
||||||
|
}
|
||||||
|
frame := make([]byte, width*height*8/3)
|
||||||
|
|
||||||
|
if err := g.GenerateFrame(frame, 0); err != nil {
|
||||||
|
t.Fatalf("GenerateFrame(0): %v", err)
|
||||||
|
}
|
||||||
|
if y, cb, cr := sampleV210(frame, width, 960, 540); y != 753 || cb != 253 || cr != 207 {
|
||||||
|
t.Fatalf("tick 0 center = %d/%d/%d, want inverted magenta 753/253/207", y, cb, cr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.GenerateFrame(frame, 79); err != nil {
|
||||||
|
t.Fatalf("GenerateFrame(79): %v", err)
|
||||||
|
}
|
||||||
|
if y, cb, cr := sampleV210(frame, width, 960, 540); y != 251 || cb != 771 || cr != 817 {
|
||||||
|
t.Fatalf("old square position = %d/%d/%d, want restored magenta 251/771/817", y, cb, cr)
|
||||||
|
}
|
||||||
|
if y, cb, cr := sampleV210(frame, width, 1840, 540); y != 940 || cb != 512 || cr != 512 {
|
||||||
|
t.Fatalf("shifted square = %d/%d/%d, want inverted black 940/512/512", y, cb, cr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCPUStaticPatterns(t *testing.T) {
|
||||||
|
const width, height = 1920, 1080
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
x int
|
||||||
|
wantY, wantCb, wantCr uint32
|
||||||
|
}{
|
||||||
|
{name: "ebu100", x: 0, wantY: 940, wantCb: 512, wantCr: 512},
|
||||||
|
{name: "ebu100", x: 300, wantY: 877, wantCb: 64, wantCr: 553},
|
||||||
|
{name: "ebu100", x: 960, wantY: 313, wantCb: 857, wantCr: 919},
|
||||||
|
{name: "gray-bars", x: 0, wantY: 64, wantCb: 512, wantCr: 512},
|
||||||
|
{name: "gray-bars", x: 960, wantY: 502, wantCb: 512, wantCr: 512},
|
||||||
|
{name: "gray-bars", x: 1800, wantY: 940, wantCb: 512, wantCr: 512},
|
||||||
|
{name: "gray-ramp", x: 0, wantY: 64, wantCb: 512, wantCr: 512},
|
||||||
|
{name: "gray-ramp", x: 6, wantY: 66, wantCb: 512, wantCr: 512},
|
||||||
|
{name: "gray-ramp", x: 960, wantY: 502, wantCb: 512, wantCr: 512},
|
||||||
|
{name: "gray-ramp", x: 1918, wantY: 939, wantCb: 512, wantCr: 512},
|
||||||
|
}
|
||||||
|
|
||||||
|
frames := make(map[string][]byte)
|
||||||
|
for _, tc := range tests {
|
||||||
|
frame, ok := frames[tc.name]
|
||||||
|
if !ok {
|
||||||
|
g, err := NewCPUPatternGenerator(width, height, tc.name)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewCPUPatternGenerator(%q): %v", tc.name, err)
|
||||||
|
}
|
||||||
|
frame = make([]byte, width*height*8/3)
|
||||||
|
if err := g.GenerateFrame(frame, 0); err != nil {
|
||||||
|
t.Fatalf("GenerateFrame(%q): %v", tc.name, err)
|
||||||
|
}
|
||||||
|
frames[tc.name] = frame
|
||||||
|
}
|
||||||
|
|
||||||
|
y, cb, cr := sampleV210(frame, width, tc.x, 100)
|
||||||
|
if y != tc.wantY || cb != tc.wantCb || cr != tc.wantCr {
|
||||||
|
t.Errorf("%s x=%d: got %d/%d/%d, want %d/%d/%d",
|
||||||
|
tc.name, tc.x, y, cb, cr, tc.wantY, tc.wantCb, tc.wantCr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCPUMovingPatternUsesOwnBaseColor(t *testing.T) {
|
||||||
|
const width, height = 1920, 1080
|
||||||
|
g, err := NewCPUPatternGenerator(width, height, "ebu100-move")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewCPUPatternGenerator: %v", err)
|
||||||
|
}
|
||||||
|
frame := make([]byte, width*height*8/3)
|
||||||
|
if err := g.GenerateFrame(frame, 0); err != nil {
|
||||||
|
t.Fatalf("GenerateFrame: %v", err)
|
||||||
|
}
|
||||||
|
if y, cb, cr := sampleV210(frame, width, 960, 540); y != 691 || cb != 167 || cr != 105 {
|
||||||
|
t.Fatalf("center = %d/%d/%d, want inverted EBU100 magenta 691/167/105", y, cb, cr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCPUSMPTEIsNotFakedByAnotherPattern(t *testing.T) {
|
||||||
|
_, err := NewCPUPatternGenerator(1920, 1080, "smpte")
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "not implemented") {
|
||||||
|
t.Fatalf("error = %v, want not implemented error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewCPUPatternGeneratorRejectsUnknownPattern(t *testing.T) {
|
||||||
|
_, err := NewCPUPatternGenerator(1920, 1080, "unknown")
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "not implemented") {
|
||||||
|
t.Fatalf("error = %v, want unsupported pattern error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +1,37 @@
|
|||||||
package generator
|
package generator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func fillFrame(value byte) FrameRenderer {
|
||||||
|
return func(dst []byte, _, _, _ int) error {
|
||||||
|
for i := range dst {
|
||||||
|
dst[i] = value
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNewCPUGeneratorValidation(t *testing.T) {
|
func TestNewCPUGeneratorValidation(t *testing.T) {
|
||||||
sampler := func(_, _, _, _, _ int) YCbCr10 { return YCbCr10{} }
|
renderer := fillFrame(0)
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
width uint
|
width uint
|
||||||
height uint
|
height uint
|
||||||
sampler PixelSampler
|
renderer FrameRenderer
|
||||||
wantErrSub string
|
wantErrSub string
|
||||||
}{
|
}{
|
||||||
{name: "zero width", height: 1, sampler: sampler, wantErrSub: "greater than 0"},
|
{name: "zero width", height: 1, renderer: renderer, wantErrSub: "greater than zero"},
|
||||||
{name: "zero height", width: 6, sampler: sampler, wantErrSub: "greater than 0"},
|
{name: "zero height", width: 6, renderer: renderer, wantErrSub: "greater than zero"},
|
||||||
{name: "unaligned width", width: 7, height: 1, sampler: sampler, wantErrSub: "divisible by 6"},
|
{name: "unaligned width", width: 7, height: 1, renderer: renderer, wantErrSub: "divisible by 6"},
|
||||||
{name: "nil sampler", width: 6, height: 1, wantErrSub: "sampler is nil"},
|
{name: "nil renderer", width: 6, height: 1, wantErrSub: "renderer is nil"},
|
||||||
}
|
}
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
_, err := NewCPUGenerator(tc.width, tc.height, tc.sampler)
|
_, err := NewCPUGenerator(tc.width, tc.height, tc.renderer, nil)
|
||||||
if err == nil || !strings.Contains(err.Error(), tc.wantErrSub) {
|
if err == nil || !strings.Contains(err.Error(), tc.wantErrSub) {
|
||||||
t.Fatalf("error = %v, want substring %q", err, tc.wantErrSub)
|
t.Fatalf("error = %v, want substring %q", err, tc.wantErrSub)
|
||||||
}
|
}
|
||||||
@@ -29,63 +39,84 @@ func TestNewCPUGeneratorValidation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCPUGeneratorGenerateFrame(t *testing.T) {
|
func TestCPUGeneratorCopiesBaseAndRestoresBeforePatch(t *testing.T) {
|
||||||
const width, height = 12, 2
|
patch := func(dst []byte, _, _, frameIndex int) error {
|
||||||
const frameIndex = 7
|
switch frameIndex {
|
||||||
g, err := NewCPUGenerator(width, height, func(x, y, _, _ int, tick int) YCbCr10 {
|
case 1:
|
||||||
return YCbCr10{
|
dst[0] = 0x22
|
||||||
Y: uint32(100 + x + 10*y + tick),
|
case 2:
|
||||||
Cb: uint32(200 + x + 10*y + tick),
|
dst[16] = 0x33
|
||||||
Cr: uint32(300 + x + 10*y + tick),
|
|
||||||
}
|
}
|
||||||
})
|
return nil
|
||||||
|
}
|
||||||
|
g, err := NewCPUGenerator(12, 1, fillFrame(0x11), patch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("NewCPUGenerator: %v", err)
|
t.Fatalf("NewCPUGenerator: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
frame := make([]byte, width*height*8/3)
|
frame := make([]byte, 32)
|
||||||
if err := g.GenerateFrame(frame, frameIndex); err != nil {
|
if err := g.GenerateFrame(frame, 1); err != nil {
|
||||||
t.Fatalf("GenerateFrame: %v", err)
|
t.Fatalf("GenerateFrame(1): %v", err)
|
||||||
|
}
|
||||||
|
if frame[0] != 0x22 {
|
||||||
|
t.Fatalf("frame 1 patch byte = %#x, want 0x22", frame[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
for y := 0; y < height; y++ {
|
if err := g.GenerateFrame(frame, 2); err != nil {
|
||||||
for x := 0; x < width; x++ {
|
t.Fatalf("GenerateFrame(2): %v", err)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
if frame[0] != 0x11 {
|
||||||
|
t.Fatalf("old patch byte = %#x, want restored base 0x11", frame[0])
|
||||||
|
}
|
||||||
|
if frame[16] != 0x33 {
|
||||||
|
t.Fatalf("frame 2 patch byte = %#x, want 0x33", frame[16])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPackV210Block(t *testing.T) {
|
||||||
|
var pixels [6]YCbCr10
|
||||||
|
for i := range pixels {
|
||||||
|
pixels[i] = YCbCr10{
|
||||||
|
Y: uint32(100 + i),
|
||||||
|
Cb: uint32(200 + i),
|
||||||
|
Cr: uint32(300 + i),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
frame := make([]byte, 16)
|
||||||
|
packV210Block(frame, pixels)
|
||||||
|
|
||||||
|
for x := range pixels {
|
||||||
|
y, cb, cr := sampleV210(frame, 6, x, 0)
|
||||||
|
chromaX := x - x%2
|
||||||
|
if y != pixels[x].Y || cb != pixels[chromaX].Cb || cr != pixels[chromaX].Cr {
|
||||||
|
t.Fatalf("pixel %d: got %d/%d/%d, want %d/%d/%d",
|
||||||
|
x, y, cb, cr, pixels[x].Y, pixels[chromaX].Cb, pixels[chromaX].Cr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCPUGeneratorMasksComponentsToTenBits(t *testing.T) {
|
func TestPackV210BlockMasksComponents(t *testing.T) {
|
||||||
g, err := NewCPUGenerator(6, 1, func(_, _, _, _, _ int) YCbCr10 {
|
pixel := YCbCr10{Y: 0xC01, Cb: 0xC02, Cr: 0xC03}
|
||||||
return YCbCr10{Y: 0xC01, Cb: 0xC02, Cr: 0xC03}
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewCPUGenerator: %v", err)
|
|
||||||
}
|
|
||||||
frame := make([]byte, 16)
|
frame := make([]byte, 16)
|
||||||
if err := g.GenerateFrame(frame, 0); err != nil {
|
packV210Block(frame, [6]YCbCr10{pixel, pixel, pixel, pixel, pixel, pixel})
|
||||||
t.Fatalf("GenerateFrame: %v", err)
|
|
||||||
}
|
|
||||||
y, cb, cr := sampleV210(frame, 6, 0, 0)
|
y, cb, cr := sampleV210(frame, 6, 0, 0)
|
||||||
if y != 1 || cb != 2 || cr != 3 {
|
if y != 1 || cb != 2 || cr != 3 {
|
||||||
t.Fatalf("masked components = %d/%d/%d, want 1/2/3", y, cb, cr)
|
t.Fatalf("masked components = %d/%d/%d, want 1/2/3", y, cb, cr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCPUGeneratorRejectsSmallDestination(t *testing.T) {
|
func TestCPUGeneratorErrors(t *testing.T) {
|
||||||
g, err := NewCPUGenerator(6, 1, func(_, _, _, _, _ int) YCbCr10 { return YCbCr10{} })
|
wantErr := errors.New("patch failed")
|
||||||
|
g, err := NewCPUGenerator(6, 1, fillFrame(0), func([]byte, int, int, int) error {
|
||||||
|
return wantErr
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("NewCPUGenerator: %v", err)
|
t.Fatalf("NewCPUGenerator: %v", err)
|
||||||
}
|
}
|
||||||
if err := g.GenerateFrame(make([]byte, 15), 0); err == nil || !strings.Contains(err.Error(), "too small") {
|
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)
|
t.Fatalf("small destination error = %v", err)
|
||||||
|
}
|
||||||
|
if err := g.GenerateFrame(make([]byte, 16), 4); !errors.Is(err, wantErr) {
|
||||||
|
t.Fatalf("patch error = %v, want wrapped %v", err, wantErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package video
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"mxl-pattern-generator/internal/generator"
|
||||||
|
)
|
||||||
|
|
||||||
|
type generatorFactory func() (generator.FrameGenerator, error)
|
||||||
|
|
||||||
|
func newFrameGenerator(cfg Config, pattern pattern) (generator.FrameGenerator, Backend, error) {
|
||||||
|
newGPU := func() (generator.FrameGenerator, error) {
|
||||||
|
return generator.NewWGPUGenerator(cfg.Width(), cfg.Height(), pattern.kernelSource)
|
||||||
|
}
|
||||||
|
newCPU := func() (generator.FrameGenerator, error) {
|
||||||
|
return generator.NewCPUPatternGenerator(cfg.Width(), cfg.Height(), cfg.Pattern)
|
||||||
|
}
|
||||||
|
return selectFrameGenerator(cfg.Backend, newGPU, newCPU)
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectFrameGenerator(
|
||||||
|
backend Backend,
|
||||||
|
newGPU, newCPU generatorFactory,
|
||||||
|
) (generator.FrameGenerator, Backend, error) {
|
||||||
|
switch backend {
|
||||||
|
case BackendGPU:
|
||||||
|
gen, err := newGPU()
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", fmt.Errorf("initialize GPU video generator: %w", err)
|
||||||
|
}
|
||||||
|
return gen, BackendGPU, nil
|
||||||
|
|
||||||
|
case BackendCPU:
|
||||||
|
gen, err := newCPU()
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", fmt.Errorf("initialize CPU video generator: %w", err)
|
||||||
|
}
|
||||||
|
return gen, BackendCPU, nil
|
||||||
|
|
||||||
|
case "", BackendAuto:
|
||||||
|
gpuGen, gpuErr := newGPU()
|
||||||
|
if gpuErr == nil {
|
||||||
|
return gpuGen, BackendGPU, nil
|
||||||
|
}
|
||||||
|
log.Printf("GPU video generator unavailable, falling back to CPU: %v", gpuErr)
|
||||||
|
|
||||||
|
cpuGen, cpuErr := newCPU()
|
||||||
|
if cpuErr == nil {
|
||||||
|
return cpuGen, BackendCPU, nil
|
||||||
|
}
|
||||||
|
return nil, "", errors.Join(
|
||||||
|
fmt.Errorf("initialize GPU video generator: %w", gpuErr),
|
||||||
|
fmt.Errorf("initialize CPU video generator: %w", cpuErr),
|
||||||
|
)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, "", fmt.Errorf("unsupported video backend %q", backend)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package video
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"mxl-pattern-generator/internal/generator"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stubFrameGenerator struct{}
|
||||||
|
|
||||||
|
func (*stubFrameGenerator) GenerateFrame([]byte, int) error { return nil }
|
||||||
|
func (*stubFrameGenerator) Close() error { return nil }
|
||||||
|
|
||||||
|
func TestSelectFrameGenerator(t *testing.T) {
|
||||||
|
gpuErr := errors.New("no GPU")
|
||||||
|
cpuErr := errors.New("no CPU pattern")
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
backend Backend
|
||||||
|
gpuErr error
|
||||||
|
cpuErr error
|
||||||
|
wantBackend Backend
|
||||||
|
wantGPUCalls int
|
||||||
|
wantCPUCalls int
|
||||||
|
wantErrSubstr []string
|
||||||
|
}{
|
||||||
|
{name: "explicit GPU", backend: BackendGPU, wantBackend: BackendGPU, wantGPUCalls: 1},
|
||||||
|
{name: "explicit CPU", backend: BackendCPU, wantBackend: BackendCPU, wantCPUCalls: 1},
|
||||||
|
{name: "auto prefers GPU", backend: BackendAuto, wantBackend: BackendGPU, wantGPUCalls: 1},
|
||||||
|
{name: "zero value is auto", wantBackend: BackendGPU, wantGPUCalls: 1},
|
||||||
|
{
|
||||||
|
name: "auto falls back to CPU",
|
||||||
|
backend: BackendAuto,
|
||||||
|
gpuErr: gpuErr,
|
||||||
|
wantBackend: BackendCPU,
|
||||||
|
wantGPUCalls: 1,
|
||||||
|
wantCPUCalls: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "auto reports both failures",
|
||||||
|
backend: BackendAuto,
|
||||||
|
gpuErr: gpuErr,
|
||||||
|
cpuErr: cpuErr,
|
||||||
|
wantGPUCalls: 1,
|
||||||
|
wantCPUCalls: 1,
|
||||||
|
wantErrSubstr: []string{"GPU video generator", "CPU video generator"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "explicit GPU does not fall back",
|
||||||
|
backend: BackendGPU,
|
||||||
|
gpuErr: gpuErr,
|
||||||
|
wantGPUCalls: 1,
|
||||||
|
wantErrSubstr: []string{"GPU video generator"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "explicit CPU does not try GPU",
|
||||||
|
backend: BackendCPU,
|
||||||
|
cpuErr: cpuErr,
|
||||||
|
wantCPUCalls: 1,
|
||||||
|
wantErrSubstr: []string{"CPU video generator"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid backend",
|
||||||
|
backend: Backend("invalid"),
|
||||||
|
wantErrSubstr: []string{"unsupported video backend"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
gpuCalls, cpuCalls := 0, 0
|
||||||
|
factory := func(calls *int, err error) generatorFactory {
|
||||||
|
return func() (generator.FrameGenerator, error) {
|
||||||
|
*calls++
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &stubFrameGenerator{}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gen, backend, err := selectFrameGenerator(
|
||||||
|
tc.backend,
|
||||||
|
factory(&gpuCalls, tc.gpuErr),
|
||||||
|
factory(&cpuCalls, tc.cpuErr),
|
||||||
|
)
|
||||||
|
if gpuCalls != tc.wantGPUCalls || cpuCalls != tc.wantCPUCalls {
|
||||||
|
t.Fatalf("factory calls GPU/CPU = %d/%d, want %d/%d",
|
||||||
|
gpuCalls, cpuCalls, tc.wantGPUCalls, tc.wantCPUCalls)
|
||||||
|
}
|
||||||
|
if len(tc.wantErrSubstr) == 0 {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("selectFrameGenerator: %v", err)
|
||||||
|
}
|
||||||
|
if gen == nil || backend != tc.wantBackend {
|
||||||
|
t.Fatalf("generator/backend = %v/%q, want non-nil/%q", gen, backend, tc.wantBackend)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("selectFrameGenerator returned nil error")
|
||||||
|
}
|
||||||
|
for _, substring := range tc.wantErrSubstr {
|
||||||
|
if !strings.Contains(err.Error(), substring) {
|
||||||
|
t.Errorf("error = %q, want substring %q", err, substring)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,11 +19,11 @@ func Run(ctx context.Context, inst *mxl.Instance, cfg Config) (runErr error) {
|
|||||||
return fmt.Errorf("unknown video pattern %q", cfg.Pattern)
|
return fmt.Errorf("unknown video pattern %q", cfg.Pattern)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: fall back to a CPU generator if GPU initialization fails.
|
gen, backend, err := newFrameGenerator(cfg, pattern)
|
||||||
gen, err := generator.NewWGPUGenerator(cfg.Width(), cfg.Height(), pattern.kernelSource)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("initialize wgpu video generator: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
log.Printf("video generator backend: %s", backend)
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := gen.Close(); err != nil {
|
if err := gen.Close(); err != nil {
|
||||||
runErr = errors.Join(runErr, fmt.Errorf("close video generator: %w", err))
|
runErr = errors.Join(runErr, fmt.Errorf("close video generator: %w", err))
|
||||||
|
|||||||
Reference in New Issue
Block a user