From 885ee7fa045e31273a50829ceaf5c087b2d438d8 Mon Sep 17 00:00:00 2001 From: Oleksandr Berezovskyi Date: Sat, 12 Sep 2026 10:10:29 +0300 Subject: [PATCH] feat(cli): add the binary that wires capture, slot and client together Sets up logging, starts the network thread, runs the PipeWire loop in the main thread and shuts down in order so HyperHDR gets a Clear before exit. --- src/main.rs | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 src/main.rs diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6458f34 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,107 @@ +use std::io::IsTerminal; +use std::process::ExitCode; +use std::sync::Arc; +use std::thread; +use std::time::{Duration, Instant}; + +use anyhow::Context as _; +use clap::Parser; +use tracing::{error, info, warn}; +use tracing_subscriber::EnvFilter; + +use gamescope_hyperhdr::capture::{self, CaptureConfig}; +use gamescope_hyperhdr::config::Config; +use gamescope_hyperhdr::hyperhdr::{client, ClientConfig}; +use gamescope_hyperhdr::slot::FrameSlot; +use gamescope_hyperhdr::stats::Stats; + +/// How long the network thread gets to send `Clear` and close on shutdown. +const NETWORK_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(1); + +fn init_logging(level: &str) { + let filter = EnvFilter::try_new(level).unwrap_or_else(|e| { + eprintln!("invalid log level {level:?} ({e}), using info"); + EnvFilter::new("info") + }); + // Under systemd, journald adds its own timestamps. + let journal = std::env::var_os("JOURNAL_STREAM").is_some(); + let builder = tracing_subscriber::fmt() + .with_writer(std::io::stderr) + .with_env_filter(filter) + .with_target(false) + .with_ansi(!journal && std::io::stderr().is_terminal()); + if journal { + builder.without_time().init(); + } else { + builder.init(); + } +} + +fn main() -> ExitCode { + let cfg = Config::parse(); + init_logging(&cfg.log_level); + + match cfg.validate() { + Ok(warnings) => warnings.iter().for_each(|w| warn!("{w}")), + Err(e) => { + error!("invalid configuration: {e}"); + return ExitCode::from(2); + } + } + info!( + host = %cfg.host, + port = cfg.port, + priority = cfg.priority, + origin = %cfg.origin, + size = %cfg.size, + supersample = cfg.supersample, + requested_size = %cfg.requested_size(), + max_fps = cfg.max_fps, + keepalive_ms = cfg.keepalive_ms, + idle_clear_secs = cfg.idle_clear_secs, + target = %cfg.target, + connect_timeout_ms = cfg.connect_timeout_ms, + write_timeout_ms = cfg.write_timeout_ms, + "gamescope-hyperhdr {} starting", + env!("CARGO_PKG_VERSION") + ); + + let slot = Arc::new(FrameSlot::new()); + let stats = Arc::new(Stats::default()); + let capture_cfg = CaptureConfig { + target: cfg.target.clone(), + requested: cfg.requested_size(), + output_max: cfg.size, + }; + let mut network = None; + let result = capture::run(capture_cfg, slot.clone(), stats.clone(), || { + let handle = client::spawn(ClientConfig::from_config(&cfg), slot.clone(), stats) + .context("cannot start the network thread")?; + network = Some(handle); + Ok(()) + }); + + slot.shutdown(); + if let Some(network) = network { + let deadline = Instant::now() + NETWORK_SHUTDOWN_TIMEOUT; + while !network.is_finished() && Instant::now() < deadline { + thread::sleep(Duration::from_millis(5)); + } + if network.is_finished() { + let _ = network.join(); + } else { + warn!("network thread did not finish within {NETWORK_SHUTDOWN_TIMEOUT:?}"); + } + } + + match result { + Ok(()) => { + info!("exiting"); + ExitCode::SUCCESS + } + Err(e) => { + error!("{e:#}"); + ExitCode::FAILURE + } + } +}