use crate::app_mode::AppMode; use crate::color::Color; use crate::primitives::Rect; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct FrameId(pub u64); impl FrameId { pub const ZERO: Self = Self(0); pub const fn new(value: u64) -> Self { Self(value) } pub const fn get(self) -> u64 { self.0 } pub const fn next(self) -> Self { Self(self.0.wrapping_add(1)) } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct RenderSubmission { pub frame_id: FrameId, pub app_mode: AppMode, pub ownership: RenderOwnership, pub packet: RenderSubmissionPacket, } impl RenderSubmission { pub const fn game2d(frame_id: FrameId, packet: Game2DFramePacket) -> Self { Self { frame_id, app_mode: AppMode::Game, ownership: RenderOwnership::new(0, AppMode::Game, 0), packet: RenderSubmissionPacket::Game2D(packet), } } pub const fn shell_ui(frame_id: FrameId, packet: ShellUiFramePacket) -> Self { Self { frame_id, app_mode: AppMode::Shell, ownership: RenderOwnership::new(0, AppMode::Shell, 0), packet: RenderSubmissionPacket::ShellUi(packet), } } pub const fn with_ownership(mut self, ownership: RenderOwnership) -> Self { self.ownership = ownership; self } pub const fn is_coherent(&self) -> bool { matches!( (self.app_mode, &self.packet), (AppMode::Game, RenderSubmissionPacket::Game2D(_)) | (AppMode::Shell, RenderSubmissionPacket::ShellUi(_)) ) } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct RenderOwnership { pub epoch: u64, pub app_mode: AppMode, pub app_id: u32, } impl RenderOwnership { pub const fn new(epoch: u64, app_mode: AppMode, app_id: u32) -> Self { Self { epoch, app_mode, app_id } } } #[derive(Debug, Clone, PartialEq, Eq)] pub enum RenderSubmissionPacket { Game2D(Game2DFramePacket), ShellUi(ShellUiFramePacket), } #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct Game2DFramePacket { pub composer: ComposerFramePacket, pub gfx2d: Vec, } impl Game2DFramePacket { pub fn new(composer: ComposerFramePacket, gfx2d: Vec) -> Self { Self { composer, gfx2d } } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ShellUiFramePacket { pub commands: Vec, } impl ShellUiFramePacket { pub fn new(commands: Vec) -> Self { Self { commands } } } #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct ComposerFramePacket { pub bound_scene: Option, pub camera: Camera2D, pub sprites: Vec, pub hud: HudPacket, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct BoundScenePacket { pub bank_id: i32, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct Camera2D { pub x: i32, pub y: i32, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct GameSpritePacket { pub glyph_id: i32, pub palette_id: i32, pub x: i32, pub y: i32, pub layer: i32, pub bank_id: i32, pub flip_x: bool, pub flip_y: bool, pub priority: i32, } #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct HudPacket { pub commands: Vec, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum HudCommand { Clear { color: Color }, FillRect { rect: Rect, color: Color }, DrawText { x: i32, y: i32, text: String, color: Color }, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum Gfx2dCommand { Clear { color: Color }, FillRect { rect: Rect, color: Color }, DrawLine { x0: i32, y0: i32, x1: i32, y1: i32, color: Color }, DrawCircle { x: i32, y: i32, radius: i32, color: Color }, DrawDisc { x: i32, y: i32, radius: i32, border_color: Color, fill_color: Color }, DrawSquare { rect: Rect, border_color: Color, fill_color: Color }, DrawText { x: i32, y: i32, text: String, color: Color }, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum GfxUiCommand { Clear { color: Color }, FillRect { rect: Rect, color: Color }, DrawLine { x0: i32, y0: i32, x1: i32, y1: i32, color: Color }, DrawCircle { x: i32, y: i32, radius: i32, color: Color }, DrawDisc { x: i32, y: i32, radius: i32, border_color: Color, fill_color: Color }, DrawSquare { rect: Rect, border_color: Color, fill_color: Color }, DrawText { x: i32, y: i32, text: String, color: Color }, } #[cfg(test)] mod tests { use super::*; #[test] fn constructors_pin_app_mode_to_packet_kind() { let game = RenderSubmission::game2d(FrameId::new(7), Game2DFramePacket::default()); assert_eq!(game.app_mode, AppMode::Game); assert!(matches!(game.packet, RenderSubmissionPacket::Game2D(_))); assert!(game.is_coherent()); let shell = RenderSubmission::shell_ui(FrameId::new(8), ShellUiFramePacket::new(Vec::new())); assert_eq!(shell.app_mode, AppMode::Shell); assert!(matches!(shell.packet, RenderSubmissionPacket::ShellUi(_))); assert!(shell.is_coherent()); } #[test] fn coherence_check_rejects_mismatched_envelope() { let submission = RenderSubmission { frame_id: FrameId::new(1), app_mode: AppMode::Game, ownership: RenderOwnership::new(0, AppMode::Game, 0), packet: RenderSubmissionPacket::ShellUi(ShellUiFramePacket::new(Vec::new())), }; assert!(!submission.is_coherent()); } #[test] fn ownership_can_be_stamped_by_runtime() { let submission = RenderSubmission::game2d(FrameId::new(3), Game2DFramePacket::default()) .with_ownership(RenderOwnership::new(9, AppMode::Game, 42)); assert_eq!(submission.ownership, RenderOwnership::new(9, AppMode::Game, 42)); } #[test] fn closed_submission_owns_command_payload() { let mut commands = vec![Gfx2dCommand::DrawText { x: 1, y: 2, text: "before".to_string(), color: Color::WHITE, }]; let submission = RenderSubmission::game2d( FrameId::new(2), Game2DFramePacket::new(Default::default(), commands.clone()), ); commands.push(Gfx2dCommand::Clear { color: Color::BLACK }); let RenderSubmissionPacket::Game2D(packet) = submission.packet else { panic!("expected game packet"); }; assert_eq!(packet.gfx2d.len(), 1); assert_eq!( packet.gfx2d[0], Gfx2dCommand::DrawText { x: 1, y: 2, text: "before".to_string(), color: Color::WHITE } ); } #[test] fn frame_id_advances_monotonically_with_wrapping_arithmetic() { assert_eq!(FrameId::new(41).next(), FrameId::new(42)); assert_eq!(FrameId::new(u64::MAX).next(), FrameId::ZERO); } }