diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d76b74e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build +.DS_Store diff --git a/cmd/wgpu-gen/main.go b/cmd/wgpu-gen/main.go index 8232f5b..2c2743f 100644 --- a/cmd/wgpu-gen/main.go +++ b/cmd/wgpu-gen/main.go @@ -10,6 +10,7 @@ import ( "os/signal" "syscall" + "github.com/google/uuid" "github.com/qvest-digital/go-mxl/mxl" "github.com/spf13/pflag" @@ -17,6 +18,11 @@ import ( "mxl-pattern-generator/internal/generator" ) +const ( + APP_NAME = "MXL pattern generator" + APP_VER = "0.1.0" +) + type appArgs struct { showHelp bool @@ -27,17 +33,164 @@ type appArgs struct { listPatterns bool textOverlay string - videoWidth uint16 - videoHeight uint16 - videoFPS float32 + videoWidth uint + videoHeight uint + videoFPS string + videoUUID string audioChannels uint8 - audioSamplingFreq uint32 + audioSamplingFreq string + audioUUID string +} + +var frameRates = map[string]mxl.Rational{ + "23.97": {Num: 24000, Den: 1001}, + "24": {Num: 24, Den: 1}, + "25": {Num: 25, Den: 1}, + "29.97": {Num: 30000, Den: 1001}, + "30": {Num: 30, Den: 1}, + "50": {Num: 50, Den: 1}, + "59.94": {Num: 60000, Den: 1001}, + "60": {Num: 60, Den: 1}, + "120": {Num: 120, Den: 1}, + "240": {Num: 240, Den: 1}, +} + +var samplingRates = map[string]mxl.Rational{ + "44.1": {Num: 44100, Den: 1}, + "48": {Num: 48000, Den: 1}, + "96": {Num: 96000, Den: 1}, + "192": {Num: 192000, Den: 1}, +} + +func printHelp(fs *pflag.FlagSet) { + fmt.Printf("%s %s\n", APP_NAME, APP_VER) + fmt.Println("Usage: mxl-gen -d [-v ] [-a ] [options]") + fmt.Println(" or: mxl-gen -d [--with ] [--height ] [--fps ] \\") + fmt.Println(" [-c ] [-f ]") + fmt.Println(" or: mxl-gen -d with default params") + fmt.Println("Video and audio feeds params will be ignored, if flow definition file provided.") + fmt.Println() + fs.PrintDefaults() } func printUsage() { - fmt.Println("CLI USAGE FUNC") - fmt.Fprintln(os.Stderr, "Usage: ") + fmt.Fprintln(os.Stderr, "Usage: mxl-gen -d [-v ] [-a ] [options]") + fmt.Fprintln(os.Stderr, "Try 'mxl-gen -h' for more information.") +} + +func checkArgs(args appArgs) { + printUsageAndExit := func() { + printUsage() + os.Exit(2) + } + // domain + if args.domain == "" { + fmt.Fprintf(os.Stderr, "Domain is required\n") + printUsageAndExit() + } + fi, err := os.Stat(args.domain) + if err != nil || !fi.IsDir() { + fmt.Fprintf(os.Stderr, "Invalid MXL domain: %s\n", args.domain) + printUsageAndExit() + } + if ok, err := mxl.IsTmpFs(args.domain); err != nil || !ok { + fmt.Fprintf(os.Stderr, "Invalid MXL domain: %s\n", args.domain) + fmt.Fprintln(os.Stderr, "Domain must be directory in tmps.") + printUsageAndExit() + } + // FlowDef + checkFlowDef := func(label, flowDef string) { + fi, err := os.Stat(flowDef) + if err != nil || fi.IsDir() { + fmt.Fprintf(os.Stderr, "%s flow definition .json file is not accesible\n", label) + printUsageAndExit() + } + } + videoFlowDefProvided, audioFlowDefProvided := false, false + if args.videoFlowDefFile != "" { + checkFlowDef("Video", args.videoFlowDefFile) + videoFlowDefProvided = true + } + if args.audioFlowDefFile != "" { + checkFlowDef("Audio", args.audioFlowDefFile) + audioFlowDefProvided = true + } + + // TODO: check selected pattern + if !videoFlowDefProvided { + if args.videoWidth == 0 || args.videoWidth%6 != 0 { + // width%6 == 0 - because of v210 (6 pixels per 16-byte block) + fmt.Fprintf(os.Stderr, "Video width must be > 0 and divisible by 6\n") + printUsageAndExit() + } + if args.videoHeight == 0 { + fmt.Fprintf(os.Stderr, "Video height must be > 0\n") + printUsageAndExit() + } + if _, exists := frameRates[args.videoFPS]; !exists { + fmt.Fprintf(os.Stderr, "FPS %s is not in available list.\n", args.videoFPS) + fmt.Fprintln(os.Stderr, "If you need more complex solution, use flow definition .json instead.") + fmt.Fprintln(os.Stderr, "Available list:") + for key, _ := range frameRates { + fmt.Fprintf(os.Stderr, " %s\n", key) + } + printUsageAndExit() + } + if args.videoUUID != "" { + if _, err := uuid.Parse(args.videoUUID); err != nil { + fmt.Fprintf(os.Stderr, "Video UUID %s is not valid.\n", args.videoUUID) + printUsageAndExit() + } + } else { + args.videoUUID = uuid.NewString() + } + } + + if !audioFlowDefProvided { + if args.audioChannels == 0 { + fmt.Fprintln(os.Stderr, "Audio channels amount must be > 0") + printUsageAndExit() + } + if _, exists := samplingRates[args.audioSamplingFreq]; !exists { + fmt.Fprintf(os.Stderr, "Sample rate %s is not in available list.\n", args.audioSamplingFreq) + fmt.Fprintln(os.Stderr, "If you need more complex solution, use flow definition .json instead.") + fmt.Fprintln(os.Stderr, "Available list:") + for key, _ := range samplingRates { + fmt.Fprintf(os.Stderr, " %s\n", key) + } + printUsageAndExit() + } + if args.audioUUID != "" { + if err := uuid.Validate(args.audioUUID); err != nil { + fmt.Fprintf(os.Stderr, "Audio UUID %s is not valid.\n", args.audioUUID) + printUsageAndExit() + } + } else { + args.audioUUID = uuid.NewString() + } + } +} + +func flagSetAddFlags(fs *pflag.FlagSet, args *appArgs) { + // common flags + fs.BoolVarP(&args.showHelp, "help", "h", false, "Show help message and exit") + // MXL flags + fs.StringVarP(&args.domain, "domain", "d", "", "MXL domain") + fs.StringVarP(&args.videoFlowDefFile, "video", "v", "", "Video flow definition JSON file path") + fs.StringVarP(&args.audioFlowDefFile, "audio", "a", "", "Audio flow definition JSON file path [TODO]") + // Video pattern flags + fs.Uint8VarP(&args.pattern, "pattern", "p", 0, "Video pattern type [TODO]") + fs.BoolVar(&args.listPatterns, "list-patterns", false, "List video available video patterns and exit [TODO]") + fs.StringVarP(&args.textOverlay, "text", "t", "", "Text overlay above video pattern [TODO]") + fs.UintVar(&args.videoWidth, "width", 1920, "Video pattern width") + fs.UintVar(&args.videoHeight, "height", 1080, "Video pattern height") + fs.StringVar(&args.videoFPS, "fps", "25", "Video pattern FPS") + fs.StringVar(&args.videoUUID, "video-id", "", "Video UUID. Will be created, if not provided [TODO]") + // Audio pattern flags + fs.Uint8VarP(&args.audioChannels, "channel", "c", 2, "Amount of audio channels. Each channel: num * 1kHz [TODO]") + fs.StringVarP(&args.audioSamplingFreq, "freq", "f", "48", "Sampling frequency of test audio feed in kHz [TODO]") + fs.StringVar(&args.audioUUID, "audio-id", "", "Audio UUID. Will be created, if not provided [TODO]") } func main() { @@ -45,74 +198,87 @@ func main() { flagSet := pflag.NewFlagSet("args", pflag.ContinueOnError) flagSet.SortFlags = false flagSet.Usage = func() { printUsage() } - // common flags - flagSet.BoolVarP(&args.showHelp, "help", "h", false, "Show help message and exit") - // MXL flags - flagSet.StringVarP(&args.domain, "domain", "d", "", "MXL domain") - flagSet.StringVarP(&args.videoFlowDefFile, "video", "v", "", "Video flow definition JSON file path") - flagSet.StringVarP(&args.audioFlowDefFile, "audio", "a", "", "Audio flow definition JSON file path [TODO]") - // Video pattern flags - flagSet.Uint8VarP(&args.pattern, "pattern", "p", 0, "Video pattern type [TODO]") - flagSet.BoolVar(&args.listPatterns, "list-patterns", false, "List video available video patterns and exit [TODO]") - flagSet.StringVarP(&args.textOverlay, "overlay", "o", "", "Text overlay above video pattern [TODO]") - flagSet.Uint16VarP(&args.videoWidth, "width", "w", 1920, "Video pattern width") - // flagSet.Uint16VarP(&args.videoHeight, "height", "h", 1920, "Video pattern height") - args.videoFPS = *flagSet.Float32P("fps", "f", 1920, "Video pattern FPS") - // Audio pattern flags - flagSet.Uint8VarP(&args.audioChannels, "channel", "c", 2, "Amount of audio channels. Each channel: num * 1kHz [TODO]") - flagSet.Uint32VarP(&args.audioSamplingFreq, "freq", "f", 0, "Sampling frequency of test audio feed [TODO]") + flagSetAddFlags(flagSet, &args) - var width, height uint = 1920, 1080 - var fpsNum, fpsDen uint = 25, 1 - flowUUID := "8f1d2a4b-6c3e-4f5a-9b2c-1d7e8a3f0b5d" - domain := "/dev/shm/mxl" + if err := flagSet.Parse(os.Args[1:]); err != nil { + fmt.Fprintln(os.Stderr, err) + printUsage() + os.Exit(2) + } + if args.showHelp { + printHelp(flagSet) + return + } + if args.listPatterns { + // TODO + printUsage() + return + } + checkArgs(args) - // create domain if not exist - if _, err := os.Stat(domain); err != nil { - err := os.MkdirAll(domain, 0775) - if err != nil { - log.Fatalf("Could not create domain dir: %v", err) + var mxlDomain string = "/dev/shm/mxl" + type videoInfo struct { + uuid string + width uint + height uint + fps mxl.Rational + } + var vi videoInfo + var videoFlowDef string + if args.videoFlowDefFile == "" { + args.videoUUID = "8f1d2a4b-6c3e-4f5a-9b2c-1d7e8a3f0b5d" // TODO: remove before public release + vi = videoInfo{ + uuid: args.videoUUID, + width: args.videoWidth, + height: args.videoHeight, + fps: frameRates[args.videoFPS], } - } - // TODO: move to parse args - if _, err := mxl.IsTmpFs(domain); err != nil { - log.Fatalf("Domain is not tmpfs dir: %v", err) + flowDef, err := flowdef.NewFlowDefJSON( + flowdef.TYPE_VIDEO, + vi.uuid, + vi.width, + vi.height, + uint(vi.fps.Num), + uint(vi.fps.Den), + ) + videoFlowDef = flowDef + if err != nil { + log.Fatalf("Could not create Flow Definition: %v", err) + } + } else { + flowDef, err := flowdef.ReadFlowDefFile(args.videoFlowDefFile) + if err != nil { + log.Fatalf("Could not read video flow def .json: %s. Reason: %v", args.videoFlowDefFile, err) + } + videoFlowDef = flowDef } - log.Printf("Go Pattern Gen (wgpu)") - log.Printf("Video: %dx%dp%d/%d", width, height, fpsNum, fpsDen) - log.Printf("UUID: %s", flowUUID) + log.Printf("%s %s", APP_NAME, APP_VER) + log.Printf("Domain: %s", mxlDomain) + log.Printf("Video: %dx%d %d/%d", vi.width, vi.height, vi.fps.Num, vi.fps.Den) + log.Printf("Video UUID: %s", vi.uuid) - // gen, err := generator.NewWGPUGenerator(width, height, "kernels/v210_bars.wgsl") - gen, err := generator.NewWGPUGenerator(width, height, "kernels/v210_bars_move.wgsl") + // TODO: if init failed -> CPU generator + // But before i should create same patterns for both generators + // and text overlays + gen, err := generator.NewWGPUGenerator(vi.width, vi.height, "kernels/v210_bars_move.wgsl") if err != nil { log.Fatalf("wgpu init failed: %v", err) } defer gen.Close() - inst, err := mxl.NewInstance(domain, "") + inst, err := mxl.NewInstance(mxlDomain, "") 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) + writer, isCreated, err := inst.NewWriter(videoFlowDef) if err != nil { log.Fatalf("Failed to create MXL writer: %v", err) } if !isCreated { - log.Printf("reusing existing flow: %s, domain: %s", flowUUID, domain) + log.Printf("reusing existing flow: %s, domain: %s", vi.uuid, mxlDomain) } defer writer.Close() @@ -124,8 +290,11 @@ func main() { stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt, syscall.SIGTERM) + // core loop var written int64 - var tick uint32 // animation clock: small counter, not the huge grain index + // animation clock: small counter, not the huge grain index. + // Reason: current wgpu shaders limitations + var tick uint32 for { select { case <-stop: diff --git a/go.mod b/go.mod index f40f11d..63f8bdd 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.26.0 require ( github.com/gogpu/gputypes v0.8.0 github.com/gogpu/wgpu v0.34.3 + github.com/google/uuid v1.6.0 github.com/qvest-digital/go-mxl v1.1.0-rc.4 github.com/spf13/pflag v1.0.10 ) diff --git a/go.sum b/go.sum index a7fecb0..690359c 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ 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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= diff --git a/internal/flow-def/flow-def.go b/internal/flow-def/flow-def.go index c0497d3..e2d9e82 100644 --- a/internal/flow-def/flow-def.go +++ b/internal/flow-def/flow-def.go @@ -4,6 +4,7 @@ package flowdef import ( "encoding/json" "errors" + "os" ) const ( @@ -109,3 +110,11 @@ func NewFlowDefJSON( } return string(jsonBytes), nil } + +func ReadFlowDefFile(path string) (string, error) { + data, err := os.ReadFile(path) + if err != nil { + return "", err + } + return string(data), nil +}