Args parse

This commit is contained in:
Dmitry Sergeev
2026-09-13 13:41:47 +03:00
parent 787b89474c
commit 94ff41a124
5 changed files with 238 additions and 55 deletions
+2
View File
@@ -0,0 +1,2 @@
build
.DS_Store
+224 -55
View File
@@ -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 <domain> [-v <flowDef.json>] [-a <flowDef.json>] [options]")
fmt.Println(" or: mxl-gen -d <domain> [--with <width px>] [--height <height px>] [--fps <framerate>] \\")
fmt.Println(" [-c <channels amount>] [-f <sample rate>]")
fmt.Println(" or: mxl-gen -d <domain> 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 <domain> [-v <flowDef.json>] [-a <flowDef.json>] [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:
+1
View File
@@ -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
)
+2
View File
@@ -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=
+9
View File
@@ -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
}