#include "AppConfig.hpp" #include #include #include #include #include #include namespace { bool parseUint32( const char* text, uint32_t& value) { uint32_t parsed = 0; bool hasDigit = false; for (int i = 0; text[i] != '\0'; ++i) { if (text[i] < '0' || text[i] > '9') { return false; } hasDigit = true; parsed = parsed * 10u + static_cast(text[i] - '0'); } if (!hasDigit) { return false; } value = parsed; return true; } bool parseFeedIndex( const char* text, uint32_t maxFeeds, uint32_t& index) { uint32_t parsed = 0; if (!parseUint32(text, parsed) || parsed >= maxFeeds) { return false; } index = parsed; return true; } bool parseGrid( std::string_view text, uint32_t& cols, uint32_t& rows) { const size_t xPos = text.find('x'); if (xPos == std::string_view::npos) { return false; } const std::string colText(text.substr(0, xPos)); const std::string rowText(text.substr(xPos + 1)); uint32_t parsedCols = 0; uint32_t parsedRows = 0; if (!parseUint32(colText.c_str(), parsedCols) || !parseUint32(rowText.c_str(), parsedRows) || parsedCols == 0 || parsedRows == 0 || parsedCols > MaxFeedCount / parsedRows) { return false; } cols = parsedCols; rows = parsedRows; return true; } uint32_t feedCount(const AppConfig& config) { return config.gridCols * config.gridRows; } void applyDefaultFeeds(AppConfig& config) { config.feeds.assign(feedCount(config), FeedConfig{}); } } std::optional parseFeedKind(std::string_view text) { if (text == "nosignal" || text == "none" || text == "fake") { return FeedKind::NoSignal; } if (text == "mxl") { return FeedKind::MxlPlaceholder; } if (text == "smpte") { return FeedKind::SmpteBars; } if (text == "mxlsdk") { return FeedKind::MxlSdk; } return std::nullopt; } std::optional parsePresentMode(std::string_view text) { if (text == "fifo") { return PresentModeConfig::Fifo; } if (text == "mailbox") { return PresentModeConfig::Mailbox; } if (text == "immediate") { return PresentModeConfig::Immediate; } return std::nullopt; } std::string_view feedKindName(FeedKind kind) { switch (kind) { case FeedKind::MxlPlaceholder: return "mxl"; case FeedKind::SmpteBars: return "smpte"; case FeedKind::MxlSdk: return "mxlsdk"; case FeedKind::NoSignal: default: return "nosignal"; } } void printUsage(const char* executableName) { std::cout << "Usage: " << executableName << " [options] [feed...]\n" << "Options:\n" << " --no-fps Disable FPS logging\n" << " --perf Log per-frame CPU timing breakdown\n" << " --verbose Enable detailed startup and SDK logs\n" << " --grid x Set multiview grid (default 2x2, max 16 feeds)\n" << " --fps-cap Limit render loop FPS (default 60)\n" << " --present Present mode: fifo, mailbox, immediate (default mailbox)\n" << " --feed Set feed slot kind explicitly\n" << " --domain Set MXL domain for feed slot\n" << " --flow Set MXL flow UUID for feed slot\n" << " --version Print version\n" << " --help Print this help\n" << "Feed kinds: nosignal, none, fake, mxl, smpte, mxlsdk\n" << "Default: 2x2 grid, all slots nosignal" << std::endl; } ConfigParseResult parseAppConfig( int argc, char* argv[]) { ConfigParseResult result; for (int i = 1; i < argc; ++i) { if (std::string_view(argv[i]) != "--grid") { continue; } if (i + 1 >= argc || !parseGrid( argv[i + 1], result.config.gridCols, result.config.gridRows)) { std::cerr << "--grid must be x with 1-" << MaxFeedCount << " total feeds" << std::endl; result.shouldExit = true; result.exitCode = 1; return result; } ++i; } applyDefaultFeeds(result.config); for (int i = 1; i < argc; ++i) { const std::string_view arg(argv[i]); if (arg == "--help" || arg == "-h") { printUsage(argv[0]); result.shouldExit = true; result.exitCode = 0; return result; } } for (int i = 1; i < argc; ++i) { const std::string_view arg(argv[i]); if (arg == "--version" || arg == "-v") { std::cout << AppName << " " << AppVersionMajor << "." << AppVersionMinor << "." << AppVersionPatch << std::endl; result.shouldExit = true; result.exitCode = 0; return result; } } std::vector feedArgs; for (int i = 1; i < argc; ++i) { const std::string_view arg(argv[i]); if (arg == "--no-fps") { result.config.logFps = false; continue; } if (arg == "--perf") { result.config.logPerf = true; continue; } if (arg == "--verbose") { result.config.verbose = true; continue; } if (arg == "--grid") { ++i; continue; } if (arg == "--fps-cap") { if (i + 1 >= argc) { std::cerr << "--fps-cap requires " << std::endl; result.shouldExit = true; result.exitCode = 1; return result; } uint32_t fpsCap = 0; if (!parseUint32(argv[i + 1], fpsCap) || fpsCap == 0 || fpsCap > 240) { std::cerr << "--fps-cap must be 1-240" << std::endl; result.shouldExit = true; result.exitCode = 1; return result; } result.config.fpsCap = fpsCap; ++i; continue; } if (arg == "--present") { if (i + 1 >= argc) { std::cerr << "--present requires " << std::endl; result.shouldExit = true; result.exitCode = 1; return result; } const std::optional mode = parsePresentMode(argv[i + 1]); if (!mode.has_value()) { std::cerr << "--present must be fifo, mailbox, or immediate" << std::endl; result.shouldExit = true; result.exitCode = 1; return result; } result.config.presentMode = mode.value(); ++i; continue; } if (arg == "--feed") { if (i + 2 >= argc) { std::cerr << "--feed requires " << std::endl; result.shouldExit = true; result.exitCode = 1; return result; } uint32_t index = 0; if (!parseFeedIndex( argv[i + 1], feedCount(result.config), index)) { std::cerr << "--feed index must be 0-" << (feedCount(result.config) - 1) << std::endl; result.shouldExit = true; result.exitCode = 1; return result; } const std::optional kind = parseFeedKind(argv[i + 2]); if (!kind.has_value()) { std::cerr << "Unknown feed kind: " << argv[i + 2] << std::endl; printUsage(argv[0]); result.shouldExit = true; result.exitCode = 1; return result; } result.config.feeds[index].kind = kind.value(); i += 2; continue; } if (arg == "--domain") { if (i + 2 >= argc) { std::cerr << "--domain requires " << std::endl; result.shouldExit = true; result.exitCode = 1; return result; } uint32_t index = 0; if (!parseFeedIndex( argv[i + 1], feedCount(result.config), index)) { std::cerr << "--domain index must be 0-" << (feedCount(result.config) - 1) << std::endl; result.shouldExit = true; result.exitCode = 1; return result; } result.config.feeds[index].mxlDomain = std::string(argv[i + 2]); i += 2; continue; } if (arg == "--flow") { if (i + 2 >= argc) { std::cerr << "--flow requires " << std::endl; result.shouldExit = true; result.exitCode = 1; return result; } uint32_t index = 0; if (!parseFeedIndex( argv[i + 1], feedCount(result.config), index)) { std::cerr << "--flow index must be 0-" << (feedCount(result.config) - 1) << std::endl; result.shouldExit = true; result.exitCode = 1; return result; } result.config.feeds[index].mxlFlowId = std::string(argv[i + 2]); result.config.feeds[index].kind = FeedKind::MxlSdk; i += 2; continue; } feedArgs.push_back(arg); } if (feedArgs.size() > result.config.feeds.size()) { std::cerr << "Too many feed arguments" << std::endl; printUsage(argv[0]); result.shouldExit = true; result.exitCode = 1; return result; } size_t feedArgIndex = 0; for (uint32_t slot = 0; slot < result.config.feeds.size() && feedArgIndex < feedArgs.size(); ++slot) { if (result.config.feeds[slot].kind == FeedKind::MxlSdk) { continue; } const std::optional parsed = parseFeedKind(feedArgs[feedArgIndex]); if (!parsed.has_value()) { std::cerr << "Unknown feed kind: " << feedArgs[feedArgIndex] << std::endl; printUsage(argv[0]); result.shouldExit = true; result.exitCode = 1; return result; } result.config.feeds[slot].kind = parsed.value(); ++feedArgIndex; } if (feedArgIndex < feedArgs.size()) { std::cerr << "Too many feed arguments for available slots" << std::endl; printUsage(argv[0]); result.shouldExit = true; result.exitCode = 1; return result; } return result; }