alpha patterns

This commit is contained in:
Dmitry Sergeev
2026-09-18 10:11:07 +03:00
parent 9b194b5591
commit dea2e6a59f
12 changed files with 643 additions and 3 deletions
+92
View File
@@ -0,0 +1,92 @@
package generator
import (
"encoding/binary"
"fmt"
"math"
)
const (
alphaTransparent uint32 = 64
alphaOpaque uint32 = 940
)
func packAlphaBlock(dst []byte, samples [3]uint32) {
const mask uint32 = 0x3ff
word := samples[0]&mask |
(samples[1]&mask)<<10 |
(samples[2]&mask)<<20
binary.LittleEndian.PutUint32(dst, word)
}
func fillAlphaPlane(
dst []byte,
width, height int,
value uint32,
) error {
need := AlphaFrameSize(width, height)
if len(dst) < need {
return fmt.Errorf(
"alpha: destination is too small: got %d bytes, need %d",
len(dst),
need,
)
}
stride := AlphaLineSize(width)
for y := 0; y < height; y++ {
row := dst[y*stride : (y+1)*stride]
for x := 0; x < width; x += 3 {
var samples [3]uint32
for i := range samples {
if x+i < width {
samples[i] = value
}
}
packAlphaBlock(row[x/3*4:], samples)
}
}
return nil
}
func patchAlphaMovingSquare(dst []byte, width, height, frameIndex int) error {
need := AlphaFrameSize(width, height)
if len(dst) < need {
return fmt.Errorf(
"alpha: destination is too small: got %d bytes, need %d",
len(dst),
need,
)
}
bounds := movingSquareBounds(width, height, frameIndex)
firstPixelX := max(0, int(math.Floor(bounds.minX)))
lastPixelX := min(width, int(math.Ceil(bounds.maxX)))
firstBlockX := firstPixelX / 3 * 3
lastBlockX := min(width, (lastPixelX+2)/3*3)
firstY := max(0, int(math.Floor(bounds.minY)))
lastY := min(height, int(math.Ceil(bounds.maxY)))
stride := AlphaLineSize(width)
for y := firstY; y < lastY; y++ {
for blockX := firstBlockX; blockX < lastBlockX; blockX += 3 {
var samples [3]uint32
for i := range samples {
x := blockX + i
switch {
case x >= width:
samples[i] = 0
case bounds.contains(x, y):
samples[i] = alphaTransparent
default:
samples[i] = alphaOpaque
}
}
offset := y*stride + blockX/3*4
packAlphaBlock(dst[offset:], samples)
}
}
return nil
}
+161
View File
@@ -0,0 +1,161 @@
package generator
import (
"encoding/binary"
"strings"
"testing"
)
func TestPackAlphaBlock(t *testing.T) {
var dst [4]byte
packAlphaBlock(dst[:], [3]uint32{64, 512, 940})
word := binary.LittleEndian.Uint32(dst[:])
if got := word & 0x3ff; got != 64 {
t.Errorf("sample 0 = %d, want 64", got)
}
if got := (word >> 10) & 0x3ff; got != 512 {
t.Errorf("sample 1 = %d, want 512", got)
}
if got := (word >> 20) & 0x3ff; got != 940 {
t.Errorf("sample 2 = %d, want 940", got)
}
if got := word >> 30; got != 0 {
t.Errorf("unused bits = %d, want 0", got)
}
}
func TestPackAlphaBlockMasksSamples(t *testing.T) {
var dst [4]byte
packAlphaBlock(dst[:], [3]uint32{0x401, 0x802, 0xc03})
word := binary.LittleEndian.Uint32(dst[:])
if got := word & 0x3ff; got != 1 {
t.Errorf("sample 0 = %d, want 1", got)
}
if got := (word >> 10) & 0x3ff; got != 2 {
t.Errorf("sample 1 = %d, want 2", got)
}
if got := (word >> 20) & 0x3ff; got != 3 {
t.Errorf("sample 2 = %d, want 3", got)
}
}
func TestFillAlphaPlaneCompleteBlocks(t *testing.T) {
const width, height = 6, 2
dst := make([]byte, AlphaFrameSize(width, height))
if err := fillAlphaPlane(dst, width, height, alphaOpaque); err != nil {
t.Fatalf("fillAlphaPlane: %v", err)
}
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
if got := sampleAlpha(dst, width, x, y); got != alphaOpaque {
t.Errorf("sample (%d,%d) = %d, want %d", x, y, got, alphaOpaque)
}
}
}
}
func TestFillAlphaPlaneZerosPartialBlockPadding(t *testing.T) {
const width, height = 4, 2
dst := make([]byte, AlphaFrameSize(width, height))
if err := fillAlphaPlane(dst, width, height, alphaTransparent); err != nil {
t.Fatalf("fillAlphaPlane: %v", err)
}
stride := AlphaLineSize(width)
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
if got := sampleAlpha(dst, width, x, y); got != alphaTransparent {
t.Errorf("sample (%d,%d) = %d, want %d", x, y, got, alphaTransparent)
}
}
lastWord := binary.LittleEndian.Uint32(dst[y*stride+4:])
if got := (lastWord >> 10) & 0x3ff; got != 0 {
t.Errorf("row %d padding sample 1 = %d, want 0", y, got)
}
if got := (lastWord >> 20) & 0x3ff; got != 0 {
t.Errorf("row %d padding sample 2 = %d, want 0", y, got)
}
if got := lastWord >> 30; got != 0 {
t.Errorf("row %d unused bits = %d, want 0", y, got)
}
}
}
func TestFillAlphaPlaneRejectsSmallDestination(t *testing.T) {
const width, height = 6, 2
dst := make([]byte, AlphaFrameSize(width, height)-1)
err := fillAlphaPlane(dst, width, height, alphaOpaque)
if err == nil || !strings.Contains(err.Error(), "destination is too small") {
t.Fatalf("error = %v, want destination size error", err)
}
}
func TestPatchAlphaMovingSquare(t *testing.T) {
const width, height = 304, 200
dst := make([]byte, AlphaFrameSize(width, height))
if err := fillAlphaPlane(dst, width, height, alphaOpaque); err != nil {
t.Fatalf("fillAlphaPlane: %v", err)
}
if err := patchAlphaMovingSquare(dst, width, height, 0); err != nil {
t.Fatalf("patchAlphaMovingSquare: %v", err)
}
if got := sampleAlpha(dst, width, width/2, height/2); got != alphaTransparent {
t.Errorf("square center = %d, want transparent %d", got, alphaTransparent)
}
if got := sampleAlpha(dst, width, 10, height/2); got != alphaOpaque {
t.Errorf("outside square = %d, want opaque %d", got, alphaOpaque)
}
// At frame zero the square begins at x=77. Its first three-sample word
// therefore contains two opaque samples followed by one transparent sample.
for x, want := range []uint32{alphaOpaque, alphaOpaque, alphaTransparent} {
if got := sampleAlpha(dst, width, 75+x, height/2); got != want {
t.Errorf("boundary sample x=%d = %d, want %d", 75+x, got, want)
}
}
}
func TestPatchAlphaMovingSquarePreservesPartialBlockPadding(t *testing.T) {
const width, height = 100, 200
dst := make([]byte, AlphaFrameSize(width, height))
if err := fillAlphaPlane(dst, width, height, alphaOpaque); err != nil {
t.Fatalf("fillAlphaPlane: %v", err)
}
if err := patchAlphaMovingSquare(dst, width, height, 0); err != nil {
t.Fatalf("patchAlphaMovingSquare: %v", err)
}
lastWordOffset := height/2*AlphaLineSize(width) + (width/3)*4
lastWord := binary.LittleEndian.Uint32(dst[lastWordOffset:])
if got := lastWord & 0x3ff; got != alphaTransparent {
t.Errorf("last visible sample = %d, want %d", got, alphaTransparent)
}
if got := lastWord >> 10; got != 0 {
t.Errorf("partial-block padding bits = %#x, want 0", got)
}
}
func TestPatchAlphaMovingSquareRejectsSmallDestination(t *testing.T) {
const width, height = 100, 200
err := patchAlphaMovingSquare(
make([]byte, AlphaFrameSize(width, height)-1),
width,
height,
0,
)
if err == nil || !strings.Contains(err.Error(), "destination is too small") {
t.Fatalf("error = %v, want destination size error", err)
}
}
func sampleAlpha(buf []byte, width, x, y int) uint32 {
offset := y*AlphaLineSize(width) + x/3*4
word := binary.LittleEndian.Uint32(buf[offset:])
return (word >> uint(x%3*10)) & 0x3ff
}
+17
View File
@@ -18,3 +18,20 @@ func V210LineSize(width int) int {
func V210FrameSize(width, height int) int {
return V210LineSize(width) * height
}
// AlphaLineSize returns the byte stride of one packed 10-bit alpha row. Each
// little-endian 32-bit word contains three alpha samples and two unused bits.
func AlphaLineSize(width int) int {
return ((width + 2) / 3) * 4
}
// AlphaFrameSize returns the size of the alpha plane in a v210a frame.
func AlphaFrameSize(width, height int) int {
return AlphaLineSize(width) * height
}
// V210AFrameSize returns the total size of a v210a payload: the complete v210
// fill plane followed by the complete packed 10-bit alpha plane.
func V210AFrameSize(width, height int) int {
return V210FrameSize(width, height) + AlphaFrameSize(width, height)
}
+41
View File
@@ -2,6 +2,7 @@ package generator
import (
"encoding/binary"
"fmt"
"testing"
)
@@ -30,6 +31,46 @@ func TestV210Sizes(t *testing.T) {
}
}
func TestAlphaSizes(t *testing.T) {
tests := []struct {
width int
height int
lineSize int
frameSize int
v210aSize int
}{
{
width: 1920, height: 1080,
lineSize: 2560, frameSize: 2_764_800, v210aSize: 8_294_400,
},
{
width: 1280, height: 720,
lineSize: 1708, frameSize: 1_229_760, v210aSize: 3_718_080,
},
{
width: 100, height: 2,
lineSize: 136, frameSize: 272, v210aSize: 1040,
},
}
for _, tc := range tests {
t.Run(fmt.Sprintf("%dx%d", tc.width, tc.height), func(t *testing.T) {
if got := AlphaLineSize(tc.width); got != tc.lineSize {
t.Errorf("AlphaLineSize(%d) = %d, want %d", tc.width, got, tc.lineSize)
}
if got := AlphaFrameSize(tc.width, tc.height); got != tc.frameSize {
t.Errorf("AlphaFrameSize(%d, %d) = %d, want %d", tc.width, tc.height, got, tc.frameSize)
}
if got := V210AFrameSize(tc.width, tc.height); got != tc.v210aSize {
t.Errorf("V210AFrameSize(%d, %d) = %d, want %d", tc.width, tc.height, got, tc.v210aSize)
}
if got := V210AFrameSize(tc.width, tc.height) - AlphaFrameSize(tc.width, tc.height); got != V210FrameSize(tc.width, tc.height) {
t.Errorf("alpha plane starts at byte %d, want %d", got, V210FrameSize(tc.width, tc.height))
}
})
}
}
func sampleV210(buf []byte, width, x, y int) (yc, cb, cr uint32) {
offset := y*V210LineSize(width) + x/6*16
w0 := binary.LittleEndian.Uint32(buf[offset:])
+63
View File
@@ -0,0 +1,63 @@
package generator
import "fmt"
// V210AGenerator combines a v210 fill generator with a packed 10-bit alpha
// plane. It owns the fill generator and closes it from Close.
type V210AGenerator struct {
fill FrameGenerator
width int
height int
fillSize int
alphaBase []byte
}
var _ FrameGenerator = (*V210AGenerator)(nil)
func NewV210AGenerator(
fill FrameGenerator,
width, height uint,
) (*V210AGenerator, error) {
if fill == nil {
return nil, fmt.Errorf("v210a: fill generator is nil")
}
if width == 0 || height == 0 {
return nil, fmt.Errorf("v210a: width and height must be greater than zero, got %dx%d", width, height)
}
if width%2 != 0 {
return nil, fmt.Errorf("v210a: width must be even for 4:2:2 video, got %d", width)
}
g := &V210AGenerator{
fill: fill,
width: int(width),
height: int(height),
fillSize: V210FrameSize(int(width), int(height)),
alphaBase: make([]byte, AlphaFrameSize(int(width), int(height))),
}
if err := fillAlphaPlane(g.alphaBase, g.width, g.height, alphaOpaque); err != nil {
return nil, fmt.Errorf("v210a: initialize alpha plane: %w", err)
}
return g, nil
}
func (g *V210AGenerator) GenerateFrame(dst []byte, frameIndex int) error {
need := V210AFrameSize(g.width, g.height)
if len(dst) < need {
return fmt.Errorf("v210a: destination is too small: got %d bytes, need %d", len(dst), need)
}
if err := g.fill.GenerateFrame(dst[:g.fillSize], frameIndex); err != nil {
return fmt.Errorf("v210a: generate fill frame %d: %w", frameIndex, err)
}
alpha := dst[g.fillSize:need]
copy(alpha, g.alphaBase)
if err := patchAlphaMovingSquare(alpha, g.width, g.height, frameIndex); err != nil {
return fmt.Errorf("v210a: patch alpha frame %d: %w", frameIndex, err)
}
return nil
}
func (g *V210AGenerator) Close() error {
return g.fill.Close()
}
+116
View File
@@ -0,0 +1,116 @@
package generator
import (
"errors"
"strings"
"testing"
)
type fakeFrameGenerator struct {
generateErr error
closeErr error
closed bool
calls int
}
func (g *fakeFrameGenerator) GenerateFrame(dst []byte, frameIndex int) error {
g.calls++
if g.generateErr != nil {
return g.generateErr
}
for i := range dst {
dst[i] = byte(frameIndex)
}
return nil
}
func (g *fakeFrameGenerator) Close() error {
g.closed = true
return g.closeErr
}
func TestV210AGeneratorLayoutAndAlpha(t *testing.T) {
const width, height = 304, 200
fill := &fakeFrameGenerator{}
g, err := NewV210AGenerator(fill, width, height)
if err != nil {
t.Fatalf("NewV210AGenerator: %v", err)
}
dst := make([]byte, V210AFrameSize(width, height))
if err := g.GenerateFrame(dst, 7); err != nil {
t.Fatalf("GenerateFrame: %v", err)
}
fillSize := V210FrameSize(width, height)
for i, b := range dst[:fillSize] {
if b != 7 {
t.Fatalf("fill byte %d = %#x, want 0x07", i, b)
}
}
alpha := dst[fillSize:]
if got := sampleAlpha(alpha, width, width/2, height/2); got != alphaTransparent {
t.Errorf("square center = %d, want transparent %d", got, alphaTransparent)
}
if got := sampleAlpha(alpha, width, 10, height/2); got != alphaOpaque {
t.Errorf("outside square = %d, want opaque %d", got, alphaOpaque)
}
}
func TestV210AGeneratorRestoresAlphaBase(t *testing.T) {
const width, height = 304, 200
g, err := NewV210AGenerator(&fakeFrameGenerator{}, width, height)
if err != nil {
t.Fatalf("NewV210AGenerator: %v", err)
}
dst := make([]byte, V210AFrameSize(width, height))
if err := g.GenerateFrame(dst, 0); err != nil {
t.Fatalf("GenerateFrame(0): %v", err)
}
alpha := dst[V210FrameSize(width, height):]
if got := sampleAlpha(alpha, width, width/2, height/2); got != alphaTransparent {
t.Fatalf("frame 0 center = %d, want transparent %d", got, alphaTransparent)
}
if err := g.GenerateFrame(dst, 79); err != nil {
t.Fatalf("GenerateFrame(79): %v", err)
}
if got := sampleAlpha(alpha, width, width/2, height/2); got != alphaOpaque {
t.Errorf("old square position = %d, want restored opaque %d", got, alphaOpaque)
}
if got := sampleAlpha(alpha, width, 250, height/2); got != alphaTransparent {
t.Errorf("new square position = %d, want transparent %d", got, alphaTransparent)
}
}
func TestV210AGeneratorErrors(t *testing.T) {
if _, err := NewV210AGenerator(nil, 1920, 1080); err == nil || !strings.Contains(err.Error(), "nil") {
t.Fatalf("nil fill error = %v", err)
}
fillErr := errors.New("fill failed")
g, err := NewV210AGenerator(&fakeFrameGenerator{generateErr: fillErr}, 100, 20)
if err != nil {
t.Fatalf("NewV210AGenerator: %v", err)
}
if err := g.GenerateFrame(make([]byte, V210AFrameSize(100, 20)-1), 0); err == nil || !strings.Contains(err.Error(), "too small") {
t.Fatalf("small destination error = %v", err)
}
if err := g.GenerateFrame(make([]byte, V210AFrameSize(100, 20)), 3); !errors.Is(err, fillErr) {
t.Fatalf("fill error = %v, want wrapped %v", err, fillErr)
}
}
func TestV210AGeneratorClosesFill(t *testing.T) {
closeErr := errors.New("close failed")
fill := &fakeFrameGenerator{closeErr: closeErr}
g, err := NewV210AGenerator(fill, 100, 20)
if err != nil {
t.Fatalf("NewV210AGenerator: %v", err)
}
if err := g.Close(); !errors.Is(err, closeErr) {
t.Fatalf("Close = %v, want %v", err, closeErr)
}
if !fill.closed {
t.Fatal("wrapped fill generator was not closed")
}
}