WGPU is coming

This commit is contained in:
Dmitry Sergeev
2026-09-07 17:18:03 +03:00
commit f48195a8bf
19 changed files with 2230 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
# Binaries that mix libmxl (cgo) and wgpu/goffi must use the internal
# linker: goffi's dlopen stubs (SDYNIMPORT relocations) do not survive
# external linking, which the go tool auto-selects for that call graph.
# Harmless for binaries that don't need it.
LD = -ldflags=-linkmode=internal
.PHONY: build test vet wgpu-gen clean
build:
go build $(LD) ./...
test:
go test $(LD) ./...
vet:
go vet ./...
wgpu-gen:
go run $(LD) ./cmd/wgpu-gen
clean:
go clean
+159
View File
@@ -0,0 +1,159 @@
package main
import (
"encoding/binary"
"log"
flowdef "mxl-pattern-generator/internal/flow-def"
"mxl-pattern-generator/internal/generator"
"os"
"os/signal"
"syscall"
"github.com/qvest-digital/go-mxl/mxl"
)
func main() {
var width, height uint = 1920, 1080
var fps_num, fps_den uint = 25, 1
flowUUID := "5fbec3b1-1b0f-417d-9059-8b94a47197ed"
log.Printf("Go Pattern Gen")
log.Printf("Video: %dx%dp%d/%d", width, height, fps_num, fps_den)
log.Printf("UUID: %s", flowUUID)
domain := "/dev/shm/mxl"
inst, err := mxl.NewInstance(domain, "")
if err != nil {
log.Fatalf("MXL Init Failed: %v", err)
}
defer inst.Close()
flowDef, err := flowdef.NewFlowDefJSON(
flowdef.TYPE_VIDEO,
flowUUID,
width,
height,
fps_num,
fps_den,
)
if err != nil {
log.Fatalf("Could not create Flow Definition: %v", err)
}
writer, isCreated, err := inst.NewWriter(flowDef)
if err != nil {
log.Fatalf("Failed to create MXL writer: %v", err)
}
if !isCreated {
log.Printf("reusing existing flow: %s, domain: %s", flowUUID, domain)
}
defer writer.Close()
var gen *generator.OpenCLGenerator
gen, err = generator.NewOpenCLGenerator(width, height, "/home/itten/codeproj/go-mxl-pattern-gen/kernels/v210_pack.cl")
gen, err = generator.NewOpenCLGenerator(width, height, "/home/itten/codeproj/go-mxl-pattern-gen/kernels/v210_pack.cl")
if err != nil {
log.Fatalf("OpenCL init failed: %v", err)
}
defer gen.Close()
gen.SetText(generator.TextConfig{
Str: "MXL PATTERN GEN",
X: 0, Y: 0, Scale: 8,
FG: [3]uint32{940, 512, 512},
BG: [3]uint32{64, 512, 512},
Boxed: true,
})
flowCfg := writer.Config()
rate := flowCfg.Common.GrainRate
idx := mxl.CurrentIndex(rate)
log.Printf("writing flow grainRate=%d/%d starting at idx=%d", rate.Num, rate.Den, idx)
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
var written int64
for {
select {
case <-stop:
log.Printf("stopping after %d grains", written)
return
default:
}
gwa, err := writer.OpenGrain(idx)
if err != nil {
log.Fatalf("OpenGrain(%d): %v", idx, err)
}
// fillGrainRamp(gwa.Payload, idx)
// smpteBars(gwa.Payload)
gen.GenerateFrame(gwa.Payload, int(idx))
// if err != nil {
// log.Fatalf("Frame Generator: %v", err)
// }
if err := gwa.Commit(gwa.TotalSlices, 0); err != nil {
log.Fatalf("Commit(%d): %v", idx, err)
}
written++
idx++
if written%100 == 0 {
log.Printf("grains written=%d, index=%d", written, idx)
}
// Pace ourselves to roughly the grain rate
mxl.SleepNs(mxl.NsUntilIndex(idx, rate))
}
}
// fillGrainRamp writes byte((i+start)&0xFF) across buf. A matching reader
// uses the same pattern to verify grain continuit
func fillGrainRamp(buf []byte, start uint64) {
for i := range buf {
buf[i] = byte((uint64(i) + start) & 0xFF)
}
}
func smpteBars(dest []byte) {
const width = 1920
// SMPTE 75% bars, Rec.709, 10-bit Y'CbCr: {Y, Cb, Cr}
bars := [7][3]uint32{
{721, 512, 512}, // 75% white
{674, 176, 543}, // 75% yellow
{581, 589, 176}, // 75% cyan
{534, 253, 207}, // 75% green
{251, 771, 817}, // 75% magenta
{204, 435, 848}, // 75% red
{111, 848, 481}, // 75% blue
}
barIdx := func(x int) int {
idx := x * len(bars) / width
if idx >= len(bars) {
return len(bars) - 1
}
return idx
}
// v210: each 16-byte block carries 6 pixels (Y0..Y5, chroma co-sited at 0/2/4)
for block := 0; block*16+16 <= len(dest); block++ {
baseX := (block * 6) % width
var y [6]uint32
for i := range y {
y[i] = bars[barIdx(baseX+i)][0]
}
cb0, cr0 := bars[barIdx(baseX)][1], bars[barIdx(baseX)][2]
cb2, cr2 := bars[barIdx(baseX+2)][1], bars[barIdx(baseX+2)][2]
cb4, cr4 := bars[barIdx(baseX+4)][1], bars[barIdx(baseX+4)][2]
w0 := (cb0 & 0x3FF) | ((y[0] & 0x3FF) << 10) | ((cr0 & 0x3FF) << 20)
w1 := (y[1] & 0x3FF) | ((cb2 & 0x3FF) << 10) | ((y[2] & 0x3FF) << 20)
w2 := (cr2 & 0x3FF) | ((y[3] & 0x3FF) << 10) | ((cb4 & 0x3FF) << 20)
w3 := (y[4] & 0x3FF) | ((cr4 & 0x3FF) << 10) | ((y[5] & 0x3FF) << 20)
byteIdx := block * 16
binary.LittleEndian.PutUint32(dest[byteIdx+0:], w0)
binary.LittleEndian.PutUint32(dest[byteIdx+4:], w1)
binary.LittleEndian.PutUint32(dest[byteIdx+8:], w2)
binary.LittleEndian.PutUint32(dest[byteIdx+12:], w3)
}
}
+99
View File
@@ -0,0 +1,99 @@
// MXL pattern generator feed rendered on the GPU via wgpu (WebGPU/Vulkan).
// Run from the repo root: make wgpu-gen
// (mixing libmxl cgo with wgpu/goffi needs the internal linker)
package main
import (
"log"
"os"
"os/signal"
"syscall"
"github.com/qvest-digital/go-mxl/mxl"
flowdef "mxl-pattern-generator/internal/flow-def"
"mxl-pattern-generator/internal/generator"
)
func main() {
var width, height uint = 1920, 1080
var fpsNum, fpsDen uint = 25, 1
flowUUID := "8f1d2a4b-6c3e-4f5a-9b2c-1d7e8a3f0b5d"
domain := "/dev/shm/mxl"
log.Printf("Go Pattern Gen (wgpu)")
log.Printf("Video: %dx%dp%d/%d", width, height, fpsNum, fpsDen)
log.Printf("UUID: %s", flowUUID)
// gen, err := generator.NewWGPUGenerator(width, height, "kernels/v210_bars.wgsl")
gen, err := generator.NewWGPUGenerator(width, height, "kernels/v210_bars_move.wgsl")
if err != nil {
log.Fatalf("wgpu init failed: %v", err)
}
defer gen.Close()
inst, err := mxl.NewInstance(domain, "")
if err != nil {
log.Fatalf("MXL Init Failed: %v", err)
}
defer inst.Close()
flowDef, err := flowdef.NewFlowDefJSON(
flowdef.TYPE_VIDEO,
flowUUID,
width,
height,
fpsNum,
fpsDen,
)
if err != nil {
log.Fatalf("Could not create Flow Definition: %v", err)
}
writer, isCreated, err := inst.NewWriter(flowDef)
if err != nil {
log.Fatalf("Failed to create MXL writer: %v", err)
}
if !isCreated {
log.Printf("reusing existing flow: %s, domain: %s", flowUUID, domain)
}
defer writer.Close()
flowCfg := writer.Config()
rate := flowCfg.Common.GrainRate
idx := mxl.CurrentIndex(rate)
log.Printf("writing flow grainRate=%d/%d starting at idx=%d", rate.Num, rate.Den, idx)
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
var written int64
var tick uint32 // animation clock: small counter, not the huge grain index
for {
select {
case <-stop:
log.Printf("stopping after %d grains", written)
return
default:
}
gwa, err := writer.OpenGrain(idx)
if err != nil {
log.Fatalf("OpenGrain(%d): %v", idx, err)
}
if err := gen.GenerateFrame(gwa.Payload, int(tick)); err != nil {
log.Fatalf("GenerateFrame(%d): %v", idx, err)
}
if err := gwa.Commit(gwa.TotalSlices, 0); err != nil {
log.Fatalf("Commit(%d): %v", idx, err)
}
written++
idx++
tick++
if written%100 == 0 {
log.Printf("grains written=%d, index=%d", written, idx)
}
// Pace ourselves to roughly the grain rate
mxl.SleepNs(mxl.NsUntilIndex(idx, rate))
}
}
+282
View File
@@ -0,0 +1,282 @@
// Command wgpu-sample renders 75% SMPTE color bars (Rec.709, 10-bit v210)
// with a wgpu compute shader, verifies the output against the reference
// table, and reports per-frame timings.
//
// Run from the repo root: go run ./cmd/wgpu-sample
package main
import (
"context"
"encoding/binary"
"fmt"
"log"
"os"
"time"
"github.com/gogpu/gputypes"
"github.com/gogpu/wgpu"
// Register all available GPU backends (Vulkan, DX12, GLES, Metal, etc.)
_ "github.com/gogpu/wgpu/hal/allbackends"
)
const (
width, height = 1920, 1080
wgSize = 64
blocks = width * height / 6
frameSize = uint64(blocks * 16)
benchFrames = 100
)
// 75% SMPTE bars, Rec.709, 10-bit {Y, Cb, Cr}
var bars = [7][3]uint32{
{721, 512, 512}, {674, 176, 543}, {581, 589, 176},
{534, 253, 207}, {251, 771, 817}, {204, 435, 848}, {111, 848, 481},
}
func main() {
if err := run(); err != nil {
log.Fatalf("FATAL: %v", err)
}
}
func run() error {
instance, err := wgpu.CreateInstance(nil)
if err != nil {
return fmt.Errorf("CreateInstance: %w", err)
}
defer instance.Release()
adapter, err := instance.RequestAdapter(nil)
if err != nil {
return fmt.Errorf("RequestAdapter: %w", err)
}
defer adapter.Release()
info := adapter.Info()
fmt.Printf("adapter: %s (%s, %s, driver %s)\n", info.Name, info.Vendor, info.DeviceType, info.Driver)
device, err := adapter.RequestDevice(nil)
if err != nil {
return fmt.Errorf("RequestDevice: %w", err)
}
defer device.Release()
queue := device.Queue()
wgsl, err := os.ReadFile("kernels/v210_bars.wgsl")
if err != nil {
return fmt.Errorf("read kernel: %w", err)
}
out, err := device.CreateBuffer(&wgpu.BufferDescriptor{
Label: "v210-out", Size: frameSize,
Usage: wgpu.BufferUsageStorage | wgpu.BufferUsageCopySrc,
})
if err != nil {
return err
}
defer out.Release()
staging, err := device.CreateBuffer(&wgpu.BufferDescriptor{
Label: "v210-staging", Size: frameSize,
Usage: wgpu.BufferUsageCopyDst | wgpu.BufferUsageMapRead,
})
if err != nil {
return err
}
defer staging.Release()
params := make([]byte, 16)
binary.LittleEndian.PutUint32(params[0:], width)
binary.LittleEndian.PutUint32(params[4:], height)
uniform, err := device.CreateBuffer(&wgpu.BufferDescriptor{
Label: "params", Size: uint64(len(params)),
Usage: wgpu.BufferUsageUniform | wgpu.BufferUsageCopyDst,
})
if err != nil {
return err
}
defer uniform.Release()
if err := queue.WriteBuffer(uniform, 0, params); err != nil {
return fmt.Errorf("write params: %w", err)
}
shader, err := device.CreateShaderModule(&wgpu.ShaderModuleDescriptor{
Label: "bars-shader", WGSL: string(wgsl),
})
if err != nil {
return fmt.Errorf("shader: %w", err)
}
defer shader.Release()
bgl, err := device.CreateBindGroupLayout(&wgpu.BindGroupLayoutDescriptor{
Label: "bars-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}},
},
})
if err != nil {
return err
}
defer bgl.Release()
bg, err := device.CreateBindGroup(&wgpu.BindGroupDescriptor{
Label: "bars-bg", Layout: bgl,
Entries: []wgpu.BindGroupEntry{
{Binding: 0, Buffer: out, Size: frameSize},
{Binding: 1, Buffer: uniform, Size: uint64(len(params))},
},
})
if err != nil {
return err
}
defer bg.Release()
pl, err := device.CreatePipelineLayout(&wgpu.PipelineLayoutDescriptor{
Label: "bars-pl", BindGroupLayouts: []*wgpu.BindGroupLayout{bgl},
})
if err != nil {
return err
}
defer pl.Release()
pipeline, err := device.CreateComputePipeline(&wgpu.ComputePipelineDescriptor{
Label: "bars-pipeline", Layout: pl, Module: shader, EntryPoint: "main",
})
if err != nil {
return fmt.Errorf("pipeline: %w", err)
}
defer pipeline.Release()
render := func(frame uint32) error {
binary.LittleEndian.PutUint32(params[8:], frame)
if err := queue.WriteBuffer(uniform, 0, params); err != nil {
return err
}
encoder, err := device.CreateCommandEncoder(nil)
if err != nil {
return err
}
pass, err := encoder.BeginComputePass(nil)
if err != nil {
return err
}
pass.SetPipeline(pipeline)
pass.SetBindGroup(0, bg, nil)
pass.Dispatch((blocks+wgSize-1)/wgSize, 1, 1)
if err := pass.End(); err != nil {
return err
}
encoder.CopyBufferToBuffer(out, 0, staging, 0, frameSize)
cmd, err := encoder.Finish()
if err != nil {
return err
}
if _, err := queue.Submit(cmd); err != nil {
return err
}
return nil
}
readback := func() ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := staging.Map(ctx, wgpu.MapModeRead, 0, frameSize); err != nil {
return nil, fmt.Errorf("map: %w", err)
}
rng, err := staging.MappedRange(0, frameSize)
if err != nil {
_ = staging.Unmap()
return nil, fmt.Errorf("mapped range: %w", err)
}
data := make([]byte, frameSize)
copy(data, rng.Bytes())
if err := staging.Unmap(); err != nil {
return nil, err
}
return data, nil
}
// Correctness: render one frame and verify every pixel.
fmt.Printf("frame: %dx%d v210, %d bytes\n", width, height, frameSize)
if err := render(0); err != nil {
return fmt.Errorf("render: %w", err)
}
data, err := readback()
if err != nil {
return fmt.Errorf("readback: %w", err)
}
if err := verify(data); err != nil {
return err
}
fmt.Println("verify: PASS (all 1920x1080 pixels match the 75% Rec.709 table)")
// Rough timing: full dispatch + copy + map/unmap per frame.
for i := 0; i < 10; i++ {
if err := render(uint32(i)); err != nil {
return err
}
if _, err := readback(); err != nil {
return err
}
}
start := time.Now()
for i := 0; i < benchFrames; i++ {
if err := render(uint32(i)); err != nil {
return err
}
if _, err := readback(); err != nil {
return err
}
}
perFrame := time.Since(start) / benchFrames
fmt.Printf("timing: %v/frame (dispatch+copy+readback), %.1f fps equivalent\n",
perFrame, float64(time.Second)/float64(perFrame))
return nil
}
// verify decodes the v210 frame and checks Y at every pixel and the
// co-sited chroma (even columns) against the reference table.
func verify(data []byte) error {
barOf := func(x int) int {
if b := x * 7 / width; b < 7 {
return b
}
return 6
}
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
p := y*width + x
off := (p / 6) * 16
w0 := binary.LittleEndian.Uint32(data[off:])
w1 := binary.LittleEndian.Uint32(data[off+4:])
w2 := binary.LittleEndian.Uint32(data[off+8:])
w3 := binary.LittleEndian.Uint32(data[off+12:])
var yv, cb, cr uint32
switch p % 6 {
case 0:
yv, cb, cr = (w0>>10)&0x3FF, w0&0x3FF, (w0>>20)&0x3FF
case 1:
yv = w1 & 0x3FF
case 2:
yv, cb, cr = (w1>>20)&0x3FF, (w1>>10)&0x3FF, w2&0x3FF
case 3:
yv = (w2 >> 10) & 0x3FF
case 4:
yv, cb, cr = w3&0x3FF, (w2>>20)&0x3FF, (w3>>10)&0x3FF
case 5:
yv = (w3 >> 20) & 0x3FF
}
b := bars[barOf(x)]
if yv != b[0] {
return fmt.Errorf("pixel (%d,%d): Y=%d want %d", x, y, yv, b[0])
}
if x%2 == 0 && (cb != b[1] || cr != b[2]) {
return fmt.Errorf("pixel (%d,%d): Cb=%d Cr=%d want %d/%d", x, y, cb, cr, b[1], b[2])
}
}
}
return nil
}
+17
View File
@@ -0,0 +1,17 @@
module mxl-pattern-generator
go 1.26.0
require (
github.com/gogpu/gputypes v0.8.0
github.com/gogpu/wgpu v0.34.3
github.com/qvest-digital/go-mxl v1.1.0-rc.4
)
require (
github.com/go-webgpu/goffi v0.6.3 // indirect
github.com/go-webgpu/webgpu v0.5.5 // indirect
github.com/gogpu/gpucontext v0.31.3 // indirect
github.com/gogpu/naga v0.19.0 // indirect
golang.org/x/sys v0.47.0 // indirect
)
+16
View File
@@ -0,0 +1,16 @@
github.com/go-webgpu/goffi v0.6.3 h1:p4gKGikHBAQ/8iUiew9MV4C5M1ZGIsk8QGFGuJjMj+A=
github.com/go-webgpu/goffi v0.6.3/go.mod h1:wfoxNsJkU+5RFbV1kNN1kunhc1lFHuJKK3zpgx08/uM=
github.com/go-webgpu/webgpu v0.5.5 h1:pIrXzRg0LRlNjNmR+ZNo/ERN88YaObzbCY5oYhir5SA=
github.com/go-webgpu/webgpu v0.5.5/go.mod h1:vgIuNTa1UlZ4njCGY6Pmp/0c5T5o2Ml2fQ0znLc7B98=
github.com/gogpu/gpucontext v0.31.3 h1:SMnYOQgr+s5issi1R/Oezbo8lV5Lb6HWUZ4HujFgrZA=
github.com/gogpu/gpucontext v0.31.3/go.mod h1:pnyWQA9lpvESPAKoqzKlsg3pDMFSkxdjonu4gWvnNtA=
github.com/gogpu/gputypes v0.8.0 h1:Iw4g9lqydTcAFKKQWfgbUpdY6WxObGtjf9SuRkzYMJw=
github.com/gogpu/gputypes v0.8.0/go.mod h1:cnXrDMwTpWTvJLW1Vreop3PcT6a2YP/i3s91rPaOavw=
github.com/gogpu/naga v0.19.0 h1:+Pu7kahdMhvRWtpEbTW5YFQZPoklccMO4vryEk6w9zU=
github.com/gogpu/naga v0.19.0/go.mod h1:15sQaHKkbqXcwTN+hHYGLsA0WBBnkmYzne/eF5p5WEg=
github.com/gogpu/wgpu v0.34.3 h1:oV7K5ueBqxZuvp1w9KpArzj15+eOrm/l9ZnM3jcKbXA=
github.com/gogpu/wgpu v0.34.3/go.mod h1:erhaIjD9psJKjHqAhn5MWdJETcACR1NGoqLhkTAHkFM=
github.com/qvest-digital/go-mxl v1.1.0-rc.4 h1:qgOGSIv53HYQLTB06m39c6WE2xqMceObViZy5A3rZlA=
github.com/qvest-digital/go-mxl v1.1.0-rc.4/go.mod h1:cYzyT+S/AONytsKr/DozzFQP2OrNrg/JsQOSjvwn+Rk=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+111
View File
@@ -0,0 +1,111 @@
// MXL Flow Definition helper
package flowdef
import (
"encoding/json"
"errors"
)
const (
TYPE_VIDEO = iota
TYPE_VIDEO_ALPHA
TYPE_AUDIO
)
var (
ErrUnknownFlowType = errors.New("Unknown flow type provided")
ErrIncorrectFlowUUID = errors.New("Incorrect flow uuid")
)
type flowDef struct {
Description string `json:"description"`
Id string `json:"id"`
Tags map[string][]string `json:"tags"`
Format string `json:"format"`
Label string `json:"label"`
Parents []string `json:"parents"`
MediaType string `json:"media_type"`
GrainRate grainRate `json:"grain_rate"`
FrameWidth uint `json:"frame_width"`
FrameHeight uint `json:"frame_height"`
InterlaceMode string `json:"interlace_mode"`
ColorSpace string `json:"colorspace"`
Components [3]videoComponent `json:"components"`
}
type grainRate struct {
Numerator uint `json:"numerator"`
Denominator uint `json:"denominator"`
}
type videoComponent struct {
Name string `json:"name"`
Width uint `json:"width"`
Height uint `json:"height"`
BitDepth uint `json:"bit_depth"`
}
func NewFlowDefJSON(
feedType int,
uuid string,
width uint,
height uint,
fpsNum uint,
fpsDen uint,
) (string, error) {
if feedType != TYPE_VIDEO &&
feedType != TYPE_VIDEO_ALPHA &&
feedType != TYPE_AUDIO {
return "", ErrUnknownFlowType
}
if uuid == "" {
return "", ErrIncorrectFlowUUID
}
tags := map[string][]string{
"urn:x-nmos:tag:grouphint/v1.0": {
"___one day I will follow NMOS specs___:Video",
},
}
flowDef := flowDef{
Description: "go-mxl-pattern-gen generated feed",
Id: uuid,
Tags: tags,
Format: "urn:x-nmos:format:video",
Label: "go-mxl-pattern-gen generated feed",
Parents: nil,
MediaType: "video/v210",
GrainRate: grainRate{
Numerator: fpsNum,
Denominator: fpsDen,
},
FrameWidth: width,
FrameHeight: height,
InterlaceMode: "progressive",
ColorSpace: "BT709",
Components: [3]videoComponent{
videoComponent{
Name: "Y",
Width: width,
Height: height,
BitDepth: 10,
},
videoComponent{
Name: "Cb",
Width: width / 2,
Height: height,
BitDepth: 10,
},
videoComponent{
Name: "Cr",
Width: width / 2,
Height: height,
BitDepth: 10,
},
},
}
jsonBytes, err := json.Marshal(flowDef)
if err != nil {
return "", err
}
return string(jsonBytes), nil
}
+102
View File
@@ -0,0 +1,102 @@
package generator
// 8x16 1-bpp bitmap font baked from DejaVu Sans Mono 13px (public-domain
// style console glyphs). ASCII 32..127, one byte per row, LSB = leftmost pixel.
var fontAtlas8x16 = [96 * 16]byte{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x24, 0x24, 0x24, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x48, 0x68, 0x28, 0xFE, 0x24, 0x24, 0x7F, 0x16, 0x12, 0x12, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x3C, 0x16, 0x02, 0x1C, 0x38, 0x40, 0x62, 0x3C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x0E, 0x09, 0x09, 0x6E, 0x18, 0x76, 0xD0, 0xD0, 0x70, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3C, 0x06, 0x04, 0x0C, 0x9A, 0xD3, 0x63, 0x62, 0x5C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x10, 0x10, 0x18, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x18, 0x10, 0x10, 0x00, 0x00, 0x00,
0x00, 0x0C, 0x08, 0x18, 0x18, 0x10, 0x10, 0x10, 0x10, 0x10, 0x18, 0x08, 0x0C, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x08, 0x4A, 0x1C, 0x1C, 0x4A, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x18, 0x7F, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x08, 0x08, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x60, 0x20, 0x30, 0x10, 0x10, 0x08, 0x08, 0x0C, 0x04, 0x06, 0x02, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3C, 0x26, 0x62, 0x42, 0x5A, 0x42, 0x62, 0x26, 0x3C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x1E, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x7C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3C, 0x22, 0x60, 0x20, 0x30, 0x18, 0x0C, 0x06, 0x7E, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3C, 0x22, 0x60, 0x20, 0x3C, 0x60, 0x60, 0x62, 0x3C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x30, 0x38, 0x28, 0x24, 0x26, 0x22, 0x7E, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3E, 0x06, 0x06, 0x3E, 0x20, 0x60, 0x60, 0x22, 0x1C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3C, 0x06, 0x02, 0x3E, 0x66, 0x42, 0x42, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x7E, 0x60, 0x20, 0x30, 0x10, 0x18, 0x18, 0x08, 0x0C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3C, 0x66, 0x62, 0x26, 0x3C, 0x66, 0x42, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3C, 0x26, 0x62, 0x62, 0x66, 0x7C, 0x60, 0x20, 0x1C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x18, 0x18, 0x08, 0x08, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x38, 0x06, 0x06, 0x38, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x1C, 0x70, 0x70, 0x1C, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3C, 0x60, 0x60, 0x30, 0x18, 0x08, 0x00, 0x08, 0x08, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3C, 0x46, 0x42, 0xF3, 0xC9, 0xC9, 0xC9, 0xF3, 0x02, 0x06, 0x38, 0x00, 0x00,
0x00, 0x00, 0x00, 0x18, 0x18, 0x3C, 0x34, 0x24, 0x66, 0x7E, 0x42, 0xC3, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3E, 0x62, 0x62, 0x62, 0x3E, 0x62, 0x42, 0x62, 0x3E, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x38, 0x44, 0x06, 0x02, 0x02, 0x02, 0x06, 0x44, 0x38, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x1E, 0x22, 0x62, 0x42, 0x42, 0x42, 0x62, 0x22, 0x1E, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x7E, 0x06, 0x06, 0x06, 0x7E, 0x06, 0x06, 0x06, 0x7E, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x7E, 0x06, 0x06, 0x06, 0x7E, 0x06, 0x06, 0x06, 0x06, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3C, 0x46, 0x02, 0x02, 0x72, 0x42, 0x42, 0x46, 0x3C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x42, 0x42, 0x42, 0x42, 0x7E, 0x42, 0x42, 0x42, 0x42, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x7E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7E, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3C, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x32, 0x1E, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x42, 0x22, 0x12, 0x0E, 0x1E, 0x12, 0x32, 0x62, 0x42, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x7E, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x67, 0x67, 0x67, 0x5F, 0x5B, 0x5B, 0x43, 0x43, 0x43, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x46, 0x46, 0x4E, 0x4A, 0x5A, 0x52, 0x72, 0x62, 0x62, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3C, 0x66, 0x62, 0x42, 0x42, 0x42, 0x62, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3E, 0x66, 0x46, 0x46, 0x66, 0x3E, 0x06, 0x06, 0x06, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3C, 0x66, 0x62, 0x42, 0x42, 0x42, 0x62, 0x66, 0x3C, 0x30, 0x20, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3E, 0x62, 0x62, 0x62, 0x3E, 0x32, 0x62, 0x42, 0xC2, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3C, 0x06, 0x02, 0x06, 0x3C, 0x60, 0x40, 0x62, 0x3C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x43, 0x42, 0x62, 0x26, 0x24, 0x24, 0x3C, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xC1, 0xC3, 0xC3, 0x5B, 0x5A, 0x5A, 0x76, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x42, 0x66, 0x3C, 0x18, 0x18, 0x3C, 0x24, 0x66, 0x43, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x43, 0x66, 0x24, 0x3C, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x7E, 0x60, 0x20, 0x30, 0x18, 0x08, 0x04, 0x06, 0x7E, 0x00, 0x00, 0x00, 0x00,
0x00, 0x38, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x38, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x02, 0x06, 0x04, 0x0C, 0x08, 0x08, 0x10, 0x10, 0x30, 0x20, 0x60, 0x00, 0x00,
0x00, 0x1C, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1C, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x18, 0x3C, 0x26, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00,
0x00, 0x00, 0x0C, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x22, 0x60, 0x7C, 0x62, 0x62, 0x7C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x02, 0x02, 0x02, 0x02, 0x3E, 0x66, 0x46, 0x42, 0x46, 0x66, 0x3E, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x44, 0x06, 0x06, 0x06, 0x44, 0x38, 0x00, 0x00, 0x00, 0x00,
0x00, 0x60, 0x60, 0x60, 0x60, 0x7C, 0x66, 0x62, 0x62, 0x62, 0x66, 0x7C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x66, 0x42, 0x7E, 0x02, 0x46, 0x3C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x70, 0x18, 0x08, 0x08, 0x7E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x66, 0x62, 0x62, 0x62, 0x66, 0x7C, 0x60, 0x20, 0x1C, 0x00,
0x00, 0x02, 0x02, 0x02, 0x02, 0x3E, 0x66, 0x66, 0x62, 0x62, 0x62, 0x62, 0x00, 0x00, 0x00, 0x00,
0x00, 0x18, 0x00, 0x00, 0x00, 0x1C, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7E, 0x00, 0x00, 0x00, 0x00,
0x00, 0x10, 0x00, 0x00, 0x00, 0x1C, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x18, 0x0E, 0x00,
0x00, 0x06, 0x06, 0x06, 0x06, 0x66, 0x36, 0x1E, 0x1E, 0x36, 0x66, 0x46, 0x00, 0x00, 0x00, 0x00,
0x00, 0x0E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x70, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x66, 0x66, 0x62, 0x62, 0x62, 0x62, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x66, 0x42, 0x42, 0x42, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x66, 0x46, 0x42, 0x46, 0x66, 0x3E, 0x02, 0x02, 0x02, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x66, 0x62, 0x62, 0x62, 0x66, 0x7C, 0x60, 0x60, 0x60, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x26, 0x06, 0x3C, 0x20, 0x22, 0x3C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x08, 0x08, 0x7E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x78, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0x62, 0x62, 0x62, 0x66, 0x66, 0x7C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x62, 0x26, 0x24, 0x34, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0xC1, 0xC3, 0x5A, 0x5A, 0x7E, 0x66, 0x26, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0x24, 0x18, 0x18, 0x3C, 0x24, 0x42, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x66, 0x24, 0x24, 0x3C, 0x18, 0x18, 0x18, 0x08, 0x06, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x20, 0x10, 0x18, 0x0C, 0x04, 0x7E, 0x00, 0x00, 0x00, 0x00,
0x00, 0x30, 0x18, 0x18, 0x18, 0x08, 0x0E, 0x08, 0x18, 0x18, 0x18, 0x18, 0x30, 0x00, 0x00, 0x00,
0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00,
0x00, 0x0E, 0x08, 0x18, 0x18, 0x18, 0x30, 0x18, 0x18, 0x18, 0x08, 0x08, 0x0E, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x7E, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x7E, 0x00, 0x00,
}
+6
View File
@@ -0,0 +1,6 @@
package generator
type FrameGenerator interface {
GenerateFrame(dest []byte, frameIndex int) error
Close() error
}
+302
View File
@@ -0,0 +1,302 @@
// OpenCL pattern gen
package generator
/*
#cgo linux LDFLAGS: -lOpenCL
#cgo windows LDFLAGS: -lOpenCL
#include <CL/cl.h>
#include <stdlib.h>
static cl_program build_cl_program(cl_context ctx, cl_device_id dev, const char* src, cl_int* err) {
cl_program prog = clCreateProgramWithSource(ctx, 1, &src, NULL, err);
if (*err != CL_SUCCESS) return NULL;
*err = clBuildProgram(prog, 1, &dev, NULL, NULL, NULL);
return prog;
}
static const char* get_build_log(cl_program prog, cl_device_id dev) {
static char buf[4096];
size_t n = 0;
if (clGetProgramBuildInfo(prog, dev, CL_PROGRAM_BUILD_LOG, sizeof(buf) - 1, buf, &n) != CL_SUCCESS) return "";
buf[n < sizeof(buf) - 1 ? n : sizeof(buf) - 1] = '\0';
return buf;
}
*/
import "C"
import (
"fmt"
"os"
"unsafe"
)
type OpenCLGenerator struct {
ctx C.cl_context
queue C.cl_command_queue
kernel C.cl_kernel
prog C.cl_program
dev C.cl_device_id
atlasBuf C.cl_mem
textBuf C.cl_mem
width int
height int
}
// TextConfig describes a burn-in text overlay rendered by the kernel
// with the baked 8x16 bitmap font. FG/BG are 10-bit {Y, Cb, Cr}.
type TextConfig struct {
Str string
X, Y int
Scale int
FG [3]uint32
BG [3]uint32
Boxed bool
}
func (g *OpenCLGenerator) setArg(idx int, size C.size_t, p unsafe.Pointer) error {
if err := C.clSetKernelArg(g.kernel, C.cl_uint(idx), size, p); err != C.CL_SUCCESS {
return fmt.Errorf("opencl: set arg %d failed: %d", idx, err)
}
return nil
}
// setTextArgs pushes the overlay arguments (kernel args 5..16) to the kernel.
// buf is the text byte buffer; a placeholder is allowed when len(cfg.Str) == 0,
// because the kernel checks text_len before dereferencing the text pointer.
func (g *OpenCLGenerator) setTextArgs(buf C.cl_mem, cfg TextConfig) error {
textLen := C.int(len(cfg.Str))
tx, ty := C.int(cfg.X), C.int(cfg.Y)
scale := C.int(cfg.Scale)
if scale < 1 {
scale = 1
}
hasBG := C.int(0)
if cfg.Boxed {
hasBG = 1
}
fgY, fgCb, fgCr := C.cl_uint(cfg.FG[0]), C.cl_uint(cfg.FG[1]), C.cl_uint(cfg.FG[2])
bgY, bgCb, bgCr := C.cl_uint(cfg.BG[0]), C.cl_uint(cfg.BG[1]), C.cl_uint(cfg.BG[2])
args := []struct {
idx int
p unsafe.Pointer
sz C.size_t
}{
{5, unsafe.Pointer(&buf), C.sizeof_cl_mem},
{6, unsafe.Pointer(&textLen), C.sizeof_int},
{7, unsafe.Pointer(&tx), C.sizeof_int},
{8, unsafe.Pointer(&ty), C.sizeof_int},
{9, unsafe.Pointer(&scale), C.sizeof_int},
{10, unsafe.Pointer(&hasBG), C.sizeof_int},
{11, unsafe.Pointer(&fgY), C.sizeof_cl_uint},
{12, unsafe.Pointer(&fgCb), C.sizeof_cl_uint},
{13, unsafe.Pointer(&fgCr), C.sizeof_cl_uint},
{14, unsafe.Pointer(&bgY), C.sizeof_cl_uint},
{15, unsafe.Pointer(&bgCb), C.sizeof_cl_uint},
{16, unsafe.Pointer(&bgCr), C.sizeof_cl_uint},
}
for _, a := range args {
if err := g.setArg(a.idx, a.sz, a.p); err != nil {
return err
}
}
return nil
}
// SetText installs (or, with an empty string, removes) the burn-in overlay.
func (g *OpenCLGenerator) SetText(cfg TextConfig) error {
if len(cfg.Str) > 0 && (cfg.Scale < 1 || cfg.Scale > 8) {
return fmt.Errorf("opencl: text scale must be 1..8, got %d", cfg.Scale)
}
if len(cfg.Str) > 1024 {
return fmt.Errorf("opencl: text too long: %d bytes", len(cfg.Str))
}
var err C.cl_int
buf := g.atlasBuf // placeholder: text_len = 0 keeps the kernel off it
if len(cfg.Str) > 0 {
cData := C.CBytes([]byte(cfg.Str))
defer C.free(cData)
buf = C.clCreateBuffer(g.ctx, C.CL_MEM_READ_ONLY|C.CL_MEM_COPY_HOST_PTR,
C.size_t(len(cfg.Str)), cData, &err)
if err != C.CL_SUCCESS {
return fmt.Errorf("opencl: text buffer upload failed: %d", err)
}
}
if err := g.setTextArgs(buf, cfg); err != nil {
if len(cfg.Str) > 0 {
C.clReleaseMemObject(buf)
}
return err
}
if g.textBuf != nil && g.textBuf != buf {
C.clReleaseMemObject(g.textBuf)
}
if len(cfg.Str) > 0 {
g.textBuf = buf
} else {
g.textBuf = nil
}
return nil
}
func NewOpenCLGenerator(width, height uint, kernelPath string) (*OpenCLGenerator, error) {
var err C.cl_int
var platform C.cl_platform_id
if err = C.clGetPlatformIDs(1, &platform, nil); err != C.CL_SUCCESS {
return nil, fmt.Errorf("opencl: platform not found: %d", err)
}
g := &OpenCLGenerator{width: int(width), height: int(height)}
if err = C.clGetDeviceIDs(platform, C.CL_DEVICE_TYPE_GPU, 1, &g.dev, nil); err != C.CL_SUCCESS {
return nil, fmt.Errorf("opencl: no suitable gpu device found")
}
g.ctx = C.clCreateContext(nil, 1, &g.dev, nil, nil, &err)
if err != C.CL_SUCCESS {
return nil, fmt.Errorf("opencl: ctx creation error")
}
g.queue = C.clCreateCommandQueueWithProperties(g.ctx, g.dev, nil, &err)
if err != C.CL_SUCCESS {
g.queue = C.clCreateCommandQueue(g.ctx, g.dev, 0, &err)
}
src, errGo := os.ReadFile(kernelPath)
if errGo != nil {
g.Close()
return nil, fmt.Errorf("opencl: read kernel error: %w", errGo)
}
cSrc := C.CString(string(src))
defer C.free(unsafe.Pointer(cSrc))
g.prog = C.build_cl_program(g.ctx, g.dev, cSrc, &err)
if err != C.CL_SUCCESS {
msg := "clCreateProgramWithSource failed"
if g.prog != nil {
msg = C.GoString(C.get_build_log(g.prog, g.dev))
}
g.Close()
return nil, fmt.Errorf("opencl: compilation failed: %s", msg)
}
cKName := C.CString("generate_v210_pattern")
defer C.free(unsafe.Pointer(cKName))
g.kernel = C.clCreateKernel(g.prog, cKName, &err)
if err != C.CL_SUCCESS {
g.Close()
return nil, fmt.Errorf("opencl: kernel init failed")
}
g.atlasBuf = C.clCreateBuffer(g.ctx, C.CL_MEM_READ_ONLY|C.CL_MEM_COPY_HOST_PTR,
C.size_t(len(fontAtlas8x16)), unsafe.Pointer(&fontAtlas8x16[0]), &err)
if err != C.CL_SUCCESS {
g.Close()
return nil, fmt.Errorf("opencl: font atlas upload failed: %d", err)
}
if err := g.setArg(4, C.sizeof_cl_mem, unsafe.Pointer(&g.atlasBuf)); err != nil {
g.Close()
return nil, err
}
// Overlay defaults: no text, white on black if SetText is ever called.
if err := g.setTextArgs(g.atlasBuf, TextConfig{
Scale: 1,
FG: [3]uint32{940, 512, 512},
BG: [3]uint32{64, 512, 512},
}); err != nil {
g.Close()
return nil, err
}
return g, nil
}
func (g *OpenCLGenerator) GenerateFrame(dest []byte, frameIndex int) error {
var err C.cl_int
byteSize := C.size_t(len(dest))
// ВАЖНО: Мы маппим память MXL SDK ([]byte) напрямую в OpenCL буфер.
// GPU запишет данные прямо в общую память MXL без лишнего копирования (Zero-Copy).
if want := (g.width * g.height / 6) * 16; len(dest) < want {
return fmt.Errorf("opencl: dest too small: %d bytes, need %d", len(dest), want)
}
clMem := C.clCreateBuffer(
g.ctx,
C.CL_MEM_WRITE_ONLY|C.CL_MEM_USE_HOST_PTR,
byteSize,
unsafe.Pointer(&dest[0]),
&err,
)
if err != C.CL_SUCCESS {
return fmt.Errorf("opencl: mxl host pointer mapping failed: %d", err)
}
defer C.clReleaseMemObject(clMem)
if err := g.setArg(0, C.sizeof_cl_mem, unsafe.Pointer(&clMem)); err != nil {
return err
}
w, h, f := C.int(g.width), C.int(g.height), C.int(frameIndex)
if err := g.setArg(1, C.sizeof_int, unsafe.Pointer(&w)); err != nil {
return err
}
if err := g.setArg(2, C.sizeof_int, unsafe.Pointer(&h)); err != nil {
return err
}
if err := g.setArg(3, C.sizeof_int, unsafe.Pointer(&f)); err != nil {
return err
}
// Каждый поток GPU пакует блок из 6 пикселей (16 байт)
globalWorkSize := C.size_t((g.width * g.height) / 6)
err = C.clEnqueueNDRangeKernel(g.queue, g.kernel, 1, nil, &globalWorkSize, nil, 0, nil, nil)
if err != C.CL_SUCCESS {
return fmt.Errorf("opencl: dispatch failed: %d", err)
}
// Гарантированная синхронизация записи GPU в host-память: map/unmap.
// На discrete GPU драйвер держит копию в device memory; одного clFinish
// недостаточно — данные обязаны появиться в host_ptr только после unmap.
mapped := C.clEnqueueMapBuffer(g.queue, clMem, C.CL_TRUE, C.CL_MAP_READ, 0, byteSize, 0, nil, nil, &err)
if err != C.CL_SUCCESS {
return fmt.Errorf("opencl: map failed: %d", err)
}
if err = C.clEnqueueUnmapMemObject(g.queue, clMem, mapped, 0, nil, nil); err != C.CL_SUCCESS {
return fmt.Errorf("opencl: unmap failed: %d", err)
}
if err = C.clFinish(g.queue); err != C.CL_SUCCESS {
return fmt.Errorf("opencl: finish failed: %d", err)
}
return nil
}
func (g *OpenCLGenerator) Close() error {
if g.textBuf != nil {
C.clReleaseMemObject(g.textBuf)
g.textBuf = nil
}
if g.atlasBuf != nil {
C.clReleaseMemObject(g.atlasBuf)
g.atlasBuf = nil
}
if g.kernel != nil {
C.clReleaseKernel(g.kernel)
g.kernel = nil
}
if g.prog != nil {
C.clReleaseProgram(g.prog)
g.prog = nil
}
if g.queue != nil {
C.clReleaseCommandQueue(g.queue)
g.queue = nil
}
if g.ctx != nil {
C.clReleaseContext(g.ctx)
g.ctx = nil
}
return nil
}
+236
View File
@@ -0,0 +1,236 @@
// WGPU pattern gen: renders v210 frames with a wgpu (WebGPU/Vulkan)
// compute shader. Pure Go — no cgo in the GPU layer.
package generator
import (
"context"
"encoding/binary"
"fmt"
"os"
"time"
"github.com/gogpu/gputypes"
"github.com/gogpu/wgpu"
// Register all available GPU backends (Vulkan, DX12, GLES, Metal, etc.)
_ "github.com/gogpu/wgpu/hal/allbackends"
)
const wgpuWorkgroupSize = 64
// WGPUGenerator renders v210 frames on the GPU via wgpu and implements
// FrameGenerator. Per frame the kernel packs the pattern into a storage
// buffer, the GPU DMAs it into a persistent host-visible buffer, and the
// mapped contents are copied straight into the destination grain.
type WGPUGenerator struct {
instance *wgpu.Instance
adapter *wgpu.Adapter
device *wgpu.Device
queue *wgpu.Queue
shader *wgpu.ShaderModule
bgl *wgpu.BindGroupLayout
bg *wgpu.BindGroup
pl *wgpu.PipelineLayout
pipeline *wgpu.ComputePipeline
out *wgpu.Buffer
host *wgpu.Buffer
uniform *wgpu.Buffer
params []byte
width int
height int
blocks int
frameSize uint64
}
var _ FrameGenerator = (*WGPUGenerator)(nil)
func NewWGPUGenerator(width, height uint, kernelPath string) (*WGPUGenerator, error) {
g := &WGPUGenerator{
width: int(width),
height: int(height),
blocks: int(width*height) / 6,
params: make([]byte, 16),
}
g.frameSize = uint64(g.blocks) * 16
binary.LittleEndian.PutUint32(g.params[0:], uint32(width))
binary.LittleEndian.PutUint32(g.params[4:], uint32(height))
var err error
if g.instance, err = wgpu.CreateInstance(nil); err != nil {
return nil, fmt.Errorf("wgpu: create instance: %w", err)
}
if g.adapter, err = g.instance.RequestAdapter(nil); err != nil {
g.Close()
return nil, fmt.Errorf("wgpu: request adapter: %w", err)
}
if g.device, err = g.adapter.RequestDevice(nil); err != nil {
g.Close()
return nil, fmt.Errorf("wgpu: request device: %w", err)
}
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),
}); err != nil {
g.Close()
return nil, fmt.Errorf("wgpu: shader: %w", err)
}
if g.out, err = g.device.CreateBuffer(&wgpu.BufferDescriptor{
Label: "v210-out", Size: g.frameSize,
Usage: wgpu.BufferUsageStorage | wgpu.BufferUsageCopySrc,
}); err != nil {
g.Close()
return nil, fmt.Errorf("wgpu: out buffer: %w", err)
}
if g.host, err = g.device.CreateBuffer(&wgpu.BufferDescriptor{
Label: "v210-host", Size: g.frameSize,
Usage: wgpu.BufferUsageCopyDst | wgpu.BufferUsageMapRead,
}); err != nil {
g.Close()
return nil, fmt.Errorf("wgpu: host buffer: %w", err)
}
if g.uniform, err = g.device.CreateBuffer(&wgpu.BufferDescriptor{
Label: "v210-params", Size: uint64(len(g.params)),
Usage: wgpu.BufferUsageUniform | wgpu.BufferUsageCopyDst,
}); err != nil {
g.Close()
return nil, fmt.Errorf("wgpu: params buffer: %w", err)
}
if err := g.queue.WriteBuffer(g.uniform, 0, g.params); err != nil {
g.Close()
return nil, fmt.Errorf("wgpu: write params: %w", err)
}
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}},
},
}); 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))},
},
}); err != nil {
g.Close()
return nil, fmt.Errorf("wgpu: bind group: %w", err)
}
if g.pl, err = g.device.CreatePipelineLayout(&wgpu.PipelineLayoutDescriptor{
Label: "v210-pl", BindGroupLayouts: []*wgpu.BindGroupLayout{g.bgl},
}); err != nil {
g.Close()
return nil, fmt.Errorf("wgpu: pipeline layout: %w", err)
}
if g.pipeline, err = g.device.CreateComputePipeline(&wgpu.ComputePipelineDescriptor{
Label: "v210-pipeline", Layout: g.pl, Module: g.shader, EntryPoint: "main",
}); err != nil {
g.Close()
return nil, fmt.Errorf("wgpu: pipeline: %w", err)
}
return g, nil
}
func (g *WGPUGenerator) GenerateFrame(dest []byte, frameIndex int) error {
if uint64(len(dest)) < g.frameSize {
return fmt.Errorf("wgpu: dest too small: %d bytes, need %d", len(dest), g.frameSize)
}
binary.LittleEndian.PutUint32(g.params[8:], uint32(frameIndex))
if err := g.queue.WriteBuffer(g.uniform, 0, g.params); err != nil {
return fmt.Errorf("wgpu: write params: %w", err)
}
encoder, err := g.device.CreateCommandEncoder(nil)
if err != nil {
return fmt.Errorf("wgpu: encoder: %w", err)
}
pass, err := encoder.BeginComputePass(nil)
if err != nil {
return fmt.Errorf("wgpu: compute pass: %w", err)
}
pass.SetPipeline(g.pipeline)
pass.SetBindGroup(0, g.bg, nil)
pass.Dispatch(uint32((g.blocks+wgpuWorkgroupSize-1)/wgpuWorkgroupSize), 1, 1)
if err := pass.End(); err != nil {
return fmt.Errorf("wgpu: end pass: %w", err)
}
encoder.CopyBufferToBuffer(g.out, 0, g.host, 0, g.frameSize)
cmd, err := encoder.Finish()
if err != nil {
return fmt.Errorf("wgpu: finish: %w", err)
}
if _, err := g.queue.Submit(cmd); err != nil {
return fmt.Errorf("wgpu: submit: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := g.host.Map(ctx, wgpu.MapModeRead, 0, g.frameSize); err != nil {
return fmt.Errorf("wgpu: map: %w", err)
}
rng, err := g.host.MappedRange(0, g.frameSize)
if err != nil {
_ = g.host.Unmap()
return fmt.Errorf("wgpu: mapped range: %w", err)
}
copy(dest, rng.Bytes())
return g.host.Unmap()
}
func (g *WGPUGenerator) Close() error {
if g.pipeline != nil {
g.pipeline.Release()
g.pipeline = nil
}
if g.pl != nil {
g.pl.Release()
g.pl = nil
}
if g.bg != nil {
g.bg.Release()
g.bg = nil
}
if g.bgl != nil {
g.bgl.Release()
g.bgl = nil
}
if g.shader != nil {
g.shader.Release()
g.shader = nil
}
if g.uniform != nil {
g.uniform.Release()
g.uniform = nil
}
if g.host != nil {
g.host.Release()
g.host = nil
}
if g.out != nil {
g.out.Release()
g.out = nil
}
if g.device != nil {
g.device.Release()
g.device = nil
}
if g.adapter != nil {
g.adapter.Release()
g.adapter = nil
}
if g.instance != nil {
g.instance.Release()
g.instance = nil
}
return nil
}
+74
View File
@@ -0,0 +1,74 @@
package generator
import (
"encoding/binary"
"path/filepath"
"testing"
)
func TestWGPUMoveSquare(t *testing.T) {
const width, height = 1920, 1080
g, err := NewWGPUGenerator(width, height, filepath.Join("..", "..", "kernels", "v210_bars_move.wgsl"))
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
}
}
// square center x=960 sits on the green bar (534/253/207):
// inverted -> Y=1004-534=470, Cb=1024-253=771, Cr=1024-207=817
const invY, invCb, invCr = 470, 771, 817
const greenY = 534 // bar 3 covers x in [823,1098)
// tick 0: offset = 0, square centered at (960,540), half = 75
if err := g.GenerateFrame(buf, 0); err != nil {
t.Fatalf("tick 0: %v", err)
}
if y, cb, cr := sample(960, 540); y != invY || cb != invCb || cr != invCr {
t.Fatalf("tick 0 center: got %d/%d/%d, want inverted green 470/771/817", y, cb, cr)
}
if y, _, _ := sample(860, 540); y != greenY {
t.Fatalf("tick 0 left of square: Y=%d, want green %d", y, greenY)
}
// amplitude = centerX - half = 885: tick 79 (sin~1) puts the square at
// the far right, x in [1770,1920), over the blue bar (111/848/481):
// inverted -> Y=1004-111=893, Cb=1024-848=176, Cr=1024-481=543
const invBlueY, invBlueCb, invBlueCr = 893, 176, 543
if err := g.GenerateFrame(buf, 79); err != nil {
t.Fatalf("tick 79: %v", err)
}
if y, cb, cr := sample(1840, 540); y != invBlueY || cb != invBlueCb || cr != invBlueCr {
t.Fatalf("tick 79 shifted center: got %d/%d/%d, want inverted blue 893/176/543", y, cb, cr)
}
if y, _, _ := sample(960, 540); y != greenY {
t.Fatalf("tick 79 old center: Y=%d, want green %d (square moved away)", y, greenY)
}
// bars untouched far from the square
if y, _, _ := sample(100, 100); y != 721 {
t.Fatalf("tick 79 bars: Y=%d, want white 721", y)
}
}
+67
View File
@@ -0,0 +1,67 @@
package generator
import (
"encoding/binary"
"path/filepath"
"testing"
)
func TestWGPUGenerator(t *testing.T) {
const width, height = 1920, 1080
g, err := NewWGPUGenerator(width, height, filepath.Join("..", "..", "kernels", "v210_bars.wgsl"))
if err != nil {
t.Fatalf("init: %v", err)
}
defer g.Close()
buf := make([]byte, width*height*8/3)
if err := g.GenerateFrame(buf, 0); err != nil {
t.Fatalf("GenerateFrame: %v", err)
}
if err := g.GenerateFrame(buf, 1); err != nil {
t.Fatalf("GenerateFrame 2: %v", err)
}
want := [7][3]uint32{
{721, 512, 512}, {674, 176, 543}, {581, 589, 176},
{534, 253, 207}, {251, 771, 817}, {204, 435, 848}, {111, 848, 481},
}
barOf := func(x int) int {
if b := x * 7 / width; b < 7 {
return b
}
return 6
}
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
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:])
var yv, cb, cr uint32
switch p % 6 {
case 0:
yv, cb, cr = (w0>>10)&0x3FF, w0&0x3FF, (w0>>20)&0x3FF
case 1:
yv = w1 & 0x3FF
case 2:
yv, cb, cr = (w1>>20)&0x3FF, (w1>>10)&0x3FF, w2&0x3FF
case 3:
yv = (w2 >> 10) & 0x3FF
case 4:
yv, cb, cr = w3&0x3FF, (w2>>20)&0x3FF, (w3>>10)&0x3FF
case 5:
yv = (w3 >> 20) & 0x3FF
}
b := want[barOf(x)]
if yv != b[0] {
t.Fatalf("pixel (%d,%d): Y=%d want %d", x, y, yv, b[0])
}
if x%2 == 0 && (cb != b[1] || cr != b[2]) {
t.Fatalf("pixel (%d,%d): Cb=%d Cr=%d want %d/%d", x, y, cb, cr, b[1], b[2])
}
}
}
}
+50
View File
@@ -0,0 +1,50 @@
// 75% SMPTE color bars, Rec.709, 10-bit Y'CbCr, packed as v210 (4:2:2).
// One work-item per 16-byte block = 6 pixels (6 Y + 3 Cb + 3 Cr).
struct Params {
width: u32,
height: u32,
frame: u32,
_pad0: u32,
};
@group(0) @binding(0) var<storage, read_write> out: array<u32>;
@group(0) @binding(1) var<uniform> params: Params;
fn bar_index(px: u32) -> u32 {
return min((px * 7u) / params.width, 6u);
}
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
let block = gid.x;
let total = (params.width * params.height) / 6u;
if (block >= total) {
return;
}
let x = (block * 6u) % params.width;
// Bar order: 75% white, yellow, cyan, green, magenta, red, blue
let y_tab = array<u32, 7>(721u, 674u, 581u, 534u, 251u, 204u, 111u);
let cb_tab = array<u32, 7>(512u, 176u, 589u, 253u, 771u, 435u, 848u);
let cr_tab = array<u32, 7>(512u, 543u, 176u, 207u, 817u, 848u, 481u);
var y: array<u32, 6>;
var cb: array<u32, 6>;
var cr: array<u32, 6>;
for (var i = 0u; i < 6u; i++) {
let b = bar_index(x + i);
y[i] = y_tab[b];
cb[i] = cb_tab[b];
cr[i] = cr_tab[b];
}
// v210 word layout, chroma co-sited with luma samples 0/2/4:
// w0 = Cb0|Y0<<10|Cr0<<20; w1 = Y1|Cb2<<10|Y2<<20;
// w2 = Cr2|Y3<<10|Cb4<<20; w3 = Y4|Cr4<<10|Y5<<20
out[block * 4u + 0u] = (cb[0] & 0x3FFu) | ((y[0] & 0x3FFu) << 10u) | ((cr[0] & 0x3FFu) << 20u);
out[block * 4u + 1u] = (y[1] & 0x3FFu) | ((cb[2] & 0x3FFu) << 10u) | ((y[2] & 0x3FFu) << 20u);
out[block * 4u + 2u] = (cr[2] & 0x3FFu) | ((y[3] & 0x3FFu) << 10u) | ((cb[4] & 0x3FFu) << 20u);
out[block * 4u + 3u] = (y[4] & 0x3FFu) | ((cr[4] & 0x3FFu) << 10u) | ((y[5] & 0x3FFu) << 20u);
}
+77
View File
@@ -0,0 +1,77 @@
// 75% SMPTE color bars + moving square, Rec.709, 10-bit Y'CbCr, packed as v210 (4:2:2).
// One work-item per 16-byte block = 6 pixels (6 Y + 3 Cb + 3 Cr).
struct Params {
width: u32,
height: u32,
frame: u32, // animation tick (0,1,2,... per generated frame), NOT the raw grain index
_pad0: u32,
};
@group(0) @binding(0) var<storage, read_write> out: array<u32>;
@group(0) @binding(1) var<uniform> params: Params;
fn bar_index(px: u32) -> u32 {
return min((px * 7u) / params.width, 6u);
}
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
let block = gid.x;
let total = (params.width * params.height) / 6u;
if (block >= total) {
return;
}
let x = (block * 6u) % params.width;
let py = (block * 6u) / params.width;
// Bar order: 75% white, yellow, cyan, green, magenta, red, blue
let y_tab = array<u32, 7>(721u, 674u, 581u, 534u, 251u, 204u, 111u);
let cb_tab = array<u32, 7>(512u, 176u, 589u, 253u, 771u, 435u, 848u);
let cr_tab = array<u32, 7>(512u, 543u, 176u, 207u, 817u, 848u, 481u);
// Moving square: horizontal oscillation around screen center.
// frame is a small tick; converting the huge raw grain index here
// would destroy f32 precision and freeze the motion.
let centerX = f32(params.width) / 2.0;
let centerY = f32(params.height) / 2.0;
let fps = 25.0;
let t = f32(params.frame) / fps;
const squareSize = 150u;
let half = f32(squareSize) / 2.0;
let offsetPixels = sin(t * 0.5) * (centerX - half);
let sq_x_min = centerX - half + offsetPixels;
let sq_x_max = centerX + half + offsetPixels;
let sq_y_min = centerY - half;
let sq_y_max = centerY + half;
let py_f = f32(py);
var y: array<u32, 6>;
var cb: array<u32, 6>;
var cr: array<u32, 6>;
for (var i = 0u; i < 6u; i++) {
let px = f32(x + i);
let b = bar_index(x + i);
if (px >= sq_x_min && px < sq_x_max && py_f >= sq_y_min && py_f < sq_y_max) {
// inverted bar: luma mirrored across studio range (64..940),
// chroma mirrored across neutral 512
y[i] = 1004u - y_tab[b];
cb[i] = 1024u - cb_tab[b];
cr[i] = 1024u - cr_tab[b];
} else {
y[i] = y_tab[b];
cb[i] = cb_tab[b];
cr[i] = cr_tab[b];
}
}
// v210 word layout, chroma co-sited with luma samples 0/2/4:
// w0 = Cb0|Y0<<10|Cr0<<20; w1 = Y1|Cb2<<10|Y2<<20;
// w2 = Cr2|Y3<<10|Cb4<<20; w3 = Y4|Cr4<<10|Y5<<20
out[block * 4u + 0u] = (cb[0] & 0x3FFu) | ((y[0] & 0x3FFu) << 10u) | ((cr[0] & 0x3FFu) << 20u);
out[block * 4u + 1u] = (y[1] & 0x3FFu) | ((cb[2] & 0x3FFu) << 10u) | ((y[2] & 0x3FFu) << 20u);
out[block * 4u + 2u] = (cr[2] & 0x3FFu) | ((y[3] & 0x3FFu) << 10u) | ((cb[4] & 0x3FFu) << 20u);
out[block * 4u + 3u] = (y[4] & 0x3FFu) | ((cr[4] & 0x3FFu) << 10u) | ((y[5] & 0x3FFu) << 20u);
}
+72
View File
@@ -0,0 +1,72 @@
// 75% SMPTE color bars, Rec.709, 10-bit Y'CbCr, packed as v210 (4:2:2),
// with an 8x16 bitmap-font text overlay (burn-in).
// Each work-item packs 6 pixels (6 Y + 3 Cb + 3 Cr) into one 16-byte block.
#define GW 8
#define GH 16
// Text overlay lookup. Atlas: 96 glyphs (ASCII 32..127) x GH rows,
// one byte per row, bit n = pixel at column n (LSB = leftmost).
// Returns: 1 = glyph pixel (foreground), 0 = inside text box (background),
// -1 = outside the text box (pattern shows through).
int text_pixel(__global const uchar* atlas, __global const uchar* text, int text_len,
int px, int py, int txt_x, int txt_y, int scale) {
if (text_len <= 0) return -1;
int tx = px - txt_x;
int ty = py - txt_y;
if (tx < 0 || ty < 0) return -1;
if (tx >= text_len * GW * scale || ty >= GH * scale) return -1;
int c = text[tx / (GW * scale)];
if (c < 32 || c > 127) return 0;
uchar row = atlas[(c - 32) * GH + ty / scale];
return (row >> ((tx / scale) % GW)) & 1;
}
__kernel void generate_v210_pattern(
__global uint* output, int width, int height, int frame_index,
__global const uchar* atlas, __global const uchar* text, int text_len,
int txt_x, int txt_y, int scale, int has_bg,
uint fg_y, uint fg_cb, uint fg_cr,
uint bg_y, uint bg_cb, uint bg_cr) {
int g_id = get_global_id(0);
int total_blocks = (width * height) / 6;
if (g_id >= total_blocks) return;
int x = (g_id * 6) % width;
int py = (g_id * 6) / width;
// Bar order: 75% white, yellow, cyan, green, magenta, red, blue
ushort Y_table[7] = {721, 674, 581, 534, 251, 204, 111};
ushort U_table[7] = {512, 176, 589, 253, 771, 435, 848};
ushort V_table[7] = {512, 543, 176, 207, 817, 848, 481};
#define BAR(px) min(((px) * 7) / width, 6)
uint y[6], cb[6], cr[6];
for (int i = 0; i < 6; i++) {
int px = x + i;
int b = BAR(px);
uint yv = Y_table[b], cbv = U_table[b], crv = V_table[b];
int st = text_pixel(atlas, text, text_len, px, py, txt_x, txt_y, scale);
if (st > 0) {
yv = fg_y; cbv = fg_cb; crv = fg_cr;
} else if (st == 0 && has_bg) {
yv = bg_y; cbv = bg_cb; crv = bg_cr;
}
y[i] = yv; cb[i] = cbv; cr[i] = crv;
}
// v210 word layout, chroma co-sited with luma samples 0/2/4:
// w0 = Cb0|Y0<<10|Cr0<<20; w1 = Y1|Cb2<<10|Y2<<20;
// w2 = Cr2|Y3<<10|Cb4<<20; w3 = Y4|Cr4<<10|Y5<<20
uint word0 = (cb[0] & 0x3FF) | ((y[0] & 0x3FF) << 10) | ((cr[0] & 0x3FF) << 20);
uint word1 = (y[1] & 0x3FF) | ((cb[2] & 0x3FF) << 10) | ((y[2] & 0x3FF) << 20);
uint word2 = (cr[2] & 0x3FF) | ((y[3] & 0x3FF) << 10) | ((cb[4] & 0x3FF) << 20);
uint word3 = (y[4] & 0x3FF) | ((cr[4] & 0x3FF) << 10) | ((y[5] & 0x3FF) << 20);
int out_idx = g_id * 4;
output[out_idx + 0] = word0;
output[out_idx + 1] = word1;
output[out_idx + 2] = word2;
output[out_idx + 3] = word3;
}
Executable
BIN
View File
Binary file not shown.
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""Bake an 8x16 1-bpp bitmap font (ASCII 32..127) from DejaVu Sans Mono 13px.
Output: Go source file with the atlas. One byte per glyph row, bit n = pixel
at column n (LSB = leftmost), matching the kernel's (row >> col) & 1 lookup.
"""
from PIL import Image, ImageDraw, ImageFont
FONT = "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"
SIZE = 13
BASELINE = 12 # baseline row inside the 16px cell
THRESH = 110
font = ImageFont.truetype(FONT, SIZE)
atlas = []
for c in range(32, 128):
img = Image.new("L", (8, 16), 0)
d = ImageDraw.Draw(img)
d.text((0, BASELINE), chr(c), font=font, fill=255, anchor="ls")
px = img.load()
for y in range(16):
b = 0
for x in range(8):
if px[x, y] >= THRESH:
b |= 1 << x
atlas.append(b)
def art(c):
print(f"--- '{chr(c)}' ---")
for y in range(16):
row = atlas[(c - 32) * 16 + y]
print("".join("#" if (row >> x) & 1 else "." for x in range(8)))
for c in "AgM%m0(":
art(ord(c))
lines = []
for i in range(0, len(atlas), 16):
chunk = ", ".join(f"0x{b:02X}" for b in atlas[i:i+16])
lines.append(f"\t{chunk},")
body = "\n".join(lines)
src = f'''package generator
// 8x16 1-bpp bitmap font baked from DejaVu Sans Mono {SIZE}px (public-domain
// style console glyphs). ASCII 32..127, one byte per row, LSB = leftmost pixel.
var fontAtlas8x16 = [96 * 16]byte{{
{body}
}}
'''
with open("internal/generator/font.go", "w") as f:
f.write(src)
print("wrote internal/generator/font.go,", len(atlas), "bytes of glyph data")
+485
View File
@@ -0,0 +1,485 @@
# Архитектура v210 Генератора для Media eXchange Layer (go-mxl)
Профессиональная Zero-GC и Zero-Copy архитектура гибридного **CPU/GPU v210 тестового генератора**, оптимизированная под работу с официальным C SDK Media eXchange Layer (`libmxl`) через официальные Go-биндинги `go-mxl` от Qvest Digital.
## 🏢 Финальная структура проекта
```text
v210-generator/
├── cmd/
│ └── v210-gen/
│ └── main.go # Cobra CLI & главный цикл записи в MXL Flow
├── internal/
│ └── generator/
│ ├── generator.go # Интерфейс генератора
│ ├── cpu.go # CPU битовый упаковщик (Пишет сразу в []byte)
│ └── opencl.go # OpenCL Cgo упаковщик (Маппит []byte из MXL в GPU)
└── kernels/
└── v210_pack.cl # Шейдер упаковки v210
```
---
## 1. Архитектура Генератора: `internal/generator/generator.go`
Так как `go-mxl` предоставляет память в виде стандартных байтовых срезов (`[]byte`), мы адаптируем интерфейс. Генераторы больше не создают буферы, они принимают сырой `[]byte`, выделенный с помощью SDK.
```go
package generator
type FrameGenerator interface {
// GenerateFrame упаковывает данные напрямую в предоставленный байтовый срез.
// Этот срез будет являться физической памятью MXL Shared Memory.
GenerateFrame(dest []byte, frameIndex int) error
Close() error
}
```
---
## 2. GPU Движок под MXL: `internal/generator/opencl.go`
Поскольку `go-mxl` дает нам прямой доступ к разделяемой памяти, мы берем этот срез `[]byte`, берем его адрес в RAM через `unsafe.Pointer` и отдаем драйверу OpenCL через флаг `CL_MEM_USE_HOST_PTR`. Видеокарта запишет v210 паттерн напрямую в MXL, минуя кэш Go.
```go
package generator
/*
#cgo linux LDFLAGS: -lOpenCL
#cgo windows LDFLAGS: -lOpenCL
#include <CL/cl.h>
#include <stdlib.h>
static cl_program build_cl_program(cl_context ctx, cl_device_id dev, const char* src, cl_int* err) {
cl_program prog = clCreateProgramWithSource(ctx, 1, &src, NULL, err);
if (*err != CL_SUCCESS) return NULL;
*err = clBuildProgram(prog, 1, &dev, NULL, NULL, NULL);
return prog;
}
*/
import "C"
import (
"fmt"
"os"
"unsafe"
)
type OpenCLGenerator struct {
ctx C.cl_context
queue C.cl_command_queue
kernel C.cl_kernel
prog C.cl_program
dev C.cl_device_id
width int
height int
}
func NewOpenCLGenerator(width, height int, kernelPath string) (*OpenCLGenerator, error) {
var err C.cl_int
var platform C.cl_platform_id
if err = C.clGetPlatformIDs(1, &platform, nil); err != C.CL_SUCCESS {
return nil, fmt.Errorf("opencl: platform not found: %d", err)
}
g := &OpenCLGenerator{width: width, height: height}
if err = C.clGetDeviceIDs(platform, C.CL_DEVICE_TYPE_GPU, 1, &g.dev, nil); err != C.CL_SUCCESS {
return nil, fmt.Errorf("opencl: no suitable gpu device found")
}
g.ctx = C.clCreateContext(nil, 1, &g.dev, nil, nil, &err)
if err != C.CL_SUCCESS {
return nil, fmt.Errorf("opencl: ctx creation error")
}
g.queue = C.clCreateCommandQueueWithProperties(g.ctx, g.dev, nil, &err)
if err != C.CL_SUCCESS {
g.queue = C.clCreateCommandQueue(g.ctx, g.dev, 0, &err)
}
src, errGo := os.ReadFile(kernelPath)
if errGo != nil {
g.Close()
return nil, fmt.Errorf("opencl: read kernel error: %w", errGo)
}
cSrc := C.CString(string(src))
defer C.free(unsafe.Pointer(cSrc))
g.prog = C.build_cl_program(g.ctx, g.dev, cSrc, &err)
if err != C.CL_SUCCESS {
g.Close()
return nil, fmt.Errorf("opencl: compilation failed")
}
cKName := C.CString("generate_v210_pattern")
defer C.free(unsafe.Pointer(cKName))
g.kernel = C.clCreateKernel(g.prog, cKName, &err)
if err != C.CL_SUCCESS {
g.Close()
return nil, fmt.Errorf("opencl: kernel init failed")
}
return g, nil
}
func (g *OpenCLGenerator) GenerateFrame(dest []byte, frameIndex int) error {
var err C.cl_int
byteSize := C.size_t(len(dest))
// ВАЖНО: Мы маппим память MXL SDK ([]byte) напрямую в OpenCL буфер.
// GPU запишет данные прямо в общую память MXL без лишнего копирования (Zero-Copy).
clMem := C.clCreateBuffer(
g.ctx,
C.CL_MEM_WRITE_ONLY|C.CL_MEM_USE_HOST_PTR,
byteSize,
unsafe.Pointer(&dest), // Указатель на первый элемент MXL-слайса
&err,
)
if err != C.CL_SUCCESS {
return fmt.Errorf("opencl: mxl host pointer mapping failed: %d", err)
}
defer C.clReleaseMemObject(clMem)
C.clSetKernelArg(g.kernel, 0, C.sizeof_cl_mem, unsafe.Pointer(&clMem))
w, h, f := C.int(g.width), C.int(g.height), C.int(frameIndex)
C.clSetKernelArg(g.kernel, 1, C.sizeof_int, unsafe.Pointer(&w))
C.clSetKernelArg(g.kernel, 2, C.sizeof_int, unsafe.Pointer(&h))
C.clSetKernelArg(g.kernel, 3, C.sizeof_int, unsafe.Pointer(&f))
// Каждый поток GPU пакует блок из 4 пикселей (16 байт)
globalWorkSize := C.size_t((g.width * g.height) / 4)
err = C.clEnqueueNDRangeKernel(g.queue, g.kernel, 1, nil, &globalWorkSize, nil, 0, nil, nil)
if err != C.CL_SUCCESS {
return fmt.Errorf("opencl: dispatch failed: %d", err)
}
// Ждем окончания записи видеокарты в память MXL
C.clFinish(g.queue)
return nil
}
func (g *OpenCLGenerator) Close() error {
if g.kernel != nil { C.clReleaseKernel(g.kernel) }
if g.prog != nil { C.clReleaseProgram(g.prog) }
if g.queue != nil { C.clReleaseCommandQueue(g.queue) }
if g.ctx != nil { C.clReleaseContext(g.ctx) }
return nil
}
```
---
## 3. CPU Паковщик: `internal/generator/cpu.go`
CPU-генератор делает то же самое, но использует безопасную кастомную битовую маску для работы с `[]byte` напрямую вместо `[]uint32`, чтобы избежать преобразования типов данных.
```go
package generator
import "encoding/binary"
type CPUGenerator struct {
width int
height int
}
func NewCPUGenerator(width, height int) *CPUGenerator {
return &CPUGenerator{width: width, height: height}
}
func (g *CPUGenerator) GenerateFrame(dest []byte, frameIndex int) error {
totalBlocks := (g.width * g.height) / 4
for blockID := 0; blockID < totalBlocks; blockID++ {
baseX := (blockID * 4) % g.width
y0 := uint32((baseX * 1023) / g.width)
y1 := uint32(((baseX + 1) * 1023) / g.width)
y2 := uint32(((baseX + 2) * 1023) / g.width)
y3 := uint32(((baseX + 3) * 1023) / g.width)
u0, v0, u2, v2 := uint32(512), uint32(512), uint32(512), uint32(512)
w0 := (u0 & 0x3FF) | ((y0 & 0x3FF) << 10) | ((v0 & 0x3FF) << 20)
w1 := (y1 & 0x3FF) | ((u2 & 0x3FF) << 10) | ((y2 & 0x3FF) << 20)
w2 := (v2 & 0x3FF) | ((y3 & 0x3FF) << 10) | ((u0 & 0x3FF) << 20)
w3 := uint32(0)
// Записываем 4 слова (16 байт) прямо в байтовый слайс памяти MXL
byteIdx := blockID * 16
binary.LittleEndian.PutUint32(dest[byteIdx+0:], w0)
binary.LittleEndian.PutUint32(dest[byteIdx+4:], w1)
binary.LittleEndian.PutUint32(dest[byteIdx+8:], w2)
binary.LittleEndian.PutUint32(dest[byteIdx+12:], w3)
}
return nil
}
func (g *CPUGenerator) Close() error { return nil }
```
---
## 4. Интеграция и Главный Цикл MXL: `cmd/v210-gen/main.go`
Здесь мы запускаем MXL инстанс и пишем фреймы. Обратите внимание, что мы **не используем `sync.Pool`**. Нам больше не нужно менеджить память в Go — MXL SDK сам выдает нам кусок разделяемой памяти для текущего индекса зерна (Grain). Наша задача — наполнить его и вызвать `Commit()`.
```go
package main
import (
"fmt"
"log"
"time"
"v210-generator/internal/generator"
"github.com/qvest-digital/go-mxl/mxl"
)
func main() {
useGPU := true
width, height := 1920, 1080
flowUUID := "your-video-flow-uuid-here"
// 1. Инициализируем инстанс MXL (работает через /dev/shm/mxl)
inst, err := mxl.NewInstance("/dev/shm/mxl", "")
if err != nil {
log.Fatalf("MXL Init failed: %v", err)
}
defer inst.Close()
// 2. Создаем Writer для отправки потока данных в MXL Fabric
writer, err := inst.NewWriter(flowUUID)
if err != nil {
log.Fatalf("Failed to create MXL Writer: %v", err)
}
defer writer.Close()
// 3. Выбираем движок рендеринга
var engine generator.FrameGenerator
if useGPU {
engine, err = generator.NewOpenCLGenerator(width, height, "../../kernels/v210_pack.cl")
if err != nil {
log.Printf("⚠️ GPU OpenCL failed: %v. Falling back to CPU.", err)
engine = generator.NewCPUGenerator(width, height)
}
} else {
engine = generator.NewCPUGenerator(width, height)
}
defer engine.Close()
// Получаем параметры таймингов из конфига потока
info, _ := writer.Info()
rate := info.Config.Common.GrainRate
idx := mxl.CurrentIndex(rate)
fmt.Println("📺 Broadcasting v210 patterns directly into MXL Shared Memory...")
for {
// Алоцируем Grain (зерно данных) в разделяемой памяти MXL.
// В этот момент SDK выделяет структуру под капот.
grain, err := writer.OpenGrain(idx, 100*time.Millisecond)
if err != nil {
log.Printf("Failed to open MXL grain for index %d: %v", idx, err)
time.Sleep(10 * time.Millisecond)
continue
}
// grain.Payload — это []byte, ссылающийся напрямую на Linux Shared Memory
// Мы передаем его в наш генератор
err = engine.GenerateFrame(grain.Payload, int(idx))
if err != nil {
log.Printf("Render error at index %d: %v", idx, err)
grain.Cancel() // Отменяем транзакцию для этого кадра, если произошла ошибка
idx++
continue
}
// Фиксируем (коммитим) кадр. С этого момента MXL рассылает его подписчикам (Readers)
grain.Commit()
idx++
}
}
```
## 🏎️ Почему эта связка идеальна?
* **Полный Zero-Copy**: Благодаря `go-mxl` от Qvest Digital, буферы памяти выделяются прямо в Linux Shared Memory (`/dev/shm/mxl`). Мы берем указатель на этот слайс и маппим его в OpenCL с помощью `CL_MEM_USE_HOST_PTR`. Видеокарта выполняет операции упаковки и складывает кадры прямо в оперативную память обмена.
* **0% нагрузки на GC в Go**: В главном цикле `for` отсутствуют динамические аллокации в куче (heap), рантайм Go работает без пауз на очистку памяти (STW), гарантируя идеальный frame pacing для Real-time Broadcast систем.
## BARS
__kernel void generate_v210_pattern(__global uint* output, int width, int height, int frame_index) {
int g_id = get_global_id(0);
int total_blocks = (width * height) / 4;
if (g_id >= total_blocks) return;
int pixel_x = (g_id * 4) % width;
int pixel_y = (g_id * 4) / width;
// В SMPTE RP 219 полосы занимают верхние 67% экрана. Ниже идут другие элементы.
// Для простоты примера сделаем полосы на весь экран, либо вы можете сделать split по pixel_y.
// Вычисляем ширину одной полосы (всего их 7)
int bar_width = width / 7;
int bar_index = pixel_x / bar_width;
if (bar_index > 6) bar_index = 6; // Защита от выхода за границы из-за округления
// Массивы констант для 75% SMPTE полос
ushort Y_table[7] = {721, 647, 569, 495, 315, 241, 163};
ushort U_table[7] = {512, 176, 599, 263, 761, 425, 848};
ushort V_table[7] = {512, 557, 176, 221, 803, 848, 467};
// Так как поток берет 4 пикселя подряд, определим цвета для каждого из них.
// (В 95% случаев они попадут в одну полосу, но на стыках полос пиксели будут разными)
ushort y0 = Y_table[(pixel_x + 0) / bar_width > 6 ? 6 : (pixel_x + 0) / bar_width];
ushort y1 = Y_table[(pixel_x + 1) / bar_width > 6 ? 6 : (pixel_x + 1) / bar_width];
ushort y2 = Y_table[(pixel_x + 2) / bar_width > 6 ? 6 : (pixel_x + 2) / bar_width];
ushort y3 = Y_table[(pixel_x + 3) / bar_width > 6 ? 6 : (pixel_x + 3) / bar_width];
// v210 — это 4:2:2. Берем Хрому (U/V) от первого пикселя каждой пары (субдискретизация)
ushort u0 = U_table[(pixel_x + 0) / bar_width > 6 ? 6 : (pixel_x + 0) / bar_width];
ushort v0 = V_table[(pixel_x + 0) / bar_width > 6 ? 6 : (pixel_x + 0) / bar_width];
ushort u2 = U_table[(pixel_x + 2) / bar_width > 6 ? 6 : (pixel_x + 2) / bar_width];
ushort v2 = V_table[(pixel_x + 2) / bar_width > 6 ? 6 : (pixel_x + 2) / bar_width];
// Битовая упаковка v210 в четыре 32-битных слова
uint word0 = (u0 & 0x3FF) | ((y0 & 0x3FF) << 10) | ((v0 & 0x3FF) << 20);
uint word1 = (y1 & 0x3FF) | ((u2 & 0x3FF) << 10) | ((y2 & 0x3FF) << 20);
uint word2 = (v2 & 0x3FF) | ((y3 & 0x3FF) << 10) | ((u0 & 0x3FF) << 20);
uint word3 = 0; // 4-е слово в схеме v210 несет только выравнивание (высшие биты пусты)
int out_idx = g_id * 4;
output[out_idx + 0] = word0;
output[out_idx + 1] = word1;
output[out_idx + 2] = word2;
output[out_idx + 3] = word3;
}
## BARS CPU
package generator
import "encoding/binary"
type CPUGenerator struct {
width int
height int
}
func NewCPUGenerator(width, height int) *CPUGenerator {
return &CPUGenerator{width: width, height: height}
}
func (g *CPUGenerator) GenerateFrame(dest []byte, frameIndex int) error {
totalBlocks := (g.width * g.height) / 4
barWidth := g.width / 7
// SMPTE Таблицы констант
Y_table := [7]uint32{721, 647, 569, 495, 315, 241, 163}
U_table := [7]uint32{512, 176, 599, 263, 761, 425, 848}
V_table := [7]uint32{512, 557, 176, 221, 803, 848, 467}
getIdx := func(x int) int {
idx := x / barWidth
if idx > 6 { return 6 }
return idx
}
for blockID := 0; blockID < totalBlocks; blockID++ {
baseX := (blockID * 4) % g.width
// Запрашиваем индексы цветов для 4 пикселей в блоке
idx0 := getIdx(baseX + 0)
idx1 := getIdx(baseX + 1)
idx2 := getIdx(baseX + 2)
idx3 := getIdx(baseX + 3)
y0, y1, y2, y3 := Y_table[idx0], Y_table[idx1], Y_table[idx2], Y_table[idx3]
u0, v0 := U_table[idx0], V_table[idx0]
u2, v2 := U_table[idx2], V_table[idx2]
// Упаковка по спецификации v210
w0 := (u0 & 0x3FF) | ((y0 & 0x3FF) << 10) | ((v0 & 0x3FF) << 20)
w1 := (y1 & 0x3FF) | ((u2 & 0x3FF) << 10) | ((y2 & 0x3FF) << 20)
w2 := (v2 & 0x3FF) | ((y3 & 0x3FF) << 10) | ((u0 & 0x3FF) << 20)
w3 := uint32(0)
byteIdx := blockID * 16
binary.LittleEndian.PutUint32(dest[byteIdx+0:], w0)
binary.LittleEndian.PutUint32(dest[byteIdx+4:], w1)
binary.LittleEndian.PutUint32(dest[byteIdx+8:], w2)
binary.LittleEndian.PutUint32(dest[byteIdx+12:], w3)
}
return nil
}
func (g *CPUGenerator) Close() error { return nil }
struct Uniforms {
u_dt: f32, // Delta time for this frame (4 bytes)
u_frame: u32, // Current frame number (4 bytes)
};
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) color: vec4<f32>,
};
// We store the persistent position on the GPU using a global variable
// so we can incrementally add 'dt' to it frame over frame.
var<private> squarePosition: vec2<f32> = vec2<f32>(0.0, 0.0);
var<private> velocity: vec2<f32> = vec2<f32>(0.4, 0.2); // Moves diagonally
@vertex
fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
var pos = array<vec2<f32>, 6>(
vec2<f32>(-0.2, 0.2),
vec2<f32>(-0.2, -0.2),
vec2<f32>( 0.2, 0.2),
vec2<f32>( 0.2, 0.2),
vec2<f32>(-0.2, -0.2),
vec2<f32>( 0.2, -0.2)
);
var output: VertexOutput;
// 1. Frame-based logic: Change color every 60 frames
var color = vec4<f32>(0.3, 0.6, 0.9, 1.0); // Light blue
if ((uniforms.u_frame / 60u) % 2u == 0u) {
color = vec4<f32>(0.9, 0.4, 0.3, 1.0); // Switch to Coral Red
}
// 2. DT-based logic: Calculate movement using uniform velocity * dt
// Note: Because vertex shaders run per-vertex, complex state changes are usually calculated
// using total accumulated time or updated via JS. Here we use an explicit math function:
let speedMultiplier = 1.5;
let accumulatedGuess = f32(uniforms.u_frame) * uniforms.u_dt * speedMultiplier;
let offsetX = sin(accumulatedGuess) * 0.5;
let offsetY = cos(accumulatedGuess * 0.5) * 0.3;
let animatedPosition = pos[vertexIndex] + vec2<f32>(offsetX, offsetY);
output.position = vec4<f32>(animatedPosition, 0.0, 1.0);
output.color = color;
return output;
}
@fragment
fn fs_main(@location(0) color: vec4<f32>) -> @location(0) vec4<f32> {
return color;
}
## font
Option Kernel cost Quality Effort
1-bpp bitmap (current) 1 bit test blocky, aliased, integer scale only, 96 glyphs done
8-bpp AA atlas + 1 lerp smooth glyphs, fractional positioning, proper color blend ~1 hour — same architecture, atlas becomes grayscale, mix(bg, fg, a) instead of a bit test
MSDF atlas (Valve/msdfgen) + 1 fetch + smoothstep crisp at any scale from one atlas, outlines/shadows/glow nearly free ~1 day — offline bake step, shader grows ~40 lines
CPU rasterize → upload mask (x/image/font, any TTF) same as AA perfect (hinting, kerning, Unicode), arbitrary fonts at init ~half day + a dependency; CPU cost only on text change (timecode = once/sec, nothing)