package video import ( "context" "encoding/json" "errors" "fmt" "log" "mxl-pattern-generator/assets" "mxl-pattern-generator/internal/generator" "github.com/qvest-digital/go-mxl/mxl" ) func Run(ctx context.Context, inst *mxl.Instance, cfg Config) (runErr error) { pattern, ok := patterns[cfg.Pattern] if !ok { return fmt.Errorf("unknown video pattern %q", cfg.Pattern) } // TODO: fall back to a CPU generator if GPU initialization fails. gen, err := generator.NewWGPUGenerator(cfg.Width(), cfg.Height(), pattern.kernelSource) if err != nil { return fmt.Errorf("initialize wgpu video generator: %w", err) } defer func() { if err := gen.Close(); err != nil { runErr = errors.Join(runErr, fmt.Errorf("close video generator: %w", err)) } }() overlay, err := buildTextOverlay(cfg) if err != nil { return err } flowJSON, err := json.Marshal(cfg.Definition) if err != nil { return fmt.Errorf("marshal video flow definition: %w", err) } writer, isCreated, err := inst.NewWriter(string(flowJSON)) if err != nil { return fmt.Errorf("create video writer: %w", err) } defer func() { if err := writer.Close(); err != nil { runErr = errors.Join(runErr, fmt.Errorf("close video writer: %w", err)) } }() if !isCreated { log.Printf("reusing existing video flow: %s", cfg.ID()) } rate := writer.Config().Common.GrainRate idx := mxl.CurrentIndex(rate) log.Printf("writing video flow grainRate=%d/%d starting at idx=%d", rate.Num, rate.Den, idx) var grainsWritten int64 var tick uint32 // Small animation counter; shaders cannot use the full grain index yet. for { select { case <-ctx.Done(): log.Printf("stopping video after %d grains", grainsWritten) return nil default: } grain, err := writer.OpenGrain(idx) if err != nil { return fmt.Errorf("open video grain %d: %w", idx, err) } if err := gen.GenerateFrame(grain.Payload, int(tick)); err != nil { return cancelGrain(grain, fmt.Errorf("generate frame for grain %d: %w", idx, err)) } if overlay != nil { if err := overlay.ApplyV210(grain.Payload); err != nil { return cancelGrain(grain, fmt.Errorf("apply text overlay to grain %d: %w", idx, err)) } } if err := grain.Commit(grain.TotalSlices, 0); err != nil { return fmt.Errorf("commit video grain %d: %w", idx, err) } grainsWritten++ idx++ tick++ if grainsWritten%100 == 0 { log.Printf("video grains written=%d, next index=%d", grainsWritten, idx) } mxl.SleepNs(mxl.NsUntilIndex(idx, rate)) } } func cancelGrain(grain *mxl.GrainWriteAccess, cause error) error { if err := grain.Cancel(); err != nil { return errors.Join(cause, fmt.Errorf("cancel video grain: %w", err)) } return cause } func buildTextOverlay(cfg Config) (overlay *generator.TextOverlay, resultErr error) { if cfg.Overlay.Text == "" { return nil, nil } face, err := generator.NewFace(assets.JetBrainsMono, 48) if err != nil { return nil, fmt.Errorf("load text overlay font: %w", err) } defer func() { if err := face.Close(); err != nil { resultErr = errors.Join(resultErr, fmt.Errorf("close text overlay font: %w", err)) } }() overlay, err = generator.NewTextOverlay( cfg.Overlay.Text, int(cfg.Width()), int(cfg.Height()), cfg.Overlay.X, cfg.Overlay.Y, cfg.Overlay.Position, face, ) if err != nil { return nil, fmt.Errorf("create text overlay: %w", err) } return overlay, nil }