129 lines
3.3 KiB
Go
129 lines
3.3 KiB
Go
package video
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"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.kernelPath)
|
|
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.LoadFace("assets/fonts/JetBrainsMonoNLNerdFontMono-Regular.ttf", 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
|
|
}
|