use core::mem::swap; use core::sync::atomic::AtomicBool; use core::sync::atomic::Ordering; use std::error; use std::io::Read; use std::path; use std::process; use std::sync::Arc; use std::sync::Mutex; use std::thread; use clap::Parser; use piston_window as pw; use piston_window::PistonWindow; use piston_window::WindowSettings; #[derive(Debug, Parser)] #[command(author, version, about, long_about = None)] struct Cli { /// Executable to run executable: path::PathBuf, /// Number of pixels in a row cols: usize, /// Number of rows in a frame rows: usize, /// Free-format arguments to pass to the executable args: Vec, /// Use RGBW pixel format #[arg(short, long, action = clap::ArgAction::SetTrue)] white: bool, /// Frame rate for displaying #[arg(short, long, default_value_t = 25)] frame_rate: u64, /// Turn debugging information on #[arg(short, long, action = clap::ArgAction::Count)] debug: u8, } struct Buffer { frame: Vec, count: u64, } impl Buffer { fn new(size: usize) -> Buffer { Buffer { frame: vec![0; size], count: 0, } } fn is_newer(&self, other: &Buffer) -> bool { self.count > other.count } } fn main() -> Result<(), Box> { let cli = Cli::parse(); if cli.debug >= 2 { eprintln!("CLI: {:?}", cli); } let mut app = process::Command::new(&cli.executable) .arg(cli.cols.to_string()) .arg(cli.rows.to_string()) .args(cli.args) .stdout(process::Stdio::piped()) .spawn()?; if cli.debug >= 1 { eprintln!( "Running {} as a child process with pid {}", cli.executable.display(), app.id() ); } let mut stdout = app .stdout .take() .ok_or("opening app process stdout failed")?; let colors = if cli.white { 4 } else { 3 }; // TODO check for overflow and excessive size let buffer_size = cli.cols * cli.rows * colors; let mut frame_count = 0; let x_size = cli.cols; let y_size = cli.rows; let mut window: PistonWindow = WindowSettings::new( format!("pixelfoo-viewer {}", cli.executable.display()), [20 * (x_size as u32), 20 * (y_size as u32)], ) .exit_on_esc(true) .vsync(true) .build()?; // Shared buffer for one frame of data from the app. let buffer0 = Arc::new(Mutex::new(Buffer::new(buffer_size))); let buffer1 = buffer0.clone(); let receiver_stop = AtomicBool::new(false); thread::scope(|scope| { let receiver_thread = scope.spawn(|| { let mut buffer = Buffer::new(buffer_size); while !receiver_stop.load(Ordering::Relaxed) { // Read a frame from app process. stdout .read_exact(&mut buffer.frame) .expect("pipe read failed"); if cli.debug >= 3 { eprintln!("Received frame {}", frame_count); } frame_count += 1; buffer.count = frame_count; // Put the frame in the shared buffer. { let mut b = buffer0.lock().unwrap(); swap(&mut *b, &mut buffer); } } }); let mut buffer = Buffer::new(buffer_size); while let Some(event) = window.next() { window.draw_2d(&event, |context, graphics, _device| { let [vsx, vsy] = context.get_view_size(); // Compute cell size let csx = (vsx as f64) / (x_size as f64); let csy = (vsy as f64) / (y_size as f64); let cs = csx.min(csy); pw::clear([0.5, 0.5, 0.5, 1.0], graphics); // Swap the shared buffer with our own buffer if it is newer. { let mut b = buffer1.lock().unwrap(); if b.is_newer(&buffer) { swap(&mut *b, &mut buffer); } }; // Display the buffer. for y in 0..y_size { for x in 0..x_size { let i = colors * (y * x_size + x); let color = if colors == 3 { let r = f32::from(buffer.frame[i + 0]) / 255.0; let g = f32::from(buffer.frame[i + 1]) / 255.0; let b = f32::from(buffer.frame[i + 2]) / 255.0; [r, g, b, 1.0] } else { let r = f32::from(buffer.frame[i + 0]) / 255.0; let g = f32::from(buffer.frame[i + 1]) / 255.0; let b = f32::from(buffer.frame[i + 2]) / 255.0; let w = f32::from(buffer.frame[i + 3]) / 255.0; [ (0.5 * r + 0.8 * w).min(1.0), (0.5 * g + 0.7 * w).min(1.0), (0.5 * b + 0.6 * w).min(1.0), 1.0, ] }; let rectangle = [(x as f64) * cs, (y as f64) * cs, cs, cs]; pw::rectangle(color, rectangle, context.transform, graphics); } } }); } receiver_stop.store(true, Ordering::Relaxed); let _result = receiver_thread.join(); }); app.kill()?; Ok(()) }