dvd logo remove
This commit is contained in:
@@ -1,76 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -35,8 +35,6 @@ type WGPUGenerator struct {
|
||||
out *wgpu.Buffer
|
||||
host *wgpu.Buffer
|
||||
uniform *wgpu.Buffer
|
||||
logo *wgpu.Buffer
|
||||
logoPlane []uint32
|
||||
params []byte
|
||||
width int
|
||||
height int
|
||||
@@ -49,14 +47,6 @@ var _ FrameGenerator = (*WGPUGenerator)(nil)
|
||||
// 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),
|
||||
@@ -121,48 +111,22 @@ func NewWGPUGenerator(width, height uint, kernelPath string, opts ...WGPUOption)
|
||||
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: bglEntries,
|
||||
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}},
|
||||
},
|
||||
}); 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: bgEntries,
|
||||
Entries: []wgpu.BindGroupEntry{
|
||||
{Binding: 0, Buffer: g.out, Size: g.frameSize},
|
||||
{Binding: 1, Buffer: g.uniform, Size: uint64(len(g.params))},
|
||||
},
|
||||
}); err != nil {
|
||||
g.Close()
|
||||
return nil, fmt.Errorf("wgpu: bind group: %w", err)
|
||||
@@ -254,10 +218,6 @@ 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
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
func TestWGPUMoveSquare(t *testing.T) {
|
||||
const width, height = 1920, 1080
|
||||
g, err := NewWGPUGenerator(width, height, filepath.Join("..", "..", "kernels", "v210_bars_move.wgsl"))
|
||||
g, err := NewWGPUGenerator(width, height, filepath.Join("..", "..", "kernels", "dynamic", "smpteBars.wgsl"))
|
||||
if err != nil {
|
||||
t.Fatalf("init: %v", err)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
func TestWGPUGenerator(t *testing.T) {
|
||||
const width, height = 1920, 1080
|
||||
g, err := NewWGPUGenerator(width, height, filepath.Join("..", "..", "kernels", "v210_bars.wgsl"))
|
||||
g, err := NewWGPUGenerator(width, height, filepath.Join("..", "..", "kernels", "static", "smpteBars.wgsl"))
|
||||
if err != nil {
|
||||
t.Fatalf("init: %v", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user