Text + MXL logo pattern

This commit is contained in:
Dmitry Sergeev
2026-09-13 16:33:50 +03:00
parent 94ff41a124
commit 97dec92b8b
15 changed files with 999 additions and 23 deletions
+76
View File
@@ -0,0 +1,76 @@
// Logo plane construction for texture-sampling kernels (v210_dvd_logo.wgsl).
package generator
import (
"fmt"
"image"
"math"
"os"
// PNG decoder registration for LoadLogoPNG.
_ "image/png"
)
// BuildLogoPlane converts an image into the packed logo plane consumed by
// v210_dvd_logo.wgsl as read-only storage binding 2:
//
// logo[0] = width | height<<16
// logo[1+dy*w+dx] = opaque<<30 | Cr<<20 | Cb<<10 | Y
//
// Rec.709 full-range RGB -> 10-bit studio swing, the same transform behind
// the color-bar tables in the kernels (verified against all 21 bar values).
// Semi-transparent pixels (alpha >= 50%) count as opaque; the rest show the
// kernel background through.
func BuildLogoPlane(img image.Image) ([]uint32, error) {
b := img.Bounds()
w, h := b.Dx(), b.Dy()
if w <= 0 || h <= 0 || w > 0xFFFF || h > 0xFFFF {
return nil, fmt.Errorf("logo: bad dimensions %dx%d", w, h)
}
plane := make([]uint32, 1+w*h)
plane[0] = uint32(w) | uint32(h)<<16
for yy := 0; yy < h; yy++ {
for xx := 0; xx < w; xx++ {
r, g, bl, a := img.At(b.Min.X+xx, b.Min.Y+yy).RGBA()
if a < 1<<15 {
continue // transparent: shader falls back to background
}
// At().RGBA() is premultiplied; unpremultiply to full-range RGB.
rf := float64(r) / float64(a)
gf := float64(g) / float64(a)
bf := float64(bl) / float64(a)
luma := 0.2126*rf + 0.7152*gf + 0.0722*bf
yc := clamp10(64 + math.Round(876*luma))
cb := clamp10(512 + math.Round(448*(bf-luma)/(1-0.0722)))
cr := clamp10(512 + math.Round(448*(rf-luma)/(1-0.2126)))
plane[1+yy*w+xx] = 1<<30 | cr<<20 | cb<<10 | yc
}
}
return plane, nil
}
// clamp10 keeps a 10-bit code inside the legal 4..1019 range.
func clamp10(v float64) uint32 {
switch {
case v < 4:
return 4
case v > 1019:
return 1019
default:
return uint32(v)
}
}
// LoadLogoPNG reads a PNG file and builds the logo plane.
func LoadLogoPNG(path string) ([]uint32, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("logo: %w", err)
}
defer f.Close()
img, _, err := image.Decode(f)
if err != nil {
return nil, fmt.Errorf("logo: decode %s: %w", path, err)
}
return BuildLogoPlane(img)
}
+64
View File
@@ -0,0 +1,64 @@
package generator
import (
"image"
"image/color"
"testing"
)
func TestBuildLogoPlane(t *testing.T) {
// 8x4 logo: white left half, near-75% red right half (8-bit 191 is
// 0.749, a hair under the exact-0.75 bars value, hence Y=203 not 204),
// one transparent pixel at (2,1).
img := image.NewRGBA(image.Rect(0, 0, 8, 4))
for yy := 0; yy < 4; yy++ {
for xx := 0; xx < 8; xx++ {
if xx < 4 {
img.Set(xx, yy, color.RGBA{255, 255, 255, 255})
} else {
img.Set(xx, yy, color.RGBA{191, 0, 0, 255})
}
}
}
img.Set(2, 1, color.RGBA{0, 0, 0, 0})
plane, err := BuildLogoPlane(img)
if err != nil {
t.Fatalf("BuildLogoPlane: %v", err)
}
if len(plane) != 1+8*4 {
t.Fatalf("plane len=%d, want 33", len(plane))
}
if hdr := plane[0]; hdr != 8|4<<16 {
t.Fatalf("header=%#x, want w=8 h=4", hdr)
}
cases := []struct {
dx, dy int
opaque bool
y, cb, cr uint32
}{
{1, 1, true, 940, 512, 512}, // white
{5, 2, true, 203, 435, 848}, // near-75% red
{2, 1, false, 0, 0, 0}, // transparent
}
for _, c := range cases {
s := plane[1+c.dy*8+c.dx]
if got := (s >> 30) & 1; got != boolToBit(c.opaque) {
t.Fatalf("pixel (%d,%d): opaque=%d, want %v", c.dx, c.dy, got, c.opaque)
}
if !c.opaque {
continue
}
if s&0x3FF != c.y || (s>>10)&0x3FF != c.cb || (s>>20)&0x3FF != c.cr {
t.Fatalf("pixel (%d,%d): got %d/%d/%d, want %d/%d/%d",
c.dx, c.dy, s&0x3FF, (s>>10)&0x3FF, (s>>20)&0x3FF, c.y, c.cb, c.cr)
}
}
}
func boolToBit(b bool) uint32 {
if b {
return 1
}
return 0
}
+151
View File
@@ -0,0 +1,151 @@
// Static text overlay for v210 frames: text is rasterized with real font
// anti-aliasing onto a black backing box, pre-packed into whole v210 blocks
// once, and stamped per frame with a block-aligned copy (microseconds).
package generator
import (
"encoding/binary"
"fmt"
"image"
"os"
"golang.org/x/image/font"
"golang.org/x/image/font/opentype"
"golang.org/x/image/math/fixed"
)
type TextOverlay struct {
frameW int
x, y int // box top-left in frame pixels
w, h int // box size in pixels (w is a multiple of 6)
cov []byte // 8-bit anti-alias coverage, row-major w*h
blocks []byte // pre-packed v210 tile: h rows x (w/6) blocks x 16 bytes
}
// LoadFace parses a TTF/OTF file and builds a render-ready face at the given
// pixel size (DPI 72, full hinting for crisp video text). Call once and
// reuse for every overlay.
func LoadFace(path string, size float64) (font.Face, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("text: %w", err)
}
f, err := opentype.Parse(data)
if err != nil {
return nil, fmt.Errorf("text: parse %s: %w", path, err)
}
face, err := opentype.NewFace(f, &opentype.FaceOptions{
Size: size,
DPI: 72,
Hinting: font.HintingFull,
})
if err != nil {
return nil, fmt.Errorf("text: face %s: %w", path, err)
}
return face, nil
}
// NewTextOverlay rasterizes text (white, anti-aliased) onto a black backing
// box, centered horizontally. Box width and x are multiples of 6 so the box
// covers whole v210 blocks and the per-frame copy is word-aligned.
func NewTextOverlay(text string, frameW, frameH int, face font.Face) (*TextOverlay, error) {
if text == "" {
return nil, fmt.Errorf("text: empty string")
}
if frameW <= 0 || frameW%6 != 0 {
return nil, fmt.Errorf("text: frame width %d not a multiple of 6", frameW)
}
pix := func(v fixed.Int26_6) int { return int(v+63) / 64 } // ceil to px
m := face.Metrics()
textH := pix(m.Ascent+m.Descent) + 2 // slack for AA/hinting overshoot
// Box width: sum of per-rune advances (uniform for monospace fonts).
spaceAdv, ok := face.GlyphAdvance(' ')
if !ok {
spaceAdv = m.Height
}
var total fixed.Int26_6
for _, r := range text {
a, ok := face.GlyphAdvance(r)
if !ok {
a = spaceAdv
}
total += a
}
textW := pix(total)
padX := pix(m.Ascent) / 3
padY := padX / 2
w := 2*padX + textW
if r := w % 6; r != 0 {
w += 6 - r
}
if w > frameW {
return nil, fmt.Errorf("text: box %dpx wider than frame %dpx", w, frameW)
}
h := 2*padY + textH
const topMargin = 48
if topMargin+h > frameH {
return nil, fmt.Errorf("text: box %dpx does not fit frame height %dpx", topMargin+h, frameH)
}
o := &TextOverlay{
frameW: frameW,
x: ((frameW - w) / 2 / 6) * 6,
y: topMargin,
w: w,
h: h,
cov: make([]byte, w*h),
}
// Rasterize: drawing white onto *image.Alpha writes pure glyph coverage.
dst := image.NewAlpha(image.Rect(0, 0, textW, textH))
(&font.Drawer{
Dst: dst, Src: image.White, Face: face,
Dot: fixed.P(0, pix(m.Ascent)),
}).DrawString(text)
for row := 0; row < textH; row++ {
copy(o.cov[(padY+row)*w+padX:], dst.Pix[row*dst.Stride:row*dst.Stride+textW])
}
o.pack()
return o, nil
}
// pack pre-renders the box into v210 blocks: white text (Y=940) blended by
// coverage over a black box (Y=64), neutral chroma (512). Same word layout
// as the kernels, chroma co-sited with luma samples 0/2/4.
func (o *TextOverlay) pack() {
const neutral = uint32(512)
o.blocks = make([]byte, o.h*(o.w/6)*16)
for row := 0; row < o.h; row++ {
for blk := 0; blk < o.w/6; blk++ {
var y [6]uint32
for i := 0; i < 6; i++ {
c := int(o.cov[row*o.w+blk*6+i])
y[i] = 64 + uint32((c*876+127)/255)
}
off := (row*(o.w/6) + blk) * 16
binary.LittleEndian.PutUint32(o.blocks[off+0:], neutral|(y[0]<<10)|(neutral<<20))
binary.LittleEndian.PutUint32(o.blocks[off+4:], y[1]|(neutral<<10)|(y[2]<<20))
binary.LittleEndian.PutUint32(o.blocks[off+8:], neutral|(y[3]<<10)|(neutral<<20))
binary.LittleEndian.PutUint32(o.blocks[off+12:], y[4]|(neutral<<10)|(y[5]<<20))
}
}
}
// ApplyV210 stamps the pre-packed text tile over a packed v210 frame.
// The tile occupies whole 16-byte blocks, so this is a row-wise copy.
func (o *TextOverlay) ApplyV210(dest []byte) error {
lastPixel := (o.y+o.h-1)*o.frameW + o.x + o.w - 1
if need := (lastPixel/6 + 1) * 16; len(dest) < need {
return fmt.Errorf("text: dest %d bytes too small, need %d", len(dest), need)
}
tileStride := (o.w / 6) * 16
for row := 0; row < o.h; row++ {
frameBlock := ((o.y+row)*o.frameW + o.x) / 6
src := o.blocks[row*tileStride : (row+1)*tileStride]
copy(dest[frameBlock*16:frameBlock*16+tileStride], src)
}
return nil
}
+156
View File
@@ -0,0 +1,156 @@
package generator
import (
"encoding/binary"
"path/filepath"
"testing"
"golang.org/x/image/font"
)
func testFace(t *testing.T, size float64) font.Face {
t.Helper()
face, err := LoadFace(filepath.Join("..", "..", "assets", "fonts",
"JetBrainsMonoNLNerdFontMono-Regular.ttf"), size)
if err != nil {
t.Fatalf("LoadFace: %v", err)
}
t.Cleanup(func() { face.Close() })
return face
}
func TestNewTextOverlay(t *testing.T) {
const frameW, frameH = 1920, 1080
o, err := NewTextOverlay("AB", frameW, frameH, testFace(t, 48))
if err != nil {
t.Fatalf("NewTextOverlay: %v", err)
}
if o.w%6 != 0 {
t.Fatalf("w=%d not a multiple of 6", o.w)
}
if o.x%6 != 0 {
t.Fatalf("x=%d not a multiple of 6", o.x)
}
if center := o.x + o.w/2; center < frameW/2-3 || center > frameW/2+3 {
t.Fatalf("box center %d not near frame center %d", center, frameW/2)
}
if o.y != 48 {
t.Fatalf("y=%d, want 48", o.y)
}
if o.blocks == nil || len(o.blocks) != o.h*(o.w/6)*16 {
t.Fatalf("pre-packed tile has wrong size")
}
if o.cov[2*o.w+2] != 0 {
t.Fatalf("padding pixel (2,2) covered: %d", o.cov[2*o.w+2])
}
nonzero, full := 0, 0
for _, c := range o.cov {
if c > 0 {
nonzero++
}
if c == 255 {
full++
}
}
if nonzero < 100 {
t.Fatalf("too few covered pixels: %d", nonzero)
}
if full == 0 {
t.Fatalf("no fully covered (255) pixels")
}
// Text without descenders ("AB") must leave the bottom quarter empty;
// text with a descender ("Ag") must have coverage there.
for _, tc := range []struct {
text string
wantAny bool
}{
{"AB", false},
{"Ag", true},
} {
o, err := NewTextOverlay(tc.text, frameW, frameH, testFace(t, 48))
if err != nil {
t.Fatalf("NewTextOverlay(%q): %v", tc.text, err)
}
any := false
for row := o.h * 3 / 4; row < o.h; row++ {
for col := 0; col < o.w; col++ {
if o.cov[row*o.w+col] > 0 {
any = true
}
}
}
if any != tc.wantAny {
t.Fatalf("%q: coverage in bottom quarter = %v, want %v", tc.text, any, tc.wantAny)
}
}
}
func TestTextOverlayApplyV210(t *testing.T) {
const frameW, frameH = 1920, 1080
o, err := NewTextOverlay("MXL", frameW, frameH, testFace(t, 48))
if err != nil {
t.Fatalf("NewTextOverlay: %v", err)
}
frame := make([]byte, frameW*frameH*8/3)
for i := range frame {
frame[i] = 0x5A // marker: untouched regions must survive
}
if err := o.ApplyV210(frame); err != nil {
t.Fatalf("ApplyV210: %v", err)
}
sample := func(x, y int) (yc, cb, cr uint32) {
p := y*frameW + x
off := (p / 6) * 16
w0 := binary.LittleEndian.Uint32(frame[off:])
w1 := binary.LittleEndian.Uint32(frame[off+4:])
w2 := binary.LittleEndian.Uint32(frame[off+8:])
w3 := binary.LittleEndian.Uint32(frame[off+12:])
switch p % 6 {
case 0:
return (w0 >> 10) & 0x3FF, w0 & 0x3FF, (w0 >> 20) & 0x3FF
case 1:
return w1 & 0x3FF, w0 & 0x3FF, (w0 >> 20) & 0x3FF
case 2:
return (w1 >> 20) & 0x3FF, (w1 >> 10) & 0x3FF, w2 & 0x3FF
case 3:
return (w2 >> 10) & 0x3FF, (w1 >> 10) & 0x3FF, w2 & 0x3FF
case 4:
return w3 & 0x3FF, (w2 >> 20) & 0x3FF, (w3 >> 10) & 0x3FF
default:
return (w3 >> 20) & 0x3FF, (w2 >> 20) & 0x3FF, (w3 >> 10) & 0x3FF
}
}
// Every box pixel: Y from coverage, neutral chroma at even columns.
for row := 0; row < o.h; row++ {
for col := 0; col < o.w; col++ {
wantY := uint32(64 + (int(o.cov[row*o.w+col])*876+127)/255)
yc, cb, cr := sample(o.x+col, o.y+row)
if yc != wantY {
t.Fatalf("box pixel (%d,%d): Y=%d, want %d", o.x+col, o.y+row, yc, wantY)
}
if (o.x+col)%2 == 0 && (cb != 512 || cr != 512) {
t.Fatalf("box pixel (%d,%d): Cb=%d Cr=%d, want 512/512", o.x+col, o.y+row, cb, cr)
}
}
}
// Blocks just outside the box (right edge and below) must be untouched.
for _, p := range [][2]int{
{o.x + o.w, o.y}, // right of box, block-aligned
{o.x, o.y + o.h}, // below box
{o.x + o.w, o.y + o.h}, // corner
{100, 1000}, // far away
} {
off := ((p[1]*frameW + p[0]) / 6) * 16
for i := 0; i < 16; i++ {
if frame[off+i] != 0x5A {
t.Fatalf("block at (%d,%d) modified outside the box", p[0], p[1])
}
}
}
}
+56 -10
View File
@@ -35,6 +35,8 @@ type WGPUGenerator struct {
out *wgpu.Buffer
host *wgpu.Buffer
uniform *wgpu.Buffer
logo *wgpu.Buffer
logoPlane []uint32
params []byte
width int
height int
@@ -44,13 +46,27 @@ type WGPUGenerator struct {
var _ FrameGenerator = (*WGPUGenerator)(nil)
func NewWGPUGenerator(width, height uint, kernelPath string) (*WGPUGenerator, error) {
// WGPUOption customizes NewWGPUGenerator.
type WGPUOption func(*WGPUGenerator)
// WithLogo attaches a packed logo plane (see BuildLogoPlane) as read-only
// storage binding 2 for kernels that sample it (v210_dvd_logo.wgsl).
func WithLogo(plane []uint32) WGPUOption {
return func(g *WGPUGenerator) {
g.logoPlane = plane
}
}
func NewWGPUGenerator(width, height uint, kernelPath string, opts ...WGPUOption) (*WGPUGenerator, error) {
g := &WGPUGenerator{
width: int(width),
height: int(height),
blocks: int(width*height) / 6,
params: make([]byte, 16),
}
for _, opt := range opts {
opt(g)
}
g.frameSize = uint64(g.blocks) * 16
binary.LittleEndian.PutUint32(g.params[0:], uint32(width))
binary.LittleEndian.PutUint32(g.params[4:], uint32(height))
@@ -105,22 +121,48 @@ func NewWGPUGenerator(width, height uint, kernelPath string) (*WGPUGenerator, er
g.Close()
return nil, fmt.Errorf("wgpu: write params: %w", err)
}
bglEntries := []gputypes.BindGroupLayoutEntry{
{Binding: 0, Visibility: wgpu.ShaderStageCompute, Buffer: &gputypes.BufferBindingLayout{Type: gputypes.BufferBindingTypeStorage}},
{Binding: 1, Visibility: wgpu.ShaderStageCompute, Buffer: &gputypes.BufferBindingLayout{Type: gputypes.BufferBindingTypeUniform}},
}
bgEntries := []wgpu.BindGroupEntry{
{Binding: 0, Buffer: g.out, Size: g.frameSize},
{Binding: 1, Buffer: g.uniform, Size: uint64(len(g.params))},
}
if g.logoPlane != nil {
logoBytes := make([]byte, 4*len(g.logoPlane))
for i, v := range g.logoPlane {
binary.LittleEndian.PutUint32(logoBytes[4*i:], v)
}
if g.logo, err = g.device.CreateBuffer(&wgpu.BufferDescriptor{
Label: "v210-logo", Size: uint64(len(logoBytes)),
Usage: wgpu.BufferUsageStorage | wgpu.BufferUsageCopyDst,
}); err != nil {
g.Close()
return nil, fmt.Errorf("wgpu: logo buffer: %w", err)
}
if err := g.queue.WriteBuffer(g.logo, 0, logoBytes); err != nil {
g.Close()
return nil, fmt.Errorf("wgpu: write logo: %w", err)
}
bglEntries = append(bglEntries, gputypes.BindGroupLayoutEntry{
Binding: 2, Visibility: wgpu.ShaderStageCompute,
Buffer: &gputypes.BufferBindingLayout{Type: gputypes.BufferBindingTypeReadOnlyStorage},
})
bgEntries = append(bgEntries, wgpu.BindGroupEntry{
Binding: 2, Buffer: g.logo, Size: uint64(len(logoBytes)),
})
}
if g.bgl, err = g.device.CreateBindGroupLayout(&wgpu.BindGroupLayoutDescriptor{
Label: "v210-bgl",
Entries: []gputypes.BindGroupLayoutEntry{
{Binding: 0, Visibility: wgpu.ShaderStageCompute, Buffer: &gputypes.BufferBindingLayout{Type: gputypes.BufferBindingTypeStorage}},
{Binding: 1, Visibility: wgpu.ShaderStageCompute, Buffer: &gputypes.BufferBindingLayout{Type: gputypes.BufferBindingTypeUniform}},
},
Label: "v210-bgl",
Entries: bglEntries,
}); err != nil {
g.Close()
return nil, fmt.Errorf("wgpu: bind group layout: %w", err)
}
if g.bg, err = g.device.CreateBindGroup(&wgpu.BindGroupDescriptor{
Label: "v210-bg", Layout: g.bgl,
Entries: []wgpu.BindGroupEntry{
{Binding: 0, Buffer: g.out, Size: g.frameSize},
{Binding: 1, Buffer: g.uniform, Size: uint64(len(g.params))},
},
Entries: bgEntries,
}); err != nil {
g.Close()
return nil, fmt.Errorf("wgpu: bind group: %w", err)
@@ -212,6 +254,10 @@ func (g *WGPUGenerator) Close() error {
g.uniform.Release()
g.uniform = nil
}
if g.logo != nil {
g.logo.Release()
g.logo = nil
}
if g.host != nil {
g.host.Release()
g.host = nil
+169
View File
@@ -0,0 +1,169 @@
package generator
import (
"encoding/binary"
"image"
"image/color"
"math"
"path/filepath"
"testing"
)
const logoW, logoH = 8, 4
func TestWGPUDVDLogo(t *testing.T) {
const width, height = 1920, 1080
// Synthetic 8x4 logo: white left half, near-75% red right half, one
// transparent pixel at (2,1) to exercise the alpha fallback.
img := image.NewRGBA(image.Rect(0, 0, logoW, logoH))
for yy := 0; yy < logoH; yy++ {
for xx := 0; xx < logoW; xx++ {
if xx < 4 {
img.Set(xx, yy, color.RGBA{255, 255, 255, 255})
} else {
img.Set(xx, yy, color.RGBA{191, 0, 0, 255})
}
}
}
img.Set(2, 1, color.RGBA{0, 0, 0, 0})
plane, err := BuildLogoPlane(img)
if err != nil {
t.Fatalf("plane: %v", err)
}
g, err := NewWGPUGenerator(width, height,
filepath.Join("..", "..", "kernels", "v210_dvd_logo.wgsl"), WithLogo(plane))
if err != nil {
t.Fatalf("init: %v", err)
}
defer g.Close()
buf := make([]byte, width*height*8/3)
sample := func(x, y int) (yc, cb, cr uint32) {
p := y*width + x
off := (p / 6) * 16
w0 := binary.LittleEndian.Uint32(buf[off:])
w1 := binary.LittleEndian.Uint32(buf[off+4:])
w2 := binary.LittleEndian.Uint32(buf[off+8:])
w3 := binary.LittleEndian.Uint32(buf[off+12:])
switch p % 6 {
case 0:
return (w0 >> 10) & 0x3FF, w0 & 0x3FF, (w0 >> 20) & 0x3FF
case 1:
return w1 & 0x3FF, w0 & 0x3FF, (w0 >> 20) & 0x3FF
case 2:
return (w1 >> 20) & 0x3FF, (w1 >> 10) & 0x3FF, w2 & 0x3FF
case 3:
return (w2 >> 10) & 0x3FF, (w1 >> 10) & 0x3FF, w2 & 0x3FF
case 4:
return w3 & 0x3FF, (w2 >> 20) & 0x3FF, (w3 >> 10) & 0x3FF
default:
return (w3 >> 20) & 0x3FF, (w2 >> 20) & 0x3FF, (w3 >> 10) & 0x3FF
}
}
// Float32 mirrors of the shader position math and bilinear tap blend
// (kernels/v210_dvd_logo.wgsl), so expectations track the shader exactly.
tri01 := func(x float32) float32 {
f := x - float32(math.Floor(float64(x)))
if f > 0.5 {
return 2 - 2*f
}
return 2 * f
}
type logoPos struct {
bx, by int
fx, fy float32
}
logoOrigin := func(frame uint32) logoPos {
tick := float32(frame) / 25.0
lx := tri01(tick*0.061) * (float32(width) - logoW)
ly := tri01(tick*0.088) * (float32(height) - logoH)
return logoPos{int(lx), int(ly), lx - float32(int(lx)), ly - float32(int(ly))}
}
tap := func(dx, dy int) [3]float32 {
if dx < 0 || dy < 0 || dx >= logoW || dy >= logoH {
return [3]float32{}
}
s := plane[1+dy*logoW+dx]
if s>>30 == 0 {
return [3]float32{}
}
return [3]float32{float32(s&0x3FF) - 64, float32((s>>10)&0x3FF) - 512, float32((s>>20)&0x3FF) - 512}
}
mix3 := func(a, b [3]float32, k float32) [3]float32 {
return [3]float32{a[0] + k*(b[0]-a[0]), a[1] + k*(b[1]-a[1]), a[2] + k*(b[2]-a[2])}
}
expect := func(p logoPos, px, py int) (yc, cb, cr uint32) {
m, n := px-p.bx, py-p.by
top := mix3(tap(m, n-1), tap(m-1, n-1), p.fx)
bot := mix3(tap(m, n), tap(m-1, n), p.fx)
d := mix3(bot, top, p.fy)
return uint32(64 + d[0] + 0.5), uint32(512 + d[1] + 0.5), uint32(512 + d[2] + 0.5)
}
// tol allows +-1 code for blended pixels (FMA contraction on GPU may
// differ from Go's separately-rounded float32 ops by 1 ulp).
check := func(p logoPos, px, py int, tol int, desc string) {
t.Helper()
yc, cb, cr := sample(px, py)
wy, wcb, wcr := expect(p, px, py)
dy, dcb, dcr := int(yc)-int(wy), int(cb)-int(wcb), int(cr)-int(wcr)
if abs(dy) > tol || abs(dcb) > tol || abs(dcr) > tol {
t.Fatalf("%s: (%d,%d) got %d/%d/%d, want %d/%d/%d (diff %d/%d/%d, tol %d)",
desc, px, py, yc, cb, cr, wy, wcb, wcr, dy, dcb, dcr, tol)
}
}
// tick 0: fx=fy=0 -> pure nearest lookup, exact values known upfront
if err := g.GenerateFrame(buf, 0); err != nil {
t.Fatalf("tick 0: %v", err)
}
p0 := logoOrigin(0)
if p0.bx != 0 || p0.by != 0 || p0.fx != 0 || p0.fy != 0 {
t.Fatalf("tick 0 origin %+v, want (0,0,0,0)", p0)
}
check(p0, 1, 1, 0, "tick 0 white pixel")
if y, cb, cr := sample(1, 1); y != 940 || cb != 512 || cr != 512 {
t.Fatalf("tick 0 white pixel: got %d/%d/%d, want 940/512/512", y, cb, cr)
}
check(p0, 2, 1, 0, "tick 0 transparent pixel")
if y, _, _ := sample(2, 1); y != 64 {
t.Fatalf("tick 0 transparent pixel: Y=%d, want background 64", y)
}
check(p0, 5, 2, 0, "tick 0 red pixel")
if y, cb, cr := sample(5, 2); y != 203 || cb != 435 || cr != 848 {
t.Fatalf("tick 0 red pixel: got %d/%d/%d, want 203/435/848", y, cb, cr)
}
check(p0, 10, 1, 0, "tick 0 right of logo")
check(p0, 1, 5, 0, "tick 0 below logo")
// tick 100: origin (933,757), fx~0.056 fy~0.504 -> bilinear blending.
// (934,758) lands on solid white (all 4 taps white) and must reproduce
// the texel exactly; (935,759) straddles the transparent pixel (2,1)
// and must match the mirrored blend within +-1.
if err := g.GenerateFrame(buf, 100); err != nil {
t.Fatalf("tick 100: %v", err)
}
p100 := logoOrigin(100)
if p100.bx != 933 || p100.by != 757 {
t.Fatalf("tick 100 origin (%d,%d), want (933,757)", p100.bx, p100.by)
}
check(p100, 934, 758, 0, "tick 100 solid pixel exact")
if y, _, _ := sample(934, 758); y != 940 {
t.Fatalf("tick 100 solid pixel: Y=%d, want exact texel 940", y)
}
check(p100, 935, 759, 1, "tick 100 blended over transparent pixel")
check(p100, 960, 540, 0, "tick 100 frame center")
if y, _, _ := sample(960, 540); y != 64 {
t.Fatalf("tick 100 frame center: Y=%d, want background 64", y)
}
check(p100, 2, 1, 0, "tick 100 old logo position")
}
func abs(v int) int {
if v < 0 {
return -v
}
return v
}