refactoring finished
This commit is contained in:
@@ -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
|
||||
@@ -33,9 +33,19 @@ func LoadFace(path string, size float64) (font.Face, error) {
|
||||
if err != nil {
|
||||
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)
|
||||
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{
|
||||
Size: size,
|
||||
@@ -43,7 +53,7 @@ func LoadFace(path string, size float64) (font.Face, error) {
|
||||
Hinting: font.HintingFull,
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"mxl-pattern-generator/assets"
|
||||
|
||||
"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)
|
||||
face, err := NewFace(assets.JetBrainsMono, size)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadFace: %v", err)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gogpu/gputypes"
|
||||
@@ -47,7 +46,7 @@ var _ FrameGenerator = (*WGPUGenerator)(nil)
|
||||
// WGPUOption customizes NewWGPUGenerator.
|
||||
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{
|
||||
width: int(width),
|
||||
height: int(height),
|
||||
@@ -75,13 +74,8 @@ func NewWGPUGenerator(width, height uint, kernelPath string, opts ...WGPUOption)
|
||||
}
|
||||
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{
|
||||
Label: "v210-shader", WGSL: string(wgsl),
|
||||
Label: "v210-shader", WGSL: wgsl,
|
||||
}); err != nil {
|
||||
g.Close()
|
||||
return nil, fmt.Errorf("wgpu: shader: %w", err)
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"mxl-pattern-generator/kernels"
|
||||
)
|
||||
|
||||
func TestWGPUMoveSquare(t *testing.T) {
|
||||
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 {
|
||||
t.Fatalf("init: %v", err)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"mxl-pattern-generator/kernels"
|
||||
)
|
||||
|
||||
func TestWGPUSMPTEPattern(t *testing.T) {
|
||||
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 {
|
||||
t.Fatalf("init: %v", err)
|
||||
}
|
||||
|
||||
+35
-22
@@ -1,6 +1,11 @@
|
||||
package video
|
||||
|
||||
import "sort"
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"mxl-pattern-generator/kernels"
|
||||
)
|
||||
|
||||
type PatternInfo struct {
|
||||
Name string
|
||||
@@ -9,52 +14,60 @@ type PatternInfo struct {
|
||||
|
||||
type pattern struct {
|
||||
PatternInfo
|
||||
kernelPath string
|
||||
kernelSource string
|
||||
}
|
||||
|
||||
var patterns = map[string]pattern{
|
||||
"ebu75": {
|
||||
PatternInfo: PatternInfo{Name: "ebu75", Description: "EBU 75% Color Bar Signal"},
|
||||
kernelPath: "kernels/static/ebu75.wgsl",
|
||||
PatternInfo: PatternInfo{Name: "ebu75", Description: "EBU 75% Color Bar Signal"},
|
||||
kernelSource: mustReadKernel("static/ebu75.wgsl"),
|
||||
},
|
||||
"ebu75-move": {
|
||||
PatternInfo: PatternInfo{Name: "ebu75-move", Description: "EBU 75% Color Bar Signal with moving square"},
|
||||
kernelPath: "kernels/dynamic/ebu75.wgsl",
|
||||
PatternInfo: PatternInfo{Name: "ebu75-move", Description: "EBU 75% Color Bar Signal with moving square"},
|
||||
kernelSource: mustReadKernel("dynamic/ebu75.wgsl"),
|
||||
},
|
||||
"ebu100": {
|
||||
PatternInfo: PatternInfo{Name: "ebu100", Description: "EBU 100% Color Bar Signal"},
|
||||
kernelPath: "kernels/static/ebu100.wgsl",
|
||||
PatternInfo: PatternInfo{Name: "ebu100", Description: "EBU 100% Color Bar Signal"},
|
||||
kernelSource: mustReadKernel("static/ebu100.wgsl"),
|
||||
},
|
||||
"ebu100-move": {
|
||||
PatternInfo: PatternInfo{Name: "ebu100-move", Description: "EBU 100% Color Bar Signal with moving square"},
|
||||
kernelPath: "kernels/dynamic/ebu100.wgsl",
|
||||
PatternInfo: PatternInfo{Name: "ebu100-move", Description: "EBU 100% Color Bar Signal with moving square"},
|
||||
kernelSource: mustReadKernel("dynamic/ebu100.wgsl"),
|
||||
},
|
||||
"smpte": {
|
||||
PatternInfo: PatternInfo{Name: "smpte", Description: "SMPTE RP-219 Color Bar Signal"},
|
||||
kernelPath: "kernels/static/smpteBars.wgsl",
|
||||
PatternInfo: PatternInfo{Name: "smpte", Description: "SMPTE RP-219 Color Bar Signal"},
|
||||
kernelSource: mustReadKernel("static/smpteBars.wgsl"),
|
||||
},
|
||||
"smpte-move": {
|
||||
PatternInfo: PatternInfo{Name: "smpte-move", Description: "SMPTE RP-219 Color Bar Signal with moving square"},
|
||||
kernelPath: "kernels/dynamic/smpteBars.wgsl",
|
||||
PatternInfo: PatternInfo{Name: "smpte-move", Description: "SMPTE RP-219 Color Bar Signal with moving square"},
|
||||
kernelSource: mustReadKernel("dynamic/smpteBars.wgsl"),
|
||||
},
|
||||
"gray-bars": {
|
||||
PatternInfo: PatternInfo{Name: "gray-bars", Description: "13-step grayscale bars (Y 64..940)"},
|
||||
kernelPath: "kernels/static/yBars.wgsl",
|
||||
PatternInfo: PatternInfo{Name: "gray-bars", Description: "13-step grayscale bars (Y 64..940)"},
|
||||
kernelSource: mustReadKernel("static/yBars.wgsl"),
|
||||
},
|
||||
"gray-bars-move": {
|
||||
PatternInfo: PatternInfo{Name: "gray-bars-move", Description: "13-step grayscale bars (Y 64..940) with moving square"},
|
||||
kernelPath: "kernels/dynamic/yBars.wgsl",
|
||||
PatternInfo: PatternInfo{Name: "gray-bars-move", Description: "13-step grayscale bars (Y 64..940) with moving square"},
|
||||
kernelSource: mustReadKernel("dynamic/yBars.wgsl"),
|
||||
},
|
||||
"gray-ramp": {
|
||||
PatternInfo: PatternInfo{Name: "gray-ramp", Description: "Y gradient (black -> 100% white)"},
|
||||
kernelPath: "kernels/static/yRamp.wgsl",
|
||||
PatternInfo: PatternInfo{Name: "gray-ramp", Description: "Y gradient (black -> 100% white)"},
|
||||
kernelSource: mustReadKernel("static/yRamp.wgsl"),
|
||||
},
|
||||
"gray-ramp-move": {
|
||||
PatternInfo: PatternInfo{Name: "gray-ramp-move", Description: "Y gradient with moving square"},
|
||||
kernelPath: "kernels/dynamic/yRamp.wgsl",
|
||||
PatternInfo: PatternInfo{Name: "gray-ramp-move", Description: "Y gradient with moving square"},
|
||||
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 {
|
||||
_, ok := patterns[name]
|
||||
return ok
|
||||
|
||||
@@ -40,8 +40,8 @@ func TestPatternRegistryKeysMatchNames(t *testing.T) {
|
||||
if pattern.Name != name {
|
||||
t.Errorf("pattern map key %q does not match pattern name %q", name, pattern.Name)
|
||||
}
|
||||
if pattern.kernelPath == "" {
|
||||
t.Errorf("pattern %q has an empty kernel path", name)
|
||||
if pattern.kernelSource == "" {
|
||||
t.Errorf("pattern %q has empty kernel source", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"mxl-pattern-generator/assets"
|
||||
"mxl-pattern-generator/internal/generator"
|
||||
|
||||
"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.
|
||||
gen, err := generator.NewWGPUGenerator(cfg.Width(), cfg.Height(), pattern.kernelPath)
|
||||
gen, err := generator.NewWGPUGenerator(cfg.Width(), cfg.Height(), pattern.kernelSource)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
face, err := generator.LoadFace("assets/fonts/JetBrainsMonoNLNerdFontMono-Regular.ttf", 48)
|
||||
face, err := generator.NewFace(assets.JetBrainsMono, 48)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load text overlay font: %w", err)
|
||||
}
|
||||
|
||||
@@ -34,9 +34,6 @@ func TestBuildTextOverlayDisabled(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBuildTextOverlayPositioning(t *testing.T) {
|
||||
// Production assets are resolved from the repository root.
|
||||
t.Chdir("../..")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
overlay OverlayConfig
|
||||
@@ -59,8 +56,6 @@ func TestBuildTextOverlayPositioning(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBuildTextOverlayRejectsInvalidPosition(t *testing.T) {
|
||||
t.Chdir("../..")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
overlay OverlayConfig
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user