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" "os/signal"
"syscall" "syscall"
"github.com/google/uuid"
"github.com/qvest-digital/go-mxl/mxl" "github.com/qvest-digital/go-mxl/mxl"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@@ -17,6 +18,11 @@ import (
"mxl-pattern-generator/internal/generator" "mxl-pattern-generator/internal/generator"
) )
const (
APP_NAME = "MXL pattern generator"
APP_VER = "0.1.0"
)
type appArgs struct { type appArgs struct {
showHelp bool showHelp bool
@@ -27,17 +33,164 @@ type appArgs struct {
listPatterns bool listPatterns bool
textOverlay string textOverlay string
videoWidth uint16 videoWidth uint
videoHeight uint16 videoHeight uint
videoFPS float32 videoFPS string
videoUUID string
audioChannels uint8 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() { func printUsage() {
fmt.Println("CLI USAGE FUNC") fmt.Fprintln(os.Stderr, "Usage: mxl-gen -d <domain> [-v <flowDef.json>] [-a <flowDef.json>] [options]")
fmt.Fprintln(os.Stderr, "Usage: ") 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() { func main() {
@@ -45,74 +198,87 @@ func main() {
flagSet := pflag.NewFlagSet("args", pflag.ContinueOnError) flagSet := pflag.NewFlagSet("args", pflag.ContinueOnError)
flagSet.SortFlags = false flagSet.SortFlags = false
flagSet.Usage = func() { printUsage() } flagSet.Usage = func() { printUsage() }
// common flags flagSetAddFlags(flagSet, &args)
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]")
var width, height uint = 1920, 1080 if err := flagSet.Parse(os.Args[1:]); err != nil {
var fpsNum, fpsDen uint = 25, 1 fmt.Fprintln(os.Stderr, err)
flowUUID := "8f1d2a4b-6c3e-4f5a-9b2c-1d7e8a3f0b5d" printUsage()
domain := "/dev/shm/mxl" os.Exit(2)
}
if args.showHelp {
printHelp(flagSet)
return
}
if args.listPatterns {
// TODO
printUsage()
return
}
checkArgs(args)
// create domain if not exist var mxlDomain string = "/dev/shm/mxl"
if _, err := os.Stat(domain); err != nil { type videoInfo struct {
err := os.MkdirAll(domain, 0775) uuid string
if err != nil { width uint
log.Fatalf("Could not create domain dir: %v", err) 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],
} }
} flowDef, err := flowdef.NewFlowDefJSON(
// TODO: move to parse args flowdef.TYPE_VIDEO,
if _, err := mxl.IsTmpFs(domain); err != nil { vi.uuid,
log.Fatalf("Domain is not tmpfs dir: %v", err) 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("%s %s", APP_NAME, APP_VER)
log.Printf("Video: %dx%dp%d/%d", width, height, fpsNum, fpsDen) log.Printf("Domain: %s", mxlDomain)
log.Printf("UUID: %s", flowUUID) 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") // TODO: if init failed -> CPU generator
gen, err := generator.NewWGPUGenerator(width, height, "kernels/v210_bars_move.wgsl") // 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 { if err != nil {
log.Fatalf("wgpu init failed: %v", err) log.Fatalf("wgpu init failed: %v", err)
} }
defer gen.Close() defer gen.Close()
inst, err := mxl.NewInstance(domain, "") inst, err := mxl.NewInstance(mxlDomain, "")
if err != nil { if err != nil {
log.Fatalf("MXL Init Failed: %v", err) log.Fatalf("MXL Init Failed: %v", err)
} }
defer inst.Close() defer inst.Close()
flowDef, err := flowdef.NewFlowDefJSON( writer, isCreated, err := inst.NewWriter(videoFlowDef)
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 { if err != nil {
log.Fatalf("Failed to create MXL writer: %v", err) log.Fatalf("Failed to create MXL writer: %v", err)
} }
if !isCreated { 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() defer writer.Close()
@@ -124,8 +290,11 @@ func main() {
stop := make(chan os.Signal, 1) stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM) signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
// core loop
var written int64 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 { for {
select { select {
case <-stop: case <-stop:
+1
View File
@@ -5,6 +5,7 @@ go 1.26.0
require ( require (
github.com/gogpu/gputypes v0.8.0 github.com/gogpu/gputypes v0.8.0
github.com/gogpu/wgpu v0.34.3 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/qvest-digital/go-mxl v1.1.0-rc.4
github.com/spf13/pflag v1.0.10 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/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 h1:oV7K5ueBqxZuvp1w9KpArzj15+eOrm/l9ZnM3jcKbXA=
github.com/gogpu/wgpu v0.34.3/go.mod h1:erhaIjD9psJKjHqAhn5MWdJETcACR1NGoqLhkTAHkFM= 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 h1:qgOGSIv53HYQLTB06m39c6WE2xqMceObViZy5A3rZlA=
github.com/qvest-digital/go-mxl v1.1.0-rc.4/go.mod h1:cYzyT+S/AONytsKr/DozzFQP2OrNrg/JsQOSjvwn+Rk= 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= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
+9
View File
@@ -4,6 +4,7 @@ package flowdef
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"os"
) )
const ( const (
@@ -109,3 +110,11 @@ func NewFlowDefJSON(
} }
return string(jsonBytes), nil return string(jsonBytes), nil
} }
func ReadFlowDefFile(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", err
}
return string(data), nil
}