refactoring finished

This commit is contained in:
Dmitry Sergeev
2026-09-17 20:42:38 +03:00
parent de4dfcf101
commit e30cb7a168
11 changed files with 92 additions and 48 deletions
+8
View File
@@ -0,0 +1,8 @@
package assets
import _ "embed"
// JetBrainsMono contains the font used for video text overlays.
//
//go:embed fonts/JetBrainsMonoNLNerdFontMono-Regular.ttf
var JetBrainsMono []byte
+12 -2
View File
@@ -33,9 +33,19 @@ func LoadFace(path string, size float64) (font.Face, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("text: %w", err) return nil, fmt.Errorf("text: %w", err)
} }
face, err := NewFace(data, size)
if err != nil {
return nil, fmt.Errorf("text: load %s: %w", path, err)
}
return face, nil
}
// NewFace parses TTF/OTF data and builds a render-ready face at the given
// pixel size (DPI 72, full hinting for crisp video text).
func NewFace(data []byte, size float64) (font.Face, error) {
f, err := opentype.Parse(data) f, err := opentype.Parse(data)
if err != nil { if err != nil {
return nil, fmt.Errorf("text: parse %s: %w", path, err) return nil, fmt.Errorf("parse font: %w", err)
} }
face, err := opentype.NewFace(f, &opentype.FaceOptions{ face, err := opentype.NewFace(f, &opentype.FaceOptions{
Size: size, Size: size,
@@ -43,7 +53,7 @@ func LoadFace(path string, size float64) (font.Face, error) {
Hinting: font.HintingFull, Hinting: font.HintingFull,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("text: face %s: %w", path, err) return nil, fmt.Errorf("create font face: %w", err)
} }
return face, nil return face, nil
} }
+3 -3
View File
@@ -1,17 +1,17 @@
package generator package generator
import ( import (
"path/filepath"
"strings" "strings"
"testing" "testing"
"mxl-pattern-generator/assets"
"golang.org/x/image/font" "golang.org/x/image/font"
) )
func testFace(t *testing.T, size float64) font.Face { func testFace(t *testing.T, size float64) font.Face {
t.Helper() t.Helper()
face, err := LoadFace(filepath.Join("..", "..", "assets", "fonts", face, err := NewFace(assets.JetBrainsMono, size)
"JetBrainsMonoNLNerdFontMono-Regular.ttf"), size)
if err != nil { if err != nil {
t.Fatalf("LoadFace: %v", err) t.Fatalf("LoadFace: %v", err)
} }
+2 -8
View File
@@ -6,7 +6,6 @@ import (
"context" "context"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"os"
"time" "time"
"github.com/gogpu/gputypes" "github.com/gogpu/gputypes"
@@ -47,7 +46,7 @@ var _ FrameGenerator = (*WGPUGenerator)(nil)
// WGPUOption customizes NewWGPUGenerator. // WGPUOption customizes NewWGPUGenerator.
type WGPUOption func(*WGPUGenerator) type WGPUOption func(*WGPUGenerator)
func NewWGPUGenerator(width, height uint, kernelPath string, opts ...WGPUOption) (*WGPUGenerator, error) { func NewWGPUGenerator(width, height uint, wgsl string, opts ...WGPUOption) (*WGPUGenerator, error) {
g := &WGPUGenerator{ g := &WGPUGenerator{
width: int(width), width: int(width),
height: int(height), height: int(height),
@@ -75,13 +74,8 @@ func NewWGPUGenerator(width, height uint, kernelPath string, opts ...WGPUOption)
} }
g.queue = g.device.Queue() g.queue = g.device.Queue()
wgsl, err := os.ReadFile(kernelPath)
if err != nil {
g.Close()
return nil, fmt.Errorf("wgpu: read kernel: %w", err)
}
if g.shader, err = g.device.CreateShaderModule(&wgpu.ShaderModuleDescriptor{ if g.shader, err = g.device.CreateShaderModule(&wgpu.ShaderModuleDescriptor{
Label: "v210-shader", WGSL: string(wgsl), Label: "v210-shader", WGSL: wgsl,
}); err != nil { }); err != nil {
g.Close() g.Close()
return nil, fmt.Errorf("wgpu: shader: %w", err) return nil, fmt.Errorf("wgpu: shader: %w", err)
+7 -2
View File
@@ -1,13 +1,18 @@
package generator package generator
import ( import (
"path/filepath"
"testing" "testing"
"mxl-pattern-generator/kernels"
) )
func TestWGPUMoveSquare(t *testing.T) { func TestWGPUMoveSquare(t *testing.T) {
const width, height = 1920, 1080 const width, height = 1920, 1080
g, err := NewWGPUGenerator(width, height, filepath.Join("..", "..", "kernels", "dynamic", "smpteBars.wgsl")) shader, err := kernels.Read("dynamic/smpteBars.wgsl")
if err != nil {
t.Fatalf("read shader: %v", err)
}
g, err := NewWGPUGenerator(width, height, string(shader))
if err != nil { if err != nil {
t.Fatalf("init: %v", err) t.Fatalf("init: %v", err)
} }
+7 -2
View File
@@ -1,13 +1,18 @@
package generator package generator
import ( import (
"path/filepath"
"testing" "testing"
"mxl-pattern-generator/kernels"
) )
func TestWGPUSMPTEPattern(t *testing.T) { func TestWGPUSMPTEPattern(t *testing.T) {
const width, height = 1920, 1080 const width, height = 1920, 1080
g, err := NewWGPUGenerator(width, height, filepath.Join("..", "..", "kernels", "static", "smpteBars.wgsl")) shader, err := kernels.Read("static/smpteBars.wgsl")
if err != nil {
t.Fatalf("read shader: %v", err)
}
g, err := NewWGPUGenerator(width, height, string(shader))
if err != nil { if err != nil {
t.Fatalf("init: %v", err) t.Fatalf("init: %v", err)
} }
+25 -12
View File
@@ -1,6 +1,11 @@
package video package video
import "sort" import (
"fmt"
"sort"
"mxl-pattern-generator/kernels"
)
type PatternInfo struct { type PatternInfo struct {
Name string Name string
@@ -9,52 +14,60 @@ type PatternInfo struct {
type pattern struct { type pattern struct {
PatternInfo PatternInfo
kernelPath string kernelSource string
} }
var patterns = map[string]pattern{ var patterns = map[string]pattern{
"ebu75": { "ebu75": {
PatternInfo: PatternInfo{Name: "ebu75", Description: "EBU 75% Color Bar Signal"}, PatternInfo: PatternInfo{Name: "ebu75", Description: "EBU 75% Color Bar Signal"},
kernelPath: "kernels/static/ebu75.wgsl", kernelSource: mustReadKernel("static/ebu75.wgsl"),
}, },
"ebu75-move": { "ebu75-move": {
PatternInfo: PatternInfo{Name: "ebu75-move", Description: "EBU 75% Color Bar Signal with moving square"}, PatternInfo: PatternInfo{Name: "ebu75-move", Description: "EBU 75% Color Bar Signal with moving square"},
kernelPath: "kernels/dynamic/ebu75.wgsl", kernelSource: mustReadKernel("dynamic/ebu75.wgsl"),
}, },
"ebu100": { "ebu100": {
PatternInfo: PatternInfo{Name: "ebu100", Description: "EBU 100% Color Bar Signal"}, PatternInfo: PatternInfo{Name: "ebu100", Description: "EBU 100% Color Bar Signal"},
kernelPath: "kernels/static/ebu100.wgsl", kernelSource: mustReadKernel("static/ebu100.wgsl"),
}, },
"ebu100-move": { "ebu100-move": {
PatternInfo: PatternInfo{Name: "ebu100-move", Description: "EBU 100% Color Bar Signal with moving square"}, PatternInfo: PatternInfo{Name: "ebu100-move", Description: "EBU 100% Color Bar Signal with moving square"},
kernelPath: "kernels/dynamic/ebu100.wgsl", kernelSource: mustReadKernel("dynamic/ebu100.wgsl"),
}, },
"smpte": { "smpte": {
PatternInfo: PatternInfo{Name: "smpte", Description: "SMPTE RP-219 Color Bar Signal"}, PatternInfo: PatternInfo{Name: "smpte", Description: "SMPTE RP-219 Color Bar Signal"},
kernelPath: "kernels/static/smpteBars.wgsl", kernelSource: mustReadKernel("static/smpteBars.wgsl"),
}, },
"smpte-move": { "smpte-move": {
PatternInfo: PatternInfo{Name: "smpte-move", Description: "SMPTE RP-219 Color Bar Signal with moving square"}, PatternInfo: PatternInfo{Name: "smpte-move", Description: "SMPTE RP-219 Color Bar Signal with moving square"},
kernelPath: "kernels/dynamic/smpteBars.wgsl", kernelSource: mustReadKernel("dynamic/smpteBars.wgsl"),
}, },
"gray-bars": { "gray-bars": {
PatternInfo: PatternInfo{Name: "gray-bars", Description: "13-step grayscale bars (Y 64..940)"}, PatternInfo: PatternInfo{Name: "gray-bars", Description: "13-step grayscale bars (Y 64..940)"},
kernelPath: "kernels/static/yBars.wgsl", kernelSource: mustReadKernel("static/yBars.wgsl"),
}, },
"gray-bars-move": { "gray-bars-move": {
PatternInfo: PatternInfo{Name: "gray-bars-move", Description: "13-step grayscale bars (Y 64..940) with moving square"}, PatternInfo: PatternInfo{Name: "gray-bars-move", Description: "13-step grayscale bars (Y 64..940) with moving square"},
kernelPath: "kernels/dynamic/yBars.wgsl", kernelSource: mustReadKernel("dynamic/yBars.wgsl"),
}, },
"gray-ramp": { "gray-ramp": {
PatternInfo: PatternInfo{Name: "gray-ramp", Description: "Y gradient (black -> 100% white)"}, PatternInfo: PatternInfo{Name: "gray-ramp", Description: "Y gradient (black -> 100% white)"},
kernelPath: "kernels/static/yRamp.wgsl", kernelSource: mustReadKernel("static/yRamp.wgsl"),
}, },
"gray-ramp-move": { "gray-ramp-move": {
PatternInfo: PatternInfo{Name: "gray-ramp-move", Description: "Y gradient with moving square"}, PatternInfo: PatternInfo{Name: "gray-ramp-move", Description: "Y gradient with moving square"},
kernelPath: "kernels/dynamic/yRamp.wgsl", kernelSource: mustReadKernel("dynamic/yRamp.wgsl"),
}, },
} }
func mustReadKernel(name string) string {
source, err := kernels.Read(name)
if err != nil {
panic(fmt.Sprintf("read embedded video kernel %q: %v", name, err))
}
return string(source)
}
func HasPattern(name string) bool { func HasPattern(name string) bool {
_, ok := patterns[name] _, ok := patterns[name]
return ok return ok
+2 -2
View File
@@ -40,8 +40,8 @@ func TestPatternRegistryKeysMatchNames(t *testing.T) {
if pattern.Name != name { if pattern.Name != name {
t.Errorf("pattern map key %q does not match pattern name %q", name, pattern.Name) t.Errorf("pattern map key %q does not match pattern name %q", name, pattern.Name)
} }
if pattern.kernelPath == "" { if pattern.kernelSource == "" {
t.Errorf("pattern %q has an empty kernel path", name) t.Errorf("pattern %q has empty kernel source", name)
} }
} }
} }
+4 -2
View File
@@ -6,6 +6,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"log" "log"
"mxl-pattern-generator/assets"
"mxl-pattern-generator/internal/generator" "mxl-pattern-generator/internal/generator"
"github.com/qvest-digital/go-mxl/mxl" "github.com/qvest-digital/go-mxl/mxl"
@@ -18,7 +20,7 @@ func Run(ctx context.Context, inst *mxl.Instance, cfg Config) (runErr error) {
} }
// TODO: fall back to a CPU generator if GPU initialization fails. // TODO: fall back to a CPU generator if GPU initialization fails.
gen, err := generator.NewWGPUGenerator(cfg.Width(), cfg.Height(), pattern.kernelPath) 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 fmt.Errorf("initialize wgpu video generator: %w", err)
} }
@@ -102,7 +104,7 @@ func buildTextOverlay(cfg Config) (overlay *generator.TextOverlay, resultErr err
return nil, nil return nil, nil
} }
face, err := generator.LoadFace("assets/fonts/JetBrainsMonoNLNerdFontMono-Regular.ttf", 48) face, err := generator.NewFace(assets.JetBrainsMono, 48)
if err != nil { if err != nil {
return nil, fmt.Errorf("load text overlay font: %w", err) return nil, fmt.Errorf("load text overlay font: %w", err)
} }
-5
View File
@@ -34,9 +34,6 @@ func TestBuildTextOverlayDisabled(t *testing.T) {
} }
func TestBuildTextOverlayPositioning(t *testing.T) { func TestBuildTextOverlayPositioning(t *testing.T) {
// Production assets are resolved from the repository root.
t.Chdir("../..")
tests := []struct { tests := []struct {
name string name string
overlay OverlayConfig overlay OverlayConfig
@@ -59,8 +56,6 @@ func TestBuildTextOverlayPositioning(t *testing.T) {
} }
func TestBuildTextOverlayRejectsInvalidPosition(t *testing.T) { func TestBuildTextOverlayRejectsInvalidPosition(t *testing.T) {
t.Chdir("../..")
tests := []struct { tests := []struct {
name string name string
overlay OverlayConfig overlay OverlayConfig
+12
View File
@@ -0,0 +1,12 @@
package kernels
import "embed"
// files contains every built-in video pattern shader.
//
//go:embed static/*.wgsl dynamic/*.wgsl
var files embed.FS
func Read(name string) ([]byte, error) {
return files.ReadFile(name)
}