diff --git a/Cargo.lock b/Cargo.lock index 09fb07b..81a731f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,56 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -38,6 +88,58 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "clap" +version = "4.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "crc32fast" version = "1.5.0" @@ -70,12 +172,36 @@ dependencies = [ name = "flood-rs" version = "0.1.0" dependencies = [ + "clap", + "gif", "libc", "nix", "png", "socket2", ] +[[package]] +name = "gif" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "libc" version = "0.2.174" @@ -114,6 +240,12 @@ dependencies = [ "memoffset", ] +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "png" version = "0.17.16" @@ -127,6 +259,24 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -143,6 +293,41 @@ dependencies = [ "winapi", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "winapi" version = "0.3.9" @@ -164,3 +349,18 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/Cargo.toml b/Cargo.toml index e8b1f61..7be5c21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,5 @@ png = "0.17" libc = "0.2" nix = { version = "0.29", features = ["socket", "uio"] } socket2 = "0.4.7" +clap = { version = "4", features = ["derive"] } +gif = "0.13" diff --git a/images/netto.gif b/images/netto.gif new file mode 100644 index 0000000..bf76d3a Binary files /dev/null and b/images/netto.gif differ diff --git a/src/display.rs b/src/display.rs index c0e178e..782a580 100644 --- a/src/display.rs +++ b/src/display.rs @@ -62,7 +62,7 @@ impl Display { /// /// The returned Display can create additional writers via /// `create_writer()` for multi-threaded drawing. - pub fn new(host: &str, port: u16) -> Self { + pub fn new(host: &str, port: u16, interface: Option<&str>) -> Self { let shared = Arc::new(SharedState { pending: Mutex::new(VecDeque::with_capacity(QUEUE_LEN)), condvar: Condvar::new(), @@ -80,7 +80,7 @@ impl Display { for _ in [1,2,3,4,5] { // Spawn the sender thread. let shared_sender = Arc::clone(&shared); - match RawSender::new(host, port, 0) { + match RawSender::new(host, port, 0, interface) { Ok(raw_sender) => { eprintln!("[display] Using raw AF_PACKET sender"); thread::Builder::new() diff --git a/src/gif_image.rs b/src/gif_image.rs new file mode 100644 index 0000000..8f0cf5a --- /dev/null +++ b/src/gif_image.rs @@ -0,0 +1,164 @@ +use std::fs::File; + +use crate::drawable::Drawable; +use crate::pixel::Pixel; +use crate::pixel_buf::PixelBuf; + +/// A precomputed frame: the offset pixels ready to draw and its duration. +struct GifFrame { + /// Precomputed pixels at the fixed screen position. + pixels: Vec, + /// Duration of this frame in seconds. + duration_secs: f32, +} + +/// A fixed-position animated GIF drawable. +/// +/// All frames are decoded and their opaque pixels precomputed at load time. +/// During playback the elapsed time selects which frame to display. +pub struct GifImage { + frames: Vec, + /// Total loop duration (sum of all frame durations) in seconds. + total_duration: f32, +} + +impl GifImage { + /// Loads a GIF file and precomputes the pixel data for every frame. + /// + /// `x`, `y` set the fixed screen position of the top-left corner. + pub fn new(path: &str, x: i32, y: i32) -> Self { + let file = File::open(path).expect("Failed to open GIF file"); + let mut decoder = gif::DecodeOptions::new(); + decoder.set_color_output(gif::ColorOutput::RGBA); + let mut reader = decoder.read_info(file).expect("Failed to read GIF info"); + + let width = reader.width() as u32; + let height = reader.height() as u32; + + // Canvas holds the composited RGBA image, built up across frames + // to handle disposal methods correctly. + let mut canvas = vec![0u8; (width * height * 4) as usize]; + let mut frames = Vec::new(); + + while let Some(frame) = reader.read_next_frame().expect("Failed to decode GIF frame") { + let fw = frame.width as u32; + let fh = frame.height as u32; + let fx = frame.left as u32; + let fy = frame.top as u32; + + // Composite this frame onto the canvas. + for row in 0..fh { + for col in 0..fw { + let src_idx = ((row * fw + col) * 4) as usize; + let dst_x = fx + col; + let dst_y = fy + row; + if dst_x >= width || dst_y >= height { + continue; + } + let dst_idx = ((dst_y * width + dst_x) * 4) as usize; + let a = frame.buffer[src_idx + 3]; + if a > 0 { + canvas[dst_idx] = frame.buffer[src_idx]; + canvas[dst_idx + 1] = frame.buffer[src_idx + 1]; + canvas[dst_idx + 2] = frame.buffer[src_idx + 2]; + canvas[dst_idx + 3] = a; + } + } + } + + // Extract opaque pixels from the composited canvas. + let mut pixels = Vec::new(); + for sy in 0..height { + for sx in 0..width { + let idx = ((sy * width + sx) * 4) as usize; + let a = canvas[idx + 3]; + if a > 0 { + pixels.push(Pixel { + x: x as u16 + sx as u16, + y: y as u16 + sy as u16, + r: canvas[idx], + g: canvas[idx + 1], + b: canvas[idx + 2], + }); + } + } + } + + // GIF frame delay is in centiseconds; 0 is conventionally ~100ms. + let delay_cs = frame.delay; + let duration_secs = if delay_cs == 0 { + 0.1 + } else { + delay_cs as f32 / 10.0 + }; + + frames.push(GifFrame { + pixels, + duration_secs, + }); + + // Handle disposal: if RestoreToBackground, clear the frame region. + if frame.dispose == gif::DisposalMethod::Background { + for row in 0..fh { + for col in 0..fw { + let dst_x = fx + col; + let dst_y = fy + row; + if dst_x < width && dst_y < height { + let idx = ((dst_y * width + dst_x) * 4) as usize; + canvas[idx] = 0; + canvas[idx + 1] = 0; + canvas[idx + 2] = 0; + canvas[idx + 3] = 0; + } + } + } + } + // DisposalMethod::Previous is rare and complex; we ignore it + // (treat as Keep, which is usually fine). + } + + let total_duration: f32 = frames.iter().map(|f| f.duration_secs).sum(); + + eprintln!( + "[gif] {}: {}x{}, {} frames, {:.2}s total loop", + path, + width, + height, + frames.len(), + total_duration, + ); + + GifImage { + frames, + total_duration, + } + } +} + +impl Drawable for GifImage { + fn draw(&self, buf: &mut PixelBuf, elapsed: f32) { + if self.frames.is_empty() || self.total_duration <= 0.0 { + return; + } + + // Loop the animation. + let t = elapsed % self.total_duration; + + // Find which frame corresponds to time t. + let mut accumulated = 0.0; + let mut frame_idx = 0; + for (i, frame) in self.frames.iter().enumerate() { + accumulated += frame.duration_secs; + if t < accumulated { + frame_idx = i; + break; + } + } + + buf.set_pixels_ref(&self.frames[frame_idx].pixels); + } + + fn update(&mut self, _dt: f32, _elapsed: f32) { + // Fixed position, nothing to update. + } +} diff --git a/src/main-new.rs b/src/main-new.rs deleted file mode 100644 index 7af1867..0000000 --- a/src/main-new.rs +++ /dev/null @@ -1,272 +0,0 @@ - -use std::fs::File; -use std::io::BufReader; -use std::net::{ToSocketAddrs, UdpSocket}; -use std::os::unix::io::{AsRawFd, RawFd}; -use std::time::Duration; - -// Nix crate for sendmmsg -use nix::sys::socket::{sendmmsg, MsgFlags, SendMmsgData}; -use nix::sys::uio::IoVec; - -// Constants from the C code -const QUEUE_LEN: usize = 1000; -const MSG_PAYLOAD_SIZE: usize = 7 * 160; -const MSGSIZE: usize = 2 + MSG_PAYLOAD_SIZE; - -const DISPLAY_HOST: &str = "100.65.0.2"; -const DISPLAY_PORT: u16 = 5005; -const DISPLAY_WIDTH: i32 = 1920; -const DISPLAY_HEIGHT: i32 = 1080; - -/// Represents the data decoded from a PNG file. -struct PngData { - width: u32, - height: u32, - pixels: Vec, -} - -impl PngData { - /// Loads and decodes a PNG image from the given path. - fn open(path: &str) -> Result { - let file = File::open(path).expect("Failed to open PNG file"); - let decoder = png::Decoder::new(BufReader::new(file)); - let mut reader = decoder.read_info()?; - let mut buf = vec![0; reader.output_buffer_size()]; - let info = reader.next_frame(&mut buf)?; - - Ok(PngData { - width: info.width, - height: info.height, - pixels: buf, - }) - } -} - -/// Represents a bouncing image on the screen. -struct BouncingImage { - img: PngData, - x: i32, - y: i32, - x1: i32, - y1: i32, - x2: i32, - y2: i32, - move_x: i32, - move_y: i32, - rate: u32, -} - -impl BouncingImage { - /// Initializes a new BouncingImage. - fn new(img_file: &str, move_x: i32, move_y: i32, rate: u32, start_x: i32, start_y: i32) -> Self { - let img = PngData::open(img_file).expect("Could not load image"); - let mut bb = BouncingImage { - x2: DISPLAY_WIDTH - img.width as i32, - y2: DISPLAY_HEIGHT - img.height as i32, - img, - x: start_x, - y: start_y, - x1: 0, - y1: 0, - move_x, - move_y, - rate, - }; - if bb.x == -1 { - bb.x = (bb.x1 + bb.x2) / 2; - } - if bb.y == -1 { - bb.y = (bb.y1 + bb.y2) / 2; - } - bb - } - - /// Draws the image and updates its position. - fn draw_and_move(&mut self, display: &mut Display) { - display.draw_png(&self.img, self.x, self.y); - - self.x += self.move_x; - self.y += self.move_y; - - if self.x < self.x1 || self.x > self.x2 { - self.move_x *= -1; - } - if self.y < self.y1 || self.y > self.y2 { - self.move_y *= -1; - } - } -} - -/// Manages the connection and data sent to the display. -struct Display { - fd: RawFd, - bufs: Vec<[u8; MSGSIZE]>, - lens: Vec, // Stores the actual length of data in each buffer - next_buf: usize, // The next buffer in the queue to be filled - send_next: usize, // The next buffer in the queue to be sent - pos_in_buf: usize, - // Keep the socket alive to keep the file descriptor valid - _socket: UdpSocket, -} - -impl Display { - /// Creates a new Display and connects to the specified host and port. - fn new(host: &str, port: u16) -> Self { - let remote_addr = (host, port) - .to_socket_addrs() - .expect("Invalid remote address") - .next() - .expect("Could not resolve host"); - - let socket = UdpSocket::bind("0.0.0.0:0").expect("Could not bind to local port"); - socket.connect(remote_addr).expect("Could not connect to remote"); - let fd = socket.as_raw_fd(); - - let mut bufs = vec![[0; MSGSIZE]; QUEUE_LEN]; - for buf in bufs.iter_mut() { - buf[0] = 0x00; - buf[1] = 0x01; - } - - Display { - fd, - bufs, - lens: vec![0; QUEUE_LEN], - next_buf: 0, - send_next: 0, - pos_in_buf: 0, - _socket: socket, - } - } - - /// Marks the current buffer as ready to be sent and moves to the next one. - fn mark_buffer_ready(&mut self) { - if self.pos_in_buf > 0 { - self.lens[self.next_buf] = 2 + self.pos_in_buf * 7; - self.next_buf = (self.next_buf + 1) % QUEUE_LEN; - self.pos_in_buf = 0; - - // If we've wrapped around and caught up to the send queue, - // we must flush to avoid overwriting data that hasn't been sent. - if self.next_buf == self.send_next { - eprintln!("Warning: Buffer queue full. Forcing a flush."); - self.flush_all_pending(); - } - } - } - - /// Sends all queued packets using the efficient `sendmmsg` syscall. - fn flush_all_pending(&mut self) { - // First, ensure the current, partially-filled buffer is marked as ready. - self.mark_buffer_ready(); - - if self.send_next == self.next_buf { - return; // Nothing to send. - } - - // We build a temporary list of message headers to pass to sendmmsg. - // This is the cleanest way to handle the circular buffer. - let mut iovecs_storage = Vec::new(); - let mut messages_to_send = Vec::new(); - - let mut current_idx = self.send_next; - while current_idx != self.next_buf { - let data_slice = &self.bufs[current_idx][..self.lens[current_idx]]; - iovecs_storage.push(IoVec::from_slice(data_slice)); - current_idx = (current_idx + 1) % QUEUE_LEN; - } - - // Since we used `connect()`, the kernel knows the destination address, - // so we can pass `None` for the address in `SendMmsgData`. - for iov in &iovecs_storage { - messages_to_send.push(SendMmsgData { - iov: &[*iov], - addr: None, - cmsgs: &[], - _phantom: std::marker::PhantomData, - }); - } - - if messages_to_send.is_empty() { - return; - } - - // Perform the `sendmmsg` syscall - match sendmmsg(self.fd, &messages_to_send, MsgFlags::empty()) { - Ok(num_sent) => { - // Advance the send queue by the number of packets actually sent. - self.send_next = (self.send_next + num_sent) % QUEUE_LEN; - } - Err(e) => { - // Non-blocking sockets might return an error indicating to try again. - // For this example, we'll just log other errors. - if e != nix::errno::Errno::EAGAIN && e != nix::errno::Errno::EWOULDBLOCK { - eprintln!("Failed to send messages with sendmmsg: {}", e); - } - } - } - } - - /// Sets a pixel color at a specific coordinate. - fn set_pixel(&mut self, x: u16, y: u16, r: u8, g: u8, b: u8) { - let offset = 2 + self.pos_in_buf * 7; - let buf = &mut self.bufs[self.next_buf][offset..offset + 7]; - buf[0] = x as u8; - buf[1] = (x >> 8) as u8; - buf[2] = y as u8; - buf[3] = (y >> 8) as u8; - buf[4] = r; - buf[5] = g; - buf[6] = b; - - self.pos_in_buf += 1; - if self.pos_in_buf == 160 { - self.mark_buffer_ready(); - } - } - - /// Draws a PNG image at the given coordinates. - fn draw_png(&mut self, png: &PngData, x: i32, y: i32) { - for sy in 0..png.height { - for sx in 0..png.width { - let index = (sy * png.width + sx) as usize * 4; - let rgba = &png.pixels[index..index + 4]; - if rgba[3] > 0 { // Check alpha channel - self.set_pixel((x + sx as i32) as u16, (y + sy as i32) as u16, rgba[0], rgba[1], rgba[2]); - } - } - } - } -} - -fn main() { - let mut images = vec![ - BouncingImage::new("images/unicorn_cc.png", 13, -10, 1, -1, -1), - BouncingImage::new("images/windows_logo.png", -8, 3, 2, -1, -1), - BouncingImage::new("images/spade.png", 32, -12, 1, 0, 0), - BouncingImage::new("images/dvdvideo.png", 20, 6, 5, 1000, 800), - BouncingImage::new("images/hackaday.png", 40, 18, 3, 500, 800), - ]; - - let mut display = Display::new(DISPLAY_HOST, DISPLAY_PORT); - let mut frame_counter: u32 = 0; - - loop { - for bb in images.iter_mut() { - if bb.rate > 0 && frame_counter % bb.rate != 0 { - continue; - } - bb.draw_and_move(&mut display); - } - - // Send all queued packets for this frame in a single batch. - display.flush_all_pending(); - - frame_counter += 1; - - // A small delay to control the frame rate (approx 60 FPS). - std::thread::sleep(Duration::from_millis(16)); - } -} - diff --git a/src/main.rs b/src/main.rs index ad679ca..71924d9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ use std::sync::{Arc, Barrier, Mutex, RwLock}; use std::thread; use std::time::{Duration, Instant}; +use clap::Parser; use socket2::{Domain, Socket, Type}; mod bouncing_image; @@ -15,19 +16,33 @@ mod pixel_buf; mod png_data; mod raw_socket; mod pixel; +mod gif_image; use bouncing_image::BouncingImage; use circle::Circle; use display::Display; use drawable::Drawable; +use gif_image::GifImage; use pixel_buf::PixelBuf; -// Display configuration constants -pub const DISPLAY_HOST: &str = "100.65.0.2"; -pub const DISPLAY_PORT: u16 = 5005; pub const DISPLAY_WIDTH: i32 = 1920; pub const DISPLAY_HEIGHT: i32 = 1080; +#[derive(Parser)] +#[command(name = "flood-rs", about = "Pixel flooding tool")] +struct Args { + /// Target IP address or hostname + target: String, + + /// Target port + #[arg(short, long, default_value_t = 5005)] + port: u16, + + /// Source network interface (e.g. eth0). Auto-discovered if not set. + #[arg(short = 'i', long)] + interface: Option, +} + /// Unpacks a 4-byte slice into two u16 values (little-endian). fn unpack_coordinates(buffer: &[u8]) -> Option<(u16, u16)> { if buffer.len() != 4 { @@ -41,6 +56,8 @@ fn unpack_coordinates(buffer: &[u8]) -> Option<(u16, u16)> { } fn main() { + let args = Args::parse(); + let x: Arc> = Arc::new(Mutex::new(0)); let x_thread = x.clone(); @@ -53,15 +70,16 @@ fn main() { let circle = Box::new(Circle::new(x, y, set)); let mut images: Vec> = vec![ // Velocities are in pixels per second. - Box::new(BouncingImage::new("images/unicorn_cc.png", 30.0, -30.0, -1, -1)), - Box::new(BouncingImage::new("images/windows_logo.png", -20.0, 20.0, -1, -1)), - Box::new(BouncingImage::new("images/spade.png", 90.0, -60.0, 0, 0)), - Box::new(BouncingImage::new("images/dvdvideo.png", 60.0, 18.0, 1000, 800)), - Box::new(BouncingImage::new("images/hackaday.png", 40.0, 50.0, 500, 800)), +// Box::new(BouncingImage::new("images/unicorn_cc.png", 30.0, -30.0, -1, -1)), +// Box::new(BouncingImage::new("images/windows_logo.png", -20.0, 20.0, -1, -1)), +// Box::new(BouncingImage::new("images/spade.png", 90.0, -60.0, 0, 0)), +// Box::new(BouncingImage::new("images/dvdvideo.png", 60.0, 18.0, 1000, 800)), +// Box::new(BouncingImage::new("images/hackaday.png", 40.0, 50.0, 500, 800)), circle, + Box::new(GifImage::new("images/netto.gif", 200, 200)), ]; - let mut display = Display::new(DISPLAY_HOST, DISPLAY_PORT); + let mut display = Display::new(&args.target, args.port, args.interface.as_deref()); // Spawn a UDP listener thread for receiving coordinates thread::spawn(move || { diff --git a/src/pixel_buf.rs b/src/pixel_buf.rs index 91606ec..83721fa 100644 --- a/src/pixel_buf.rs +++ b/src/pixel_buf.rs @@ -34,4 +34,13 @@ impl PixelBuf { self.pixels.push(pixel.format0()) } } + + /// Appends pixels from a borrowed slice (avoids cloning precomputed data). + #[inline] + pub fn set_pixels_ref(&mut self, pixels: &[Pixel]) { + self.pixels.reserve(pixels.len()); + for pixel in pixels { + self.pixels.push(pixel.format0()) + } + } } diff --git a/src/raw_socket.rs b/src/raw_socket.rs index 39d0dab..bc022cd 100644 --- a/src/raw_socket.rs +++ b/src/raw_socket.rs @@ -30,7 +30,7 @@ impl RawSender { /// from the routing table and ARP cache. /// /// Requires `CAP_NET_RAW` or root. - pub fn new(dst_host: &str, dst_port: u16, src_port: u16) -> io::Result { + pub fn new(dst_host: &str, dst_port: u16, src_port: u16, interface: Option<&str>) -> io::Result { // Resolve destination IP. let dst_ip: Ipv4Addr = dst_host .parse() @@ -44,16 +44,21 @@ impl RawSender { .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Could not resolve host")) })?; - // Discover source IP and interface via a temporary connected UDP socket. - let probe = UdpSocket::bind("0.0.0.0:0")?; - probe.connect((dst_ip, dst_port))?; - let src_ip: Ipv4Addr = match probe.local_addr()? { - std::net::SocketAddr::V4(a) => *a.ip(), - _ => return Err(io::Error::new(io::ErrorKind::Other, "Not IPv4")), + // Discover or use the specified source interface and IP. + let (ifname, ifindex, src_ip) = if let Some(ifname) = interface { + let ifindex = get_ifindex(ifname)?; + let src_ip = get_interface_ip(ifname)?; + (ifname.to_owned(), ifindex, src_ip) + } else { + let probe = UdpSocket::bind("0.0.0.0:0")?; + probe.connect((dst_ip, dst_port))?; + let src_ip: Ipv4Addr = match probe.local_addr()? { + std::net::SocketAddr::V4(a) => *a.ip(), + _ => return Err(io::Error::new(io::ErrorKind::Other, "Not IPv4")), + }; + let (ifname, ifindex) = discover_interface(dst_ip)?; + (ifname, ifindex, src_ip) }; - - // Discover interface name and index from routing. - let (ifname, ifindex) = discover_interface(dst_ip)?; eprintln!("[raw] interface: {} (index {})", ifname, ifindex); // Read source MAC from sysfs. @@ -311,6 +316,42 @@ fn ip_checksum(header: &[u8]) -> u16 { !(sum as u16) } +/// Gets the ifindex for a named interface from sysfs. +fn get_ifindex(ifname: &str) -> io::Result { + let path = format!("/sys/class/net/{}/ifindex", ifname); + let s = fs::read_to_string(&path).map_err(|e| { + io::Error::new(e.kind(), format!("Interface '{}' not found: {}", ifname, e)) + })?; + s.trim().parse().map_err(|e| { + io::Error::new(io::ErrorKind::InvalidData, format!("Bad ifindex: {}", e)) + }) +} + +/// Gets the IPv4 address of a named interface using ioctl. +fn get_interface_ip(ifname: &str) -> io::Result { + let sock = UdpSocket::bind("0.0.0.0:0")?; + let fd = sock.as_raw_fd(); + let mut ifr: libc::ifreq = unsafe { std::mem::zeroed() }; + let name_bytes = ifname.as_bytes(); + let copy_len = name_bytes.len().min(libc::IFNAMSIZ - 1); + unsafe { + std::ptr::copy_nonoverlapping( + name_bytes.as_ptr(), + ifr.ifr_name.as_mut_ptr() as *mut u8, + copy_len, + ); + } + let ret = unsafe { libc::ioctl(fd, libc::SIOCGIFADDR, &mut ifr) }; + if ret < 0 { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("Could not get IP for interface '{}'", ifname), + )); + } + let addr = unsafe { &*(&ifr.ifr_ifru as *const _ as *const libc::sockaddr_in) }; + Ok(Ipv4Addr::from(u32::from_be(addr.sin_addr.s_addr))) +} + /// Discovers the outbound interface name and index for a destination IP. fn discover_interface(dst_ip: Ipv4Addr) -> io::Result<(String, i32)> { // Use a temporary UDP socket to discover the interface.