diff --git a/crates/servers/display-server/Cargo.toml b/crates/servers/display-server/Cargo.toml index 8c3b045..84efda3 100644 --- a/crates/servers/display-server/Cargo.toml +++ b/crates/servers/display-server/Cargo.toml @@ -4,3 +4,5 @@ version = "0.1.0" edition = "2024" [dependencies] +types = { workspace = true } +server-framework = { workspace = true } diff --git a/crates/servers/display-server/src/adapters/mock.rs b/crates/servers/display-server/src/adapters/mock.rs new file mode 100644 index 0000000..09b9110 --- /dev/null +++ b/crates/servers/display-server/src/adapters/mock.rs @@ -0,0 +1,66 @@ +use crate::framebuffer::Framebuffer; + +pub struct MockFramebuffer { + width: u16, + height: u16, + pixels: [(u16, u16, u32); 4096], + pixel_count: usize, + flushed: bool, +} + +impl MockFramebuffer { + pub fn new(width: u16, height: u16) -> Self { + Self { + width, + height, + pixels: [(0, 0, 0); 4096], + pixel_count: 0, + flushed: false, + } + } + + pub fn pixel_count(&self) -> usize { + self.pixel_count + } + + pub fn was_flushed(&self) -> bool { + self.flushed + } + + pub fn pixel_at(&self, index: usize) -> (u16, u16, u32) { + self.pixels[index] + } +} + +impl Framebuffer for MockFramebuffer { + fn width(&self) -> u16 { + self.width + } + + fn height(&self) -> u16 { + self.height + } + + fn write_pixel(&mut self, x: u16, y: u16, color: u32) { + if self.pixel_count < self.pixels.len() { + self.pixels[self.pixel_count] = (x, y, color); + self.pixel_count += 1; + } + } + + fn fill_rect(&mut self, x: u16, y: u16, w: u16, h: u16, color: u32) { + for dy in 0..h { + for dx in 0..w { + self.write_pixel(x + dx, y + dy, color); + } + } + } + + fn clear(&mut self, _color: u32) { + self.pixel_count = 0; + } + + fn flush(&mut self) { + self.flushed = true; + } +} diff --git a/crates/servers/display-server/src/adapters/mod.rs b/crates/servers/display-server/src/adapters/mod.rs new file mode 100644 index 0000000..9afc1d5 --- /dev/null +++ b/crates/servers/display-server/src/adapters/mod.rs @@ -0,0 +1 @@ +pub mod mock; diff --git a/crates/servers/display-server/src/framebuffer.rs b/crates/servers/display-server/src/framebuffer.rs new file mode 100644 index 0000000..d477eb3 --- /dev/null +++ b/crates/servers/display-server/src/framebuffer.rs @@ -0,0 +1,8 @@ +pub trait Framebuffer { + fn width(&self) -> u16; + fn height(&self) -> u16; + fn write_pixel(&mut self, x: u16, y: u16, color: u32); + fn fill_rect(&mut self, x: u16, y: u16, w: u16, h: u16, color: u32); + fn clear(&mut self, color: u32); + fn flush(&mut self); +} diff --git a/crates/servers/display-server/src/lib.rs b/crates/servers/display-server/src/lib.rs index b93cf3f..1994575 100644 --- a/crates/servers/display-server/src/lib.rs +++ b/crates/servers/display-server/src/lib.rs @@ -1,14 +1,10 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} +#![no_std] + +pub mod adapters; +pub mod framebuffer; +pub mod protocol; +pub mod server; #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +#[path = "tests/mod.rs"] +mod tests; diff --git a/crates/servers/display-server/src/protocol.rs b/crates/servers/display-server/src/protocol.rs new file mode 100644 index 0000000..bc50326 --- /dev/null +++ b/crates/servers/display-server/src/protocol.rs @@ -0,0 +1,76 @@ +use server_framework::{Decode, Encode}; + +#[derive(Debug, PartialEq)] +pub enum DisplayRequest { + DrawRect { x: u16, y: u16, w: u16, h: u16, color: u32 }, + Clear { color: u32 }, + Flush, + GetDimensions, +} + +#[derive(Debug, PartialEq)] +pub enum DisplayResponse { + Ok, + Dimensions { width: u16, height: u16 }, + Error, +} + +impl Encode for DisplayRequest { + fn encode(&self) -> [usize; 4] { + match self { + DisplayRequest::DrawRect { x, y, w, h, color } => { + let xy = (*x as usize) | ((*y as usize) << 16); + let wh = (*w as usize) | ((*h as usize) << 16); + [0, xy, wh, *color as usize] + } + DisplayRequest::Clear { color } => [1, *color as usize, 0, 0], + DisplayRequest::Flush => [2, 0, 0, 0], + DisplayRequest::GetDimensions => [3, 0, 0, 0], + } + } +} + +impl Decode for DisplayRequest { + fn decode(words: [usize; 4]) -> Option { + match words[0] { + 0 => { + let x = (words[1] & 0xFFFF) as u16; + let y = ((words[1] >> 16) & 0xFFFF) as u16; + let w = (words[2] & 0xFFFF) as u16; + let h = ((words[2] >> 16) & 0xFFFF) as u16; + let color = words[3] as u32; + Some(DisplayRequest::DrawRect { x, y, w, h, color }) + } + 1 => Some(DisplayRequest::Clear { color: words[1] as u32 }), + 2 => Some(DisplayRequest::Flush), + 3 => Some(DisplayRequest::GetDimensions), + _ => None, + } + } +} + +impl Encode for DisplayResponse { + fn encode(&self) -> [usize; 4] { + match self { + DisplayResponse::Ok => [0, 0, 0, 0], + DisplayResponse::Dimensions { width, height } => { + [1, *width as usize, *height as usize, 0] + } + DisplayResponse::Error => [2, 0, 0, 0], + } + } +} + +impl Decode for DisplayResponse { + fn decode(words: [usize; 4]) -> Option { + match words[0] { + 0 => Some(DisplayResponse::Ok), + 1 => Some(DisplayResponse::Dimensions { + width: words[1] as u16, + height: words[2] as u16, + }), + 2 => Some(DisplayResponse::Error), + _ => None, + } + } +} diff --git a/crates/servers/display-server/src/server.rs b/crates/servers/display-server/src/server.rs new file mode 100644 index 0000000..279daae --- /dev/null +++ b/crates/servers/display-server/src/server.rs @@ -0,0 +1,44 @@ +use server_framework::Server; + +use crate::framebuffer::Framebuffer; +use crate::protocol::{DisplayRequest, DisplayResponse}; + +pub struct DisplayServer { + fb: F, +} + +impl DisplayServer { + pub fn new(fb: F) -> Self { + Self { fb } + } + + pub fn framebuffer(&self) -> &F { + &self.fb + } +} + +impl Server for DisplayServer { + type Request = DisplayRequest; + type Response = DisplayResponse; + + fn handle(&mut self, request: DisplayRequest) -> DisplayResponse { + match request { + DisplayRequest::DrawRect { x, y, w, h, color } => { + self.fb.fill_rect(x, y, w, h, color); + DisplayResponse::Ok + } + DisplayRequest::Clear { color } => { + self.fb.clear(color); + DisplayResponse::Ok + } + DisplayRequest::Flush => { + self.fb.flush(); + DisplayResponse::Ok + } + DisplayRequest::GetDimensions => DisplayResponse::Dimensions { + width: self.fb.width(), + height: self.fb.height(), + }, + } + } +} diff --git a/crates/servers/display-server/src/tests/mod.rs b/crates/servers/display-server/src/tests/mod.rs new file mode 100644 index 0000000..aa77244 --- /dev/null +++ b/crates/servers/display-server/src/tests/mod.rs @@ -0,0 +1,2 @@ +mod protocol; +mod server; diff --git a/crates/servers/display-server/src/tests/protocol.rs b/crates/servers/display-server/src/tests/protocol.rs new file mode 100644 index 0000000..fcb1c7d --- /dev/null +++ b/crates/servers/display-server/src/tests/protocol.rs @@ -0,0 +1,54 @@ +use crate::protocol::{DisplayRequest, DisplayResponse}; +use server_framework::{Decode, Encode}; + +#[test] +fn draw_rect_roundtrip() { + let req = DisplayRequest::DrawRect { + x: 10, + y: 20, + w: 100, + h: 50, + color: 0xFF0000, + }; + let words = req.encode(); + let decoded = DisplayRequest::decode(words).unwrap(); + assert_eq!(decoded, req); +} + +#[test] +fn clear_roundtrip() { + let req = DisplayRequest::Clear { color: 0x00FF00 }; + let words = req.encode(); + let decoded = DisplayRequest::decode(words).unwrap(); + assert_eq!(decoded, req); +} + +#[test] +fn flush_roundtrip() { + let words = DisplayRequest::Flush.encode(); + let decoded = DisplayRequest::decode(words).unwrap(); + assert_eq!(decoded, DisplayRequest::Flush); +} + +#[test] +fn get_dimensions_roundtrip() { + let words = DisplayRequest::GetDimensions.encode(); + let decoded = DisplayRequest::decode(words).unwrap(); + assert_eq!(decoded, DisplayRequest::GetDimensions); +} + +#[test] +fn dimensions_response_roundtrip() { + let resp = DisplayResponse::Dimensions { + width: 320, + height: 240, + }; + let words = resp.encode(); + let decoded = DisplayResponse::decode(words).unwrap(); + assert_eq!(decoded, resp); +} + +#[test] +fn invalid_request_returns_none() { + assert!(DisplayRequest::decode([99, 0, 0, 0]).is_none()); +} diff --git a/crates/servers/display-server/src/tests/server.rs b/crates/servers/display-server/src/tests/server.rs new file mode 100644 index 0000000..891f522 --- /dev/null +++ b/crates/servers/display-server/src/tests/server.rs @@ -0,0 +1,75 @@ +use crate::adapters::mock::MockFramebuffer; +use crate::protocol::{DisplayRequest, DisplayResponse}; +use crate::server::DisplayServer; +use server_framework::Server; + +fn test_server() -> DisplayServer { + DisplayServer::new(MockFramebuffer::new(320, 240)) +} + +#[test] +fn draw_rect_writes_pixels() { + let mut server = test_server(); + let resp = server.handle(DisplayRequest::DrawRect { + x: 0, + y: 0, + w: 4, + h: 3, + color: 0xFF, + }); + assert_eq!(resp, DisplayResponse::Ok); + assert_eq!(server.framebuffer().pixel_count(), 12); +} + +#[test] +fn clear_resets_pixels() { + let mut server = test_server(); + server.handle(DisplayRequest::DrawRect { + x: 0, + y: 0, + w: 10, + h: 10, + color: 0xFF, + }); + server.handle(DisplayRequest::Clear { color: 0 }); + assert_eq!(server.framebuffer().pixel_count(), 0); +} + +#[test] +fn flush_sets_flag() { + let mut server = test_server(); + assert!(!server.framebuffer().was_flushed()); + server.handle(DisplayRequest::Flush); + assert!(server.framebuffer().was_flushed()); +} + +#[test] +fn get_dimensions_returns_size() { + let mut server = test_server(); + let resp = server.handle(DisplayRequest::GetDimensions); + assert_eq!( + resp, + DisplayResponse::Dimensions { + width: 320, + height: 240 + } + ); +} + +#[test] +fn draw_rect_correct_pixel_positions() { + let mut server = test_server(); + server.handle(DisplayRequest::DrawRect { + x: 5, + y: 10, + w: 2, + h: 2, + color: 0xAB, + }); + let fb = server.framebuffer(); + assert_eq!(fb.pixel_count(), 4); + assert_eq!(fb.pixel_at(0), (5, 10, 0xAB)); + assert_eq!(fb.pixel_at(1), (6, 10, 0xAB)); + assert_eq!(fb.pixel_at(2), (5, 11, 0xAB)); + assert_eq!(fb.pixel_at(3), (6, 11, 0xAB)); +}