commit 08874882ec00720163cb1de19af17b33d5834627 Author: 0m.ax Date: Fri Jul 18 22:01:25 2025 +0200 inital rust pooort diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb5a316 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..68f3f8c --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,133 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flood-rs" +version = "0.1.0" +dependencies = [ + "libc", + "nix", + "png", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2a394d9 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "flood-rs" +version = "0.1.0" +edition = "2024" + +[dependencies] +png = "0.17" +libc = "0.2" +nix = { version = "0.29", features = ["socket", "uio"] } diff --git a/images/dvdvideo.png b/images/dvdvideo.png new file mode 100644 index 0000000..f621836 Binary files /dev/null and b/images/dvdvideo.png differ diff --git a/images/hackaday.png b/images/hackaday.png new file mode 100644 index 0000000..eef1bc3 Binary files /dev/null and b/images/hackaday.png differ diff --git a/images/spade.png b/images/spade.png new file mode 100644 index 0000000..99371ad Binary files /dev/null and b/images/spade.png differ diff --git a/images/unicorn_cc.png b/images/unicorn_cc.png new file mode 100644 index 0000000..a811d25 Binary files /dev/null and b/images/unicorn_cc.png differ diff --git a/images/windows_logo.png b/images/windows_logo.png new file mode 100644 index 0000000..b3aa882 Binary files /dev/null and b/images/windows_logo.png differ diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..23bc058 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,211 @@ +use std::fs::File; +use std::io::BufReader; +use std::net::{ToSocketAddrs, UdpSocket}; +use std::time::Duration; + +// 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 { + socket: UdpSocket, + bufs: Vec<[u8; MSGSIZE]>, + next_buf: usize, + pos_in_buf: usize, +} + +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 mut bufs = vec![[0; MSGSIZE]; QUEUE_LEN]; + for buf in bufs.iter_mut() { + buf[0] = 0x00; + buf[1] = 0x01; + } + + Display { + socket, + bufs, + next_buf: 0, + pos_in_buf: 0, + } + } + + /// Flushes the current buffer if it contains pixel data. + fn flush_frame(&mut self) { + if self.pos_in_buf > 0 { + let len = 2 + self.pos_in_buf * 7; + let buf_to_send = &self.bufs[self.next_buf][..len]; + self.socket.send(buf_to_send).expect("Failed to send data"); + self.next_buf = (self.next_buf + 1) % QUEUE_LEN; + self.pos_in_buf = 0; + } + } + + /// 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.flush_frame(); + } + } + + /// 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]); + } + } + } + } + + /// Clears the entire screen to black. + #[allow(dead_code)] + fn blank_screen(&mut self) { + for x in 0..DISPLAY_WIDTH { + for y in 0..DISPLAY_HEIGHT { + self.set_pixel(x as u16, y as u16, 0, 0, 0); + } + } + self.flush_frame(); + } +} + +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; + + // display.blank_screen(); + + loop { + for (i, bb) in images.iter_mut().enumerate() { + if bb.rate > 0 && frame_counter % bb.rate != 0 { + continue; + } + bb.draw_and_move(&mut display); + } + display.flush_frame(); + frame_counter += 1; + + // A small delay to control the frame rate + std::thread::sleep(Duration::from_millis(16)); + } +} +