Compare commits

...

10 Commits

Author SHA1 Message Date
13a4810cbf b 2025-12-27 14:09:05 +01:00
8c5aa4f948 a 2025-12-26 23:26:56 +01:00
timvisee
38ce0f0c43 Update README, describe GIF support 2023-12-30 14:40:25 +01:00
timvisee
e7a0fed049 Respect GIF frame delays, use it over --fps option 2023-12-30 14:39:44 +01:00
timvisee
708cd2794f Add support for loading GIF files directly 2023-12-30 14:15:23 +01:00
timvisee
46fd802c6b Rename arg_handler to args 2023-12-29 18:55:24 +01:00
timvisee
7461fb4831 Bump Rust to 2021 edition 2023-12-29 18:53:29 +01:00
timvisee
6c72e84a55 Update README 2023-12-29 18:50:07 +01:00
timvisee
b945034f71 Change --no-flush into --flush option 2023-12-29 18:49:11 +01:00
timvisee
f8a89223c9 Update README 2023-12-29 18:12:00 +01:00
11 changed files with 4595 additions and 172 deletions

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@
**/*.rs.bk **/*.rs.bk
/testserver /testserver
.*.swp .*.swp
video/*

4057
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
[package] [package]
name = "pixelpwnr" name = "pixelpwnr"
version = "0.1.0" version = "0.1.0"
edition = "2021"
authors = ["Tim Visée <timvisee@gmail.com>"] authors = ["Tim Visée <timvisee@gmail.com>"]
license = "GPL-3.0" license = "GPL-3.0"
readme = "README.md" readme = "README.md"
@@ -15,10 +16,24 @@ categories = [
[dependencies] [dependencies]
bufstream = "0.1" bufstream = "0.1"
clap = { version = "4.4", features = [ "derive" ] } clap = { version = "4.4", features = [ "derive" ] }
ctrlc = "3.4"
eframe = "0.26"
glob = "0.3"
image = "0.23" image = "0.23"
libc = "0.2"
num_cpus = "1.13.1" num_cpus = "1.13.1"
regex = "1.5" regex = "1.5"
rayon = "1.5.1" rayon = "1.5.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[[bin]]
name = "pixelpwnr"
path = "src/main.rs"
[[bin]]
name = "pixelpwnr-gui"
path = "src/gui.rs"
[profile.release] [profile.release]
lto = true lto = true

View File

@@ -1,4 +1,5 @@
# pixelpwnr # pixelpwnr
A quick [pixelflut][pixelflut] ([video][pixelflut-video]) client in A quick [pixelflut][pixelflut] ([video][pixelflut-video]) client in
[Rust][rust] for use at [34C3][34C3], that _pwns_ whole pixelflut panels. [Rust][rust] for use at [34C3][34C3], that _pwns_ whole pixelflut panels.
@@ -6,12 +7,10 @@ For a high performance pixelflut client and server implementations, see:
- [pixelpwnr-server][pixelpwnr-server]: server - [pixelpwnr-server][pixelpwnr-server]: server
- [pixelpwnr-cast][pixelpwnr-cast]: cast your screen to a pixelflut server - [pixelpwnr-cast][pixelpwnr-cast]: cast your screen to a pixelflut server
**Note:** This is a prototype project. Some things may not work correctly.
Or some things may work slow.
## Features ## Features
* Many concurrent drawing pipes, fast multithreading * Many concurrent drawing pipes, fast multithreading
* Animated images, with multiple frame images * Animated images, with GIFs or multiple frame images
* Control over render sizes and offset * Control over render sizes and offset
* Automatic image sizing and formatting * Automatic image sizing and formatting
* Blazingly fast [binary protocol](https://github.com/timvisee/pixelpwnr-server#the-binary-px-command) (`PB` with `--binary`) * Blazingly fast [binary protocol](https://github.com/timvisee/pixelpwnr-server#the-binary-px-command) (`PB` with `--binary`)
@@ -19,6 +18,7 @@ Or some things may work slow.
* Linux, Windows and macOS * Linux, Windows and macOS
## Usage ## Usage
Pixelflut a simple image: Pixelflut a simple image:
```bash ```bash
# Flut a simple image. # Flut a simple image.
@@ -48,6 +48,7 @@ Use the `--help` flag, or see the [help](#help) section for all available
options. options.
## Installation ## Installation
For installation, Git and Rust cargo are required. For installation, Git and Rust cargo are required.
Install the latest version of Rust with [rustup][rustup]. Install the latest version of Rust with [rustup][rustup].
@@ -83,6 +84,7 @@ cargo build --release
``` ```
## Performance & speed optimization ## Performance & speed optimization
There are many things that affect how quickly pixels can be painted on a There are many things that affect how quickly pixels can be painted on a
pixelflut server. pixelflut server.
Some of them are: Some of them are:
@@ -104,37 +106,30 @@ Things that improve painting performance:
- Use multiple machines (servers) with multiple `pixelpwnr` instances to push - Use multiple machines (servers) with multiple `pixelpwnr` instances to push
pixels to the screen. pixels to the screen.
## Future improvements
This application is still in the prototyping phase, and many things can be
improved for significantly better performance and usability.
See the [TODO](TODO.md) file for a list of future improvements.
## Help ## Help
```text ```text
pixelpwnr --help $ pixelpwnr --help
pixelpwnr 0.1 Insanely fast pixelflut client for images and animations
Tim Visee <timvisee@gmail.com>
A quick pixelflut client, that pwns pixelflut panels.
USAGE: Usage: pixelpwnr [OPTIONS] --image <PATH>... <HOST>
pixelpwnr [OPTIONS] <HOST> --image <PATH>...
FLAGS: Arguments:
--help Prints help information
-V, --version Prints version information
OPTIONS:
-i, --image <PATH>... Image paths
-w, --width <PIXELS> Draw width (def: screen width)
-h, --height <PIXELS> Draw height (def: screen height)
-x <PIXELS> Draw X offset (def: 0)
-y <PIXELS> Draw Y offset (def: 0)
-c, --count <COUNT> Number of concurrent threads (def: CPUs)
-r, --fps <RATE> Frames per second with multiple images (def: 1)
ARGS:
<HOST> The host to pwn "host:port" <HOST> The host to pwn "host:port"
Options:
--help Show this help
-i, --image <PATH>... Image path(s)
-w, --width <PIXELS> Draw width [default: screen width]
-h, --height <PIXELS> Draw height [default: screen height]
-x <PIXELS> Draw X offset [default: 0]
-y <PIXELS> Draw Y offset [default: 0]
-c, --count <COUNT> Number of concurrent threads [default: number of CPUs]
-r, --fps <RATE> Frames per second with multiple images [default: 1]
-b, --binary Use binary mode to set pixels (`PB` protocol extension) [default: off]
-f, --flush <ENABLED> Flush socket after each pixel [default: true] [default: true] [possible values: true, false]
-V, --version Print version
``` ```
## Relevant projects ## Relevant projects

View File

@@ -1,6 +1,3 @@
extern crate clap;
extern crate num_cpus;
use clap::Parser; use clap::Parser;
#[derive(Parser)] #[derive(Parser)]
@@ -13,14 +10,15 @@ pub struct Arguments {
help: Option<bool>, help: Option<bool>,
/// The host to pwn "host:port" /// The host to pwn "host:port"
host: String, #[arg(required_unless_present = "config")]
host: Option<String>,
/// Image path(s) /// Image path(s)
#[arg( #[arg(
short, short,
long, long,
value_name = "PATH", value_name = "PATH",
required = true, required_unless_present = "config",
alias = "images", alias = "images",
num_args(1..) num_args(1..)
)] )]
@@ -40,6 +38,18 @@ pub struct Arguments {
#[arg(short, value_name = "PIXELS", default_value_t = 0)] #[arg(short, value_name = "PIXELS", default_value_t = 0)]
y: u16, y: u16,
/// Path to a JSON config file
#[arg(long, value_name = "PATH")]
config: Option<String>,
/// Path to write JSON status updates
#[arg(long, value_name = "PATH")]
status: Option<String>,
/// Tile images across the screen, ignoring offsets
#[arg(short, long)]
tile: bool,
/// Number of concurrent threads [default: number of CPUs] /// Number of concurrent threads [default: number of CPUs]
#[arg(short, long, aliases = ["thread", "threads"])] #[arg(short, long, aliases = ["thread", "threads"])]
count: Option<usize>, count: Option<usize>,
@@ -52,9 +62,9 @@ pub struct Arguments {
#[arg(short, long, alias = "bin")] #[arg(short, long, alias = "bin")]
binary: bool, binary: bool,
/// Do not flush socket after each pixel [default: on] /// Flush socket after each pixel [default: true]
#[arg(short, long)] #[arg(short, long, action = clap::ArgAction::Set, value_name = "ENABLED", default_value_t = true)]
no_flush: bool, flush: bool,
} }
/// CLI argument handler. /// CLI argument handler.
@@ -71,7 +81,22 @@ impl ArgHandler {
/// Get the host property. /// Get the host property.
pub fn host(&self) -> &str { pub fn host(&self) -> &str {
self.data.host.as_str() self.data.host.as_deref().expect("Host is required")
}
/// Get the optional host property.
pub fn host_optional(&self) -> Option<&str> {
self.data.host.as_deref()
}
/// Get the config path.
pub fn config_path(&self) -> Option<&str> {
self.data.config.as_deref()
}
/// Get the status path.
pub fn status_path(&self) -> Option<&str> {
self.data.status.as_deref()
} }
/// Get the thread count. /// Get the thread count.
@@ -102,6 +127,11 @@ impl ArgHandler {
(self.data.x, self.data.y) (self.data.x, self.data.y)
} }
/// Whether to tile images across the screen.
pub fn tile(&self) -> bool {
self.data.tile
}
/// Get the FPS. /// Get the FPS.
pub fn fps(&self) -> u32 { pub fn fps(&self) -> u32 {
self.data.fps self.data.fps
@@ -112,8 +142,8 @@ impl ArgHandler {
self.data.binary self.data.binary
} }
/// Whether to prevent flushing after each pixel. /// Whether to flush after each pixel.
pub fn no_flush(&self) -> bool { pub fn flush(&self) -> bool {
self.data.no_flush self.data.flush
} }
} }

View File

@@ -1,27 +1,48 @@
use glob::glob;
use rayon::prelude::*; use rayon::prelude::*;
use std::path::Path; use std::fs;
use std::path::{Path, PathBuf};
use std::process;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::thread::sleep; use std::thread::sleep;
use std::time::Duration; use std::time::Duration;
use std::time::{SystemTime, UNIX_EPOCH};
use image; use crate::pix::canvas::Canvas;
use image::imageops::FilterType; use image::imageops::FilterType;
use image::DynamicImage; use image::{DynamicImage, RgbaImage};
use pix::canvas::Canvas; enum ImageMode {
Direct { size: (u16, u16) },
Tiled { tile_size: (u16, u16), canvas_size: (u16, u16) },
}
/// A manager that manages all images to print. /// A manager that manages all images to print.
pub struct ImageManager { pub struct ImageManager {
images: Vec<DynamicImage>, frames: Vec<PathBuf>,
// Define whether the first image has been drawn mode: ImageMode,
preprocessed: bool,
cache_dir: Option<PathBuf>,
cached_single: Option<DynamicImage>,
/// Define whether the first image has been drawn
first: bool, first: bool,
index: isize, index: usize,
} }
impl ImageManager { impl ImageManager {
/// Intantiate the image manager. /// Intantiate the image manager.
pub fn from(images: Vec<DynamicImage>) -> ImageManager { fn from(
frames: Vec<PathBuf>,
mode: ImageMode,
preprocessed: bool,
cache_dir: Option<PathBuf>,
) -> ImageManager {
ImageManager { ImageManager {
images, frames,
mode,
preprocessed,
cache_dir,
cached_single: None,
first: false, first: false,
index: 0, index: 0,
} }
@@ -30,45 +51,83 @@ impl ImageManager {
/// Instantiate the image manager, and load the images from the given paths. /// Instantiate the image manager, and load the images from the given paths.
pub fn load(paths: &[&str], size: (u16, u16)) -> ImageManager { pub fn load(paths: &[&str], size: (u16, u16)) -> ImageManager {
// Show a status message // Show a status message
println!("Load and process {} image(s)...", paths.len()); println!("Load and process {} path(s)...", paths.len());
// Load the images from the paths // Load the images from the paths
let image_manager = ImageManager::from( let frames: Vec<PathBuf> = paths
paths
.par_iter() .par_iter()
.map(|path| load_image(path, size)) .flat_map(|path| load_image_frames(path))
.collect(), .collect();
); let mode = ImageMode::Direct { size };
let (frames, preprocessed, cache_dir) = preprocess_frames(&frames, &mode);
let image_manager = ImageManager::from(frames, mode, preprocessed, cache_dir);
// TODO: process the image slices // TODO: process the image slices
// We succeeded // We succeeded
println!("All images have been loaded successfully"); println!(
"Loaded {} frame(s) from {} path(s).",
image_manager.frames.len(),
paths.len()
);
image_manager
}
/// Instantiate the image manager, and load and tile the images from the given paths.
pub fn load_tiled(
paths: &[&str],
tile_size: (u16, u16),
canvas_size: (u16, u16),
) -> ImageManager {
// Show a status message
println!("Load and process {} path(s)...", paths.len());
let frames: Vec<PathBuf> = paths
.par_iter()
.flat_map(|path| load_image_frames(path))
.collect();
let mode = ImageMode::Tiled {
tile_size,
canvas_size,
};
let (frames, preprocessed, cache_dir) = preprocess_frames(&frames, &mode);
let image_manager = ImageManager::from(frames, mode, preprocessed, cache_dir);
println!(
"Loaded {} frame(s) from {} path(s).",
image_manager.frames.len(),
paths.len()
);
image_manager image_manager
} }
/// Tick the image /// Tick the image
pub fn tick(&mut self, canvas: &mut Canvas) { ///
/// Returns the desired duration for othis frame.
pub fn tick(&mut self, canvas: &mut Canvas) -> Option<Duration> {
// Get the image index bound // Get the image index bound
let bound = self.images.len(); let bound = self.frames.len();
// Just return if the bound is one, as nothing should be updated // Just return if the bound is one, as nothing should be updated
if self.first && bound == 1 { if self.first && bound == 1 {
return; return None;
} }
// Get the image to use // Get the image to use
let image = &mut self.images[self.index as usize % bound]; let mut image = self.load_frame(self.index % bound);
// Update the image on the canvas // Update the image on the canvas
canvas.update_image(image); canvas.update_image(&mut image);
// Increase the index // Increase the index
self.index += 1; self.index += 1;
// We have rendered the first image // We have rendered the first image
self.first = true; self.first = true;
None
} }
/// Start working in the image manager. /// Start working in the image manager.
@@ -76,30 +135,233 @@ impl ImageManager {
/// This will start walking through all image frames, /// This will start walking through all image frames,
/// and pushes each frame to all painters, /// and pushes each frame to all painters,
/// with the specified frames per second. /// with the specified frames per second.
pub fn work(&mut self, canvas: &mut Canvas, fps: u32) { pub fn work<F>(
loop { &mut self,
// Tick to use the next image canvas: &mut Canvas,
self.tick(canvas); fps: u32,
shutdown: &AtomicBool,
mut on_frame: F,
)
where
F: FnMut(usize, usize),
{
let fps = fps.max(1);
let frame_ms = 1000f64 / (fps as f64);
let start = std::time::Instant::now();
let mut next_tick = 0f64;
// Sleep until we need to show the next image loop {
sleep(Duration::from_millis((1000f32 / (fps as f32)) as u64)); if shutdown.load(Ordering::SeqCst) {
break;
}
let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
let target_index = (elapsed_ms / frame_ms).floor() as usize;
if target_index != self.index {
self.index = target_index;
self.tick(canvas);
let total = self.frames.len();
let current = (self.index % total) + 1;
on_frame(current, total);
}
next_tick += frame_ms;
let sleep_ms = (next_tick - elapsed_ms).max(1.0) as u64;
sleep(Duration::from_millis(sleep_ms));
}
}
pub fn frame_count(&self) -> usize {
self.frames.len()
}
fn load_frame(&mut self, index: usize) -> DynamicImage {
if self.frames.len() == 1 {
if let Some(image) = self.cached_single.as_ref() {
return image.clone();
}
}
let path = &self.frames[index];
let image = if self.preprocessed {
image::open(path).unwrap()
} else {
match self.mode {
ImageMode::Direct { size } => image::open(path)
.unwrap()
.resize_exact(size.0 as u32, size.1 as u32, FilterType::Gaussian),
ImageMode::Tiled {
tile_size,
canvas_size,
} => tile_image(
image::open(path).unwrap(),
(tile_size.0 as u32, tile_size.1 as u32),
(canvas_size.0 as u32, canvas_size.1 as u32),
),
}
};
if self.frames.len() == 1 {
self.cached_single = Some(image.clone());
}
image
}
}
static CACHE_COUNTER: AtomicUsize = AtomicUsize::new(0);
fn preprocess_frames(
frames: &[PathBuf],
mode: &ImageMode,
) -> (Vec<PathBuf>, bool, Option<PathBuf>) {
if frames.len() <= 1 {
return (frames.to_vec(), false, None);
}
let counter = CACHE_COUNTER.fetch_add(1, Ordering::SeqCst);
let stamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis();
let cache_dir = std::env::temp_dir().join(format!(
"pixelpwnr_cache_{}_{}_{}",
process::id(),
stamp,
counter
));
fs::create_dir_all(&cache_dir).expect("failed to create cache directory");
match *mode {
ImageMode::Direct { size } => println!(
"Resizing {} frame(s) to {}x{} (cache: {})",
frames.len(),
size.0,
size.1,
cache_dir.display()
),
ImageMode::Tiled {
tile_size,
canvas_size,
} => println!(
"Resizing {} frame(s) to tile {}x{} over {}x{} (cache: {})",
frames.len(),
tile_size.0,
tile_size.1,
canvas_size.0,
canvas_size.1,
cache_dir.display()
),
}
let total = frames.len();
let progress = AtomicUsize::new(0);
let output_paths: Vec<PathBuf> = frames
.par_iter()
.enumerate()
.map(|(index, path)| {
let output = cache_dir.join(format!("{:06}.png", index));
let processed = match *mode {
ImageMode::Direct { size } => image::open(path)
.unwrap()
.resize_exact(size.0 as u32, size.1 as u32, FilterType::Gaussian),
ImageMode::Tiled {
tile_size,
canvas_size,
} => tile_image(
image::open(path).unwrap(),
(tile_size.0 as u32, tile_size.1 as u32),
(canvas_size.0 as u32, canvas_size.1 as u32),
),
};
processed
.save(&output)
.expect("failed to save cached frame");
let done = progress.fetch_add(1, Ordering::SeqCst) + 1;
if done == total || done % 100 == 0 {
println!("Resized {done}/{total} frame(s)...");
}
output
})
.collect();
println!("Cached {} frame(s).", output_paths.len());
(output_paths, true, Some(cache_dir))
}
impl Drop for ImageManager {
fn drop(&mut self) {
if let Some(cache_dir) = &self.cache_dir {
match fs::remove_dir_all(cache_dir) {
Ok(()) => println!("Removed cache directory {}", cache_dir.display()),
Err(err) => eprintln!("Failed to remove cache directory: {err}"),
}
} }
} }
} }
/// Load the image at the given path, and size it correctly fn load_image_frames(path: &str) -> Vec<PathBuf> {
fn load_image(path: &str, size: (u16, u16)) -> DynamicImage { if has_glob_pattern(path) {
// Create a path instance let mut entries: Vec<PathBuf> = glob(path)
.expect("failed to read frames glob")
.filter_map(Result::ok)
.filter(|entry| entry.is_file())
.collect();
entries.sort_by(|a, b| a.file_name().cmp(&b.file_name()));
if entries.is_empty() {
panic!("The given frames glob matched no files");
}
return entries;
}
let path = Path::new(&path); let path = Path::new(&path);
// Check whether the path exists if path.is_dir() {
let mut entries: Vec<PathBuf> = fs::read_dir(path)
.expect("failed to read frames directory")
.filter_map(|entry| entry.ok())
.map(|entry| entry.path())
.filter(|entry| entry.is_file())
.collect();
entries.sort_by(|a, b| a.file_name().cmp(&b.file_name()));
if entries.is_empty() {
panic!("The given frames directory is empty");
}
return entries;
}
if !path.is_file() { if !path.is_file() {
panic!("The given path does not exist or is not a file"); panic!("The given path does not exist or is not a file");
} }
// Load the image vec![path.to_path_buf()]
let image = image::open(path).unwrap(); }
// Resize the image to fit the screen fn has_glob_pattern(path: &str) -> bool {
image.resize_exact(size.0 as u32, size.1 as u32, FilterType::Gaussian) path.contains('*') || path.contains('?') || path.contains('[')
}
fn tile_image(image: DynamicImage, tile_size: (u32, u32), canvas_size: (u32, u32)) -> DynamicImage {
let tile = image
.resize_exact(tile_size.0, tile_size.1, FilterType::Gaussian)
.to_rgba8();
let mut tiled = RgbaImage::new(canvas_size.0, canvas_size.1);
let tile_w = tile.width();
let tile_h = tile.height();
let mut y = 0;
while y < canvas_size.1 {
let mut x = 0;
while x < canvas_size.0 {
image::imageops::overlay(&mut tiled, &tile, x, y);
x += tile_w;
}
y += tile_h;
}
DynamicImage::ImageRgba8(tiled)
} }

View File

@@ -1,9 +1,6 @@
extern crate clap; mod args;
extern crate image;
extern crate rayon;
mod arg_handler;
mod color; mod color;
mod config;
mod image_manager; mod image_manager;
mod painter; mod painter;
mod pix; mod pix;
@@ -11,10 +8,15 @@ mod rect;
use std::io::Error; use std::io::Error;
use arg_handler::ArgHandler; use args::ArgHandler;
use config::ProjectConfig;
use image_manager::ImageManager; use image_manager::ImageManager;
use pix::canvas::Canvas; use pix::canvas::Canvas;
use pix::client::Client; use pix::client::Client;
use serde::Serialize;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
/// Main application entrypoint. /// Main application entrypoint.
fn main() { fn main() {
@@ -30,28 +32,171 @@ fn start(arg_handler: &ArgHandler) {
// Start // Start
println!("Starting... (use CTRL+C to stop)"); println!("Starting... (use CTRL+C to stop)");
let shutdown = setup_shutdown_handler();
if let Some(config_path) = arg_handler.config_path() {
start_from_config(arg_handler, config_path, shutdown);
return;
}
// Gather facts about the host // Gather facts about the host
let screen_size = let screen_size =
gather_host_facts(arg_handler).expect("Failed to gather facts about pixelflut server"); gather_host_facts(arg_handler).expect("Failed to gather facts about pixelflut server");
// Determine the size to use let tile_size = arg_handler.size(Some(screen_size));
let size = arg_handler.size(Some(screen_size)); let (size, offset) = if arg_handler.tile() {
(screen_size, (0, 0))
} else {
(tile_size, arg_handler.offset())
};
// Create a new pixelflut canvas // Create a new pixelflut canvas
let mut canvas = Canvas::new( let mut canvas = Canvas::new(
arg_handler.host(), arg_handler.host(),
arg_handler.count(), arg_handler.count(),
size, size,
arg_handler.offset(), offset,
arg_handler.binary(), arg_handler.binary(),
!arg_handler.no_flush(), arg_handler.flush(),
); );
// Load the image manager // Load the image manager
let mut image_manager = ImageManager::load(&arg_handler.image_paths(), size); let mut image_manager = if arg_handler.tile() {
ImageManager::load_tiled(&arg_handler.image_paths(), tile_size, size)
} else {
ImageManager::load(&arg_handler.image_paths(), size)
};
// Start the work in the image manager, to walk through the frames // Start the work in the image manager, to walk through the frames
image_manager.work(&mut canvas, arg_handler.fps()); image_manager.work(&mut canvas, arg_handler.fps(), &shutdown, |_current, _total| {});
}
#[derive(Clone, Serialize)]
struct StatusEntry {
name: String,
fps: f64,
current: usize,
total: usize,
}
#[derive(Serialize)]
struct StatusFile {
entries: Vec<StatusEntry>,
}
fn start_from_config(arg_handler: &ArgHandler, config_path: &str, shutdown: Arc<AtomicBool>) {
let config = load_config(config_path);
let host = arg_handler
.host_optional()
.map(|host| host.to_string())
.unwrap_or_else(|| config.host.clone());
if host.trim().is_empty() {
panic!("No host specified in CLI or config");
}
let default_threads = config.threads.unwrap_or_else(|| arg_handler.count());
let binary = config.binary;
let flush = config.flush;
let status_path = arg_handler.status_path().map(|path| path.to_string());
let stats: Arc<std::sync::Mutex<Vec<StatusEntry>>> = Arc::new(std::sync::Mutex::new(
config
.entries
.iter()
.map(|entry| StatusEntry {
name: entry.name.clone(),
fps: 0.0,
current: 0,
total: 0,
})
.collect(),
));
if let Some(status_path) = status_path.clone() {
let stats = Arc::clone(&stats);
let shutdown = Arc::clone(&shutdown);
std::thread::spawn(move || {
while !shutdown.load(Ordering::SeqCst) {
if let Ok(locked) = stats.lock() {
let payload = StatusFile {
entries: locked.clone(),
};
if let Ok(json) = serde_json::to_string_pretty(&payload) {
let _ = std::fs::write(&status_path, json);
}
}
std::thread::sleep(Duration::from_millis(500));
}
});
}
let mut handles = Vec::new();
for (index, entry) in config.entries.into_iter().enumerate() {
if !entry.enabled {
continue;
}
let host = host.clone();
let threads = entry.threads.unwrap_or(default_threads);
let stats = Arc::clone(&stats);
let shutdown = Arc::clone(&shutdown);
handles.push(std::thread::spawn(move || {
let mut canvas = Canvas::new(
&host,
threads,
(entry.width, entry.height),
(entry.x, entry.y),
binary,
flush,
);
let mut image_manager = ImageManager::load(&[entry.source.as_str()], (entry.width, entry.height));
let total = image_manager.frame_count();
if let Ok(mut locked) = stats.lock() {
if let Some(stat) = locked.get_mut(index) {
stat.total = total;
}
}
let mut frames = 0usize;
let mut last_report = Instant::now();
image_manager.work(&mut canvas, entry.fps, &shutdown, |current, total| {
frames += 1;
if let Ok(mut locked) = stats.lock() {
if let Some(stat) = locked.get_mut(index) {
stat.current = current;
stat.total = total;
}
}
let elapsed = last_report.elapsed();
if elapsed >= Duration::from_secs(1) {
let fps = frames as f64 / elapsed.as_secs_f64();
if let Ok(mut locked) = stats.lock() {
if let Some(stat) = locked.get_mut(index) {
stat.fps = fps;
}
}
frames = 0;
last_report = Instant::now();
}
});
}));
}
for handle in handles {
let _ = handle.join();
}
}
fn setup_shutdown_handler() -> Arc<AtomicBool> {
let shutdown = Arc::new(AtomicBool::new(false));
let shutdown_handler = Arc::clone(&shutdown);
ctrlc::set_handler(move || {
shutdown_handler.store(true, Ordering::SeqCst);
})
.expect("Failed to set CTRL+C handler");
shutdown
}
fn load_config(path: &str) -> ProjectConfig {
let data = std::fs::read_to_string(path).expect("Failed to read config file");
serde_json::from_str::<ProjectConfig>(&data).expect("Failed to parse config file")
} }
/// Gather important facts about the host. /// Gather important facts about the host.

View File

@@ -1,11 +1,9 @@
extern crate image;
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
use std::thread::JoinHandle; use std::thread::JoinHandle;
use image::DynamicImage; use image::DynamicImage;
use rect::Rect; use crate::rect::Rect;
/// A handle to a painter thread. /// A handle to a painter thread.
/// ///
@@ -30,18 +28,19 @@ impl Handle {
/// Push an image update. /// Push an image update.
pub fn update_image(&self, full_image: &mut DynamicImage) { pub fn update_image(&self, full_image: &mut DynamicImage) {
// Crop the image to the area // Crop without mutating the source image so each painter sees full data.
let image = full_image.crop( let image = image::imageops::crop_imm(
full_image,
self.area.x as u32, self.area.x as u32,
self.area.y as u32, self.area.y as u32,
self.area.w as u32, self.area.w as u32,
self.area.h as u32, self.area.h as u32,
); )
.to_image();
let image = DynamicImage::ImageRgba8(image);
// Push a new image to the thread // Push a new image to the thread
// TODO: return this result // TODO: return this result
self.image_sender let _ = self.image_sender.send(image);
.send(image)
.expect("Failed to send image update to painter");
} }
} }

View File

@@ -3,9 +3,9 @@ use std::sync::mpsc::Receiver;
use image::{DynamicImage, Pixel}; use image::{DynamicImage, Pixel};
use color::Color; use crate::color::Color;
use pix::client::Client; use crate::pix::client::Client;
use rect::Rect; use crate::rect::Rect;
/// A painter that paints on a pixelflut panel. /// A painter that paints on a pixelflut panel.
pub struct Painter { pub struct Painter {

View File

@@ -5,10 +5,10 @@ use std::time::Duration;
use image::DynamicImage; use image::DynamicImage;
use painter::handle::Handle; use crate::painter::handle::Handle;
use painter::painter::Painter; use crate::painter::painter::Painter;
use pix::client::Client; use crate::pix::client::Client;
use rect::Rect; use crate::rect::Rect;
/// A pixflut instance /// A pixflut instance
pub struct Canvas { pub struct Canvas {
@@ -50,6 +50,31 @@ impl Canvas {
/// Spawn the painters for this canvas /// Spawn the painters for this canvas
fn spawn_painters(&mut self, binary: bool, flush: bool) { fn spawn_painters(&mut self, binary: bool, flush: bool) {
let grid = (self.painter_count as f64).sqrt() as usize;
if grid * grid == self.painter_count {
let tile_w = self.size.0 / (grid as u16);
let tile_h = self.size.1 / (grid as u16);
for row in 0..grid {
for col in 0..grid {
let x = (col as u16) * tile_w;
let y = (row as u16) * tile_h;
let w = if col == grid - 1 {
self.size.0 - x
} else {
tile_w
};
let h = if row == grid - 1 {
self.size.1 - y
} else {
tile_h
};
let painter_area = Rect::from(x, y, w, h);
self.spawn_painter(painter_area, binary, flush);
}
}
} else {
// Spawn some painters // Spawn some painters
for i in 0..self.painter_count { for i in 0..self.painter_count {
// Determine the slice width // Determine the slice width
@@ -62,6 +87,7 @@ impl Canvas {
self.spawn_painter(painter_area, binary, flush); self.spawn_painter(painter_area, binary, flush);
} }
} }
}
/// Spawn a single painter in a thread. /// Spawn a single painter in a thread.
fn spawn_painter(&mut self, area: Rect, binary: bool, flush: bool) { fn spawn_painter(&mut self, area: Rect, binary: bool, flush: bool) {

View File

@@ -1,14 +1,11 @@
extern crate bufstream;
extern crate regex;
use std::io::prelude::*; use std::io::prelude::*;
use std::io::{Error, ErrorKind}; use std::io::{Error, ErrorKind};
use std::net::TcpStream; use std::net::TcpStream;
use self::bufstream::BufStream; use bufstream::BufStream;
use self::regex::Regex; use regex::Regex;
use color::Color; use crate::color::Color;
// The default buffer size for reading the client stream. // The default buffer size for reading the client stream.
// - Big enough so we don't have to expand // - Big enough so we don't have to expand