commit 38e682644052ae4d5897459b8b2495d9519df0a9 Author: gered Date: Sun May 15 12:11:38 2022 -0400 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..16d5636 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +Cargo.lock +.DS_Store diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8e8530e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,5 @@ +[workspace] +members = [ + "libretrogd", + "examples/*", +] diff --git a/examples/.keep b/examples/.keep new file mode 100644 index 0000000..e69de29 diff --git a/libretrogd/Cargo.toml b/libretrogd/Cargo.toml new file mode 100644 index 0000000..8ecc1ed --- /dev/null +++ b/libretrogd/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "libretrogd" +description = "A 'retro'-like game development library, for funsies." +version = "0.1.0" +authors = ["Gered King "] +edition = "2021" + +[features] +low_res = [] +wide = [] + +[dependencies] +sdl2 = { version = "0.34.5", features = ["static-link", "bundled", "unsafe_textures" ] } +byte-slice-cast = "1.2.1" +byteorder = "1.4.3" +thiserror = "1.0.30" +rand = "0.8.5" +num-traits = "0.2.14" + +[dev-dependencies] +claim = "0.5.0" +criterion = "0.3.5" +anyhow = "1.0.55" +tempfile = "3.3.0" + +[[bench]] +name = "bitmap" +harness = false + +[[bench]] +name = "blit" +harness = false diff --git a/libretrogd/assets/vga.fnt b/libretrogd/assets/vga.fnt new file mode 100644 index 0000000..b32b4e5 Binary files /dev/null and b/libretrogd/assets/vga.fnt differ diff --git a/libretrogd/assets/vga.pal b/libretrogd/assets/vga.pal new file mode 100644 index 0000000..8bbb15f Binary files /dev/null and b/libretrogd/assets/vga.pal differ diff --git a/libretrogd/benches/bitmap.rs b/libretrogd/benches/bitmap.rs new file mode 100644 index 0000000..2194052 --- /dev/null +++ b/libretrogd/benches/bitmap.rs @@ -0,0 +1,28 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +use libretrogd::Bitmap; +use libretrogd::Palette; +use libretrogd::{SCREEN_HEIGHT, SCREEN_WIDTH}; + +pub fn criterion_benchmark(c: &mut Criterion) { + let mut source = Bitmap::new(SCREEN_WIDTH, SCREEN_HEIGHT).unwrap(); + let mut dest = vec![0u32; (SCREEN_WIDTH * SCREEN_HEIGHT * 4) as usize].into_boxed_slice(); + let palette = Palette::new_vga_palette().unwrap(); + + c.bench_function("deindex_bitmap_pixels", |b| { + b.iter(|| source.copy_as_argb_to(&mut dest, &palette)) + }); + + c.bench_function("set_pixel", |b| { + b.iter(|| source.set_pixel(black_box(100), black_box(100), black_box(42))) + }); + + c.bench_function("set_pixel_unchecked", |b| { + b.iter(|| unsafe { + source.set_pixel_unchecked(black_box(100), black_box(100), black_box(42)) + }) + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/libretrogd/benches/blit.rs b/libretrogd/benches/blit.rs new file mode 100644 index 0000000..63f74e0 --- /dev/null +++ b/libretrogd/benches/blit.rs @@ -0,0 +1,62 @@ +use std::path::Path; + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +use libretrogd::{Bitmap, BlitMethod, Rect}; + +pub fn criterion_benchmark(c: &mut Criterion) { + let mut framebuffer = Bitmap::new(320, 240).unwrap(); + let (bmp, _) = Bitmap::load_iff_file(Path::new("./test-assets/test-tiles.lbm")).unwrap(); + + let mut solid_bmp = Bitmap::new(16, 16).unwrap(); + solid_bmp.blit_region(BlitMethod::Solid, &bmp, &Rect::new(16, 16, 16, 16), 0, 0); + let mut trans_bmp = Bitmap::new(16, 16).unwrap(); + trans_bmp.blit_region(BlitMethod::Solid, &bmp, &Rect::new(160, 0, 16, 16), 0, 0); + + c.bench_function("blit_single_checked_solid", |b| { + b.iter(|| { + framebuffer.blit( + black_box(BlitMethod::Solid), + black_box(&solid_bmp), + black_box(100), + black_box(100), + ) + }) + }); + + c.bench_function("blit_single_unchecked_solid", |b| { + b.iter(|| unsafe { + framebuffer.blit_unchecked( + black_box(BlitMethod::Solid), + black_box(&solid_bmp), + black_box(100), + black_box(100), + ) + }) + }); + + c.bench_function("blit_single_checked_transparent", |b| { + b.iter(|| { + framebuffer.blit( + black_box(BlitMethod::Transparent(0)), + black_box(&trans_bmp), + black_box(100), + black_box(100), + ) + }) + }); + + c.bench_function("blit_single_unchecked_transparent", |b| { + b.iter(|| unsafe { + framebuffer.blit_unchecked( + black_box(BlitMethod::Transparent(0)), + black_box(&trans_bmp), + black_box(100), + black_box(100), + ) + }) + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/libretrogd/src/entities/mod.rs b/libretrogd/src/entities/mod.rs new file mode 100644 index 0000000..90ba3f8 --- /dev/null +++ b/libretrogd/src/entities/mod.rs @@ -0,0 +1,703 @@ +use std::any::{TypeId}; +use std::cell::{Ref, RefCell, RefMut}; +use std::collections::{HashMap, HashSet}; + +use crate::utils::AsAny; + +pub type EntityId = usize; + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +// alias `Component` to always be `'static` ... +pub trait Component: 'static {} +impl Component for T {} + +pub type ComponentStore = RefCell>; +pub type RefComponents<'a, T> = Ref<'a, HashMap>; +pub type RefMutComponents<'a, T> = RefMut<'a, HashMap>; + +pub trait GenericComponentStore: AsAny { + fn has(&self, entity: EntityId) -> bool; + fn remove(&mut self, entity: EntityId) -> bool; + fn clear(&mut self); +} + +impl GenericComponentStore for ComponentStore { + #[inline] + fn has(&self, entity: EntityId) -> bool { + self.borrow().contains_key(&entity) + } + + #[inline] + fn remove(&mut self, entity: EntityId) -> bool { + self.get_mut().remove(&entity).is_some() + } + + #[inline] + fn clear(&mut self) { + self.get_mut().clear(); + } +} + +#[inline] +pub fn as_component_store( + collection: &dyn GenericComponentStore, +) -> &ComponentStore { + collection.as_any().downcast_ref().unwrap() +} + +#[inline] +pub fn as_component_store_mut( + collection: &mut dyn GenericComponentStore, +) -> &mut ComponentStore { + collection.as_any_mut().downcast_mut().unwrap() +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +pub struct Entities { + entities: HashSet, + component_stores: HashMap>, + next_id: EntityId, +} + +impl Entities { + pub fn new() -> Self { + Entities { + entities: HashSet::new(), + component_stores: HashMap::new(), + next_id: 0, + } + } + + #[inline] + fn has_component_store(&self) -> bool { + let type_id = TypeId::of::(); + self.component_stores.contains_key(&type_id) + } + + fn get_component_store(&self) -> Option<&ComponentStore> { + if !self.has_component_store::() { + None + } else { + let type_id = TypeId::of::(); + Some(as_component_store( + self.component_stores.get(&type_id).unwrap().as_ref(), + )) + } + } + + fn add_component_store(&mut self) -> &ComponentStore { + if self.has_component_store::() { + self.get_component_store().unwrap() + } else { + let component_store = ComponentStore::::new(HashMap::new()); + let type_id = TypeId::of::(); + self.component_stores + .insert(type_id, Box::new(component_store)); + as_component_store(self.component_stores.get(&type_id).unwrap().as_ref()) + } + } + + #[inline] + pub fn has_entity(&self, entity: EntityId) -> bool { + self.entities.contains(&entity) + } + + pub fn new_entity(&mut self) -> EntityId { + let new_entity_id = self.next_id; + self.next_id = self.next_id.wrapping_add(1); + self.entities.insert(new_entity_id); + new_entity_id + } + + pub fn remove_entity(&mut self, entity: EntityId) -> bool { + if !self.has_entity(entity) { + return false; + } + + self.entities.remove(&entity); + for (_, component_store) in self.component_stores.iter_mut() { + component_store.remove(entity); + } + true + } + + pub fn remove_all_entities(&mut self) { + self.entities.clear(); + for (_, component_store) in self.component_stores.iter_mut() { + component_store.clear(); + } + } + + pub fn has_component(&self, entity: EntityId) -> bool { + if !self.has_entity(entity) { + false + } else { + let type_id = TypeId::of::(); + if let Some(component_store) = self.component_stores.get(&type_id) { + component_store.has(entity) + } else { + false + } + } + } + + pub fn add_component(&mut self, entity: EntityId, component: T) -> bool { + if !self.has_entity(entity) { + false + } else { + if let Some(component_store) = self.get_component_store::() { + component_store.borrow_mut().insert(entity, component); + } else { + self.add_component_store::() + .borrow_mut() + .insert(entity, component); + } + true + } + } + + pub fn remove_component(&mut self, entity: EntityId) -> bool { + if !self.has_entity(entity) { + false + } else { + let type_id = TypeId::of::(); + if let Some(component_store) = self.component_stores.get_mut(&type_id) { + component_store.remove(entity) + } else { + false + } + } + } + + #[inline] + pub fn components(&self) -> Option> { + if let Some(component_store) = self.get_component_store() { + Some(component_store.borrow()) + } else { + None + } + } + + #[inline] + pub fn components_mut(&self) -> Option> { + if let Some(component_store) = self.get_component_store() { + Some(component_store.borrow_mut()) + } else { + None + } + } + + pub fn init_components(&mut self) { + if self.get_component_store::().is_none() { + self.add_component_store::(); + } + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +// TODO: is there some fancy way to get rid of the impl duplication here ... ? + +pub trait ComponentStoreConvenience { + fn single(&self) -> Option<(&EntityId, &T)>; + fn get(&self, k: &EntityId) -> Option<&T>; +} + +pub trait ComponentStoreConvenienceMut { + fn single_mut(&mut self) -> Option<(&EntityId, &T)>; + fn get_mut(&mut self, k: &EntityId) -> Option<&mut T>; +} + +impl<'a, T: Component> ComponentStoreConvenience for Option> { + fn single(&self) -> Option<(&EntityId, &T)> { + if let Some(components) = self { + if let Some((entity_id, component)) = components.iter().next() { + return Some((entity_id, component)); + } + } + None + } + + fn get(&self, k: &EntityId) -> Option<&T> { + if let Some(components) = self { + components.get(k) + } else { + None + } + } +} + +impl<'a, T: Component> ComponentStoreConvenience for Option> { + fn single(&self) -> Option<(&EntityId, &T)> { + if let Some(components) = self { + if let Some((entity_id, component)) = components.iter().next() { + return Some((entity_id, component)); + } + } + None + } + + fn get(&self, k: &EntityId) -> Option<&T> { + if let Some(components) = self { + components.get(k) + } else { + None + } + } +} + +impl<'a, T: Component> ComponentStoreConvenienceMut for Option> { + fn single_mut(&mut self) -> Option<(&EntityId, &T)> { + if let Some(components) = self { + if let Some((entity_id, component)) = components.iter_mut().next() { + return Some((entity_id, component)); + } + } + None + } + + fn get_mut(&mut self, k: &EntityId) -> Option<&mut T> { + if let Some(components) = self { + components.get_mut(k) + } else { + None + } + } +} + +pub trait OptionComponentStore { + fn len(&self) -> usize; + fn is_empty(&self) -> bool; +} + +impl<'a, T: Component> OptionComponentStore for Option> { + fn len(&self) -> usize { + if let Some(components) = self { + components.len() + } else { + 0 + } + } + + fn is_empty(&self) -> bool { + if let Some(components) = self { + components.is_empty() + } else { + true + } + } +} + +impl<'a, T: Component> OptionComponentStore for Option> { + fn len(&self) -> usize { + if let Some(components) = self { + components.len() + } else { + 0 + } + } + + fn is_empty(&self) -> bool { + if let Some(components) = self { + components.is_empty() + } else { + true + } + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +pub type UpdateFn = fn(&mut Entities, &mut T); +pub type RenderFn = fn(&mut Entities, &mut T); + +pub struct ComponentSystems { + update_systems: Vec>, + render_systems: Vec>, +} + +impl ComponentSystems { + pub fn new() -> Self { + ComponentSystems { + update_systems: Vec::new(), + render_systems: Vec::new(), + } + } + + pub fn add_update_system(&mut self, f: UpdateFn) { + self.update_systems.push(f); + } + + pub fn add_render_system(&mut self, f: RenderFn) { + self.render_systems.push(f); + } + + pub fn reset(&mut self) { + self.update_systems.clear(); + self.render_systems.clear(); + } + + pub fn update(&mut self, entities: &mut Entities, context: &mut U) { + for f in self.update_systems.iter_mut() { + f(entities, context); + } + } + + pub fn render(&mut self, entities: &mut Entities, context: &mut R) { + for f in self.render_systems.iter_mut() { + f(entities, context); + } + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#[cfg(test)] +mod tests { + use claim::*; + + use super::*; + + #[derive(Debug, Eq, PartialEq, Hash, Clone)] + struct Name(&'static str); + #[derive(Debug, Eq, PartialEq, Hash, Clone)] + struct Position(i32, i32); + #[derive(Debug, Eq, PartialEq, Hash, Clone)] + struct Velocity(i32, i32); + #[derive(Debug, Eq, PartialEq, Hash, Clone)] + struct Health(u32); + #[derive(Debug, Eq, PartialEq, Hash, Clone)] + struct Counter(u32); + + #[test] + fn add_and_remove_entities() { + let mut em = Entities::new(); + + // add first entity + assert!(!em.has_entity(1)); + let a = em.new_entity(); + assert!(em.has_entity(a)); + + // add second entity with totally different id from first entity + let b = em.new_entity(); + assert!(em.has_entity(a)); + assert!(em.has_entity(b)); + assert_ne!(a, b); + + // remove first entity + assert!(em.remove_entity(a)); + assert!(!em.has_entity(a)); + assert!(em.has_entity(b)); + + // remove second entity + assert!(em.remove_entity(b)); + assert!(!em.has_entity(b)); + + // removing entities which don't exist shouldn't blow up + assert!(!em.remove_entity(a)); + assert!(!em.remove_entity(b)); + + // add third entity, will not re-use previous entity ids + let c = em.new_entity(); + assert!(em.has_entity(c)); + assert_ne!(a, c); + assert_ne!(b, c); + } + + #[test] + fn add_and_remove_entity_components() { + let mut em = Entities::new(); + + // create first entity + let a = em.new_entity(); + assert!(em.has_entity(a)); + + // add component + assert!(!em.has_component::(a)); + assert!(em.add_component(a, Name("Someone"))); + assert!(em.has_component::(a)); + + // verify the added component + { + let names = em.components::().unwrap(); + let name_a = names.get(&a).unwrap(); + assert_eq!("Someone", name_a.0); + } + + // create second entity + let b = em.new_entity(); + assert!(em.has_entity(b)); + + // add component to second entity + assert!(!em.has_component::(b)); + assert_none!(em.components::()); + assert!(em.add_component(b, Position(1, 2))); + assert!(em.has_component::(b)); + + // verify the added component + { + let positions = em.components::().unwrap(); + let position_b = positions.get(&b).unwrap(); + assert!(1 == position_b.0 && 2 == position_b.1); + } + + // verify current components for both entities are what we expect + assert!(em.has_component::(a)); + assert!(!em.has_component::(b)); + assert!(!em.has_component::(a)); + assert!(em.has_component::(b)); + + // add new component to first entity + assert!(em.add_component(a, Position(5, 3))); + assert!(em.has_component::(a)); + + // verify both position components for both entities + { + let positions = em.components::().unwrap(); + let position_a = positions.get(&a).unwrap(); + assert!(5 == position_a.0 && 3 == position_a.1); + let position_b = positions.get(&b).unwrap(); + assert!(1 == position_b.0 && 2 == position_b.1); + } + + // verify current components for both entities are what we expect + assert!(em.has_component::(a)); + assert!(!em.has_component::(b)); + assert!(em.has_component::(a)); + assert!(em.has_component::(b)); + + // remove position component from first entity + assert!(em.remove_component::(a)); + assert!(!em.has_component::(a)); + + // verify current components for both entities are what we expect + assert!(em.has_component::(a)); + assert!(!em.has_component::(b)); + assert!(!em.has_component::(a)); + assert!(em.has_component::(b)); + { + let positions = em.components::().unwrap(); + let position_b = positions.get(&b).unwrap(); + assert!(1 == position_b.0 && 2 == position_b.1); + } + } + + #[test] + fn modify_components() { + let mut em = Entities::new(); + + // create entities + let a = em.new_entity(); + em.add_component(a, Position(10, 20)); + let b = em.new_entity(); + em.add_component(b, Position(17, 5)); + + // change entity positions + { + let mut positions = em.components_mut::().unwrap(); + let position_a = positions.get_mut(&a).unwrap(); + assert_eq!(Position(10, 20), *position_a); + position_a.0 = 13; + position_a.1 = 23; + } + + { + let mut positions = em.components_mut::().unwrap(); + let position_b = positions.get_mut(&b).unwrap(); + assert_eq!(Position(17, 5), *position_b); + position_b.0 = 15; + position_b.1 = 8; + } + + // verify both entity position components + { + let positions = em.components::().unwrap(); + let position_a = positions.get(&a).unwrap(); + assert_eq!(Position(13, 23), *position_a); + let position_b = positions.get(&b).unwrap(); + assert_eq!(Position(15, 8), *position_b); + } + } + + #[test] + fn get_all_components_of_type() { + let mut em = Entities::new(); + + // create entities + let a = em.new_entity(); + em.add_component(a, Health(20)); + em.add_component(a, Position(10, 20)); + let b = em.new_entity(); + em.add_component(b, Health(30)); + em.add_component(b, Position(17, 5)); + + // verify initial entity positions + { + let positions = em.components::().unwrap(); + assert_eq!(2, positions.len()); + let positions: HashSet<&Position> = positions.values().collect(); + assert!(positions.contains(&Position(10, 20))); + assert!(positions.contains(&Position(17, 5))); + } + + // modify position components + { + let mut positions = em.components_mut::().unwrap(); + for mut component in positions.values_mut() { + component.0 += 5; + } + + assert_eq!(Position(15, 20), *positions.get(&a).unwrap()); + assert_eq!(Position(22, 5), *positions.get(&b).unwrap()); + } + + // modify health components + { + let mut healths = em.components_mut::().unwrap(); + for mut component in healths.values_mut() { + component.0 += 5; + } + assert_eq!(Health(25), *healths.get(&a).unwrap()); + assert_eq!(Health(35), *healths.get(&b).unwrap()); + } + } + + #[test] + fn get_all_entities_with_component() { + let mut em = Entities::new(); + + // create entities + let a = em.new_entity(); + em.add_component(a, Name("A")); + em.add_component(a, Health(20)); + em.add_component(a, Position(10, 20)); + let b = em.new_entity(); + em.add_component(b, Name("B")); + em.add_component(b, Position(17, 5)); + + // get entities with position components + { + let positions = em.components::().unwrap(); + let entities = positions.keys(); + assert_eq!(2, entities.len()); + let entities: HashSet<&EntityId> = entities.collect(); + assert!(entities.contains(&a)); + assert!(entities.contains(&b)); + } + + // + let names = em.components::().unwrap(); + for (entity, name) in names.iter() { + // just written this way to verify can grab two mutable components at once + // (since this wouldn't be an uncommon way to want to work with an entity) + let mut healths = em.components_mut::().unwrap(); + let mut positions = em.components_mut::().unwrap(); + + let health = healths.get_mut(&entity); + let position = positions.get_mut(&entity); + + println!( + "entity {}, health: {:?}, position: {:?}", + name.0, health, position + ); + + if let Some(mut health) = health { + health.0 += 5; + } + if let Some(mut position) = position { + position.0 += 5; + } + } + + let positions = em.components::().unwrap(); + assert_eq!(Position(15, 20), *positions.get(&a).unwrap()); + assert_eq!(Position(22, 5), *positions.get(&b).unwrap()); + let healths = em.components::().unwrap(); + assert_eq!(Health(25), *healths.get(&a).unwrap()); + assert!(healths.get(&b).is_none()); + } + + struct UpdateContext(f32); + struct RenderContext(f32); + + fn system_print_entity_positions(entities: &mut Entities, _context: &mut RenderContext) { + let positions = entities.components::().unwrap(); + for (entity, position) in positions.iter() { + println!("entity {} at x:{}, y:{}", entity, position.0, position.1) + } + } + + fn system_move_entities_forward(entities: &mut Entities, _context: &mut UpdateContext) { + let mut positions = entities.components_mut::().unwrap(); + let velocities = entities.components::().unwrap(); + for (entity, position) in positions.iter_mut() { + if let Some(velocity) = velocities.get(&entity) { + position.0 += velocity.0; + position.1 += velocity.1; + } + } + } + + fn system_increment_counter(entities: &mut Entities, _context: &mut UpdateContext) { + let mut counters = entities.components_mut::().unwrap(); + for (_entity, counter) in counters.iter_mut() { + counter.0 += 1; + } + } + + fn system_print_counter(entities: &mut Entities, _context: &mut RenderContext) { + let counters = entities.components::().unwrap(); + for (entity, counter) in counters.iter() { + println!("entity {} has counter {}", entity, counter.0); + } + } + + #[test] + pub fn component_systems() { + let mut em = Entities::new(); + + // create entities + let a = em.new_entity(); + em.add_component(a, Position(5, 6)); + em.add_component(a, Velocity(1, 1)); + em.add_component(a, Counter(0)); + let b = em.new_entity(); + em.add_component(b, Position(-3, 0)); + em.add_component(b, Velocity(1, 0)); + em.add_component(b, Counter(0)); + let c = em.new_entity(); + em.add_component(c, Position(2, 9)); + em.add_component(c, Counter(0)); + + // setup component systems + let mut cs = ComponentSystems::new(); + cs.add_update_system(system_move_entities_forward); + cs.add_update_system(system_increment_counter); + cs.add_render_system(system_print_entity_positions); + cs.add_render_system(system_print_counter); + + // run some update+render iterations + for _ in 0..5 { + let mut update_context = UpdateContext(0.0); + let mut render_context = RenderContext(0.0); + cs.update(&mut em, &mut update_context); + cs.render(&mut em, &mut render_context); + } + + // verify expected entity positions + let positions = em.components::().unwrap(); + let velocities = em.components::().unwrap(); + let counters = em.components::().unwrap(); + assert_eq!(Position(10, 11), *positions.get(&a).unwrap()); + assert_eq!(Velocity(1, 1), *velocities.get(&a).unwrap()); + assert_eq!(Counter(5), *counters.get(&a).unwrap()); + assert_eq!(Position(2, 0), *positions.get(&b).unwrap()); + assert_eq!(Velocity(1, 0), *velocities.get(&b).unwrap()); + assert_eq!(Counter(5), *counters.get(&b).unwrap()); + assert_eq!(Position(2, 9), *positions.get(&c).unwrap()); + assert_eq!(None, velocities.get(&c)); + assert_eq!(Counter(5), *counters.get(&c).unwrap()); + } +} diff --git a/libretrogd/src/events/mod.rs b/libretrogd/src/events/mod.rs new file mode 100644 index 0000000..a494e31 --- /dev/null +++ b/libretrogd/src/events/mod.rs @@ -0,0 +1,291 @@ +use std::collections::VecDeque; + +pub type ListenerFn = fn(event: &EventType, &mut ContextType) -> bool; + +pub struct EventPublisher { + queue: VecDeque, +} + +impl EventPublisher { + pub fn new() -> Self { + EventPublisher { + queue: VecDeque::new(), + } + } + + #[inline] + pub fn len(&self) -> usize { + self.queue.len() + } + + #[inline] + pub fn clear(&mut self) { + self.queue.clear(); + } + + #[inline] + pub fn queue(&mut self, event: EventType) { + self.queue.push_back(event); + } + + pub fn take_queue(&mut self, destination: &mut VecDeque) { + destination.clear(); + destination.append(&mut self.queue); + self.clear(); + } +} + +pub struct EventListeners { + listeners: Vec>, + dispatch_queue: VecDeque, +} + +impl EventListeners { + pub fn new() -> Self { + EventListeners { + listeners: Vec::new(), + dispatch_queue: VecDeque::new(), + } + } + + pub fn len(&self) -> usize { + self.listeners.len() + } + + pub fn clear(&mut self) { + self.listeners.clear(); + } + + pub fn add(&mut self, listener: ListenerFn) -> bool { + // HACK?: most advice i've seen right now for comparing function pointers suggests doing + // this, but i've also come across comments suggesting there are times where this + // might not be foolproof? (e.g. where generics or lifetimes come into play ... ) + if self.listeners.iter().any(|&l| l as usize == listener as usize) { + false // don't add a duplicate listener + } else { + self.listeners.push(listener); + true + } + } + + pub fn remove(&mut self, listener: ListenerFn) -> bool { + let before_size = self.listeners.len(); + // HACK?: comparing function pointers -- see above "HACK?" comment. same concern here. + self.listeners.retain(|&l| l as usize != listener as usize); + // return true if the listener was removed + return before_size != self.listeners.len() + } + + pub fn take_queue_from(&mut self, publisher: &mut EventPublisher) -> usize { + publisher.take_queue(&mut self.dispatch_queue); + self.dispatch_queue.len() + } + + pub fn dispatch_queue(&mut self, context: &mut ContextType) { + while let Some(event) = self.dispatch_queue.pop_front() { + for listener in &self.listeners { + if listener(&event, context) { + break; + } + } + } + } + +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Debug, Eq, PartialEq, Copy, Clone)] + enum TestEvent { + Dummy, + Foobar(i32), + Message(&'static str), + } + + struct DummyContext; + + struct TestContext { + pub count: i32, + pub events: Vec, + } + + impl TestContext { + pub fn new() -> Self { + TestContext { count: 0, events: Vec::new() } + } + } + + fn dummy_listener(_event: &TestEvent, _context: &mut DummyContext) -> bool { + false + } + + fn other_dummy_listener(_event: &TestEvent, _context: &mut DummyContext) -> bool { + false + } + + fn event_logger(event: &TestEvent, context: &mut TestContext) -> bool { + context.events.push(*event); + false + } + + fn event_counter(_event: &TestEvent, context: &mut TestContext) -> bool { + context.count += 1; + false + } + + fn message_filter(event: &TestEvent, _context: &mut TestContext) -> bool { + match event { + TestEvent::Message(s) => { + if *s == "filter" { + true // means event was handled, and no subsequent listeners should be called + } else { + false + } + }, + _ => false + } + } + + #[test] + pub fn adding_and_removing_listeners() { + let mut listeners = EventListeners::::new(); + + // add and remove + assert_eq!(0, listeners.len()); + assert!(listeners.add(dummy_listener)); + assert_eq!(1, listeners.len()); + assert!(!listeners.add(dummy_listener)); + assert_eq!(1, listeners.len()); + assert!(listeners.remove(dummy_listener)); + assert_eq!(0, listeners.len()); + assert!(!listeners.remove(dummy_listener)); + assert_eq!(0, listeners.len()); + + // add and remove multiple + assert!(listeners.add(dummy_listener)); + assert_eq!(1, listeners.len()); + assert!(listeners.add(other_dummy_listener)); + assert_eq!(2, listeners.len()); + assert!(listeners.remove(dummy_listener)); + assert_eq!(1, listeners.len()); + assert!(!listeners.remove(dummy_listener)); + assert_eq!(1, listeners.len()); + assert!(listeners.remove(other_dummy_listener)); + assert_eq!(0, listeners.len()); + + // clear all + assert!(listeners.add(dummy_listener)); + assert!(listeners.add(other_dummy_listener)); + assert_eq!(2, listeners.len()); + listeners.clear(); + assert_eq!(0, listeners.len()); + } + + #[test] + pub fn queueing_events() { + use TestEvent::*; + + let mut publisher = EventPublisher::::new(); + assert_eq!(0, publisher.len()); + publisher.queue(Dummy); + assert_eq!(1, publisher.len()); + publisher.queue(Foobar(1)); + assert_eq!(2, publisher.len()); + publisher.queue(Foobar(2)); + assert_eq!(3, publisher.len()); + + let mut queue = VecDeque::::new(); + publisher.take_queue(&mut queue); + assert_eq!(0, publisher.len()); + assert_eq!(Dummy, queue.pop_front().unwrap()); + assert_eq!(Foobar(1), queue.pop_front().unwrap()); + assert_eq!(Foobar(2), queue.pop_front().unwrap()); + assert!(queue.pop_front().is_none()); + + publisher.queue(Dummy); + assert_eq!(1, publisher.len()); + publisher.clear(); + assert_eq!(0, publisher.len()); + let mut queue = VecDeque::::new(); + publisher.take_queue(&mut queue); + assert_eq!(0, publisher.len()); + assert_eq!(0, queue.len()); + } + + #[test] + pub fn listeners_receive_events() { + use TestEvent::*; + + let mut listeners = EventListeners::::new(); + assert!(listeners.add(event_logger)); + + let mut publisher = EventPublisher::::new(); + publisher.queue(Dummy); + publisher.queue(Foobar(1)); + publisher.queue(Dummy); + publisher.queue(Foobar(42)); + assert_eq!(4, listeners.take_queue_from(&mut publisher)); + + let mut context = TestContext::new(); + assert!(context.events.is_empty()); + assert_eq!(0, context.count); + listeners.dispatch_queue(&mut context); + assert!(!context.events.is_empty()); + assert_eq!(0, context.count); + assert_eq!( + vec![Dummy, Foobar(1), Dummy, Foobar(42)], + context.events + ); + + let mut context = TestContext::new(); + assert!(context.events.is_empty()); + assert_eq!(0, context.count); + listeners.dispatch_queue(&mut context); + assert!(context.events.is_empty()); + + assert!(listeners.add(event_counter)); + publisher.queue(Foobar(10)); + publisher.queue(Foobar(20)); + publisher.queue(Dummy); + listeners.take_queue_from(&mut publisher); + let mut context = TestContext::new(); + listeners.dispatch_queue(&mut context); + assert!(!context.events.is_empty()); + assert_eq!(3, context.count); + assert_eq!( + vec![Foobar(10), Foobar(20), Dummy], + context.events + ); + } + + #[test] + pub fn listener_filtering() { + use TestEvent::*; + + let mut listeners = EventListeners::::new(); + assert!(listeners.add(message_filter)); + assert!(listeners.add(event_logger)); + assert!(listeners.add(event_counter)); + + let mut publisher = EventPublisher::::new(); + publisher.queue(Message("hello")); + publisher.queue(Dummy); + publisher.queue(Message("filter")); + publisher.queue(Foobar(3)); + assert_eq!(4, listeners.take_queue_from(&mut publisher)); + + let mut context = TestContext::new(); + assert!(context.events.is_empty()); + assert_eq!(0, context.count); + listeners.dispatch_queue(&mut context); + assert!(!context.events.is_empty()); + assert_eq!(3, context.count); + assert_eq!( + vec![Message("hello"), Dummy, Foobar(3)], + context.events + ); + + } +} diff --git a/libretrogd/src/graphics/bitmap/blit.rs b/libretrogd/src/graphics/bitmap/blit.rs new file mode 100644 index 0000000..63e696c --- /dev/null +++ b/libretrogd/src/graphics/bitmap/blit.rs @@ -0,0 +1,342 @@ +use crate::{Bitmap, Rect}; + +pub enum BlitMethod { + Solid, + Transparent(u8), +} + +/// Clips the region for a source bitmap to be used in a subsequent blit operation. The source +/// region will be clipped against the clipping region given for the destination bitmap. The +/// top-left coordinates of the location to blit to on the destination bitmap are also adjusted +/// only if necessary based on the clipping performed. +/// +/// # Arguments +/// +/// * `dest_clip_region`: the clipping region for the destination bitmap +/// * `src_blit_region`: the region on the source bitmap that is to be blitted, which may be +/// clipped if necessary to at least partially fit into the destination clipping region given +/// * `dest_x`: the x (left) coordinate of the location on the destination bitmap to blit the +/// source to, which may be adjusted as necessary during clipping +/// * `dest_y`: the y (top) coordinate of the location on the destination bitmap to blit the source +/// to, which may be adjusted as necessary during clipping +/// +/// returns: true if the results of the clip is partially or entirely visible on the destination +/// bitmap, or false if the blit is entirely outside of the destination bitmap (and so no blit +/// needs to occur) +pub fn clip_blit( + dest_clip_region: &Rect, + src_blit_region: &mut Rect, + dest_x: &mut i32, + dest_y: &mut i32, +) -> bool { + // off the left edge? + if *dest_x < dest_clip_region.x { + // completely off the left edge? + if (*dest_x + src_blit_region.width as i32 - 1) < dest_clip_region.x { + return false; + } + + let offset = dest_clip_region.x - *dest_x; + src_blit_region.x += offset; + src_blit_region.width = (src_blit_region.width as i32 - offset) as u32; + *dest_x = dest_clip_region.x; + } + + // off the right edge? + if *dest_x > dest_clip_region.width as i32 - src_blit_region.width as i32 { + // completely off the right edge? + if *dest_x > dest_clip_region.right() { + return false; + } + + let offset = *dest_x + src_blit_region.width as i32 - dest_clip_region.width as i32; + src_blit_region.width = (src_blit_region.width as i32 - offset) as u32; + } + + // off the top edge? + if *dest_y < dest_clip_region.y { + // completely off the top edge? + if (*dest_y + src_blit_region.height as i32 - 1) < dest_clip_region.y { + return false; + } + + let offset = dest_clip_region.y - *dest_y; + src_blit_region.y += offset; + src_blit_region.height = (src_blit_region.height as i32 - offset) as u32; + *dest_y = dest_clip_region.y; + } + + // off the bottom edge? + if *dest_y > dest_clip_region.height as i32 - src_blit_region.height as i32 { + // completely off the bottom edge? + if *dest_y > dest_clip_region.bottom() { + return false; + } + + let offset = *dest_y + src_blit_region.height as i32 - dest_clip_region.height as i32; + src_blit_region.height = (src_blit_region.height as i32 - offset) as u32; + } + + true +} + +impl Bitmap { + pub unsafe fn solid_blit(&mut self, src: &Bitmap, src_region: &Rect, dest_x: i32, dest_y: i32) { + let src_row_length = src_region.width as usize; + let src_pitch = src.width as usize; + let dest_pitch = self.width as usize; + let mut src_pixels = src.pixels_at_ptr_unchecked(src_region.x, src_region.y); + let mut dest_pixels = self.pixels_at_mut_ptr_unchecked(dest_x, dest_y); + + for _ in 0..src_region.height { + dest_pixels.copy_from(src_pixels, src_row_length); + src_pixels = src_pixels.add(src_pitch); + dest_pixels = dest_pixels.add(dest_pitch); + } + } + + pub unsafe fn transparent_blit( + &mut self, + src: &Bitmap, + src_region: &Rect, + dest_x: i32, + dest_y: i32, + transparent_color: u8, + ) { + let src_next_row_inc = (src.width - src_region.width) as usize; + let dest_next_row_inc = (self.width - src_region.width) as usize; + let mut src_pixels = src.pixels_at_ptr_unchecked(src_region.x, src_region.y); + let mut dest_pixels = self.pixels_at_mut_ptr_unchecked(dest_x, dest_y); + + for _ in 0..src_region.height { + for _ in 0..src_region.width { + let pixel = *src_pixels; + if pixel != transparent_color { + *dest_pixels = pixel; + } + + src_pixels = src_pixels.add(1); + dest_pixels = dest_pixels.add(1); + } + + src_pixels = src_pixels.add(src_next_row_inc); + dest_pixels = dest_pixels.add(dest_next_row_inc); + } + } + + pub fn blit_region( + &mut self, + method: BlitMethod, + src: &Bitmap, + src_region: &Rect, + mut dest_x: i32, + mut dest_y: i32, + ) { + let mut src_region = *src_region; + if !clip_blit( + self.clip_region(), + &mut src_region, + &mut dest_x, + &mut dest_y, + ) { + return; + } + + unsafe { + self.blit_region_unchecked(method, src, &src_region, dest_x, dest_y); + }; + } + + #[inline] + pub unsafe fn blit_region_unchecked( + &mut self, + method: BlitMethod, + src: &Bitmap, + src_region: &Rect, + dest_x: i32, + dest_y: i32, + ) { + use BlitMethod::*; + match method { + Solid => self.solid_blit(src, src_region, dest_x, dest_y), + Transparent(transparent_color) => { + self.transparent_blit(src, src_region, dest_x, dest_y, transparent_color) + } + } + } + + #[inline] + pub fn blit(&mut self, method: BlitMethod, src: &Bitmap, x: i32, y: i32) { + let src_region = Rect::new(0, 0, src.width, src.height); + self.blit_region(method, src, &src_region, x, y); + } + + #[inline] + pub unsafe fn blit_unchecked(&mut self, method: BlitMethod, src: &Bitmap, x: i32, y: i32) { + let src_region = Rect::new(0, 0, src.width, src.height); + self.blit_region_unchecked(method, src, &src_region, x, y); + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + #[test] + pub fn clip_blit_regions() { + let dest = Rect::new(0, 0, 320, 240); + + let mut src: Rect; + let mut x: i32; + let mut y: i32; + + src = Rect::new(0, 0, 16, 16); + x = 10; + y = 10; + assert!(clip_blit(&dest, &mut src, &mut x, &mut y)); + assert_eq!(src, Rect::new(0, 0, 16, 16)); + assert_eq!(10, x); + assert_eq!(10, y); + + // left edge + + src = Rect::new(0, 0, 16, 16); + x = 0; + y = 10; + assert!(clip_blit(&dest, &mut src, &mut x, &mut y)); + assert_eq!(src, Rect::new(0, 0, 16, 16)); + assert_eq!(0, x); + assert_eq!(10, y); + + src = Rect::new(0, 0, 16, 16); + x = -5; + y = 10; + assert!(clip_blit(&dest, &mut src, &mut x, &mut y)); + assert_eq!(src, Rect::new(5, 0, 11, 16)); + assert_eq!(0, x); + assert_eq!(10, y); + + src = Rect::new(0, 0, 16, 16); + x = -16; + y = 10; + assert!(!clip_blit(&dest, &mut src, &mut x, &mut y)); + + // right edge + + src = Rect::new(0, 0, 16, 16); + x = 304; + y = 10; + assert!(clip_blit(&dest, &mut src, &mut x, &mut y)); + assert_eq!(src, Rect::new(0, 0, 16, 16)); + assert_eq!(304, x); + assert_eq!(10, y); + + src = Rect::new(0, 0, 16, 16); + x = 310; + y = 10; + assert!(clip_blit(&dest, &mut src, &mut x, &mut y)); + assert_eq!(src, Rect::new(0, 0, 10, 16)); + assert_eq!(310, x); + assert_eq!(10, y); + + src = Rect::new(0, 0, 16, 16); + x = 320; + y = 10; + assert!(!clip_blit(&dest, &mut src, &mut x, &mut y)); + + // top edge + + src = Rect::new(0, 0, 16, 16); + x = 10; + y = 0; + assert!(clip_blit(&dest, &mut src, &mut x, &mut y)); + assert_eq!(src, Rect::new(0, 0, 16, 16)); + assert_eq!(10, x); + assert_eq!(0, y); + + src = Rect::new(0, 0, 16, 16); + x = 10; + y = -5; + assert!(clip_blit(&dest, &mut src, &mut x, &mut y)); + assert_eq!(src, Rect::new(0, 5, 16, 11)); + assert_eq!(10, x); + assert_eq!(0, y); + + src = Rect::new(0, 0, 16, 16); + x = 10; + y = -16; + assert!(!clip_blit(&dest, &mut src, &mut x, &mut y)); + + // bottom edge + + src = Rect::new(0, 0, 16, 16); + x = 10; + y = 224; + assert!(clip_blit(&dest, &mut src, &mut x, &mut y)); + assert_eq!(src, Rect::new(0, 0, 16, 16)); + assert_eq!(10, x); + assert_eq!(224, y); + + src = Rect::new(0, 0, 16, 16); + x = 10; + y = 229; + assert!(clip_blit(&dest, &mut src, &mut x, &mut y)); + assert_eq!(src, Rect::new(0, 0, 16, 11)); + assert_eq!(10, x); + assert_eq!(229, y); + + src = Rect::new(0, 0, 16, 16); + x = 10; + y = 240; + assert!(!clip_blit(&dest, &mut src, &mut x, &mut y)); + + src = Rect::new(16, 16, 16, 16); + x = -1; + y = 112; + assert!(clip_blit(&dest, &mut src, &mut x, &mut y)); + assert_eq!(src, Rect::new(17, 16, 15, 16)); + assert_eq!(0, x); + assert_eq!(112, y); + } + + #[test] + pub fn clip_blit_regions_large_source() { + let dest = Rect::new(0, 0, 64, 64); + + let mut src: Rect; + let mut x: i32; + let mut y: i32; + + src = Rect::new(0, 0, 128, 128); + x = 0; + y = 0; + assert!(clip_blit(&dest, &mut src, &mut x, &mut y)); + assert_eq!(src, Rect::new(0, 0, 64, 64)); + assert_eq!(0, x); + assert_eq!(0, y); + + src = Rect::new(0, 0, 128, 128); + x = -16; + y = -24; + assert!(clip_blit(&dest, &mut src, &mut x, &mut y)); + assert_eq!(src, Rect::new(16, 24, 64, 64)); + assert_eq!(0, x); + assert_eq!(0, y); + + src = Rect::new(0, 0, 32, 128); + x = 10; + y = -20; + assert!(clip_blit(&dest, &mut src, &mut x, &mut y)); + assert_eq!(src, Rect::new(0, 20, 32, 64)); + assert_eq!(10, x); + assert_eq!(0, y); + + src = Rect::new(0, 0, 128, 32); + x = -20; + y = 10; + assert!(clip_blit(&dest, &mut src, &mut x, &mut y)); + assert_eq!(src, Rect::new(20, 0, 64, 32)); + assert_eq!(0, x); + assert_eq!(10, y); + } +} diff --git a/libretrogd/src/graphics/bitmap/iff.rs b/libretrogd/src/graphics/bitmap/iff.rs new file mode 100644 index 0000000..12eb5b0 --- /dev/null +++ b/libretrogd/src/graphics/bitmap/iff.rs @@ -0,0 +1,620 @@ +use std::fs::File; +use std::io; +use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write}; +use std::path::Path; + +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use thiserror::Error; + +use crate::utils::packbits::{pack_bits, unpack_bits, PackBitsError}; +use crate::{Bitmap, Palette, PaletteError, PaletteFormat}; + +#[derive(Error, Debug)] +pub enum IffError { + #[error("Bad or unsupported IFF file: {0}")] + BadFile(String), + + #[error("IFF palette data error")] + BadPalette(#[from] PaletteError), + + #[error("PackBits error")] + PackBitsError(#[from] PackBitsError), + + #[error("IFF I/O error")] + IOError(#[from] std::io::Error), +} + +pub enum IffFormat { + Pbm, + PbmUncompressed, + Ilbm, + IlbmUncompressed, +} + +impl IffFormat { + pub fn compressed(&self) -> bool { + use IffFormat::*; + match self { + Pbm | Ilbm => true, + PbmUncompressed | IlbmUncompressed => false, + } + } + + pub fn chunky(&self) -> bool { + use IffFormat::*; + match self { + Pbm | PbmUncompressed => true, + Ilbm | IlbmUncompressed => false, + } + } + + pub fn type_id(&self) -> [u8; 4] { + use IffFormat::*; + match self { + Pbm | PbmUncompressed => *b"PBM ", + Ilbm | IlbmUncompressed => *b"ILBM", + } + } +} + +#[derive(Debug, Copy, Clone)] +#[repr(packed)] +struct IffId { + id: [u8; 4], +} + +impl IffId { + pub fn read(reader: &mut T) -> Result { + let mut id = [0u8; 4]; + reader.read_exact(&mut id)?; + Ok(IffId { id }) + } + + pub fn write(&self, writer: &mut T) -> Result<(), IffError> { + writer.write_all(&self.id)?; + Ok(()) + } +} + +#[derive(Debug, Copy, Clone)] +#[repr(packed)] +struct FormChunkHeader { + chunk_id: IffId, + size: u32, + type_id: IffId, +} + +impl FormChunkHeader { + pub fn read(reader: &mut T) -> Result { + let chunk_id = IffId::read(reader)?; + let size = reader.read_u32::()?; + let type_id = IffId::read(reader)?; + Ok(FormChunkHeader { + chunk_id, + size, + type_id, + }) + } + + pub fn write(&self, writer: &mut T) -> Result<(), IffError> { + self.chunk_id.write(writer)?; + writer.write_u32::(self.size)?; + self.type_id.write(writer)?; + Ok(()) + } +} + +#[derive(Debug, Copy, Clone)] +#[repr(packed)] +struct SubChunkHeader { + chunk_id: IffId, + size: u32, +} + +impl SubChunkHeader { + pub fn read(reader: &mut T) -> Result { + let chunk_id = IffId::read(reader)?; + let mut size = reader.read_u32::()?; + if (size & 1) == 1 { + size += 1; // account for the padding byte + } + Ok(SubChunkHeader { chunk_id, size }) + } + + pub fn write(&self, writer: &mut T) -> Result<(), IffError> { + self.chunk_id.write(writer)?; + writer.write_u32::(self.size)?; + Ok(()) + } +} + +#[derive(Debug, Copy, Clone)] +#[repr(packed)] +struct BMHDChunk { + width: u16, + height: u16, + left: u16, + top: u16, + bitplanes: u8, + masking: u8, + compress: u8, + padding: u8, + transparency: u16, + x_aspect_ratio: u8, + y_aspect_ratio: u8, + page_width: u16, + page_height: u16, +} + +impl BMHDChunk { + pub fn read(reader: &mut T) -> Result { + Ok(BMHDChunk { + width: reader.read_u16::()?, + height: reader.read_u16::()?, + left: reader.read_u16::()?, + top: reader.read_u16::()?, + bitplanes: reader.read_u8()?, + masking: reader.read_u8()?, + compress: reader.read_u8()?, + padding: reader.read_u8()?, + transparency: reader.read_u16::()?, + x_aspect_ratio: reader.read_u8()?, + y_aspect_ratio: reader.read_u8()?, + page_width: reader.read_u16::()?, + page_height: reader.read_u16::()?, + }) + } + + pub fn write(&self, writer: &mut T) -> Result<(), IffError> { + writer.write_u16::(self.width)?; + writer.write_u16::(self.height)?; + writer.write_u16::(self.left)?; + writer.write_u16::(self.top)?; + writer.write_u8(self.bitplanes)?; + writer.write_u8(self.masking)?; + writer.write_u8(self.compress)?; + writer.write_u8(self.padding)?; + writer.write_u16::(self.transparency)?; + writer.write_u8(self.x_aspect_ratio)?; + writer.write_u8(self.y_aspect_ratio)?; + writer.write_u16::(self.page_width)?; + writer.write_u16::(self.page_height)?; + Ok(()) + } +} + +fn merge_bitplane(plane: u32, src: &[u8], dest: &mut [u8], row_size: usize) { + let bitmask = 1 << plane; + for x in 0..row_size { + let data = src[x]; + if (data & 128) > 0 { + dest[x * 8] |= bitmask; + } + if (data & 64) > 0 { + dest[(x * 8) + 1] |= bitmask; + } + if (data & 32) > 0 { + dest[(x * 8) + 2] |= bitmask; + } + if (data & 16) > 0 { + dest[(x * 8) + 3] |= bitmask; + } + if (data & 8) > 0 { + dest[(x * 8) + 4] |= bitmask; + } + if (data & 4) > 0 { + dest[(x * 8) + 5] |= bitmask; + } + if (data & 2) > 0 { + dest[(x * 8) + 6] |= bitmask; + } + if (data & 1) > 0 { + dest[(x * 8) + 7] |= bitmask; + } + } +} + +fn extract_bitplane(plane: u32, src: &[u8], dest: &mut [u8], row_size: usize) { + let bitmask = 1 << plane; + let mut src_base_index = 0; + for x in 0..row_size { + let mut data = 0; + if src[src_base_index] & bitmask != 0 { + data |= 128; + } + if src[src_base_index + 1] & bitmask != 0 { + data |= 64; + } + if src[src_base_index + 2] & bitmask != 0 { + data |= 32; + } + if src[src_base_index + 3] & bitmask != 0 { + data |= 16; + } + if src[src_base_index + 4] & bitmask != 0 { + data |= 8; + } + if src[src_base_index + 5] & bitmask != 0 { + data |= 4; + } + if src[src_base_index + 6] & bitmask != 0 { + data |= 2; + } + if src[src_base_index + 7] & bitmask != 0 { + data |= 1; + } + + src_base_index += 8; + dest[x] = data; + } +} + +fn load_planar_body(reader: &mut T, bmhd: &BMHDChunk) -> Result { + let mut bitmap = Bitmap::new(bmhd.width as u32, bmhd.height as u32).unwrap(); + + let row_bytes = (((bmhd.width + 15) >> 4) << 1) as usize; + let mut buffer = vec![0u8; row_bytes]; + + for y in 0..bmhd.height { + // planar data is stored for each bitplane in sequence for the scanline. + // that is, ALL of bitplane1, followed by ALL of bitplane2, etc, NOT + // alternating after each pixel. if compression is enabled, it does NOT + // cross bitplane boundaries. each bitplane is compressed individually. + // bitplanes also do NOT cross the scanline boundary. basically, each + // scanline of pixel data, and within that, each of the bitplanes of + // pixel data found in each scanline can all be treated as they are all + // their own self-contained bit of data as far as this loading process + // is concerned (well, except that we merge all of the scanline's + // bitplanes together at the end of each line) + + // read all the bitplane rows per scanline + for plane in 0..(bmhd.bitplanes as u32) { + if bmhd.compress == 1 { + // decompress packed line for this bitplane only + buffer.clear(); + unpack_bits(reader, &mut buffer, row_bytes)? + } else { + // TODO: check this. maybe row_bytes calculation is wrong? either way, i don't + // think that DP2 or Grafx2 ever output uncompressed interleaved files ... + // just read all this bitplane's line data in as-is + reader.read_exact(&mut buffer)?; + } + + // merge this bitplane data into the final destination. after all of + // the bitplanes have been loaded and merged in this way for this + // scanline, the destination pointer will contain VGA-friendly + // "chunky pixel"-format pixel data + merge_bitplane( + plane, + &buffer, + bitmap.pixels_at_mut(0, y as i32).unwrap(), + row_bytes, + ); + } + } + + Ok(bitmap) +} + +fn load_chunky_body(reader: &mut T, bmhd: &BMHDChunk) -> Result { + let mut bitmap = Bitmap::new(bmhd.width as u32, bmhd.height as u32).unwrap(); + + for y in 0..bmhd.height { + if bmhd.compress == 1 { + // for compression-enabled, read row of pixels using PackBits + let mut writer = bitmap.pixels_at_mut(0, y as i32).unwrap(); + unpack_bits(reader, &mut writer, bmhd.width as usize)? + } else { + // for uncompressed, read row of pixels literally + let dest = &mut bitmap.pixels_at_mut(0, y as i32).unwrap()[0..bmhd.width as usize]; + reader.read_exact(dest)?; + } + } + + Ok(bitmap) +} + +fn write_planar_body( + writer: &mut T, + bitmap: &Bitmap, + bmhd: &BMHDChunk, +) -> Result<(), IffError> { + let row_bytes = (((bitmap.width() + 15) >> 4) << 1) as usize; + let mut buffer = vec![0u8; row_bytes]; + + for y in 0..bitmap.height() { + for plane in 0..(bmhd.bitplanes as u32) { + extract_bitplane( + plane, + bitmap.pixels_at(0, y as i32).unwrap(), + &mut buffer, + row_bytes, + ); + + if bmhd.compress == 1 { + // for compression-enabled, write this plane's pixels using PackBits + pack_bits(&mut buffer.as_slice(), writer, row_bytes)?; + } else { + // TODO: check this. maybe row_bytes calculation is wrong? either way, i don't + // think that DP2 or Grafx2 ever output uncompressed interleaved files ... + // for uncompressed, write this plane's pixels literally + writer.write_all(&buffer)?; + } + } + } + + Ok(()) +} + +fn write_chunky_body( + writer: &mut T, + bitmap: &Bitmap, + bmhd: &BMHDChunk, +) -> Result<(), IffError> { + for y in 0..bitmap.height() { + if bmhd.compress == 1 { + // for compression-enabled, write row of pixels using PackBits + let mut reader = bitmap.pixels_at(0, y as i32).unwrap(); + pack_bits(&mut reader, writer, bitmap.width() as usize)?; + } else { + // for uncompressed, write out the row of pixels literally + let src = &bitmap.pixels_at(0, y as i32).unwrap()[0..bitmap.width() as usize]; + writer.write_all(src)?; + } + } + + Ok(()) +} + +impl Bitmap { + pub fn load_iff_bytes( + reader: &mut T, + ) -> Result<(Bitmap, Palette), IffError> { + let form_chunk = FormChunkHeader::read(reader)?; + if form_chunk.chunk_id.id != *b"FORM" { + return Err(IffError::BadFile(String::from( + "Unexpected form chunk ID, probably not an IFF file", + ))); + } + if form_chunk.type_id.id != *b"ILBM" && form_chunk.type_id.id != *b"PBM " { + return Err(IffError::BadFile(String::from( + "Only ILBM or PBM formats are supported", + ))); + } + + let mut bmhd: Option = None; + let mut palette: Option = None; + let mut bitmap: Option = None; + + loop { + let header = match SubChunkHeader::read(reader) { + Ok(header) => header, + Err(IffError::IOError(io_error)) + if io_error.kind() == io::ErrorKind::UnexpectedEof => + { + break + } + Err(err) => return Err(err), + }; + let chunk_data_position = reader.stream_position()?; + + // todo: process chunk here + if header.chunk_id.id == *b"BMHD" { + bmhd = Some(BMHDChunk::read(reader)?); + if bmhd.as_ref().unwrap().bitplanes != 8 { + return Err(IffError::BadFile(String::from( + "Only 8bpp files are supported", + ))); + } + if bmhd.as_ref().unwrap().masking == 1 { + return Err(IffError::BadFile(String::from("Masking is not supported"))); + } + } else if header.chunk_id.id == *b"CMAP" { + if header.size != 768 { + return Err(IffError::BadFile(String::from( + "Only 256 color files are supported", + ))); + } + palette = Some(Palette::load_from_bytes(reader, PaletteFormat::Normal)?) + } else if header.chunk_id.id == *b"BODY" { + if let Some(bmhd) = &bmhd { + if form_chunk.type_id.id == *b"PBM " { + bitmap = Some(load_chunky_body(reader, bmhd)?); + } else { + bitmap = Some(load_planar_body(reader, bmhd)?); + } + } else { + // TODO: does this ever occur in practice? and if so, we can probably make some + // changes to allow for it ... + return Err(IffError::BadFile(String::from( + "BODY chunk occurs before BMHD chunk, or no BMHD chunk exists", + ))); + } + } + + reader.seek(SeekFrom::Start(chunk_data_position + header.size as u64))?; + } + + if bitmap.is_none() { + return Err(IffError::BadFile(String::from("No BODY chunk was found"))); + } + // TODO: we can probably make this optional ... + if palette.is_none() { + return Err(IffError::BadFile(String::from("No CMAP chunk was found"))); + } + + Ok((bitmap.unwrap(), palette.unwrap())) + } + + pub fn load_iff_file(path: &Path) -> Result<(Bitmap, Palette), IffError> { + let f = File::open(path)?; + let mut reader = BufReader::new(f); + Self::load_iff_bytes(&mut reader) + } + + pub fn to_iff_bytes( + &self, + writer: &mut T, + palette: &Palette, + format: IffFormat, + ) -> Result<(), IffError> { + let form_chunk_position = writer.stream_position()?; + + let mut form_chunk = FormChunkHeader { + chunk_id: IffId { id: *b"FORM" }, + type_id: IffId { + id: format.type_id(), + }, + size: 0, // filled in later once we know the size + }; + + // skip over the form chunk for now. will come back here and write it out later once we + // know what the final size is + writer.seek(SeekFrom::Current( + std::mem::size_of::() as i64 + ))?; + + let bmhd_chunk_header = SubChunkHeader { + chunk_id: IffId { id: *b"BMHD" }, + size: std::mem::size_of::() as u32, + }; + let bmhd = BMHDChunk { + width: self.width() as u16, + height: self.height() as u16, + left: 0, + top: 0, + bitplanes: 8, + masking: 0, + compress: if format.compressed() { 1 } else { 0 }, + padding: 0, + transparency: 0, + // the following values are based on what DP2 writes out in 320x200 modes. good enough. + x_aspect_ratio: 5, + y_aspect_ratio: 6, + page_width: 320, + page_height: 200, + }; + bmhd_chunk_header.write(writer)?; + bmhd.write(writer)?; + + let cmap_chunk_header = SubChunkHeader { + chunk_id: IffId { id: *b"CMAP" }, + size: 768, + }; + cmap_chunk_header.write(writer)?; + palette.to_bytes(writer, PaletteFormat::Normal)?; + + let body_position = writer.stream_position()?; + + let mut body_chunk_header = SubChunkHeader { + chunk_id: IffId { id: *b"BODY" }, + size: 0, // filled in later once we know the size + }; + + // skip over the body chunk header for now. we will again come back here and write it out + // later once we know what the final size again. + writer.seek(SeekFrom::Current( + std::mem::size_of::() as i64 + ))?; + + if format.chunky() { + write_chunky_body(writer, self, &bmhd)?; + } else { + write_planar_body(writer, self, &bmhd)?; + } + + // add a padding byte (only if necessary) to the body we just finished writing + let mut eof_pos = writer.stream_position()?; + if (eof_pos - body_position) & 1 == 1 { + writer.write_u8(0)?; + eof_pos += 1; + } + + // go back and write out the form chunk header now that we know the final file size + form_chunk.size = (eof_pos - (std::mem::size_of::() as u64 * 2)) as u32; + writer.seek(SeekFrom::Start(form_chunk_position))?; + form_chunk.write(writer)?; + + // and then write out the body chunk header since we now know the size of that too + body_chunk_header.size = eof_pos as u32 - std::mem::size_of::() as u32; + writer.seek(SeekFrom::Start(body_position))?; + body_chunk_header.write(writer)?; + + // and then go back to eof + writer.seek(SeekFrom::Start(eof_pos))?; + + Ok(()) + } + + pub fn to_iff_file( + &self, + path: &Path, + palette: &Palette, + format: IffFormat, + ) -> Result<(), IffError> { + let f = File::create(path)?; + let mut writer = BufWriter::new(f); + self.to_iff_bytes(&mut writer, palette, format) + } +} + +#[cfg(test)] +mod tests { + use tempfile::TempDir; + + use super::*; + + pub static TEST_BMP_PIXELS_RAW: &[u8] = + include_bytes!("../../../test-assets/test_bmp_pixels_raw.bin"); + + #[test] + pub fn load_and_save() -> Result<(), IffError> { + let dp2_palette = + Palette::load_from_file(Path::new("./test-assets/dp2.pal"), PaletteFormat::Normal) + .unwrap(); + let tmp_dir = TempDir::new()?; + + // ILBM format + + let (bmp, palette) = Bitmap::load_iff_file(Path::new("./test-assets/test_ilbm.lbm"))?; + assert_eq!(16, bmp.width()); + assert_eq!(16, bmp.height()); + assert_eq!(bmp.pixels(), TEST_BMP_PIXELS_RAW); + assert_eq!(palette, dp2_palette); + + let save_path = tmp_dir.path().join("test_save_ilbm.lbm"); + bmp.to_iff_file(&save_path, &palette, IffFormat::Ilbm)?; + let (reloaded_bmp, reloaded_palette) = Bitmap::load_iff_file(&save_path)?; + assert_eq!(16, reloaded_bmp.width()); + assert_eq!(16, reloaded_bmp.height()); + assert_eq!(reloaded_bmp.pixels(), TEST_BMP_PIXELS_RAW); + assert_eq!(reloaded_palette, dp2_palette); + + // PBM format + + let (bmp, palette) = Bitmap::load_iff_file(Path::new("./test-assets/test_pbm.lbm"))?; + assert_eq!(16, bmp.width()); + assert_eq!(16, bmp.height()); + assert_eq!(bmp.pixels(), TEST_BMP_PIXELS_RAW); + assert_eq!(palette, dp2_palette); + + let save_path = tmp_dir.path().join("test_save_pbm.lbm"); + bmp.to_iff_file(&save_path, &palette, IffFormat::Pbm)?; + let (reloaded_bmp, reloaded_palette) = Bitmap::load_iff_file(&save_path)?; + assert_eq!(16, reloaded_bmp.width()); + assert_eq!(16, reloaded_bmp.height()); + assert_eq!(reloaded_bmp.pixels(), TEST_BMP_PIXELS_RAW); + assert_eq!(reloaded_palette, dp2_palette); + + Ok(()) + } + + #[test] + pub fn load_larger_image() -> Result<(), IffError> { + let (bmp, _palette) = Bitmap::load_iff_file(Path::new("./test-assets/test_image.lbm"))?; + assert_eq!(320, bmp.width()); + assert_eq!(200, bmp.height()); + + Ok(()) + } +} diff --git a/libretrogd/src/graphics/bitmap/mod.rs b/libretrogd/src/graphics/bitmap/mod.rs new file mode 100644 index 0000000..6d09b5d --- /dev/null +++ b/libretrogd/src/graphics/bitmap/mod.rs @@ -0,0 +1,584 @@ +use std::path::Path; +use std::slice; + +use thiserror::Error; + +pub use crate::blit::*; +pub use crate::iff::*; +pub use crate::pcx::*; +pub use crate::primitives::*; +use crate::{Palette, Rect}; + +pub mod blit; +pub mod iff; +pub mod pcx; +pub mod primitives; + +#[derive(Error, Debug)] +pub enum BitmapError { + #[error("Invalid bitmap dimensions")] + InvalidDimensions, + + #[error("Region is not fully within bitmap boundaries")] + OutOfBounds, + + #[error("Unknown bitmap file type: {0}")] + UnknownFileType(String), + + #[error("Bitmap IFF file error")] + IffError(#[from] iff::IffError), + + #[error("Bitmap PCX file error")] + PcxError(#[from] pcx::PcxError), +} + +/// Container for 256 color 2D pixel/image data that can be rendered to the screen. Pixel data +/// is stored as contiguous bytes, where each pixel is an index into a separate 256 color palette +/// stored independently of the bitmap. The pixel data is not padded in any way, so the stride from +/// one row to the next is always exactly equal to the bitmap width. Rendering operations provided +/// here are done with respect to the bitmaps clipping region, where rendering outside of the +/// clipping region is simply not performed / stops at the clipping boundary. +#[derive(Debug, Clone)] +pub struct Bitmap { + width: u32, + height: u32, + pixels: Box<[u8]>, + clip_region: Rect, +} + +impl Bitmap { + /// Creates a new Bitmap with the specified dimensions. + /// + /// # Arguments + /// + /// * `width`: the width of the bitmap in pixels + /// * `height`: the height of the bitmap in pixels + /// + /// returns: `Result` + pub fn new(width: u32, height: u32) -> Result { + if width == 0 || height == 0 { + return Err(BitmapError::InvalidDimensions); + } + + Ok(Bitmap { + width, + height, + pixels: vec![0u8; (width * height) as usize].into_boxed_slice(), + clip_region: Rect { + x: 0, + y: 0, + width, + height, + }, + }) + } + + /// Creates a new Bitmap, copying the pixel data from a sub-region of another source Bitmap. + /// The resulting bitmap will have dimensions equal to that of the region specified. + /// + /// # Arguments + /// + /// * `source`: the source bitmap to copy from + /// * `region`: the region on the source bitmap to copy from + /// + /// returns: `Result` + pub fn from(source: &Bitmap, region: &Rect) -> Result { + if !source.full_bounds().contains_rect(region) { + return Err(BitmapError::OutOfBounds); + } + + let mut bmp = Bitmap::new(region.width, region.height)?; + unsafe { bmp.solid_blit(source, region, 0, 0) }; + Ok(bmp) + } + + pub fn load_file(path: &Path) -> Result<(Bitmap, Palette), BitmapError> { + if let Some(extension) = path.extension() { + let extension = extension.to_ascii_lowercase(); + match extension.to_str() { + Some("pcx") => Ok(Self::load_pcx_file(path)?), + Some("iff") | Some("lbm") | Some("pbm") | Some("bbm") => { + Ok(Self::load_iff_file(path)?) + } + _ => Err(BitmapError::UnknownFileType(String::from( + "Unrecognized file extension", + ))), + } + } else { + Err(BitmapError::UnknownFileType(String::from( + "No file extension", + ))) + } + } + + /// Returns the width of the bitmap in pixels. + #[inline] + pub fn width(&self) -> u32 { + self.width + } + + /// Returns the height of the bitmap in pixels. + #[inline] + pub fn height(&self) -> u32 { + self.height + } + + /// Returns the right x coordinate of the bitmap. + #[inline] + pub fn right(&self) -> u32 { + self.width - 1 + } + + /// Returns the bottom x coordinate of the bitmap. + #[inline] + pub fn bottom(&self) -> u32 { + self.height - 1 + } + + /// Returns the current clipping region set on this bitmap. + #[inline] + pub fn clip_region(&self) -> &Rect { + &self.clip_region + } + + /// Returns a rect representing the full bitmap boundaries, ignoring the current clipping + /// region set on this bitmap. + #[inline] + pub fn full_bounds(&self) -> Rect { + Rect { + x: 0, + y: 0, + width: self.width, + height: self.height, + } + } + + /// Sets a new clipping region on this bitmap. The region will be automatically clamped to + /// the maximum bitmap boundaries if the supplied region extends beyond it. + /// + /// # Arguments + /// + /// * `region`: the new clipping region + pub fn set_clip_region(&mut self, region: &Rect) { + self.clip_region = *region; + self.clip_region.clamp_to(&self.full_bounds()); + } + + /// Resets the bitmaps clipping region back to the default (full boundaries of the bitmap). + pub fn reset_clip_region(&mut self) { + self.clip_region = self.full_bounds(); + } + + /// Returns a reference to the raw pixels in this bitmap. + #[inline] + pub fn pixels(&self) -> &[u8] { + &self.pixels + } + + /// Returns a mutable reference to the raw pixels in this bitmap. + #[inline] + pub fn pixels_mut(&mut self) -> &mut [u8] { + &mut self.pixels + } + + /// Returns a reference to the subset of the raw pixels in this bitmap beginning at the + /// given coordinates and extending to the end of the bitmap. If the coordinates given are + /// outside the bitmap's current clipping region, None is returned. + #[inline] + pub fn pixels_at(&self, x: i32, y: i32) -> Option<&[u8]> { + if self.is_xy_visible(x, y) { + let offset = self.get_offset_to_xy(x, y); + Some(&self.pixels[offset..]) + } else { + None + } + } + + /// Returns a mutable reference to the subset of the raw pixels in this bitmap beginning at the + /// given coordinates and extending to the end of the bitmap. If the coordinates given are + /// outside the bitmap's current clipping region, None is returned. + #[inline] + pub fn pixels_at_mut(&mut self, x: i32, y: i32) -> Option<&mut [u8]> { + if self.is_xy_visible(x, y) { + let offset = self.get_offset_to_xy(x, y); + Some(&mut self.pixels[offset..]) + } else { + None + } + } + + /// Returns an unsafe reference to the subset of the raw pixels in this bitmap beginning at the + /// given coordinates and extending to the end of the bitmap. The coordinates are not checked + /// for validity, so it is up to you to ensure they lie within the bounds of the bitmap. + #[inline] + pub unsafe fn pixels_at_unchecked(&self, x: i32, y: i32) -> &[u8] { + let offset = self.get_offset_to_xy(x, y); + slice::from_raw_parts(self.pixels.as_ptr().add(offset), self.pixels.len() - offset) + } + + /// Returns a mutable unsafe reference to the subset of the raw pixels in this bitmap beginning + /// at the given coordinates and extending to the end of the bitmap. The coordinates are not + /// checked for validity, so it is up to you to ensure they lie within the bounds of the bitmap. + #[inline] + pub unsafe fn pixels_at_mut_unchecked(&mut self, x: i32, y: i32) -> &mut [u8] { + let offset = self.get_offset_to_xy(x, y); + slice::from_raw_parts_mut( + self.pixels.as_mut_ptr().add(offset), + self.pixels.len() - offset, + ) + } + + /// Returns a pointer to the subset of the raw pixels in this bitmap beginning at the given + /// coordinates. If the coordinates given are outside the bitmap's current clipping region, + /// None is returned. + #[inline] + pub unsafe fn pixels_at_ptr(&self, x: i32, y: i32) -> Option<*const u8> { + if self.is_xy_visible(x, y) { + let offset = self.get_offset_to_xy(x, y); + Some(self.pixels.as_ptr().add(offset)) + } else { + None + } + } + + /// Returns a mutable pointer to the subset of the raw pixels in this bitmap beginning at the + /// given coordinates. If the coordinates given are outside the bitmap's current clipping + /// region, None is returned. + #[inline] + pub unsafe fn pixels_at_mut_ptr(&mut self, x: i32, y: i32) -> Option<*mut u8> { + if self.is_xy_visible(x, y) { + let offset = self.get_offset_to_xy(x, y); + Some(self.pixels.as_mut_ptr().add(offset)) + } else { + None + } + } + + /// Returns an unsafe pointer to the subset of the raw pixels in this bitmap beginning at the + /// given coordinates. The coordinates are not checked for validity, so it is up to you to + /// ensure they lie within the bounds of the bitmap. + #[inline] + pub unsafe fn pixels_at_ptr_unchecked(&self, x: i32, y: i32) -> *const u8 { + let offset = self.get_offset_to_xy(x, y); + self.pixels.as_ptr().add(offset) + } + + /// Returns a mutable unsafe pointer to the subset of the raw pixels in this bitmap beginning + /// at the given coordinates. The coordinates are not checked for validity, so it is up to you + /// to ensure they lie within the bounds of the bitmap. + #[inline] + pub unsafe fn pixels_at_mut_ptr_unchecked(&mut self, x: i32, y: i32) -> *mut u8 { + let offset = self.get_offset_to_xy(x, y); + self.pixels.as_mut_ptr().add(offset) + } + + /// Returns an offset corresponding to the coordinates of the pixel given on this bitmap that + /// can be used with a reference to the raw pixels in this bitmap to access that pixel. The + /// coordinates given are not checked for validity. + #[inline] + pub fn get_offset_to_xy(&self, x: i32, y: i32) -> usize { + ((y * self.width as i32) + x) as usize + } + + /// Returns true if the coordinates given lie within the bitmaps clipping region. + #[inline] + pub fn is_xy_visible(&self, x: i32, y: i32) -> bool { + (x >= self.clip_region.x) + && (y >= self.clip_region.y) + && (x <= self.clip_region.right()) + && (y <= self.clip_region.bottom()) + } + + /// Copies and converts the entire pixel data from this bitmap to a destination expecting + /// 32-bit ARGB-format pixel data. This can be used to display the contents of the bitmap + /// on-screen by using an SDL Surface, OpenGL texture, etc as the destination. + /// + /// # Arguments + /// + /// * `dest`: destination 32-bit ARGB pixel buffer to copy converted pixels to + /// * `palette`: the 256 colour palette to use during pixel conversion + pub fn copy_as_argb_to(&self, dest: &mut [u32], palette: &Palette) { + for (src, dest) in self.pixels().iter().zip(dest.iter_mut()) { + *dest = palette[*src]; + } + } +} + +#[cfg(test)] +pub mod tests { + use claim::assert_matches; + + use super::*; + + #[rustfmt::skip] + static RAW_BMP_PIXELS: &[u8] = &[ + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 2, + ]; + + #[rustfmt::skip] + static RAW_BMP_PIXELS_SUBSET: &[u8] = &[ + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 2, + ]; + + #[test] + pub fn creation_and_sizing() { + assert_matches!(Bitmap::new(0, 0), Err(BitmapError::InvalidDimensions)); + assert_matches!(Bitmap::new(16, 0), Err(BitmapError::InvalidDimensions)); + assert_matches!(Bitmap::new(0, 32), Err(BitmapError::InvalidDimensions)); + let bmp = Bitmap::new(16, 32).unwrap(); + assert_eq!(16, bmp.width()); + assert_eq!(32, bmp.height()); + assert_eq!(15, bmp.right()); + assert_eq!(31, bmp.bottom()); + assert_eq!( + Rect { + x: 0, + y: 0, + width: 16, + height: 32 + }, + bmp.full_bounds() + ); + assert_eq!( + Rect { + x: 0, + y: 0, + width: 16, + height: 32 + }, + *bmp.clip_region() + ); + } + + #[test] + pub fn copy_from() { + let mut bmp = Bitmap::new(8, 8).unwrap(); + bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS); + + assert_matches!( + Bitmap::from(&bmp, &Rect::new(0, 0, 16, 16)), + Err(BitmapError::OutOfBounds) + ); + + let copy = Bitmap::from(&bmp, &Rect::new(0, 0, 8, 8)).unwrap(); + assert_eq!(bmp.pixels(), copy.pixels()); + + let copy = Bitmap::from(&bmp, &Rect::new(4, 4, 4, 4)).unwrap(); + assert_eq!(RAW_BMP_PIXELS_SUBSET, copy.pixels()); + } + + #[test] + pub fn xy_offset_calculation() { + let bmp = Bitmap::new(20, 15).unwrap(); + assert_eq!(0, bmp.get_offset_to_xy(0, 0)); + assert_eq!(19, bmp.get_offset_to_xy(19, 0)); + assert_eq!(20, bmp.get_offset_to_xy(0, 1)); + assert_eq!(280, bmp.get_offset_to_xy(0, 14)); + assert_eq!(299, bmp.get_offset_to_xy(19, 14)); + assert_eq!(227, bmp.get_offset_to_xy(7, 11)); + } + + #[test] + pub fn bounds_testing_and_clip_region() { + let mut bmp = Bitmap::new(16, 8).unwrap(); + assert!(bmp.is_xy_visible(0, 0)); + assert!(bmp.is_xy_visible(15, 0)); + assert!(bmp.is_xy_visible(0, 7)); + assert!(bmp.is_xy_visible(15, 7)); + assert!(!bmp.is_xy_visible(-1, -1)); + assert!(!bmp.is_xy_visible(16, 8)); + assert!(!bmp.is_xy_visible(4, -2)); + assert!(!bmp.is_xy_visible(11, 8)); + assert!(!bmp.is_xy_visible(-1, 3)); + assert!(!bmp.is_xy_visible(16, 6)); + + let new_clip_region = Rect::from_coords(4, 2, 12, 6); + bmp.set_clip_region(&new_clip_region); + assert_eq!( + Rect { + x: 0, + y: 0, + width: 16, + height: 8 + }, + bmp.full_bounds() + ); + assert_eq!(new_clip_region, *bmp.clip_region()); + assert!(bmp.is_xy_visible(4, 2)); + assert!(bmp.is_xy_visible(12, 2)); + assert!(bmp.is_xy_visible(4, 6)); + assert!(bmp.is_xy_visible(12, 6)); + assert!(!bmp.is_xy_visible(3, 1)); + assert!(!bmp.is_xy_visible(13, 7)); + assert!(!bmp.is_xy_visible(5, 1)); + assert!(!bmp.is_xy_visible(10, 7)); + assert!(!bmp.is_xy_visible(3, 4)); + assert!(!bmp.is_xy_visible(13, 5)); + + assert!(!bmp.is_xy_visible(0, 0)); + assert!(!bmp.is_xy_visible(15, 0)); + assert!(!bmp.is_xy_visible(0, 7)); + assert!(!bmp.is_xy_visible(15, 7)); + bmp.reset_clip_region(); + assert!(bmp.is_xy_visible(0, 0)); + assert!(bmp.is_xy_visible(15, 0)); + assert!(bmp.is_xy_visible(0, 7)); + assert!(bmp.is_xy_visible(15, 7)); + assert!(!bmp.is_xy_visible(-1, -1)); + assert!(!bmp.is_xy_visible(16, 8)); + } + + #[test] + pub fn pixels_at() { + let mut bmp = Bitmap::new(8, 8).unwrap(); + bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS); + + assert_eq!(None, bmp.pixels_at(-1, -1)); + + let offset = bmp.get_offset_to_xy(1, 1); + let pixels = bmp.pixels_at(0, 0).unwrap(); + assert_eq!(64, pixels.len()); + assert_eq!(0, pixels[0]); + assert_eq!(1, pixels[offset]); + assert_eq!(2, pixels[63]); + + let pixels = bmp.pixels_at(1, 1).unwrap(); + assert_eq!(55, pixels.len()); + assert_eq!(1, pixels[0]); + assert_eq!(2, pixels[54]); + } + + #[test] + pub fn pixels_at_mut() { + let mut bmp = Bitmap::new(8, 8).unwrap(); + bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS); + + assert_eq!(None, bmp.pixels_at_mut(-1, -1)); + + let offset = bmp.get_offset_to_xy(1, 1); + let pixels = bmp.pixels_at_mut(0, 0).unwrap(); + assert_eq!(64, pixels.len()); + assert_eq!(0, pixels[0]); + assert_eq!(1, pixels[offset]); + assert_eq!(2, pixels[63]); + + let pixels = bmp.pixels_at_mut(1, 1).unwrap(); + assert_eq!(55, pixels.len()); + assert_eq!(1, pixels[0]); + assert_eq!(2, pixels[54]); + } + + #[test] + pub fn pixels_at_unchecked() { + let mut bmp = Bitmap::new(8, 8).unwrap(); + bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS); + + let offset = bmp.get_offset_to_xy(1, 1); + let pixels = unsafe { bmp.pixels_at_unchecked(0, 0) }; + assert_eq!(64, pixels.len()); + assert_eq!(0, pixels[0]); + assert_eq!(1, pixels[offset]); + assert_eq!(2, pixels[63]); + + let pixels = unsafe { bmp.pixels_at_unchecked(1, 1) }; + assert_eq!(55, pixels.len()); + assert_eq!(1, pixels[0]); + assert_eq!(2, pixels[54]); + } + + #[test] + pub fn pixels_at_mut_unchecked() { + let mut bmp = Bitmap::new(8, 8).unwrap(); + bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS); + + let offset = bmp.get_offset_to_xy(1, 1); + let pixels = unsafe { bmp.pixels_at_mut_unchecked(0, 0) }; + assert_eq!(64, pixels.len()); + assert_eq!(0, pixels[0]); + assert_eq!(1, pixels[offset]); + assert_eq!(2, pixels[63]); + + let pixels = unsafe { bmp.pixels_at_mut_unchecked(1, 1) }; + assert_eq!(55, pixels.len()); + assert_eq!(1, pixels[0]); + assert_eq!(2, pixels[54]); + } + + #[test] + pub fn pixels_at_ptr() { + let mut bmp = Bitmap::new(8, 8).unwrap(); + bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS); + + assert_eq!(None, unsafe { bmp.pixels_at_ptr(-1, -1) }); + + let offset = bmp.get_offset_to_xy(1, 1); + let pixels = unsafe { bmp.pixels_at_ptr(0, 0).unwrap() }; + assert_eq!(0, unsafe { *pixels }); + assert_eq!(1, unsafe { *(pixels.add(offset)) }); + assert_eq!(2, unsafe { *(pixels.add(63)) }); + + let pixels = unsafe { bmp.pixels_at_ptr(1, 1).unwrap() }; + assert_eq!(1, unsafe { *pixels }); + assert_eq!(2, unsafe { *(pixels.add(54)) }); + } + + #[test] + pub fn pixels_at_mut_ptr() { + let mut bmp = Bitmap::new(8, 8).unwrap(); + bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS); + + assert_eq!(None, unsafe { bmp.pixels_at_mut_ptr(-1, -1) }); + + let offset = bmp.get_offset_to_xy(1, 1); + let pixels = unsafe { bmp.pixels_at_mut_ptr(0, 0).unwrap() }; + assert_eq!(0, unsafe { *pixels }); + assert_eq!(1, unsafe { *(pixels.add(offset)) }); + assert_eq!(2, unsafe { *(pixels.add(63)) }); + + let pixels = unsafe { bmp.pixels_at_mut_ptr(1, 1).unwrap() }; + assert_eq!(1, unsafe { *pixels }); + assert_eq!(2, unsafe { *(pixels.add(54)) }); + } + + #[test] + pub fn pixels_at_ptr_unchecked() { + let mut bmp = Bitmap::new(8, 8).unwrap(); + bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS); + + let offset = bmp.get_offset_to_xy(1, 1); + let pixels = unsafe { bmp.pixels_at_ptr_unchecked(0, 0) }; + assert_eq!(0, unsafe { *pixels }); + assert_eq!(1, unsafe { *(pixels.add(offset)) }); + assert_eq!(2, unsafe { *(pixels.add(63)) }); + + let pixels = unsafe { bmp.pixels_at_ptr_unchecked(1, 1) }; + assert_eq!(1, unsafe { *pixels }); + assert_eq!(2, unsafe { *(pixels.add(54)) }); + } + + #[test] + pub fn pixels_at_mut_ptr_unchecked() { + let mut bmp = Bitmap::new(8, 8).unwrap(); + bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS); + + let offset = bmp.get_offset_to_xy(1, 1); + let pixels = unsafe { bmp.pixels_at_mut_ptr_unchecked(0, 0) }; + assert_eq!(0, unsafe { *pixels }); + assert_eq!(1, unsafe { *(pixels.add(offset)) }); + assert_eq!(2, unsafe { *(pixels.add(63)) }); + + let pixels = unsafe { bmp.pixels_at_mut_ptr_unchecked(1, 1) }; + assert_eq!(1, unsafe { *pixels }); + assert_eq!(2, unsafe { *(pixels.add(54)) }); + } +} diff --git a/libretrogd/src/graphics/bitmap/pcx.rs b/libretrogd/src/graphics/bitmap/pcx.rs new file mode 100644 index 0000000..bc281e4 --- /dev/null +++ b/libretrogd/src/graphics/bitmap/pcx.rs @@ -0,0 +1,322 @@ +use std::fs::File; +use std::io::{BufReader, BufWriter, Cursor, Seek, SeekFrom}; +use std::path::Path; + +use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; +use thiserror::Error; + +use crate::utils::bytes::ReadFixedLengthByteArray; +use crate::{from_rgb32, Bitmap, Palette, PaletteError, PaletteFormat}; + +#[derive(Error, Debug)] +pub enum PcxError { + #[error("Bad or unsupported PCX file: {0}")] + BadFile(String), + + #[error("PCX palette data error")] + BadPalette(#[from] PaletteError), + + #[error("PCX I/O error")] + IOError(#[from] std::io::Error), +} + +#[derive(Debug, Copy, Clone)] +#[repr(packed)] +struct PcxHeader { + manufacturer: u8, + version: u8, + encoding: u8, + bpp: u8, + x1: u16, + y1: u16, + x2: u16, + y2: u16, + horizontal_dpi: u16, + vertical_dpi: u16, + ega_palette: [u8; 48], + reserved: u8, + num_color_planes: u8, + bytes_per_line: u16, + palette_type: u16, + horizontal_size: u16, + vertical_size: u16, + padding: [u8; 54], +} + +impl PcxHeader { + pub fn read(reader: &mut T) -> Result { + Ok(PcxHeader { + manufacturer: reader.read_u8()?, + version: reader.read_u8()?, + encoding: reader.read_u8()?, + bpp: reader.read_u8()?, + x1: reader.read_u16::()?, + y1: reader.read_u16::()?, + x2: reader.read_u16::()?, + y2: reader.read_u16::()?, + horizontal_dpi: reader.read_u16::()?, + vertical_dpi: reader.read_u16::()?, + ega_palette: reader.read_bytes()?, + reserved: reader.read_u8()?, + num_color_planes: reader.read_u8()?, + bytes_per_line: reader.read_u16::()?, + palette_type: reader.read_u16::()?, + horizontal_size: reader.read_u16::()?, + vertical_size: reader.read_u16::()?, + padding: reader.read_bytes()?, + }) + } + + pub fn write(&self, writer: &mut T) -> Result<(), PcxError> { + writer.write_u8(self.manufacturer)?; + writer.write_u8(self.version)?; + writer.write_u8(self.encoding)?; + writer.write_u8(self.bpp)?; + writer.write_u16::(self.x1)?; + writer.write_u16::(self.y1)?; + writer.write_u16::(self.x2)?; + writer.write_u16::(self.y2)?; + writer.write_u16::(self.horizontal_dpi)?; + writer.write_u16::(self.vertical_dpi)?; + writer.write_all(&self.ega_palette)?; + writer.write_u8(self.reserved)?; + writer.write_u8(self.num_color_planes)?; + writer.write_u16::(self.bytes_per_line)?; + writer.write_u16::(self.palette_type)?; + writer.write_u16::(self.horizontal_size)?; + writer.write_u16::(self.vertical_size)?; + writer.write_all(&self.padding)?; + Ok(()) + } +} + +fn write_pcx_data( + writer: &mut T, + run_count: u8, + pixel: u8, +) -> Result<(), PcxError> { + if (run_count > 1) || ((pixel & 0xc0) == 0xc0) { + writer.write_u8(0xc0 | run_count)?; + } + writer.write_u8(pixel)?; + Ok(()) +} + +impl Bitmap { + pub fn load_pcx_bytes( + reader: &mut T, + ) -> Result<(Bitmap, Palette), PcxError> { + let header = PcxHeader::read(reader)?; + + if header.manufacturer != 10 { + return Err(PcxError::BadFile(String::from( + "Unexpected header.manufacturer value, probably not a PCX file", + ))); + } + if header.version != 5 { + return Err(PcxError::BadFile(String::from( + "Only version 5 PCX files are supported", + ))); + } + if header.encoding != 1 { + return Err(PcxError::BadFile(String::from( + "Only RLE-compressed PCX files are supported", + ))); + } + if header.bpp != 8 { + return Err(PcxError::BadFile(String::from( + "Only 8-bit indexed (256 color palette) PCX files are supported", + ))); + } + if header.x2 == 0 || header.y2 == 0 { + return Err(PcxError::BadFile(String::from( + "Invalid PCX image dimensions", + ))); + } + + // read the PCX file's pixel data into a bitmap + + let width = (header.x2 + 1) as u32; + let height = (header.y2 + 1) as u32; + let mut bmp = Bitmap::new(width, height).unwrap(); + let mut writer = Cursor::new(bmp.pixels_mut()); + + for _y in 0..height { + // read the next scanline's worth of pixels from the PCX file + let mut x: u32 = 0; + while x < (header.bytes_per_line as u32) { + let mut data: u8; + let mut count: u32; + + // read pixel or RLE count + data = reader.read_u8()?; + + if (data & 0xc0) == 0xc0 { + // it was an RLE count, actual pixel is the next byte ... + count = (data & 0x3f) as u32; + data = reader.read_u8()?; + } else { + // it was just a single pixel + count = 1; + } + + // write the current pixel value 'data' to the bitmap 'count' number of times + while count > 0 { + if x <= width { + writer.write_u8(data)?; + } else { + writer.seek(SeekFrom::Current(1))?; + } + + x += 1; + count -= 1; + } + } + } + + // now read the palette data located at the end of the PCX file + // palette data should be for 256 colors, 3 bytes per color = 768 bytes + // the palette is preceded by a single byte, 0x0c, which we will also validate + + reader.seek(SeekFrom::End(-769))?; + + let palette_marker = reader.read_u8()?; + if palette_marker != 0x0c { + return Err(PcxError::BadFile(String::from( + "Palette not found at end of file", + ))); + } + + let palette = Palette::load_from_bytes(reader, PaletteFormat::Normal)?; + + Ok((bmp, palette)) + } + + pub fn load_pcx_file(path: &Path) -> Result<(Bitmap, Palette), PcxError> { + let f = File::open(path)?; + let mut reader = BufReader::new(f); + Self::load_pcx_bytes(&mut reader) + } + + pub fn to_pcx_bytes( + &self, + writer: &mut T, + palette: &Palette, + ) -> Result<(), PcxError> { + let header = PcxHeader { + manufacturer: 10, + version: 5, + encoding: 1, + bpp: 8, + x1: 0, + y1: 0, + x2: self.right() as u16, + y2: self.bottom() as u16, + horizontal_dpi: 320, + vertical_dpi: 200, + ega_palette: [0u8; 48], + reserved: 0, + num_color_planes: 1, + bytes_per_line: self.width() as u16, + palette_type: 1, + horizontal_size: self.width() as u16, + vertical_size: self.height() as u16, + padding: [0u8; 54], + }; + header.write(writer)?; + + let pixels = self.pixels(); + let mut i = 0; + + for _y in 0..=self.bottom() { + // write one scanline at a time. breaking runs that could have continued across + // scanlines in the process, as per the pcx standard + + let mut run_count = 0; + let mut run_pixel = 0; + + for _x in 0..=self.right() { + let pixel = pixels[i]; + i += 1; + + if run_count == 0 { + run_count = 1; + run_pixel = pixel; + } else { + if (pixel != run_pixel) || (run_count >= 63) { + write_pcx_data(writer, run_count, run_pixel)?; + run_count = 1; + run_pixel = pixel; + } else { + run_count += 1; + } + } + } + + // end the scanline, writing out whatever run we might have had going + write_pcx_data(writer, run_count, run_pixel)?; + } + + // marker for beginning of palette data + writer.write_u8(0xc)?; + + for i in 0..=255 { + let argb = palette[i]; + let (r, g, b) = from_rgb32(argb); + writer.write_u8(r)?; + writer.write_u8(g)?; + writer.write_u8(b)?; + } + + Ok(()) + } + + pub fn to_pcx_file(&self, path: &Path, palette: &Palette) -> Result<(), PcxError> { + let f = File::create(path)?; + let mut writer = BufWriter::new(f); + self.to_pcx_bytes(&mut writer, palette) + } +} + +#[cfg(test)] +pub mod tests { + use tempfile::TempDir; + + use super::*; + + pub static TEST_BMP_PIXELS_RAW: &[u8] = + include_bytes!("../../../test-assets/test_bmp_pixels_raw.bin"); + + #[test] + pub fn load_and_save() -> Result<(), PcxError> { + let dp2_palette = + Palette::load_from_file(Path::new("./test-assets/dp2.pal"), PaletteFormat::Normal) + .unwrap(); + let tmp_dir = TempDir::new()?; + + let (bmp, palette) = Bitmap::load_pcx_file(Path::new("./test-assets/test.pcx"))?; + assert_eq!(16, bmp.width()); + assert_eq!(16, bmp.height()); + assert_eq!(bmp.pixels(), TEST_BMP_PIXELS_RAW); + assert_eq!(palette, dp2_palette); + + let save_path = tmp_dir.path().join("test_save.pcx"); + bmp.to_pcx_file(&save_path, &palette)?; + let (reloaded_bmp, reloaded_palette) = Bitmap::load_pcx_file(&save_path)?; + assert_eq!(16, reloaded_bmp.width()); + assert_eq!(16, reloaded_bmp.height()); + assert_eq!(reloaded_bmp.pixels(), TEST_BMP_PIXELS_RAW); + assert_eq!(reloaded_palette, dp2_palette); + + Ok(()) + } + + #[test] + pub fn load_larger_image() -> Result<(), PcxError> { + let (bmp, _palette) = Bitmap::load_pcx_file(Path::new("./test-assets/test_image.pcx"))?; + assert_eq!(320, bmp.width()); + assert_eq!(200, bmp.height()); + + Ok(()) + } +} diff --git a/libretrogd/src/graphics/bitmap/primitives.rs b/libretrogd/src/graphics/bitmap/primitives.rs new file mode 100644 index 0000000..1093d94 --- /dev/null +++ b/libretrogd/src/graphics/bitmap/primitives.rs @@ -0,0 +1,378 @@ +use std::mem::swap; + +use crate::{Bitmap, Character, Font, FontRenderOpts, Rect}; + +impl Bitmap { + /// Fills the entire bitmap with the given color. + pub fn clear(&mut self, color: u8) { + self.pixels.fill(color); + } + + /// Sets the pixel at the given coordinates to the color specified. If the coordinates lie + /// outside of the bitmaps clipping region, no pixels will be changed. + #[inline] + pub fn set_pixel(&mut self, x: i32, y: i32, color: u8) { + if let Some(pixels) = self.pixels_at_mut(x, y) { + pixels[0] = color; + } + } + + /// Sets the pixel at the given coordinates to the color specified. The coordinates are not + /// checked for validity, so it is up to you to ensure they lie within the bounds of the + /// bitmap. + #[inline] + pub unsafe fn set_pixel_unchecked(&mut self, x: i32, y: i32, color: u8) { + let p = self.pixels_at_mut_ptr_unchecked(x, y); + *p = color; + } + + /// Gets the pixel at the given coordinates. If the coordinates lie outside of the bitmaps + /// clipping region, None is returned. + #[inline] + pub fn get_pixel(&self, x: i32, y: i32) -> Option { + if let Some(pixels) = self.pixels_at(x, y) { + Some(pixels[0]) + } else { + None + } + } + + /// Gets the pixel at the given coordinates. The coordinates are not checked for validity, so + /// it is up to you to ensure they lie within the bounds of the bitmap. + #[inline] + pub unsafe fn get_pixel_unchecked(&self, x: i32, y: i32) -> u8 { + *(self.pixels_at_ptr_unchecked(x, y)) + } + + /// Renders a single character using the font given. + #[inline] + pub fn print_char(&mut self, ch: char, x: i32, y: i32, color: u8, font: &T) { + font.character(ch) + .draw(self, x, y, FontRenderOpts::Color(color)); + } + + /// Renders the string of text using the font given. + pub fn print_string(&mut self, text: &str, x: i32, y: i32, color: u8, font: &T) { + let mut current_x = x; + let mut current_y = y; + for ch in text.chars() { + match ch { + ' ' => current_x += font.space_width() as i32, + '\n' => { + current_x = x; + current_y += font.line_height() as i32 + } + '\r' => (), + otherwise => { + self.print_char(otherwise, current_x, current_y, color, font); + current_x += font.character(otherwise).bounds().width as i32; + } + } + } + } + + /// Draws a line from x1,y1 to x2,y2. + pub fn line(&mut self, x1: i32, y1: i32, x2: i32, y2: i32, color: u8) { + let mut dx = x1; + let mut dy = y1; + let delta_x = x2 - x1; + let delta_y = y2 - y1; + let delta_x_abs = delta_x.abs(); + let delta_y_abs = delta_y.abs(); + let delta_x_sign = delta_x.signum(); + let delta_y_sign = delta_y.signum(); + let mut x = delta_x_abs / 2; + let mut y = delta_y_abs / 2; + let offset_x_inc = delta_x_sign; + let offset_y_inc = delta_y_sign * self.width as i32; + + unsafe { + // safety: while we are blindly getting a pointer to this x/y coordinate, we don't + // write to it unless we know the coordinates are in bounds. + // TODO: should be ok ... ? or am i making too many assumptions about memory layout? + let mut dest = self.pixels_at_mut_ptr_unchecked(x1, y1); + + if self.is_xy_visible(dx, dy) { + *dest = color; + } + + if delta_x_abs >= delta_y_abs { + for _ in 0..delta_x_abs { + y += delta_y_abs; + + if y >= delta_x_abs { + y -= delta_x_abs; + dy += delta_y_sign; + dest = dest.offset(offset_y_inc as isize); + } + + dx += delta_x_sign; + dest = dest.offset(offset_x_inc as isize); + + if self.is_xy_visible(dx, dy) { + *dest = color; + } + } + } else { + for _ in 0..delta_y_abs { + x += delta_x_abs; + + if x >= delta_y_abs { + x -= delta_y_abs; + dx += delta_x_sign; + dest = dest.offset(offset_x_inc as isize); + } + + dy += delta_y_sign; + dest = dest.offset(offset_y_inc as isize); + + if self.is_xy_visible(dx, dy) { + *dest = color; + } + } + } + } + } + + /// Draws a horizontal line from x1,y to x2,y. + pub fn horiz_line(&mut self, x1: i32, x2: i32, y: i32, color: u8) { + let mut region = Rect::from_coords(x1, y, x2, y); + if region.clamp_to(&self.clip_region) { + unsafe { + let dest = self.pixels_at_mut_ptr_unchecked(region.x, region.y); + dest.write_bytes(color, region.width as usize); + } + } + } + + /// Draws a vertical line from x,y1 to x,y2. + pub fn vert_line(&mut self, x: i32, y1: i32, y2: i32, color: u8) { + let mut region = Rect::from_coords(x, y1, x, y2); + if region.clamp_to(&self.clip_region) { + unsafe { + let mut dest = self.pixels_at_mut_ptr_unchecked(region.x, region.y); + for _ in 0..region.height { + *dest = color; + dest = dest.add(self.width as usize); + } + } + } + } + + /// Draws an empty box (rectangle) using the points x1,y1 and x2,y2 to form the box to be + /// drawn, assuming they are specifying the top-left and bottom-right corners respectively. + pub fn rect(&mut self, mut x1: i32, mut y1: i32, mut x2: i32, mut y2: i32, color: u8) { + // note: need to manually do all this instead of just relying on Rect::from_coords (which + // could otherwise figure all this out for us) mainly just because we need the post-swap + // x1,y1,x2,y2 values for post-region-clamping comparison purposes ... + if x2 < x1 { + swap(&mut x1, &mut x2); + } + if y2 < y1 { + swap(&mut y1, &mut y2); + } + let mut region = Rect { + x: x1, + y: y1, + width: (x2 - x1 + 1) as u32, + height: (y2 - y1 + 1) as u32, + }; + if !region.clamp_to(&self.clip_region) { + return; + } + + // top line, only if y1 was originally within bounds + if y1 == region.y { + unsafe { + let dest = self.pixels_at_mut_ptr_unchecked(region.x, region.y); + dest.write_bytes(color, region.width as usize); + } + } + + // bottom line, only if y2 was originally within bounds + if y2 == region.bottom() { + unsafe { + let dest = self.pixels_at_mut_ptr_unchecked(region.x, region.bottom()); + dest.write_bytes(color, region.width as usize); + } + } + + // left line, only if x1 was originally within bounds + if x1 == region.x { + unsafe { + let mut dest = self.pixels_at_mut_ptr_unchecked(region.x, region.y); + for _ in 0..region.height { + *dest = color; + dest = dest.add(self.width as usize); + } + } + } + + // right line, only if x2 was originally within bounds + if x2 == region.right() { + unsafe { + let mut dest = self.pixels_at_mut_ptr_unchecked(region.right(), region.y); + for _ in 0..region.height { + *dest = color; + dest = dest.add(self.width as usize); + } + } + } + } + + /// Draws a filled box (rectangle) using the points x1,y1 and x2,y2 to form the box to be + /// drawn, assuming they are specifying the top-left and bottom-right corners respectively. + pub fn filled_rect(&mut self, x1: i32, y1: i32, x2: i32, y2: i32, color: u8) { + let mut region = Rect::from_coords(x1, y1, x2, y2); + if region.clamp_to(&self.clip_region) { + unsafe { + let mut dest = self.pixels_at_mut_ptr_unchecked(region.x, region.y); + for _ in 0..region.height { + dest.write_bytes(color, region.width as usize); + dest = dest.add(self.width as usize); + } + } + } + } + + /// Draws the outline of a circle formed by the center point and radius given. + pub fn circle(&mut self, center_x: i32, center_y: i32, radius: u32, color: u8) { + // TODO: optimize + let mut x = 0; + let mut y = radius as i32; + let mut m = 5 - 4 * radius as i32; + + while x <= y { + self.set_pixel(center_x + x, center_y + y, color); + self.set_pixel(center_x + x, center_y - y, color); + self.set_pixel(center_x - x, center_y + y, color); + self.set_pixel(center_x - x, center_y - y, color); + self.set_pixel(center_x + y, center_y + x, color); + self.set_pixel(center_x + y, center_y - x, color); + self.set_pixel(center_x - y, center_y + x, color); + self.set_pixel(center_x - y, center_y - x, color); + + if m > 0 { + y -= 1; + m -= 8 * y; + } + + x += 1; + m += 8 * x + 4; + } + } + + /// Draws a filled circle formed by the center point and radius given. + pub fn filled_circle(&mut self, center_x: i32, center_y: i32, radius: u32, color: u8) { + // TODO: optimize + let mut x = 0; + let mut y = radius as i32; + let mut m = 5 - 4 * radius as i32; + + while x <= y { + self.horiz_line(center_x - x, center_x + x, center_y - y, color); + self.horiz_line(center_x - y, center_x + y, center_y - x, color); + self.horiz_line(center_x - y, center_x + y, center_y + x, color); + self.horiz_line(center_x - x, center_x + x, center_y + y, color); + + if m > 0 { + y -= 1; + m -= 8 * y; + } + + x += 1; + m += 8 * x + 4; + } + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + #[rustfmt::skip] + #[test] + pub fn set_and_get_pixel() { + let mut bmp = Bitmap::new(8, 8).unwrap(); + + assert_eq!(None, bmp.get_pixel(-1, -1)); + + assert_eq!(0, bmp.get_pixel(0, 0).unwrap()); + bmp.set_pixel(0, 0, 7); + assert_eq!(7, bmp.get_pixel(0, 0).unwrap()); + + assert_eq!( + bmp.pixels(), + &[ + 7, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + ] + ); + + assert_eq!(0, bmp.get_pixel(2, 4).unwrap()); + bmp.set_pixel(2, 4, 5); + assert_eq!(5, bmp.get_pixel(2, 4).unwrap()); + + assert_eq!( + bmp.pixels(), + &[ + 7, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 5, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + ] + ); + } + + #[rustfmt::skip] + #[test] + pub fn set_and_get_pixel_unchecked() { + let mut bmp = Bitmap::new(8, 8).unwrap(); + + assert_eq!(0, unsafe { bmp.get_pixel_unchecked(0, 0) }); + unsafe { bmp.set_pixel_unchecked(0, 0, 7) }; + assert_eq!(7, unsafe { bmp.get_pixel_unchecked(0, 0) }); + + assert_eq!( + bmp.pixels(), + &[ + 7, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + ] + ); + + assert_eq!(0, unsafe { bmp.get_pixel_unchecked(2, 4) }); + unsafe { bmp.set_pixel_unchecked(2, 4, 5) }; + assert_eq!(5, unsafe { bmp.get_pixel_unchecked(2, 4) }); + + assert_eq!( + bmp.pixels(), + &[ + 7, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 5, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + ] + ); + } +} diff --git a/libretrogd/src/graphics/bitmapatlas.rs b/libretrogd/src/graphics/bitmapatlas.rs new file mode 100644 index 0000000..423b1f7 --- /dev/null +++ b/libretrogd/src/graphics/bitmapatlas.rs @@ -0,0 +1,178 @@ +use std::ops::Index; + +use thiserror::Error; + +use crate::{Bitmap, Rect}; + +#[derive(Error, Debug)] +pub enum BitmapAtlasError { + #[error("Region is out of bounds for the Bitmap used by the BitmapAtlas")] + OutOfBounds, +} + +#[derive(Debug)] +pub struct BitmapAtlas { + bitmap: Bitmap, + bounds: Rect, + tiles: Vec, +} + +impl BitmapAtlas { + pub fn new(bitmap: Bitmap) -> BitmapAtlas { + let bounds = bitmap.full_bounds(); + BitmapAtlas { + bitmap, + bounds, + tiles: Vec::new(), + } + } + + pub fn add(&mut self, rect: Rect) -> Result { + if !self.bounds.contains_rect(&rect) { + return Err(BitmapAtlasError::OutOfBounds); + } + + self.tiles.push(rect); + Ok(self.tiles.len() - 1) + } + + pub fn add_grid( + &mut self, + tile_width: u32, + tile_height: u32, + ) -> Result { + if self.bounds.width < tile_width || self.bounds.height < tile_height { + return Err(BitmapAtlasError::OutOfBounds); + } + + for yt in 0..(self.bounds.height / tile_height) { + for xt in 0..(self.bounds.width) / tile_width { + let x = xt * tile_width; + let y = yt * tile_height; + let rect = Rect::new(x as i32, y as i32, tile_width, tile_height); + self.tiles.push(rect); + } + } + + Ok(self.tiles.len() - 1) + } + + pub fn add_custom_grid( + &mut self, + start_x: u32, + start_y: u32, + tile_width: u32, + tile_height: u32, + x_tiles: u32, + y_tiles: u32, + border: u32, + ) -> Result { + // figure out of the grid properties given would result in us creating any + // rects that lie out of the bounds of this bitmap + let grid_region = Rect::new( + start_x as i32, + start_y as i32, + (tile_width + border) * x_tiles + border, + (tile_height + border) * y_tiles + border, + ); + if !self.bounds.contains_rect(&grid_region) { + return Err(BitmapAtlasError::OutOfBounds); + } + + // all good! now create all the tiles needed for the grid specified + for yt in 0..y_tiles { + for xt in 0..x_tiles { + let x = start_x + (tile_width + border) * xt; + let y = start_y + (tile_height + border) * yt; + let rect = Rect::new(x as i32, y as i32, tile_width, tile_height); + self.tiles.push(rect); + } + } + + Ok(self.tiles.len() - 1) + } + + pub fn clear(&mut self) { + self.tiles.clear() + } + + pub fn len(&self) -> usize { + self.tiles.len() + } + + pub fn bitmap(&self) -> &Bitmap { + &self.bitmap + } +} + +impl Index for BitmapAtlas { + type Output = Rect; + + fn index(&self, index: usize) -> &Self::Output { + &self.tiles[index] + } +} + +#[cfg(test)] +pub mod tests { + use claim::assert_matches; + + use super::*; + + #[test] + pub fn adding_rects() { + let bmp = Bitmap::new(64, 64).unwrap(); + let mut atlas = BitmapAtlas::new(bmp); + + let rect = Rect::new(0, 0, 16, 16); + assert_eq!(0, atlas.add(rect.clone()).unwrap()); + assert_eq!(rect, atlas[0]); + assert_eq!(1, atlas.len()); + + let rect = Rect::new(16, 0, 16, 16); + assert_eq!(1, atlas.add(rect.clone()).unwrap()); + assert_eq!(rect, atlas[1]); + assert_eq!(2, atlas.len()); + + assert_matches!( + atlas.add(Rect::new(56, 0, 16, 16)), + Err(BitmapAtlasError::OutOfBounds) + ); + assert_eq!(2, atlas.len()); + + assert_matches!( + atlas.add(Rect::new(-8, 4, 16, 16)), + Err(BitmapAtlasError::OutOfBounds) + ); + assert_eq!(2, atlas.len()); + + assert_matches!( + atlas.add(Rect::new(0, 0, 128, 128)), + Err(BitmapAtlasError::OutOfBounds) + ); + assert_eq!(2, atlas.len()); + } + + #[test] + pub fn adding_grid() { + let bmp = Bitmap::new(64, 64).unwrap(); + let mut atlas = BitmapAtlas::new(bmp); + + assert_eq!(3, atlas.add_custom_grid(0, 0, 8, 8, 2, 2, 0).unwrap()); + assert_eq!(4, atlas.len()); + assert_eq!(Rect::new(0, 0, 8, 8), atlas[0]); + assert_eq!(Rect::new(8, 0, 8, 8), atlas[1]); + assert_eq!(Rect::new(0, 8, 8, 8), atlas[2]); + assert_eq!(Rect::new(8, 8, 8, 8), atlas[3]); + + atlas.clear(); + assert_eq!(0, atlas.len()); + + assert_eq!(3, atlas.add_custom_grid(0, 0, 4, 8, 2, 2, 1).unwrap()); + assert_eq!(4, atlas.len()); + assert_eq!(Rect::new(0, 0, 4, 8), atlas[0]); + assert_eq!(Rect::new(5, 0, 4, 8), atlas[1]); + assert_eq!(Rect::new(0, 9, 4, 8), atlas[2]); + assert_eq!(Rect::new(5, 9, 4, 8), atlas[3]); + } +} diff --git a/libretrogd/src/graphics/font.rs b/libretrogd/src/graphics/font.rs new file mode 100644 index 0000000..47dc987 --- /dev/null +++ b/libretrogd/src/graphics/font.rs @@ -0,0 +1,202 @@ +use std::fs::File; +use std::io::{BufReader, BufWriter, Cursor}; +use std::path::Path; + +use byteorder::{ReadBytesExt, WriteBytesExt}; +use thiserror::Error; + +use crate::{Bitmap, Rect}; + +pub static VGA_FONT_BYTES: &[u8] = include_bytes!("../../assets/vga.fnt"); + +pub const NUM_CHARS: usize = 256; +pub const CHAR_HEIGHT: usize = 8; +pub const CHAR_FIXED_WIDTH: usize = 8; + +#[derive(Error, Debug)] +pub enum FontError { + #[error("Invalid font file: {0}")] + InvalidFile(String), + + #[error("Font I/O error")] + IOError(#[from] std::io::Error), +} + +#[derive(Debug)] +pub enum FontRenderOpts { + Color(u8), + None, +} + +pub trait Character { + fn bounds(&self) -> &Rect; + fn draw(&self, dest: &mut Bitmap, x: i32, y: i32, opts: FontRenderOpts); +} + +pub trait Font { + type CharacterType: Character; + + fn character(&self, ch: char) -> &Self::CharacterType; + fn space_width(&self) -> u8; + fn line_height(&self) -> u8; +} + +#[derive(Debug)] +pub struct BitmaskCharacter { + bytes: [u8; CHAR_HEIGHT], + bounds: Rect, +} + +impl Character for BitmaskCharacter { + #[inline] + fn bounds(&self) -> &Rect { + &self.bounds + } + + fn draw(&self, dest: &mut Bitmap, x: i32, y: i32, opts: FontRenderOpts) { + // out of bounds check + if ((x + self.bounds.width as i32) < dest.clip_region().x) + || ((y + self.bounds.height as i32) < dest.clip_region().y) + || (x >= dest.clip_region().right()) + || (y >= dest.clip_region().bottom()) + { + return; + } + + let color = match opts { + FontRenderOpts::Color(color) => color, + _ => 0, + }; + + // TODO: i'm sure this can be optimized, lol + for char_y in 0..self.bounds.height as usize { + let mut bit_mask = 0x80; + for char_x in 0..self.bounds.width as usize { + if self.bytes[char_y] & bit_mask > 0 { + dest.set_pixel(x + char_x as i32, y + char_y as i32, color); + } + bit_mask >>= 1; + } + } + } +} + +#[derive(Debug)] +pub struct BitmaskFont { + characters: Box<[BitmaskCharacter]>, + line_height: u8, + space_width: u8, +} + +impl BitmaskFont { + pub fn new_vga_font() -> Result { + BitmaskFont::load_from_bytes(&mut Cursor::new(VGA_FONT_BYTES)) + } + + pub fn load_from_file(path: &Path) -> Result { + let f = File::open(path)?; + let mut reader = BufReader::new(f); + + BitmaskFont::load_from_bytes(&mut reader) + } + + pub fn load_from_bytes(reader: &mut T) -> Result { + let mut characters: Vec = Vec::with_capacity(NUM_CHARS); + + // read character bitmap data + for _ in 0..NUM_CHARS { + let mut buffer = [0u8; CHAR_HEIGHT]; + reader.read_exact(&mut buffer)?; + let character = BitmaskCharacter { + bytes: buffer, + // bounds are filled in below. ugh. + bounds: Rect { + x: 0, + y: 0, + width: 0, + height: 0, + }, + }; + characters.push(character); + } + + // read character widths (used for rendering) + for i in 0..NUM_CHARS { + characters[i].bounds.width = reader.read_u8()? as u32; + } + + // read global font height (used for rendering) + let line_height = reader.read_u8()?; + for i in 0..NUM_CHARS { + characters[i].bounds.height = line_height as u32; + } + + let space_width = characters[' ' as usize].bounds.width as u8; + + Ok(BitmaskFont { + characters: characters.into_boxed_slice(), + line_height, + space_width, + }) + } + + pub fn to_file(&self, path: &Path) -> Result<(), FontError> { + let f = File::create(path)?; + let mut writer = BufWriter::new(f); + self.to_bytes(&mut writer) + } + + pub fn to_bytes(&self, writer: &mut T) -> Result<(), FontError> { + // write character bitmap data + for i in 0..NUM_CHARS { + writer.write_all(&self.characters[i].bytes)?; + } + + // write character widths + for i in 0..NUM_CHARS { + writer.write_u8(self.characters[i].bounds.width as u8)?; + } + + // write global font height + writer.write_u8(self.line_height)?; + + Ok(()) + } +} + +impl Font for BitmaskFont { + type CharacterType = BitmaskCharacter; + + #[inline] + fn character(&self, ch: char) -> &Self::CharacterType { + &self.characters[ch as usize] + } + + #[inline] + fn space_width(&self) -> u8 { + self.space_width + } + + #[inline] + fn line_height(&self) -> u8 { + self.line_height + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + #[test] + pub fn load_font() -> Result<(), FontError> { + let font = BitmaskFont::load_from_file(Path::new("./assets/vga.fnt"))?; + assert_eq!(256, font.characters.len()); + assert_eq!(CHAR_FIXED_WIDTH as u8, font.space_width); + for character in font.characters.iter() { + assert_eq!(CHAR_FIXED_WIDTH as u8, character.bounds.width as u8); + assert_eq!(CHAR_HEIGHT, character.bytes.len()); + } + + Ok(()) + } +} diff --git a/libretrogd/src/graphics/mod.rs b/libretrogd/src/graphics/mod.rs new file mode 100644 index 0000000..631f704 --- /dev/null +++ b/libretrogd/src/graphics/mod.rs @@ -0,0 +1,4 @@ +pub mod bitmap; +pub mod bitmapatlas; +pub mod font; +pub mod palette; diff --git a/libretrogd/src/graphics/palette.rs b/libretrogd/src/graphics/palette.rs new file mode 100644 index 0000000..d9b5a59 --- /dev/null +++ b/libretrogd/src/graphics/palette.rs @@ -0,0 +1,563 @@ +use std::cmp::min; +use std::fs::File; +use std::io::{BufReader, BufWriter, Cursor}; +use std::ops::{Bound, Index, IndexMut, RangeBounds}; +use std::path::Path; + +use byteorder::{ReadBytesExt, WriteBytesExt}; +use thiserror::Error; + +use crate::utils::abs_diff; +use crate::NUM_COLORS; + +// silly "hack" (???) which allows us to alias the generic constraint `RangeBounds + Iterator` to `ColorRange` +pub trait ColorRange: RangeBounds + Iterator {} +impl ColorRange for T where T: RangeBounds + Iterator {} + +pub static VGA_PALETTE_BYTES: &[u8] = include_bytes!("../../assets/vga.pal"); + +/// Converts a set of individual ARGB components to a combined 32-bit color value, packed into +/// the format 0xAARRGGBB +/// +/// # Arguments +/// +/// * `a`: the alpha component (0-255) +/// * `r`: the red component (0-255) +/// * `g`: the green component (0-255) +/// * `b`: the blue component (0-255) +/// +/// returns: the u32 packed color +#[inline] +pub fn to_argb32(a: u8, r: u8, g: u8, b: u8) -> u32 { + (b as u32) + ((g as u32) << 8) + ((r as u32) << 16) + ((a as u32) << 24) +} + +/// Extracts the individual ARGB components out of a combined 32-bit color value which is in the +/// format 0xAARRGGBB +/// +/// # Arguments +/// +/// * `argb`: the 32-bit packed color +/// +/// returns: the individual ARGB color components (0-255 each) in order: alpha, red, green, blue +#[inline] +pub fn from_argb32(argb: u32) -> (u8, u8, u8, u8) { + let a = ((argb & 0xff000000) >> 24) as u8; + let r = ((argb & 0x00ff0000) >> 16) as u8; + let g = ((argb & 0x0000ff00) >> 8) as u8; + let b = (argb & 0x000000ff) as u8; + (a, r, g, b) +} + +/// Converts a set of individual RGB components to a combined 32-bit color value, packed into +/// the format 0xAARRGGBB. Substitutes a value of 255 for the missing alpha component. +/// +/// # Arguments +/// +/// * `r`: the red component (0-255) +/// * `g`: the green component (0-255) +/// * `b`: the blue component (0-255) +/// +/// returns: the u32 packed color +#[inline] +pub fn to_rgb32(r: u8, g: u8, b: u8) -> u32 { + to_argb32(255, r, g, b) +} + +/// Extracts the individual RGB components out of a combined 32-bit color value which is in the +/// format 0xAARRGGBB. Ignores the alpha component. +/// +/// # Arguments +/// +/// * `argb`: the 32-bit packed color +/// +/// returns: the individual ARGB color components (0-255 each) in order: red, green, blue +#[inline] +pub fn from_rgb32(rgb: u32) -> (u8, u8, u8) { + // ignore alpha component at 0xff000000 ... + let r = ((rgb & 0x00ff0000) >> 16) as u8; + let g = ((rgb & 0x0000ff00) >> 8) as u8; + let b = (rgb & 0x000000ff) as u8; + (r, g, b) +} + +/// Linearly interpolates between two 32-bit packed colors in the format 0xAARRGGBB. +/// +/// # Arguments +/// +/// * `a`: the first 32-bit packed color +/// * `b`: the second 32-bit packed color +/// * `t`: the amount to interpolate between the two values, specified as a fraction. +#[inline] +pub fn lerp_argb32(a: u32, b: u32, t: f32) -> u32 { + let (a1, r1, g1, b1) = from_argb32(a); + let (a2, r2, g2, b2) = from_argb32(b); + to_argb32( + ((a1 as f32) + ((a2 as f32) - (a1 as f32)) * t) as u8, + ((r1 as f32) + ((r2 as f32) - (r1 as f32)) * t) as u8, + ((g1 as f32) + ((g2 as f32) - (g1 as f32)) * t) as u8, + ((b1 as f32) + ((b2 as f32) - (b1 as f32)) * t) as u8, + ) +} + +/// Linearly interpolates between two 32-bit packed colors in the format 0xAARRGGBB. Ignores the +/// alpha component, which will always be set to 255 in the return value. +/// +/// # Arguments +/// +/// * `a`: the first 32-bit packed color +/// * `b`: the second 32-bit packed color +/// * `t`: the amount to interpolate between the two values, specified as a fraction. +#[inline] +pub fn lerp_rgb32(a: u32, b: u32, t: f32) -> u32 { + let (r1, g1, b1) = from_rgb32(a); + let (r2, g2, b2) = from_rgb32(b); + to_rgb32( + ((r1 as f32) + ((r2 as f32) - (r1 as f32)) * t) as u8, + ((g1 as f32) + ((g2 as f32) - (g1 as f32)) * t) as u8, + ((b1 as f32) + ((b2 as f32) - (b1 as f32)) * t) as u8, + ) +} + +// vga bios (0-63) format +fn read_256color_6bit_palette( + reader: &mut T, +) -> Result<[u32; NUM_COLORS], PaletteError> { + let mut colors = [0u32; NUM_COLORS]; + for color in colors.iter_mut() { + let r = reader.read_u8()?; + let g = reader.read_u8()?; + let b = reader.read_u8()?; + *color = to_rgb32(r * 4, g * 4, b * 4); + } + Ok(colors) +} + +fn write_256color_6bit_palette( + writer: &mut T, + colors: &[u32; NUM_COLORS], +) -> Result<(), PaletteError> { + for color in colors.iter() { + let (r, g, b) = from_rgb32(*color); + writer.write_u8(r / 4)?; + writer.write_u8(g / 4)?; + writer.write_u8(b / 4)?; + } + Ok(()) +} + +// normal (0-255) format +fn read_256color_8bit_palette( + reader: &mut T, +) -> Result<[u32; NUM_COLORS], PaletteError> { + let mut colors = [0u32; NUM_COLORS]; + for color in colors.iter_mut() { + let r = reader.read_u8()?; + let g = reader.read_u8()?; + let b = reader.read_u8()?; + *color = to_rgb32(r, g, b); + } + Ok(colors) +} + +fn write_256color_8bit_palette( + writer: &mut T, + colors: &[u32; NUM_COLORS], +) -> Result<(), PaletteError> { + for color in colors.iter() { + let (r, g, b) = from_rgb32(*color); + writer.write_u8(r)?; + writer.write_u8(g)?; + writer.write_u8(b)?; + } + Ok(()) +} + +#[derive(Error, Debug)] +pub enum PaletteError { + #[error("Palette I/O error")] + IOError(#[from] std::io::Error), +} + +pub enum PaletteFormat { + /// Individual RGB components in 6-bits (0-63) for VGA BIOS compatibility + Vga, + /// Individual RGB components in 8-bits (0-255) + Normal, +} + +/// Contains a 256 color palette, and provides methods useful for working with palettes. The +/// colors are all stored individually as 32-bit packed values in the format 0xAARRGGBB. +#[derive(Debug, Clone)] +pub struct Palette { + colors: [u32; NUM_COLORS], +} + +impl Palette { + /// Creates a new Palette with all black colors. + pub fn new() -> Palette { + Palette { + colors: [0; NUM_COLORS], + } + } + + /// Creates a new Palette, pre-loaded with the default VGA BIOS colors. + pub fn new_vga_palette() -> Result { + Palette::load_from_bytes(&mut Cursor::new(VGA_PALETTE_BYTES), PaletteFormat::Vga) + } + + /// Loads and returns a Palette from a palette file on disk. + /// + /// # Arguments + /// + /// * `path`: the path of the palette file to be loaded + /// * `format`: the format that the palette data is expected to be in + pub fn load_from_file(path: &Path, format: PaletteFormat) -> Result { + let f = File::open(path)?; + let mut reader = BufReader::new(f); + Self::load_from_bytes(&mut reader, format) + } + + /// Loads and returns a Palette from a reader. The data being loaded is expected to be the same + /// as if the palette was being loaded from a file on disk. + /// + /// # Arguments + /// + /// * `reader`: the reader to load the palette from + /// * `format`: the format that the palette data is expected to be in + pub fn load_from_bytes( + reader: &mut T, + format: PaletteFormat, + ) -> Result { + let colors = match format { + PaletteFormat::Vga => read_256color_6bit_palette(reader)?, + PaletteFormat::Normal => read_256color_8bit_palette(reader)?, + }; + Ok(Palette { colors }) + } + + /// Writes the palette to a file on disk. If the file already exists, it will be overwritten. + /// + /// # Arguments + /// + /// * `path`: the path of the file to save the palette to + /// * `format`: the format to write the palette data in + pub fn to_file(&self, path: &Path, format: PaletteFormat) -> Result<(), PaletteError> { + let f = File::create(path)?; + let mut writer = BufWriter::new(f); + self.to_bytes(&mut writer, format) + } + + /// Writes the palette to a writer, in the same format as if it was writing to a file on disk. + /// + /// # Arguments + /// + /// * `writer`: the writer to write palette data to + /// * `format`: the format to write the palette data in + pub fn to_bytes( + &self, + writer: &mut T, + format: PaletteFormat, + ) -> Result<(), PaletteError> { + match format { + PaletteFormat::Vga => write_256color_6bit_palette(writer, &self.colors), + PaletteFormat::Normal => write_256color_8bit_palette(writer, &self.colors), + } + } + + /// Fades a single color in the palette from its current RGB values towards the given RGB + /// values by up to the step amount given. This function is intended to be run many times + /// over a number of frames where each run completes a small step towards the complete fade. + /// + /// # Arguments + /// + /// * `color`: the color index to fade + /// * `target_r`: the target red component (0-255) to fade towards + /// * `target_g`: the target green component (0-255) to fade towards + /// * `target_b`: the target blue component (0-255) to fade towards + /// * `step`: the amount to "step" by towards the target RGB values + /// + /// returns: true if the color has reached the target RGB values, false otherwise + pub fn fade_color_toward_rgb( + &mut self, + color: u8, + target_r: u8, + target_g: u8, + target_b: u8, + step: u8, + ) -> bool { + let mut modified = false; + + let (mut r, mut g, mut b) = from_rgb32(self.colors[color as usize]); + + if r != target_r { + modified = true; + let diff_r = r.overflowing_sub(target_r).0; + if r > target_r { + r -= min(step, diff_r); + } else { + r += min(step, diff_r); + } + } + + if g != target_g { + modified = true; + let diff_g = g.overflowing_sub(target_g).0; + if g > target_g { + g -= min(step, diff_g); + } else { + g += min(step, diff_g); + } + } + + if b != target_b { + modified = true; + let diff_b = b.overflowing_sub(target_b).0; + if b > target_b { + b -= min(step, diff_b); + } else { + b += min(step, diff_b); + } + } + + if modified { + self.colors[color as usize] = to_rgb32(r, g, b); + } + + (target_r == r) && (target_g == g) && (target_b == b) + } + + /// Fades a range of colors in the palette from their current RGB values all towards the given + /// RGB values by up to the step amount given. This function is intended to be run many times + /// over a number of frames where each run completes a small step towards the complete fade. + /// + /// # Arguments + /// + /// * `colors`: the range of colors to be faded + /// * `target_r`: the target red component (0-255) to fade towards + /// * `target_g`: the target green component (0-255) to fade towards + /// * `target_b`: the target blue component (0-255) to fade towards + /// * `step`: the amount to "step" by towards the target RGB values + /// + /// returns: true if all of the colors in the range have reached the target RGB values, false + /// otherwise + pub fn fade_colors_toward_rgb( + &mut self, + colors: T, + target_r: u8, + target_g: u8, + target_b: u8, + step: u8, + ) -> bool { + let mut all_faded = true; + for color in colors { + if !self.fade_color_toward_rgb(color, target_r, target_g, target_b, step) { + all_faded = false; + } + } + all_faded + } + + /// Fades a range of colors in the palette from their current RGB values all towards the RGB + /// values in the other palette specified, by up to the step amount given. This function is + /// intended to be run many times over a number of frames where each run completes a small step + /// towards the complete fade. + /// + /// # Arguments + /// + /// * `colors`: the range of colors to be faded + /// * `palette`: the other palette to use as the target to fade towards + /// * `step`: the amount to "step" by towards the target RGB values + /// + /// returns: true if all of the colors in the range have reached the RGB values from the other + /// target palette, false otherwise + pub fn fade_colors_toward_palette( + &mut self, + colors: T, + palette: &Palette, + step: u8, + ) -> bool { + let mut all_faded = true; + for color in colors { + let (r, g, b) = from_rgb32(palette[color]); + if !self.fade_color_toward_rgb(color, r, g, b, step) { + all_faded = false; + } + } + all_faded + } + + /// Linearly interpolates between the specified colors in two palettes, storing the + /// interpolation results in this palette. + /// + /// # Arguments + /// + /// * `colors`: the range of colors to be interpolated + /// * `a`: the first palette + /// * `b`: the second palette + /// * `t`: the amount to interpolate between the two palettes, specified as a fraction + pub fn lerp(&mut self, colors: T, a: &Palette, b: &Palette, t: f32) { + for color in colors { + self[color] = lerp_rgb32(a[color], b[color], t); + } + } + + /// Rotates a range of colors in the palette by a given amount. + /// + /// # Arguments + /// + /// * `colors`: the range of colors to be rotated + /// * `step`: the number of positions (and direction) to rotate all colors by + pub fn rotate_colors(&mut self, colors: T, step: i8) { + use Bound::*; + let start = match colors.start_bound() { + Excluded(&start) => start + 1, + Included(&start) => start, + Unbounded => 0, + } as usize; + let end = match colors.end_bound() { + Excluded(&end) => end - 1, + Included(&end) => end, + Unbounded => 255, + } as usize; + let subset = &mut self.colors[start..=end]; + match step.signum() { + -1 => subset.rotate_left(step.abs() as usize), + 1 => subset.rotate_right(step.abs() as usize), + _ => {} + } + } + + /// Finds and returns the index of the closest color in this palette to the RGB values provided. + /// This will not always return great results. It depends largely on the palette and the RGB + /// values being searched (for example, searching for bright green 0,255,0 in a palette which + /// contains no green hues at all is not likely to return a useful result). + pub fn find_color(&self, r: u8, g: u8, b: u8) -> u8 { + let mut closest_distance = 255 * 3; + let mut closest = 0; + + for (index, color) in self.colors.iter().enumerate() { + let (this_r, this_g, this_b) = from_rgb32(*color); + + // this comparison method is using the sRGB Euclidean formula described here: + // https://en.wikipedia.org/wiki/Color_difference + + let distance = abs_diff(this_r, r) as u32 + + abs_diff(this_g, g) as u32 + + abs_diff(this_b, b) as u32; + + if distance < closest_distance { + closest = index as u8; + closest_distance = distance; + } + } + + closest + } +} + +impl Index for Palette { + type Output = u32; + + #[inline] + fn index(&self, index: u8) -> &Self::Output { + &self.colors[index as usize] + } +} + +impl IndexMut for Palette { + #[inline] + fn index_mut(&mut self, index: u8) -> &mut Self::Output { + &mut self.colors[index as usize] + } +} + +impl PartialEq for Palette { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.colors == other.colors + } +} + +#[cfg(test)] +mod tests { + use tempfile::TempDir; + + use super::*; + + #[test] + fn argb_conversions() { + let argb = to_argb32(0x11, 0x22, 0x33, 0x44); + assert_eq!(argb, 0x11223344); + + let argb = to_rgb32(0x22, 0x33, 0x44); + assert_eq!(argb, 0xff223344); + + let (a, r, g, b) = from_argb32(0x11223344); + assert_eq!(0x11, a); + assert_eq!(0x22, r); + assert_eq!(0x33, g); + assert_eq!(0x44, b); + + let (r, g, b) = from_rgb32(0x11223344); + assert_eq!(0x22, r); + assert_eq!(0x33, g); + assert_eq!(0x44, b); + } + + #[test] + fn get_and_set_colors() { + let mut palette = Palette::new(); + assert_eq!(0, palette[0]); + assert_eq!(0, palette[1]); + palette[0] = 0x11223344; + assert_eq!(0x11223344, palette[0]); + assert_eq!(0, palette[1]); + } + + fn assert_vga_palette(palette: &Palette) { + assert_eq!(0xff000000, palette[0]); + assert_eq!(0xff0000a8, palette[1]); + assert_eq!(0xff00a800, palette[2]); + assert_eq!(0xff00a8a8, palette[3]); + assert_eq!(0xffa80000, palette[4]); + assert_eq!(0xffa800a8, palette[5]); + assert_eq!(0xffa85400, palette[6]); + assert_eq!(0xffa8a8a8, palette[7]); + assert_eq!(0xff545454, palette[8]); + assert_eq!(0xff5454fc, palette[9]); + assert_eq!(0xff54fc54, palette[10]); + assert_eq!(0xff54fcfc, palette[11]); + assert_eq!(0xfffc5454, palette[12]); + assert_eq!(0xfffc54fc, palette[13]); + assert_eq!(0xfffcfc54, palette[14]); + assert_eq!(0xfffcfcfc, palette[15]); + } + + #[test] + fn load_and_save() -> Result<(), PaletteError> { + let tmp_dir = TempDir::new()?; + + // vga format + + let palette = Palette::load_from_file(Path::new("./assets/vga.pal"), PaletteFormat::Vga)?; + assert_vga_palette(&palette); + + let save_path = tmp_dir.path().join("test_save_vga_format.pal"); + palette.to_file(&save_path, PaletteFormat::Vga)?; + let reloaded_palette = Palette::load_from_file(&save_path, PaletteFormat::Vga)?; + assert_eq!(palette, reloaded_palette); + + // normal format + + let palette = + Palette::load_from_file(Path::new("./test-assets/dp2.pal"), PaletteFormat::Normal)?; + + let save_path = tmp_dir.path().join("test_save_normal_format.pal"); + palette.to_file(&save_path, PaletteFormat::Normal)?; + let reloaded_palette = Palette::load_from_file(&save_path, PaletteFormat::Normal)?; + assert_eq!(palette, reloaded_palette); + + Ok(()) + } +} diff --git a/libretrogd/src/lib.rs b/libretrogd/src/lib.rs new file mode 100644 index 0000000..89da3f5 --- /dev/null +++ b/libretrogd/src/lib.rs @@ -0,0 +1,68 @@ +extern crate core; +extern crate sdl2; + +pub use crate::graphics::bitmap::*; +pub use crate::graphics::bitmapatlas::*; +pub use crate::graphics::font::*; +pub use crate::graphics::palette::*; +pub use crate::graphics::*; +pub use crate::math::circle::*; +pub use crate::math::matrix3x3::*; +pub use crate::math::rect::*; +pub use crate::math::vector2::*; +pub use crate::math::*; +pub use crate::system::input_devices::keyboard::*; +pub use crate::system::input_devices::mouse::*; +pub use crate::system::input_devices::*; +pub use crate::system::*; + +pub mod entities; +pub mod events; +pub mod graphics; +pub mod math; +pub mod states; +pub mod system; +pub mod utils; + +pub const LOW_RES: bool = if cfg!(feature = "low_res") { + true +} else { + false +}; +pub const WIDE_SCREEN: bool = if cfg!(feature = "wide") { + true +} else { + false +}; + +pub const SCREEN_WIDTH: u32 = if cfg!(feature = "low_res") { + if cfg!(feature = "wide") { + 214 + } else { + 160 + } +} else { + if cfg!(feature = "wide") { + 428 + } else { + 320 + } +}; +pub const SCREEN_HEIGHT: u32 = if cfg!(feature = "low_res") { + 120 +} else { + 240 +}; + +pub const SCREEN_TOP: u32 = 0; +pub const SCREEN_LEFT: u32 = 0; +pub const SCREEN_RIGHT: u32 = SCREEN_WIDTH - 1; +pub const SCREEN_BOTTOM: u32 = SCREEN_HEIGHT - 1; + +pub const DEFAULT_SCALE_FACTOR: u32 = if cfg!(feature = "low_res") { + 6 +} else { + 3 +}; + +pub const NUM_COLORS: usize = 256; // i mean ... the number of colors is really defined by the size of u8 ... diff --git a/libretrogd/src/math/circle.rs b/libretrogd/src/math/circle.rs new file mode 100644 index 0000000..f892887 --- /dev/null +++ b/libretrogd/src/math/circle.rs @@ -0,0 +1,113 @@ +use crate::{distance_between, distance_squared_between, Vector2}; + +/// Represents a 2D circle, using integer center coordinates and radius. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct Circle { + pub x: i32, + pub y: i32, + pub radius: u32, +} + +impl Circle { + #[inline] + pub fn new(x: i32, y: i32, radius: u32) -> Self { + Circle { x, y, radius } + } + + pub fn new_encapsulating(points: &[Vector2]) -> Option { + if points.is_empty() { + return None; + } + + let mut min_x = points[0].x; + let mut min_y = points[0].y; + let mut max_x = min_x; + let mut max_y = min_y; + + for i in 0..points.len() { + let point = &points[i]; + min_x = point.x.min(min_x); + min_y = point.y.min(min_y); + max_x = point.x.max(max_x); + max_y = point.y.max(max_y); + } + + let radius = distance_between(min_x, min_y, max_x, max_y) * 0.5; + let center_x = (max_x - min_x) / 2.0; + let center_y = (max_y - min_y) / 2.0; + + Some(Circle { + x: center_x as i32, + y: center_y as i32, + radius: radius as u32, + }) + } + + /// Calculates the diameter of the circle. + #[inline] + pub fn diameter(&self) -> u32 { + self.radius * 2 + } + + /// Returns true if the given point is contained within the bounds of this circle. + pub fn contains_point(&self, x: i32, y: i32) -> bool { + let distance_squared = + distance_squared_between(self.x as f32, self.y as f32, x as f32, y as f32); + let radius_squared = (self.radius * self.radius) as f32; + distance_squared <= radius_squared + } + + /// Returns true if the given circle at least partially overlaps the bounds of this circle. + pub fn overlaps(&self, other: &Circle) -> bool { + let distance_squared = + distance_squared_between(self.x as f32, self.y as f32, other.x as f32, other.y as f32); + let minimum_distance_squared = + ((self.radius + other.radius) * (self.radius + other.radius)) as f32; + distance_squared <= minimum_distance_squared + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + #[test] + pub fn test_new() { + let c = Circle::new(1, 2, 4); + assert_eq!(1, c.x); + assert_eq!(2, c.y); + assert_eq!(4, c.radius); + } + + #[test] + pub fn test_diameter() { + let c = Circle::new(4, 4, 3); + assert_eq!(6, c.diameter()); + } + + #[test] + pub fn test_contains_point() { + let c = Circle::new(1, 1, 6); + assert!(c.contains_point(4, 4)); + assert!(!c.contains_point(8, 4)); + + let c = Circle::new(0, 1, 2); + assert!(!c.contains_point(3, 3)); + assert!(c.contains_point(0, 0)); + } + + #[test] + pub fn test_overlaps() { + let a = Circle::new(3, 4, 5); + let b = Circle::new(14, 18, 8); + assert!(!a.overlaps(&b)); + + let a = Circle::new(2, 3, 12); + let b = Circle::new(15, 28, 10); + assert!(!a.overlaps(&b)); + + let a = Circle::new(-10, 8, 30); + let b = Circle::new(14, -24, 10); + assert!(a.overlaps(&b)); + } +} diff --git a/libretrogd/src/math/matrix3x3.rs b/libretrogd/src/math/matrix3x3.rs new file mode 100644 index 0000000..7cb2f98 --- /dev/null +++ b/libretrogd/src/math/matrix3x3.rs @@ -0,0 +1,420 @@ +use std::ops::{Mul, MulAssign}; + +use crate::{nearly_equal, Vector2}; + +/// Represents a 3x3 column-major matrix and provides common methods for matrix math. +#[derive(Debug, Copy, Clone)] +pub struct Matrix3x3 { + pub m: [f32; 9], +} + +impl Matrix3x3 { + pub const M11: usize = 0; + pub const M12: usize = 3; + pub const M13: usize = 6; + pub const M21: usize = 1; + pub const M22: usize = 4; + pub const M23: usize = 7; + pub const M31: usize = 2; + pub const M32: usize = 5; + pub const M33: usize = 8; + + pub const IDENTITY: Matrix3x3 = Matrix3x3 { + m: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0], + }; + + /// Returns a new identity matrix. + #[inline] + pub fn identity() -> Matrix3x3 { + Matrix3x3::IDENTITY + } + + /// Creates a new matrix with the specified elements. + #[rustfmt::skip] + #[inline] + pub fn new( + m11: f32, m12: f32, m13: f32, + m21: f32, m22: f32, m23: f32, + m31: f32, m32: f32, m33: f32, + ) -> Matrix3x3 { + Matrix3x3 { + m: [ + m11, m21, m31, + m12, m22, m32, + m13, m23, m33 + ], + } + } + + /// Creates a new rotation matrix from a set of euler angles. + /// + /// # Arguments + /// + /// * `x`: the x angle (in radians) + /// * `y`: the y angle (in radians) + /// * `z`: the z angle (in radians) + pub fn from_euler_angles(x: f32, y: f32, z: f32) -> Matrix3x3 { + let rotate_z = Matrix3x3::new_rotation_z(z); + let rotate_y = Matrix3x3::new_rotation_y(y); + let rotate_x = Matrix3x3::new_rotation_x(x); + + // "right-to-left" column-major matrix concatenation + rotate_z * rotate_y * rotate_x + } + + /// Creates a new rotation matrix for rotation around the x axis. + /// + /// # Arguments + /// + /// * `radians`: angle to rotate the x axis around (in radians) + #[rustfmt::skip] + #[inline] + pub fn new_rotation_x(radians: f32) -> Matrix3x3 { + let (s, c) = radians.sin_cos(); + Matrix3x3::new( + 1.0, 0.0, 0.0, + 0.0, c, -s, + 0.0, s, c + ) + } + + /// Creates a new rotation matrix for rotation around the y axis. + /// + /// # Arguments + /// + /// * `radians`: angle to rotate the y axis around (in radians) + #[rustfmt::skip] + #[inline] + pub fn new_rotation_y(radians: f32) -> Matrix3x3 { + let (s, c) = radians.sin_cos(); + Matrix3x3::new( + c, 0.0, s, + 0.0, 1.0, 0.0, + -s, 0.0, c + ) + } + + /// Creates a new rotation matrix for rotation around the z axis. + /// + /// # Arguments + /// + /// * `radians`: angle to rotate the z axis around (in radians) + #[rustfmt::skip] + #[inline] + pub fn new_rotation_z(radians: f32) -> Matrix3x3 { + let (s, c) = radians.sin_cos(); + Matrix3x3::new( + c, -s, 0.0, + s, c, 0.0, + 0.0, 0.0, 1.0 + ) + } + + /// Creates a translation matrix. For use with 2D coordinates only. + /// + /// # Arguments + /// + /// * `x`: the amount to translate on the x axis + /// * `y`: the amount to translate on the y axis + #[rustfmt::skip] + #[inline] + pub fn new_2d_translation(x: f32, y: f32) -> Matrix3x3 { + Matrix3x3::new( + 1.0, 0.0, 0.0, + 0.0, 1.0, 0.0, + x, y, 1.0 + ) + } + + /// Creates a scaling matrix from scaling factors for each axis. For use with 2D coordinates + /// only. + /// + /// # Arguments + /// + /// * `x`: the scale factor for the x axis + /// * `y`: the scale factor for the y axis + #[rustfmt::skip] + #[inline] + pub fn new_2d_scaling(x: f32, y: f32) -> Matrix3x3 { + Matrix3x3::new( + x, 0.0, 0.0, + 0.0, y, 0.0, + 0.0, 0.0, 1.0 + ) + } + + /// Creates a new rotation matrix. For use with 2D coordinates only. + /// + /// # Arguments + /// + /// * `radians`: angle to rotate by (in radians) + #[inline(always)] + pub fn new_2d_rotation(radians: f32) -> Matrix3x3 { + Matrix3x3::new_rotation_z(radians) + } + + /// Calculates the determinant of this matrix. + #[rustfmt::skip] + #[inline] + pub fn determinant(&self) -> f32 { + self.m[Matrix3x3::M11] * self.m[Matrix3x3::M22] * self.m[Matrix3x3::M33] + + self.m[Matrix3x3::M12] * self.m[Matrix3x3::M23] * self.m[Matrix3x3::M31] + + self.m[Matrix3x3::M13] * self.m[Matrix3x3::M21] * self.m[Matrix3x3::M32] - + self.m[Matrix3x3::M11] * self.m[Matrix3x3::M23] * self.m[Matrix3x3::M32] - + self.m[Matrix3x3::M12] * self.m[Matrix3x3::M21] * self.m[Matrix3x3::M33] - + self.m[Matrix3x3::M13] * self.m[Matrix3x3::M22] * self.m[Matrix3x3::M31] + } + + /// Calculates the inverse of this matrix. + #[rustfmt::skip] + pub fn invert(&self) -> Matrix3x3 { + let d = self.determinant(); + if nearly_equal(d, 0.0, 0.000001) { + Matrix3x3::IDENTITY + } else { + let d = 1.0 / d; + Matrix3x3 { + m: [ + d * (self.m[Matrix3x3::M22] * self.m[Matrix3x3::M33] - self.m[Matrix3x3::M32] * self.m[Matrix3x3::M23]), + d * (self.m[Matrix3x3::M31] * self.m[Matrix3x3::M23] - self.m[Matrix3x3::M21] * self.m[Matrix3x3::M33]), + d * (self.m[Matrix3x3::M21] * self.m[Matrix3x3::M32] - self.m[Matrix3x3::M31] * self.m[Matrix3x3::M22]), + d * (self.m[Matrix3x3::M32] * self.m[Matrix3x3::M13] - self.m[Matrix3x3::M12] * self.m[Matrix3x3::M33]), + d * (self.m[Matrix3x3::M11] * self.m[Matrix3x3::M33] - self.m[Matrix3x3::M31] * self.m[Matrix3x3::M13]), + d * (self.m[Matrix3x3::M31] * self.m[Matrix3x3::M12] - self.m[Matrix3x3::M11] * self.m[Matrix3x3::M32]), + d * (self.m[Matrix3x3::M12] * self.m[Matrix3x3::M23] - self.m[Matrix3x3::M22] * self.m[Matrix3x3::M13]), + d * (self.m[Matrix3x3::M21] * self.m[Matrix3x3::M13] - self.m[Matrix3x3::M11] * self.m[Matrix3x3::M23]), + d * (self.m[Matrix3x3::M11] * self.m[Matrix3x3::M22] - self.m[Matrix3x3::M21] * self.m[Matrix3x3::M12]), + ] + } + } + } + + /// Calculates the transpose of this matrix. + #[inline] + pub fn transpose(&self) -> Matrix3x3 { + Matrix3x3::new( + self.m[Matrix3x3::M11], + self.m[Matrix3x3::M21], + self.m[Matrix3x3::M31], + self.m[Matrix3x3::M12], + self.m[Matrix3x3::M22], + self.m[Matrix3x3::M32], + self.m[Matrix3x3::M13], + self.m[Matrix3x3::M23], + self.m[Matrix3x3::M33], + ) + } + + /// Sets all of the elements of this matrix. + #[inline] + pub fn set( + &mut self, + m11: f32, + m12: f32, + m13: f32, + m21: f32, + m22: f32, + m23: f32, + m31: f32, + m32: f32, + m33: f32, + ) { + self.m[Matrix3x3::M11] = m11; + self.m[Matrix3x3::M12] = m12; + self.m[Matrix3x3::M13] = m13; + self.m[Matrix3x3::M21] = m21; + self.m[Matrix3x3::M22] = m22; + self.m[Matrix3x3::M23] = m23; + self.m[Matrix3x3::M31] = m31; + self.m[Matrix3x3::M32] = m32; + self.m[Matrix3x3::M33] = m33; + } +} + +impl Mul for Matrix3x3 { + type Output = Self; + + #[rustfmt::skip] + #[inline] + fn mul(self, rhs: Self) -> Self::Output { + Matrix3x3::new( + self.m[Matrix3x3::M11] * rhs.m[Matrix3x3::M11] + self.m[Matrix3x3::M12] * rhs.m[Matrix3x3::M21] + self.m[Matrix3x3::M13] * rhs.m[Matrix3x3::M31], + self.m[Matrix3x3::M11] * rhs.m[Matrix3x3::M12] + self.m[Matrix3x3::M12] * rhs.m[Matrix3x3::M22] + self.m[Matrix3x3::M13] * rhs.m[Matrix3x3::M32], + self.m[Matrix3x3::M11] * rhs.m[Matrix3x3::M13] + self.m[Matrix3x3::M12] * rhs.m[Matrix3x3::M23] + self.m[Matrix3x3::M13] * rhs.m[Matrix3x3::M33], + self.m[Matrix3x3::M21] * rhs.m[Matrix3x3::M11] + self.m[Matrix3x3::M22] * rhs.m[Matrix3x3::M21] + self.m[Matrix3x3::M23] * rhs.m[Matrix3x3::M31], + self.m[Matrix3x3::M21] * rhs.m[Matrix3x3::M12] + self.m[Matrix3x3::M22] * rhs.m[Matrix3x3::M22] + self.m[Matrix3x3::M23] * rhs.m[Matrix3x3::M32], + self.m[Matrix3x3::M21] * rhs.m[Matrix3x3::M13] + self.m[Matrix3x3::M22] * rhs.m[Matrix3x3::M23] + self.m[Matrix3x3::M23] * rhs.m[Matrix3x3::M33], + self.m[Matrix3x3::M31] * rhs.m[Matrix3x3::M11] + self.m[Matrix3x3::M32] * rhs.m[Matrix3x3::M21] + self.m[Matrix3x3::M33] * rhs.m[Matrix3x3::M31], + self.m[Matrix3x3::M31] * rhs.m[Matrix3x3::M12] + self.m[Matrix3x3::M32] * rhs.m[Matrix3x3::M22] + self.m[Matrix3x3::M33] * rhs.m[Matrix3x3::M32], + self.m[Matrix3x3::M31] * rhs.m[Matrix3x3::M13] + self.m[Matrix3x3::M32] * rhs.m[Matrix3x3::M23] + self.m[Matrix3x3::M33] * rhs.m[Matrix3x3::M33] + ) + } +} + +impl MulAssign for Matrix3x3 { + #[rustfmt::skip] + #[inline] + fn mul_assign(&mut self, rhs: Self) { + self.set( + self.m[Matrix3x3::M11] * rhs.m[Matrix3x3::M11] + self.m[Matrix3x3::M12] * rhs.m[Matrix3x3::M21] + self.m[Matrix3x3::M13] * rhs.m[Matrix3x3::M31], + self.m[Matrix3x3::M11] * rhs.m[Matrix3x3::M12] + self.m[Matrix3x3::M12] * rhs.m[Matrix3x3::M22] + self.m[Matrix3x3::M13] * rhs.m[Matrix3x3::M32], + self.m[Matrix3x3::M11] * rhs.m[Matrix3x3::M13] + self.m[Matrix3x3::M12] * rhs.m[Matrix3x3::M23] + self.m[Matrix3x3::M13] * rhs.m[Matrix3x3::M33], + self.m[Matrix3x3::M21] * rhs.m[Matrix3x3::M11] + self.m[Matrix3x3::M22] * rhs.m[Matrix3x3::M21] + self.m[Matrix3x3::M23] * rhs.m[Matrix3x3::M31], + self.m[Matrix3x3::M21] * rhs.m[Matrix3x3::M12] + self.m[Matrix3x3::M22] * rhs.m[Matrix3x3::M22] + self.m[Matrix3x3::M23] * rhs.m[Matrix3x3::M32], + self.m[Matrix3x3::M21] * rhs.m[Matrix3x3::M13] + self.m[Matrix3x3::M22] * rhs.m[Matrix3x3::M23] + self.m[Matrix3x3::M23] * rhs.m[Matrix3x3::M33], + self.m[Matrix3x3::M31] * rhs.m[Matrix3x3::M11] + self.m[Matrix3x3::M32] * rhs.m[Matrix3x3::M21] + self.m[Matrix3x3::M33] * rhs.m[Matrix3x3::M31], + self.m[Matrix3x3::M31] * rhs.m[Matrix3x3::M12] + self.m[Matrix3x3::M32] * rhs.m[Matrix3x3::M22] + self.m[Matrix3x3::M33] * rhs.m[Matrix3x3::M32], + self.m[Matrix3x3::M31] * rhs.m[Matrix3x3::M13] + self.m[Matrix3x3::M32] * rhs.m[Matrix3x3::M23] + self.m[Matrix3x3::M33] * rhs.m[Matrix3x3::M33] + ) + } +} + +impl Mul for Matrix3x3 { + type Output = Vector2; + + #[rustfmt::skip] + #[inline] + fn mul(self, rhs: Vector2) -> Self::Output { + Vector2 { + x: rhs.x * self.m[Matrix3x3::M11] + rhs.y * self.m[Matrix3x3::M12] + self.m[Matrix3x3::M13] + self.m[Matrix3x3::M31], + y: rhs.x * self.m[Matrix3x3::M21] + rhs.y * self.m[Matrix3x3::M22] + self.m[Matrix3x3::M23] + self.m[Matrix3x3::M32] + } + } +} + +#[cfg(test)] +pub mod tests { + use crate::math::{RADIANS_180, RADIANS_90}; + + use super::*; + + #[test] + pub fn test_new() { + let m = Matrix3x3::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0); + assert_eq!(1.0, m.m[Matrix3x3::M11]); + assert_eq!(2.0, m.m[Matrix3x3::M12]); + assert_eq!(3.0, m.m[Matrix3x3::M13]); + assert_eq!(4.0, m.m[Matrix3x3::M21]); + assert_eq!(5.0, m.m[Matrix3x3::M22]); + assert_eq!(6.0, m.m[Matrix3x3::M23]); + assert_eq!(7.0, m.m[Matrix3x3::M31]); + assert_eq!(8.0, m.m[Matrix3x3::M32]); + assert_eq!(9.0, m.m[Matrix3x3::M33]); + } + + #[test] + pub fn test_set() { + let mut m = Matrix3x3 { m: [0.0; 9] }; + m.set(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0); + assert_eq!(1.0, m.m[Matrix3x3::M11]); + assert_eq!(2.0, m.m[Matrix3x3::M12]); + assert_eq!(3.0, m.m[Matrix3x3::M13]); + assert_eq!(4.0, m.m[Matrix3x3::M21]); + assert_eq!(5.0, m.m[Matrix3x3::M22]); + assert_eq!(6.0, m.m[Matrix3x3::M23]); + assert_eq!(7.0, m.m[Matrix3x3::M31]); + assert_eq!(8.0, m.m[Matrix3x3::M32]); + assert_eq!(9.0, m.m[Matrix3x3::M33]); + } + + #[test] + pub fn test_identity() { + let m = Matrix3x3::identity(); + assert_eq!(1.0, m.m[Matrix3x3::M11]); + assert_eq!(0.0, m.m[Matrix3x3::M12]); + assert_eq!(0.0, m.m[Matrix3x3::M13]); + assert_eq!(0.0, m.m[Matrix3x3::M21]); + assert_eq!(1.0, m.m[Matrix3x3::M22]); + assert_eq!(0.0, m.m[Matrix3x3::M23]); + assert_eq!(0.0, m.m[Matrix3x3::M31]); + assert_eq!(0.0, m.m[Matrix3x3::M32]); + assert_eq!(1.0, m.m[Matrix3x3::M33]); + } + + #[rustfmt::skip] + #[test] + pub fn test_transpose() { + let m = Matrix3x3::new( + 1.0, 2.0, 3.0, + 4.0, 5.0, 6.0, + 7.0, 8.0, 9.0 + ); + let t = m.transpose(); + assert_eq!(1.0, t.m[Matrix3x3::M11]); + assert_eq!(4.0, t.m[Matrix3x3::M12]); + assert_eq!(7.0, t.m[Matrix3x3::M13]); + assert_eq!(2.0, t.m[Matrix3x3::M21]); + assert_eq!(5.0, t.m[Matrix3x3::M22]); + assert_eq!(8.0, t.m[Matrix3x3::M23]); + assert_eq!(3.0, t.m[Matrix3x3::M31]); + assert_eq!(6.0, t.m[Matrix3x3::M32]); + assert_eq!(9.0, t.m[Matrix3x3::M33]); + } + + #[test] + pub fn test_mul() { + let a = Matrix3x3::new(12.0, 8.0, 4.0, 3.0, 17.0, 14.0, 9.0, 8.0, 10.0); + let b = Matrix3x3::new(5.0, 19.0, 3.0, 6.0, 15.0, 9.0, 7.0, 8.0, 16.0); + let c = a * b; + assert!(nearly_equal(136.0, c.m[Matrix3x3::M11], 0.001)); + assert!(nearly_equal(380.0, c.m[Matrix3x3::M12], 0.001)); + assert!(nearly_equal(172.0, c.m[Matrix3x3::M13], 0.001)); + assert!(nearly_equal(215.0, c.m[Matrix3x3::M21], 0.001)); + assert!(nearly_equal(424.0, c.m[Matrix3x3::M22], 0.001)); + assert!(nearly_equal(386.0, c.m[Matrix3x3::M23], 0.001)); + assert!(nearly_equal(163.0, c.m[Matrix3x3::M31], 0.001)); + assert!(nearly_equal(371.0, c.m[Matrix3x3::M32], 0.001)); + assert!(nearly_equal(259.0, c.m[Matrix3x3::M33], 0.001)); + + let mut a = Matrix3x3::new(12.0, 8.0, 4.0, 3.0, 17.0, 14.0, 9.0, 8.0, 10.0); + let b = Matrix3x3::new(5.0, 19.0, 3.0, 6.0, 15.0, 9.0, 7.0, 8.0, 16.0); + a *= b; + assert!(nearly_equal(136.0, a.m[Matrix3x3::M11], 0.001)); + assert!(nearly_equal(380.0, a.m[Matrix3x3::M12], 0.001)); + assert!(nearly_equal(172.0, a.m[Matrix3x3::M13], 0.001)); + assert!(nearly_equal(215.0, a.m[Matrix3x3::M21], 0.001)); + assert!(nearly_equal(424.0, a.m[Matrix3x3::M22], 0.001)); + assert!(nearly_equal(386.0, a.m[Matrix3x3::M23], 0.001)); + assert!(nearly_equal(163.0, a.m[Matrix3x3::M31], 0.001)); + assert!(nearly_equal(371.0, a.m[Matrix3x3::M32], 0.001)); + assert!(nearly_equal(259.0, a.m[Matrix3x3::M33], 0.001)); + } + + #[test] + pub fn test_2d_translation() { + let v = Vector2::new(10.2, 5.7); + let m = Matrix3x3::new_2d_translation(2.0, 3.0); + let t = m * v; + assert!(nearly_equal(12.2, t.x, 0.001)); + assert!(nearly_equal(8.7, t.y, 0.001)); + } + + #[test] + pub fn test_2d_scaling() { + let v = Vector2::new(10.2, 5.7); + let m = Matrix3x3::new_2d_scaling(3.0, 4.0); + let t = m * v; + assert!(nearly_equal(30.6, t.x, 0.001)); + assert!(nearly_equal(22.8, t.y, 0.001)); + } + + #[test] + pub fn test_2d_rotation() { + let v = Vector2::new(0.0, 5.0); + let m = Matrix3x3::new_2d_rotation(RADIANS_90); + let t = m * v; + assert!(nearly_equal(-5.0, t.x, 0.001)); + assert!(nearly_equal(0.0, t.y, 0.001)); + } + + #[test] + pub fn test_2d_combined_transform() { + let a = Matrix3x3::new_2d_translation(-2.0, 0.0); + let b = Matrix3x3::new_2d_rotation(RADIANS_180); + let m = a * b; + let v = Vector2::new(0.0, 5.0); + let t = m * v; + assert!(nearly_equal(2.0, t.x, 0.001)); + assert!(nearly_equal(-5.0, t.y, 0.001)); + } +} diff --git a/libretrogd/src/math/mod.rs b/libretrogd/src/math/mod.rs new file mode 100644 index 0000000..800a914 --- /dev/null +++ b/libretrogd/src/math/mod.rs @@ -0,0 +1,272 @@ +use std::ops::{Add, Div, Mul, Sub}; + +pub mod circle; +pub mod matrix3x3; +pub mod rect; +pub mod vector2; + +pub const PI: f32 = std::f32::consts::PI; // 180 degrees +pub const HALF_PI: f32 = PI / 2.0; // 90 degrees +pub const QUARTER_PI: f32 = PI / 4.0; // 45 degrees +pub const TWO_PI: f32 = PI * 2.0; // 360 degrees + +pub const RADIANS_0: f32 = 0.0; +pub const RADIANS_45: f32 = PI / 4.0; +pub const RADIANS_90: f32 = PI / 2.0; +pub const RADIANS_135: f32 = (3.0 * PI) / 4.0; +pub const RADIANS_180: f32 = PI; +pub const RADIANS_225: f32 = (5.0 * PI) / 4.0; +pub const RADIANS_270: f32 = (3.0 * PI) / 2.0; +pub const RADIANS_315: f32 = (7.0 * PI) / 4.0; +pub const RADIANS_360: f32 = PI * 2.0; + +pub const PI_OVER_180: f32 = PI / 180.0; +pub const INVERSE_PI_OVER_180: f32 = 180.0 / PI; + +// 2d directions. intended to be usable in a 2d screen-space where the origin 0,0 is at the +// top-left of the screen, where moving up is accomplished by decrementing Y. +// TODO: this is not a perfect solution and does pose some problems in various math calculations ... +pub const UP: f32 = -RADIANS_90; +pub const DOWN: f32 = RADIANS_90; +pub const LEFT: f32 = RADIANS_180; +pub const RIGHT: f32 = RADIANS_0; + +/// Returns true if the two f32 values are "close enough" to be considered equal using the +/// precision of the provided epsilon value. +#[inline] +pub fn nearly_equal(a: f32, b: f32, epsilon: f32) -> bool { + // HACK/TODO: this is a shitty way to do this. but it's "good enough" for me ... + (a - b).abs() <= epsilon +} + +/// Linearly interpolates between two values. +/// +/// # Arguments +/// +/// * `a`: first value (low end of range) +/// * `b`: second value (high end of range) +/// * `t`: the amount to interpolate between the two values, specified as a fraction +#[inline] +pub fn lerp(a: N, b: N, t: f32) -> N +where + N: Copy + Add + Sub + Mul, +{ + a + (b - a) * t +} + +/// Given a linearly interpolated value and the original range (high and low) of the linear +/// interpolation, this will return the original interpolation factor (as a fraction) +/// +/// # Arguments +/// +/// * `a`: first value (low end of range) +/// * `b`: second value (high end of range) +/// * `lerp_result`: the interpolated value between the range given +#[inline] +pub fn inverse_lerp(a: N, b: N, lerp_result: N) -> f32 +where + N: Copy + Sub + Div, +{ + (lerp_result - a) / (b - a) +} + +/// Interpolates between two values using a cubic equation. +/// +/// # Arguments +/// +/// * `a`: first value (low end of range) +/// * `b`: second value (high end of range) +/// * `t`: the amount to interpolate between the two values, specified as a fraction +#[inline] +pub fn smooth_lerp(a: N, b: N, t: f32) -> N +where + N: Copy + Add + Sub + Mul, +{ + let t = t.clamp(0.0, 1.0); + lerp(a, b, (t * t) * (3.0 - (2.0 * t))) +} + +/// Re-scales a given value from an old min/max range to a new and different min/max range such +/// that the returned value is approximately at the same relative position within the new min/max +/// range. +/// +/// # Arguments +/// +/// * `value`: the value to be re-scaled which is currently between `old_min` and `old_max` +/// * `old_min`: original min value (low end of range) +/// * `old_max`: original max value (high end of range) +/// * `new_min`: new min value (low end of range) +/// * `new_max`: new max value (high end of range) +#[inline] +pub fn scale_range(value: N, old_min: N, old_max: N, new_min: N, new_max: N) -> N +where + N: Copy + Add + Sub + Mul + Div, +{ + (new_max - new_min) * (value - old_min) / (old_max - old_min) + new_min +} + +/// Calculates the angle (in radians) between the two points. +#[inline] +pub fn angle_between(x1: f32, y1: f32, x2: f32, y2: f32) -> f32 { + let delta_x = x2 - x1; + let delta_y = y2 - y1; + delta_y.atan2(delta_x) +} + +/// Returns the X and Y point of a normalized 2D vector that points in the same direction as +/// the given angle. +#[inline] +pub fn angle_to_direction(radians: f32) -> (f32, f32) { + let x = radians.cos(); + let y = radians.sin(); + (x, y) +} + +/// Calculates the squared distance between two 2D points. +#[inline] +pub fn distance_squared_between(x1: f32, y1: f32, x2: f32, y2: f32) -> f32 { + (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1) +} + +/// Calculates the distance between two 2D points. +#[inline] +pub fn distance_between(x1: f32, y1: f32, x2: f32, y2: f32) -> f32 { + distance_squared_between(x1, y1, x2, y2).sqrt() +} + +pub trait NearlyEqual { + type Output; + + /// Returns true if the two f32 values are "close enough" to be considered equal using the + /// precision of the provided epsilon value. + fn nearly_equal(self, other: Self::Output, epsilon: f32) -> bool; +} + +impl NearlyEqual for f32 { + type Output = f32; + + #[inline(always)] + fn nearly_equal(self, other: Self::Output, epsilon: f32) -> bool { + nearly_equal(self, other, epsilon) + } +} + +pub trait WrappingRadians { + type Type; + + /// Adds two angle values in radians together returning the result. The addition will wrap + /// around so that the returned result always lies within 0 -> 2Ï€ radians (0 -> 360 degrees). + fn wrapping_radians_add(self, other: Self::Type) -> Self::Type; + + /// Subtracts two angle values in radians returning the result. The subtraction will wrap + /// around so that the returned result always lies within 0 -> 2Ï€ radians (0 -> 360 degrees). + fn wrapping_radians_sub(self, other: Self::Type) -> Self::Type; +} + +impl WrappingRadians for f32 { + type Type = f32; + + fn wrapping_radians_add(self, other: Self::Type) -> Self::Type { + let result = self + other; + if result < RADIANS_0 { + result + RADIANS_360 + } else if result >= RADIANS_360 { + result - RADIANS_360 + } else { + result + } + } + + fn wrapping_radians_sub(self, other: Self::Type) -> Self::Type { + let result = self - other; + if result < RADIANS_0 { + result + RADIANS_360 + } else if result >= RADIANS_360 { + result - RADIANS_360 + } else { + result + } + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + #[test] + pub fn test_nearly_equal() { + assert!(nearly_equal(4.0, 4.0, 0.1)); + assert!(4.0f32.nearly_equal(4.0, 0.1)); + assert!(nearly_equal(0.1 + 0.2, 0.3, 0.01)); + assert!(!nearly_equal(1.0001, 1.0005, 0.0001)); + } + + #[test] + pub fn test_lerp() { + assert!(nearly_equal(15.0, lerp(10.0, 20.0, 0.5), 0.0001)); + assert!(nearly_equal(10.0, lerp(10.0, 20.0, 0.0), 0.0001)); + assert!(nearly_equal(20.0, lerp(10.0, 20.0, 1.0), 0.0001)); + } + + #[test] + pub fn test_inverse_lerp() { + assert_eq!(0.5, inverse_lerp(10.0, 20.0, 15.0f32)) + } + + #[test] + pub fn test_angle_between() { + let angle = angle_between(20.0, 20.0, 10.0, 10.0); + assert!(nearly_equal(-RADIANS_135, angle, 0.0001)); + let angle = angle_between(0.0, 0.0, 10.0, 10.0); + assert!(nearly_equal(RADIANS_45, angle, 0.0001)); + let angle = angle_between(5.0, 5.0, 5.0, 5.0); + assert!(nearly_equal(0.0, angle, 0.0001)); + } + + #[test] + pub fn test_angle_to_direction() { + let (x, y) = angle_to_direction(RADIANS_0); + assert!(nearly_equal(x, 1.0, 0.000001)); + assert!(nearly_equal(y, 0.0, 0.000001)); + let (x, y) = angle_to_direction(RADIANS_45); + assert!(nearly_equal(x, 0.707106, 0.000001)); + assert!(nearly_equal(y, 0.707106, 0.000001)); + let (x, y) = angle_to_direction(RADIANS_225); + assert!(nearly_equal(x, -0.707106, 0.000001)); + assert!(nearly_equal(y, -0.707106, 0.000001)); + + let (x, y) = angle_to_direction(UP); + assert!(nearly_equal(x, 0.0, 0.000001)); + assert!(nearly_equal(y, -1.0, 0.000001)); + let (x, y) = angle_to_direction(DOWN); + assert!(nearly_equal(x, 0.0, 0.000001)); + assert!(nearly_equal(y, 1.0, 0.000001)); + let (x, y) = angle_to_direction(LEFT); + assert!(nearly_equal(x, -1.0, 0.000001)); + assert!(nearly_equal(y, 0.0, 0.000001)); + let (x, y) = angle_to_direction(RIGHT); + assert!(nearly_equal(x, 1.0, 0.000001)); + assert!(nearly_equal(y, 0.0, 0.000001)); + } + + #[test] + pub fn test_distance_between() { + let x1 = -2.0; + let y1 = 1.0; + let x2 = 4.0; + let y2 = 3.0; + let distance_squared = distance_squared_between(x1, y1, x2, y2); + let distance = distance_between(x1, y1, x2, y2); + assert!(nearly_equal(distance_squared, 40.0000, 0.0001)); + assert!(nearly_equal(distance, 6.3245, 0.0001)); + } + + #[test] + pub fn test_wrapping_radians() { + assert!(nearly_equal(RADIANS_90, RADIANS_45.wrapping_radians_add(RADIANS_45), 0.0001)); + assert!(nearly_equal(RADIANS_90, RADIANS_180.wrapping_radians_sub(RADIANS_90), 0.0001)); + + assert!(nearly_equal(RADIANS_45, RADIANS_315.wrapping_radians_add(RADIANS_90), 0.0001)); + assert!(nearly_equal(RADIANS_315, RADIANS_90.wrapping_radians_sub(RADIANS_135), 0.0001)); + } +} diff --git a/libretrogd/src/math/rect.rs b/libretrogd/src/math/rect.rs new file mode 100644 index 0000000..1180007 --- /dev/null +++ b/libretrogd/src/math/rect.rs @@ -0,0 +1,284 @@ +/// Represents a 2D rectangle, using integer coordinates and dimensions. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct Rect { + pub x: i32, + pub y: i32, + pub width: u32, + pub height: u32, +} + +impl Rect { + #[inline] + pub fn new(x: i32, y: i32, width: u32, height: u32) -> Rect { + Rect { + x, + y, + width, + height, + } + } + + /// Creates a new rect from the specified coordinates. Automatically determines if the + /// coordinates provided are swapped (where the right/bottom coordinate is provided before the + /// left/top). All of the coordinates are inclusive. + /// + /// # Arguments + /// + /// * `left`: the left x coordinate + /// * `top`: the top y coordinate + /// * `right`: the right x coordinate + /// * `bottom`: the bottom y coordinate + pub fn from_coords(left: i32, top: i32, right: i32, bottom: i32) -> Rect { + let x; + let y; + let width; + let height; + + if left <= right { + x = left; + width = (right - left).abs() + 1; + } else { + x = right; + width = (left - right).abs() + 1; + } + + if top <= bottom { + y = top; + height = (bottom - top).abs() + 1; + } else { + y = bottom; + height = (top - bottom).abs() + 1; + } + + Rect { + x, + y, + width: width as u32, + height: height as u32, + } + } + + pub fn set_from_coords(&mut self, left: i32, top: i32, right: i32, bottom: i32) { + if left <= right { + self.x = left; + self.width = ((right - left).abs() + 1) as u32; + } else { + self.x = right; + self.width = ((left - right).abs() + 1) as u32; + } + + if top <= bottom { + self.y = top; + self.height = ((bottom - top).abs() + 1) as u32; + } else { + self.y = bottom; + self.height = ((top - bottom).abs() + 1) as u32; + } + } + + /// Calculates the right-most x coordinate contained by this rect. + #[inline] + pub fn right(&self) -> i32 { + if self.width > 0 { + self.x + self.width as i32 - 1 + } else { + self.x + } + } + + /// Calculates the bottom-most y coordinate contained by this rect. + #[inline] + pub fn bottom(&self) -> i32 { + if self.height > 0 { + self.y + self.height as i32 - 1 + } else { + self.y + } + } + + /// Returns true if the given point is contained within the bounds of this rect. + pub fn contains_point(&self, x: i32, y: i32) -> bool { + (self.x <= x) && (self.right() >= x) && (self.y <= y) && (self.bottom() >= y) + } + + /// Returns true if the given rect is contained completely within the bounds of this rect. + pub fn contains_rect(&self, other: &Rect) -> bool { + (other.x >= self.x && other.x < self.right()) + && (other.right() > self.x && other.right() <= self.right()) + && (other.y >= self.y && other.y < self.bottom()) + && (other.bottom() > self.y && other.bottom() <= self.bottom()) + } + + /// Returns true if the given rect at least partially overlaps the bounds of this rect. + pub fn overlaps(&self, other: &Rect) -> bool { + (self.x <= other.right()) + && (self.right() >= other.x) + && (self.y <= other.bottom()) + && (self.bottom() >= other.y) + } + + pub fn clamp_to(&mut self, other: &Rect) -> bool { + if !self.overlaps(other) { + // not possible to clamp this rect to the other rect as they do not overlap at all + false + } else { + // the rects at least partially overlap, so we will clamp this rect to the overlapping + // region of the other rect + let mut x1 = self.x; + let mut y1 = self.y; + let mut x2 = self.right(); + let mut y2 = self.bottom(); + if y1 < other.y { + y1 = other.y; + } + if y1 > other.bottom() { + y1 = other.bottom(); + } + if y2 < other.y { + y2 = other.y; + } + if y2 > other.bottom() { + y2 = other.bottom(); + } + if x1 < other.x { + x1 = other.x; + } + if x1 > other.right() { + x1 = other.right(); + } + if x2 < other.x { + x2 = other.x; + } + if x2 > other.right() { + x2 = other.right(); + } + + self.set_from_coords(x1, y1, x2, y2); + true + } + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + #[test] + pub fn right_and_left() { + let rect = Rect { + x: 5, + y: 6, + width: 16, + height: 12, + }; + assert_eq!(20, rect.right()); + assert_eq!(17, rect.bottom()); + + let rect = Rect { + x: -11, + y: -25, + width: 16, + height: 12, + }; + assert_eq!(4, rect.right()); + assert_eq!(-14, rect.bottom()); + } + + #[test] + pub fn create_from_coords() { + let rect = Rect::from_coords(10, 15, 20, 30); + assert_eq!(10, rect.x); + assert_eq!(15, rect.y); + assert_eq!(11, rect.width); + assert_eq!(16, rect.height); + assert_eq!(20, rect.right()); + assert_eq!(30, rect.bottom()); + + let rect = Rect::from_coords(-5, -13, 6, -2); + assert_eq!(-5, rect.x); + assert_eq!(-13, rect.y); + assert_eq!(12, rect.width); + assert_eq!(12, rect.height); + assert_eq!(6, rect.right()); + assert_eq!(-2, rect.bottom()); + } + + #[test] + pub fn create_from_coords_with_swapped_order() { + let rect = Rect::from_coords(20, 30, 10, 15); + assert_eq!(10, rect.x); + assert_eq!(15, rect.y); + assert_eq!(11, rect.width); + assert_eq!(16, rect.height); + assert_eq!(20, rect.right()); + assert_eq!(30, rect.bottom()); + + let rect = Rect::from_coords(6, -2, -5, -13); + assert_eq!(-5, rect.x); + assert_eq!(-13, rect.y); + assert_eq!(12, rect.width); + assert_eq!(12, rect.height); + assert_eq!(6, rect.right()); + assert_eq!(-2, rect.bottom()); + } + + #[test] + pub fn test_contains_point() { + let r = Rect::from_coords(10, 10, 20, 20); + + assert!(r.contains_point(10, 10)); + assert!(r.contains_point(15, 15)); + assert!(r.contains_point(20, 20)); + + assert!(!r.contains_point(12, 30)); + assert!(!r.contains_point(8, 12)); + assert!(!r.contains_point(25, 16)); + assert!(!r.contains_point(17, 4)); + } + + #[test] + pub fn test_contains_rect() { + let r = Rect::from_coords(10, 10, 20, 20); + + assert!(r.contains_rect(&Rect::from_coords(12, 12, 15, 15))); + assert!(r.contains_rect(&Rect::from_coords(10, 10, 15, 15))); + assert!(r.contains_rect(&Rect::from_coords(15, 15, 20, 20))); + assert!(r.contains_rect(&Rect::from_coords(10, 12, 20, 15))); + assert!(r.contains_rect(&Rect::from_coords(12, 10, 15, 20))); + + assert!(!r.contains_rect(&Rect::from_coords(5, 5, 15, 15))); + assert!(!r.contains_rect(&Rect::from_coords(15, 15, 25, 25))); + + assert!(!r.contains_rect(&Rect::from_coords(2, 2, 8, 4))); + assert!(!r.contains_rect(&Rect::from_coords(12, 21, 18, 25))); + assert!(!r.contains_rect(&Rect::from_coords(22, 12, 32, 17))); + } + + #[test] + pub fn test_overlaps() { + let r = Rect::from_coords(10, 10, 20, 20); + + assert!(r.overlaps(&Rect::from_coords(12, 12, 15, 15))); + assert!(r.overlaps(&Rect::from_coords(10, 10, 15, 15))); + assert!(r.overlaps(&Rect::from_coords(15, 15, 20, 20))); + assert!(r.overlaps(&Rect::from_coords(10, 12, 20, 15))); + assert!(r.overlaps(&Rect::from_coords(12, 10, 15, 20))); + + assert!(r.overlaps(&Rect::from_coords(12, 5, 18, 10))); + assert!(r.overlaps(&Rect::from_coords(13, 20, 16, 25))); + assert!(r.overlaps(&Rect::from_coords(5, 12, 10, 18))); + assert!(r.overlaps(&Rect::from_coords(20, 13, 25, 16))); + + assert!(r.overlaps(&Rect::from_coords(5, 5, 15, 15))); + assert!(r.overlaps(&Rect::from_coords(15, 15, 25, 25))); + + assert!(!r.overlaps(&Rect::from_coords(2, 2, 8, 4))); + assert!(!r.overlaps(&Rect::from_coords(12, 21, 18, 25))); + assert!(!r.overlaps(&Rect::from_coords(22, 12, 32, 17))); + + assert!(!r.overlaps(&Rect::from_coords(12, 5, 18, 9))); + assert!(!r.overlaps(&Rect::from_coords(13, 21, 16, 25))); + assert!(!r.overlaps(&Rect::from_coords(5, 12, 9, 18))); + assert!(!r.overlaps(&Rect::from_coords(21, 13, 25, 16))); + } +} diff --git a/libretrogd/src/math/vector2.rs b/libretrogd/src/math/vector2.rs new file mode 100644 index 0000000..f64cddb --- /dev/null +++ b/libretrogd/src/math/vector2.rs @@ -0,0 +1,479 @@ +use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; + +use crate::Matrix3x3; +use crate::{angle_between, angle_to_direction, nearly_equal, NearlyEqual}; + +/// Represents a 2D vector and provides common methods for vector math. +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct Vector2 { + pub x: f32, + pub y: f32, +} + +impl Vector2 { + pub const ZERO: Vector2 = Vector2 { x: 0.0, y: 0.0 }; + + pub const UP: Vector2 = Vector2 { x: 0.0, y: -1.0 }; + pub const DOWN: Vector2 = Vector2 { x: 0.0, y: 1.0 }; + pub const LEFT: Vector2 = Vector2 { x: -1.0, y: 0.0 }; + pub const RIGHT: Vector2 = Vector2 { x: 1.0, y: 0.0 }; + + pub const X_AXIS: Vector2 = Vector2 { x: 1.0, y: 0.0 }; + pub const Y_AXIS: Vector2 = Vector2 { x: 0.0, y: 1.0 }; + + /// Creates a vector with the specified X and Y components. + #[inline] + pub fn new(x: f32, y: f32) -> Vector2 { + Vector2 { x, y } + } + + /// Creates a normalized vector that points in the same direction as the given angle. + #[inline] + pub fn from_angle(radians: f32) -> Vector2 { + let (x, y) = angle_to_direction(radians); + Vector2 { x, y } + } + + /// Calculates the distance between this and another vector. + #[inline] + pub fn distance(&self, other: &Vector2) -> f32 { + self.distance_squared(other).sqrt() + } + + /// Calculates the squared distance between this and another vector. + #[inline] + pub fn distance_squared(&self, other: &Vector2) -> f32 { + (other.x - self.x) * (other.x - self.x) + (other.y - self.y) * (other.y - self.y) + } + + /// Calculates the dot product of this and another vector. + #[inline] + pub fn dot(&self, other: &Vector2) -> f32 { + (self.x * other.x) + (self.y * other.y) + } + + /// Calculates the length (a.k.a. magnitude) of this vector. + #[inline] + pub fn length(&self) -> f32 { + self.length_squared().sqrt() + } + + /// Calculates the squared length of this vector. + #[inline] + pub fn length_squared(&self) -> f32 { + (self.x * self.x) + (self.y * self.y) + } + + /// Returns a normalized vector from this vector. + pub fn normalize(&self) -> Vector2 { + let inverse_length = 1.0 / self.length(); + Vector2 { + x: self.x * inverse_length, + y: self.y * inverse_length, + } + } + + /// Returns an extended (or shrunk) vector from this vector, where the returned vector will + /// have a length exactly matching the specified length, but will retain the same direction. + pub fn extend(&self, length: f32) -> Vector2 { + *self * (length / self.length()) + } + + /// Returns the angle (in radians) equivalent to the direction of this vector. + #[inline] + pub fn angle(&self) -> f32 { + self.y.atan2(self.x) + } + + /// Calculates the angle (in radians) between the this and another vector. + #[inline] + pub fn angle_between(&self, other: &Vector2) -> f32 { + angle_between(self.x, self.y, other.x, other.y) + } +} + +impl Neg for Vector2 { + type Output = Self; + + #[inline] + fn neg(self) -> Self::Output { + Vector2 { + x: -self.x, + y: -self.y, + } + } +} + +impl Add for Vector2 { + type Output = Self; + + #[inline] + fn add(self, rhs: Self) -> Self::Output { + Vector2 { + x: self.x + rhs.x, + y: self.y + rhs.y, + } + } +} + +impl AddAssign for Vector2 { + #[inline] + fn add_assign(&mut self, rhs: Self) { + self.x += rhs.x; + self.y += rhs.y; + } +} + +impl Sub for Vector2 { + type Output = Self; + + #[inline] + fn sub(self, rhs: Self) -> Self::Output { + Vector2 { + x: self.x - rhs.x, + y: self.y - rhs.y, + } + } +} + +impl SubAssign for Vector2 { + #[inline] + fn sub_assign(&mut self, rhs: Self) { + self.x -= rhs.x; + self.y -= rhs.y; + } +} + +impl Mul for Vector2 { + type Output = Self; + + #[inline] + fn mul(self, rhs: Self) -> Self::Output { + Vector2 { + x: self.x * rhs.x, + y: self.y * rhs.y, + } + } +} + +impl MulAssign for Vector2 { + #[inline] + fn mul_assign(&mut self, rhs: Self) { + self.x *= rhs.x; + self.y *= rhs.y; + } +} + +impl Div for Vector2 { + type Output = Self; + + #[inline] + fn div(self, rhs: Self) -> Self::Output { + Vector2 { + x: self.x / rhs.x, + y: self.y / rhs.y, + } + } +} + +impl DivAssign for Vector2 { + #[inline] + fn div_assign(&mut self, rhs: Self) { + self.x /= rhs.x; + self.y /= rhs.y; + } +} + +impl Mul for Vector2 { + type Output = Self; + + #[inline] + fn mul(self, rhs: f32) -> Self::Output { + Vector2 { + x: self.x * rhs, + y: self.y * rhs, + } + } +} + +impl MulAssign for Vector2 { + #[inline] + fn mul_assign(&mut self, rhs: f32) { + self.x *= rhs; + self.y *= rhs; + } +} + +impl Div for Vector2 { + type Output = Self; + + #[inline] + fn div(self, rhs: f32) -> Self::Output { + Vector2 { + x: self.x / rhs, + y: self.y / rhs, + } + } +} + +impl DivAssign for Vector2 { + #[inline] + fn div_assign(&mut self, rhs: f32) { + self.x /= rhs; + self.y /= rhs; + } +} + +impl NearlyEqual for Vector2 { + type Output = Self; + + #[inline(always)] + fn nearly_equal(self, other: Self::Output, epsilon: f32) -> bool { + nearly_equal(self.x, other.x, epsilon) && nearly_equal(self.y, other.y, epsilon) + } +} + +impl MulAssign for Vector2 { + #[rustfmt::skip] + #[inline] + fn mul_assign(&mut self, rhs: Matrix3x3) { + let x = self.x * rhs.m[Matrix3x3::M11] + self.y * rhs.m[Matrix3x3::M12] + rhs.m[Matrix3x3::M13] + rhs.m[Matrix3x3::M31]; + let y = self.x * rhs.m[Matrix3x3::M21] + self.y * rhs.m[Matrix3x3::M22] + rhs.m[Matrix3x3::M23] + rhs.m[Matrix3x3::M32]; + self.x = x; + self.y = y; + } +} + +#[cfg(test)] +pub mod tests { + use crate::math::*; + + use super::*; + + #[test] + pub fn test_new() { + let v = Vector2::new(3.0, 7.0); + assert!(nearly_equal(v.x, 3.0, 0.0001)); + assert!(nearly_equal(v.y, 7.0, 0.0001)); + } + + #[test] + pub fn test_neg() { + let v = Vector2 { x: 1.0, y: 2.0 }; + let neg = -v; + assert!(nearly_equal(neg.x, -1.0, 0.0001)); + assert!(nearly_equal(neg.y, -2.0, 0.0001)); + } + + #[test] + pub fn test_add() { + let a = Vector2 { x: 3.0, y: 4.0 }; + let b = Vector2 { x: 1.0, y: 2.0 }; + let c = a + b; + assert!(nearly_equal(c.x, 4.0, 0.0001)); + assert!(nearly_equal(c.y, 6.0, 0.0001)); + + let mut a = Vector2 { x: 3.0, y: 4.0 }; + let b = Vector2 { x: 1.0, y: 2.0 }; + a += b; + assert!(nearly_equal(a.x, 4.0, 0.0001)); + assert!(nearly_equal(a.y, 6.0, 0.0001)); + } + + #[test] + pub fn test_sub() { + let a = Vector2 { x: 3.0, y: 4.0 }; + let b = Vector2 { x: 1.0, y: 2.0 }; + let c = a - b; + assert!(nearly_equal(c.x, 2.0, 0.0001)); + assert!(nearly_equal(c.y, 2.0, 0.0001)); + + let mut a = Vector2 { x: 3.0, y: 4.0 }; + let b = Vector2 { x: 1.0, y: 2.0 }; + a -= b; + assert!(nearly_equal(a.x, 2.0, 0.0001)); + assert!(nearly_equal(a.y, 2.0, 0.0001)); + } + + #[test] + pub fn test_mul() { + let a = Vector2 { x: 2.5, y: 6.0 }; + let b = Vector2 { x: 1.25, y: 2.0 }; + let c = a * b; + assert!(nearly_equal(c.x, 3.125, 0.0001)); + assert!(nearly_equal(c.y, 12.0, 0.0001)); + + let mut a = Vector2 { x: 2.5, y: 6.0 }; + let b = Vector2 { x: 1.25, y: 2.0 }; + a *= b; + assert!(nearly_equal(a.x, 3.125, 0.0001)); + assert!(nearly_equal(a.y, 12.0, 0.0001)); + } + + #[test] + pub fn test_div() { + let a = Vector2 { x: 2.5, y: 6.0 }; + let b = Vector2 { x: 1.25, y: 2.0 }; + let c = a / b; + assert!(nearly_equal(c.x, 2.0, 0.0001)); + assert!(nearly_equal(c.y, 3.0, 0.0001)); + + let mut a = Vector2 { x: 2.5, y: 6.0 }; + let b = Vector2 { x: 1.25, y: 2.0 }; + a /= b; + assert!(nearly_equal(a.x, 2.0, 0.0001)); + assert!(nearly_equal(a.y, 3.0, 0.0001)); + } + + #[test] + pub fn test_scalar_mul() { + let a = Vector2 { x: 1.0, y: 2.0 }; + let b = a * 2.0; + assert!(nearly_equal(b.x, 2.0, 0.0001)); + assert!(nearly_equal(b.y, 4.0, 0.0001)); + + let mut a = Vector2 { x: 1.0, y: 2.0 }; + a *= 2.0; + assert!(nearly_equal(b.x, 2.0, 0.0001)); + assert!(nearly_equal(b.y, 4.0, 0.0001)); + } + + #[test] + pub fn test_scalar_div() { + let a = Vector2 { x: 1.0, y: 2.0 }; + let b = a / 2.0; + assert!(nearly_equal(b.x, 0.5, 0.0001)); + assert!(nearly_equal(b.y, 1.0, 0.0001)); + + let mut a = Vector2 { x: 1.0, y: 2.0 }; + a /= 2.0; + assert!(nearly_equal(b.x, 0.5, 0.0001)); + assert!(nearly_equal(b.y, 1.0, 0.0001)); + } + + #[test] + pub fn test_nearly_equal() { + let a = Vector2 { x: 3.4, y: -7.1 }; + let b = Vector2 { x: 3.5, y: -7.1 }; + assert!(!a.nearly_equal(b, 0.0001)); + + let a = Vector2 { x: 2.0, y: 4.0 }; + let b = Vector2 { x: 2.0, y: 4.0 }; + assert!(a.nearly_equal(b, 0.0001)); + } + + #[test] + pub fn test_length() { + let v = Vector2 { x: 6.0, y: 8.0 }; + let length_squared = v.length_squared(); + let length = v.length(); + assert!(nearly_equal(length_squared, 100.0, 0.0001)); + assert!(nearly_equal(length, 10.0, 0.0001)); + } + + #[test] + pub fn test_dot() { + let a = Vector2 { x: -6.0, y: 8.0 }; + let b = Vector2 { x: 5.0, y: 12.0 }; + let dot = a.dot(&b); + assert!(nearly_equal(dot, 66.0, 0.0001)); + + let a = Vector2 { x: -12.0, y: 16.0 }; + let b = Vector2 { x: 12.0, y: 9.0 }; + let dot = a.dot(&b); + assert!(nearly_equal(dot, 0.0, 0.0001)); + } + + #[test] + pub fn test_distance() { + let a = Vector2 { x: 1.0, y: 1.0 }; + let b = Vector2 { x: 1.0, y: 3.0 }; + let distance_squared = a.distance_squared(&b); + let distance = a.distance(&b); + assert!(nearly_equal(distance_squared, 4.0, 0.0001)); + assert!(nearly_equal(distance, 2.0, 0.0001)); + } + + #[test] + pub fn test_normalize() { + let v = Vector2 { x: 3.0, y: 4.0 }; + let normalized = v.normalize(); + assert!(nearly_equal(normalized.x, 0.6, 0.0001)); + assert!(nearly_equal(normalized.y, 0.8, 0.0001)); + } + + #[test] + pub fn test_extend() { + let v = Vector2 { x: 10.0, y: 1.0 }; + let extended = v.extend(2.0); + assert!(nearly_equal(extended.x, 1.990, 0.0001)); + assert!(nearly_equal(extended.y, 0.199, 0.0001)); + } + + #[test] + #[rustfmt::skip] + pub fn test_angle() { + assert!(nearly_equal(RADIANS_0, Vector2::new(5.0, 0.0).angle(), 0.0001)); + assert!(nearly_equal(RADIANS_45, Vector2::new(5.0, 5.0).angle(), 0.0001)); + assert!(nearly_equal(RADIANS_90, Vector2::new(0.0, 5.0).angle(), 0.0001)); + assert!(nearly_equal(RADIANS_135, Vector2::new(-5.0, 5.0).angle(), 0.0001)); + assert!(nearly_equal(RADIANS_180, Vector2::new(-5.0, 0.0).angle(), 0.0001)); + assert!(nearly_equal(-RADIANS_135, Vector2::new(-5.0, -5.0).angle(), 0.0001)); + assert!(nearly_equal(-RADIANS_90, Vector2::new(0.0, -5.0).angle(), 0.0001)); + assert!(nearly_equal(-RADIANS_45, Vector2::new(5.0, -5.0).angle(), 0.0001)); + + assert!(nearly_equal(crate::math::UP, Vector2::UP.angle(), 0.0001)); + assert!(nearly_equal(crate::math::DOWN, Vector2::DOWN.angle(), 0.0001)); + assert!(nearly_equal(crate::math::LEFT, Vector2::LEFT.angle(), 0.0001)); + assert!(nearly_equal(crate::math::RIGHT, Vector2::RIGHT.angle(), 0.0001)); + } + + #[test] + pub fn test_from_angle() { + let v = Vector2::from_angle(RADIANS_0); + assert!(nearly_equal(v.x, 1.0, 0.000001)); + assert!(nearly_equal(v.y, 0.0, 0.000001)); + let v = Vector2::from_angle(RADIANS_45); + assert!(nearly_equal(v.x, 0.707106, 0.000001)); + assert!(nearly_equal(v.y, 0.707106, 0.000001)); + let v = Vector2::from_angle(RADIANS_225); + assert!(nearly_equal(v.x, -0.707106, 0.000001)); + assert!(nearly_equal(v.y, -0.707106, 0.000001)); + + let v = Vector2::from_angle(UP); + assert!(v.nearly_equal(Vector2::UP, 0.000001)); + let v = Vector2::from_angle(DOWN); + assert!(v.nearly_equal(Vector2::DOWN, 0.000001)); + let v = Vector2::from_angle(LEFT); + assert!(v.nearly_equal(Vector2::LEFT, 0.000001)); + let v = Vector2::from_angle(RIGHT); + assert!(v.nearly_equal(Vector2::RIGHT, 0.000001)); + } + + #[test] + pub fn test_angle_between() { + let a = Vector2::new(20.0, 20.0); + let b = Vector2::new(10.0, 10.0); + let angle = a.angle_between(&b); + assert!(nearly_equal(-RADIANS_135, angle, 0.0001)); + + let a = Vector2::new(0.0, 0.0); + let b = Vector2::new(10.0, 10.0); + let angle = a.angle_between(&b); + assert!(nearly_equal(RADIANS_45, angle, 0.0001)); + + let a = Vector2::new(5.0, 5.0); + let b = Vector2::new(5.0, 5.0); + let angle = a.angle_between(&b); + assert!(nearly_equal(0.0, angle, 0.0001)); + } + + #[test] + pub fn test_lerp() { + let a = Vector2 { x: 5.0, y: 1.0 }; + let b = Vector2 { x: 10.0, y: 2.0 }; + let c = lerp(a, b, 0.5); + assert!(nearly_equal(c.x, 7.5, 0.0001)); + assert!(nearly_equal(c.y, 1.5, 0.0001)); + } +} diff --git a/libretrogd/src/states/mod.rs b/libretrogd/src/states/mod.rs new file mode 100644 index 0000000..68b7958 --- /dev/null +++ b/libretrogd/src/states/mod.rs @@ -0,0 +1,1264 @@ +use std::collections::VecDeque; +use std::ops::DerefMut; + +use thiserror::Error; + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum TransitionTo { + Paused, + Dead, +} + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum TransitionDirection { + In, + Out +} + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum State { + Pending, + Active, + Paused, + TransitionIn, + TransitionOut(TransitionTo), + Dead, +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +pub enum StateChange { + Push(Box>), + Pop, +} + +pub trait GameState { + fn update(&mut self, state: State, context: &mut ContextType) -> Option>; + fn render(&mut self, state: State, context: &mut ContextType); + fn transition(&mut self, state: State, context: &mut ContextType) -> bool; + fn state_change(&mut self, new_state: State, old_state: State, context: &mut ContextType); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#[derive(Error, Debug)] +pub enum StateError { + #[error("Operation cannot currently be performed because there is already a pending state change.")] + HasPendingStateChange, + + #[error("Operation cannot currently be performed because the GameState's current state ({0:?}) does not allow it.")] + GameStateInvalidState(State), +} + +struct StateContainer { + current_state: State, + pending_state_change: Option, + state: Box>, +} + +impl StateContainer { + pub fn new(state: Box>) -> Self { + StateContainer { + current_state: State::Dead, + pending_state_change: None, + state, + } + } + + #[inline] + pub fn current_state(&self) -> State { + self.current_state + } + + #[inline] + pub fn pending_state_change(&mut self) -> Option { + self.pending_state_change.take() + } + + #[inline] + pub fn change_state(&mut self, new_state: State, context: &mut ContextType) { + let old_state = self.current_state; + self.current_state = new_state; + self.state.state_change(self.current_state, old_state, context); + } + + #[inline] + pub fn state(&mut self) -> &mut dyn GameState { + self.state.deref_mut() + } + + pub fn transition_out(&mut self, to: TransitionTo, context: &mut ContextType) -> Result<(), StateError> { + if self.current_state == State::Active { + self.change_state(State::TransitionOut(to), context); + Ok(()) + } else { + Err(StateError::GameStateInvalidState(self.current_state)) + } + } + + #[inline] + pub fn pending_transition_out(&mut self, to: TransitionTo) { + self.pending_state_change = Some(State::TransitionOut(to)); + } + + pub fn transition_in(&mut self, context: &mut ContextType) -> Result<(), StateError> { + match self.current_state { + State::Pending | State::Paused => { + self.change_state(State::TransitionIn, context); + Ok(()) + }, + _ => { + Err(StateError::GameStateInvalidState(self.current_state)) + } + } + } + + #[inline] + pub fn pending_transition_in(&mut self) { + self.pending_state_change = Some(State::TransitionIn); + } + + pub fn activate(&mut self, context: &mut ContextType) -> Result<(), StateError> { + self.change_state(State::Active, context); + Ok(()) + } + + #[inline] + pub fn pending_activate(&mut self) { + self.pending_state_change = Some(State::Active); + } + + pub fn pause(&mut self, context: &mut ContextType) -> Result<(), StateError> { + self.change_state(State::Paused, context); + Ok(()) + } + + #[inline] + pub fn pending_pause(&mut self) { + self.pending_state_change = Some(State::Paused); + } + + pub fn kill(&mut self, context: &mut ContextType) -> Result<(), StateError> { + self.change_state(State::Dead, context); + Ok(()) + } + + #[inline] + pub fn pending_kill(&mut self) { + self.pending_state_change = Some(State::Dead); + } +} + +pub struct States { + states: VecDeque>, + command: Option>, + pending_state: Option>>, +} + +impl States { + pub fn new() -> Self { + States { + states: VecDeque::new(), + command: None, + pending_state: None, + } + } + + fn can_push_or_pop(&self) -> bool { + if let Some(state) = self.states.front() { + if state.current_state != State::Active { + return false; + } + } + if self.pending_state.is_some() { + return false; + } + + true + } + + fn push_boxed_state(&mut self, boxed_state: Box>) -> Result<(), StateError> { + if !self.can_push_or_pop() { + Err(StateError::HasPendingStateChange) + } else { + self.command = Some(StateChange::Push(boxed_state)); + Ok(()) + } + } + + pub fn push(&mut self, state: impl GameState + 'static) -> Result<(), StateError> { + self.push_boxed_state(Box::new(state)) + } + + pub fn pop(&mut self) -> Result<(), StateError> { + if !self.can_push_or_pop() { + Err(StateError::HasPendingStateChange) + } else { + if !self.states.is_empty() { + self.command = Some(StateChange::Pop); + } + Ok(()) + } + } + + fn state_of_front_state(&self) -> Option { + if let Some(state) = self.states.front() { + Some(state.current_state()) + } else { + None + } + } + + fn process_state_changes(&mut self, context: &mut ContextType) -> Result<(), StateError> { + if let Some(command) = self.command.take() { + match command { + StateChange::Push(new_state) => { + self.pending_state = Some(new_state); + } + StateChange::Pop => { + if let Some(state) = self.states.front_mut() { + state.pending_transition_out(TransitionTo::Dead); + } + } + } + } + + if self.pending_state.is_some() { + if self.states.is_empty() { + // special case to bootstrap the stack of states when e.g. the system is first set + // up with the very first state pushed to it. + let mut new_state = StateContainer::new(self.pending_state.take().unwrap()); + new_state.change_state(State::Pending, context); + self.states.push_front(new_state); + } else if self.state_of_front_state() == Some(State::Active) { + // if the current state is active and there is a pending state waiting to be added, + // we need to start transitioning out the active state towards a 'paused' state + let state = self.states.front_mut().unwrap(); + state.pending_transition_out(TransitionTo::Paused); + } + } + + // handle any pending state change queued from the previous frame, so that we can + // process the state as necessary below ... + if let Some(state) = self.states.front_mut() { + if let Some(pending_state_change) = state.pending_state_change() { + match pending_state_change { + State::Dead => state.kill(context)?, + State::Paused => state.pause(context)?, + State::Active => state.activate(context)?, + State::TransitionOut(to) => state.transition_out(to, context)?, + State::TransitionIn => state.transition_in(context)?, + _ => {}, + } + } + } + + + // now figure out what state change processing is needed based on the current state ... + match self.state_of_front_state() { + Some(State::Pending) => { + // top state is just sitting there pending, lets start it up ... + let state = self.states.front_mut().unwrap(); + state.pending_transition_in(); + }, + Some(State::Paused) => { + if self.pending_state.is_some() { + // top state is paused and we have a new state waiting to be added. + // add the new state + let mut new_state = StateContainer::new(self.pending_state.take().unwrap()); + new_state.change_state(State::Pending, context); + self.states.push_front(new_state); + } + }, + Some(State::Dead) => { + // remove the dead state + self.states.pop_front(); + + if self.pending_state.is_some() { + // if there is a new pending state waiting, we can add it here right now + let mut new_state = StateContainer::new(self.pending_state.take().unwrap()); + new_state.change_state(State::Pending, context); + self.states.push_front(new_state); + } else if self.state_of_front_state() == Some(State::Paused) { + // otherwise, we're probably waking up a state that was paused and needs to + // be resumed since it's once again on top + let state = self.states.front_mut().unwrap(); + state.pending_transition_in(); + } + }, + Some(State::TransitionIn) => { + let state = self.states.front_mut().unwrap(); + if state.state().transition(State::TransitionIn, context) { + // state has indicated it is done transitioning, so we can switch it to active + state.pending_activate(); + } + }, + Some(State::TransitionOut(to)) => { + let state = self.states.front_mut().unwrap(); + if state.state().transition(State::TransitionOut(to), context) { + // state has indicated it is done transitioning, so we can switch it to whatever + // it was transitioning to + match to { + TransitionTo::Paused => { state.pending_pause(); }, + TransitionTo::Dead => { state.pending_kill(); } + } + } + }, + _ => {} + } + + Ok(()) + } + + pub fn update(&mut self, context: &mut ContextType) -> Result<(), StateError> { + self.process_state_changes(context)?; + if let Some(state) = self.states.front_mut() { + let current_state = state.current_state(); + match current_state { + State::Active | State::TransitionIn | State::TransitionOut(_) => { + if let Some(state_change) = state.state().update(current_state, context) { + match state_change { + StateChange::Push(state) => self.push_boxed_state(state)?, + StateChange::Pop => self.pop()?, + } + } + } + _ => {} + } + } + Ok(()) + } + + pub fn render(&mut self, context: &mut ContextType) { + if let Some(state) = self.states.front_mut() { + let current_state = state.current_state(); + match current_state { + State::Active | State::TransitionIn | State::TransitionOut(_) => { + state.state().render(current_state, context); + }, + _ => {} + } + } + } +} + + +#[cfg(test)] +mod tests { + use claim::*; + + use super::*; + + #[derive(Debug, Eq, PartialEq, Copy, Clone)] + enum LogEntry { + Update(u32, State), + Render(u32, State), + Transition(u32, State), + StateChange(u32, State, State), + } + + struct TestContext { + pub log: Vec, + } + + impl TestContext { + pub fn new() -> Self { + TestContext { + log: Vec::new() + } + } + + pub fn log(&mut self, entry: LogEntry) { + self.log.push(entry); + } + + pub fn take_log(&mut self) -> Vec { + let taken = self.log.to_vec(); + self.log.clear(); + taken + } + } + + struct TestState { + id: u32, + counter: u32, + transition_length: u32, + } + + impl TestState { + pub fn new(id: u32) -> Self { + TestState { + id, + counter: 0, + transition_length: 0, + } + } + + pub fn new_with_transition_length(id: u32, transition_length: u32) -> Self { + TestState { + id, + counter: 0, + transition_length, + } + } + } + + impl GameState for TestState { + fn update(&mut self, state: State, context: &mut TestContext) -> Option> { + context.log(LogEntry::Update(self.id, state)); + None + } + + fn render(&mut self, state: State, context: &mut TestContext) { + context.log(LogEntry::Render(self.id, state)); + } + + fn transition(&mut self, state: State, context: &mut TestContext) -> bool { + context.log(LogEntry::Transition(self.id, state)); + if self.counter > 0 { + self.counter -= 1; + } + if self.counter == 0 { + true + } else { + false + } + } + + fn state_change(&mut self, new_state: State, old_state: State, context: &mut TestContext) { + context.log(LogEntry::StateChange(self.id, new_state, old_state)); + match new_state { + State::TransitionIn | State::TransitionOut(_) => { + self.counter = self.transition_length; + }, + _ => {} + } + } + } + + fn tick(states: &mut States, context: &mut ContextType) -> Result<(), StateError> { + states.update(context)?; + states.render(context); + Ok(()) + } + + #[test] + fn push_and_pop_state() -> Result<(), StateError> { + use LogEntry::*; + use State::*; + + const FOO: u32 = 1; + + let mut states = States::::new(); + let mut context = TestContext::new(); + + states.push(TestState::new(FOO))?; + assert_eq!(context.take_log(), vec![]); + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(FOO, Pending, Dead)]); + // state will transition in + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FOO, TransitionIn, Pending), + Transition(FOO, TransitionIn), + Update(FOO, TransitionIn), + Render(FOO, TransitionIn) + ] + ); + // state finished transitioning in, now moves to active + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FOO, Active, TransitionIn), + Update(FOO, Active), + Render(FOO, Active) + ] + ); + + states.pop()?; + assert_eq!(context.take_log(), vec![]); + // state begins to transition out to 'dead' + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FOO, TransitionOut(TransitionTo::Dead), Active), + Transition(FOO, TransitionOut(TransitionTo::Dead)), + Update(FOO, TransitionOut(TransitionTo::Dead)), + Render(FOO, TransitionOut(TransitionTo::Dead)) + ] + ); + // state finished transitioning out, now dies + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(FOO, Dead, TransitionOut(TransitionTo::Dead))]); + + // nothing! no states anymore! + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![]); + + Ok(()) + } + + #[test] + fn push_and_pop_state_with_longer_transition() -> Result<(), StateError> { + use LogEntry::*; + use State::*; + + const FOO: u32 = 1; + + let mut states = States::::new(); + let mut context = TestContext::new(); + + states.push(TestState::new_with_transition_length(FOO, 5))?; + assert_eq!(context.take_log(), vec![]); + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(FOO, Pending, Dead)]); + // state will transition in + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FOO, TransitionIn, Pending), + Transition(FOO, TransitionIn), + Update(FOO, TransitionIn), + Render(FOO, TransitionIn) + ] + ); + // wait for transition to finish + for _ in 0..4 { + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + Transition(FOO, TransitionIn), + Update(FOO, TransitionIn), + Render(FOO, TransitionIn) + ] + ); + } + // state finished transitioning in, now moves to active + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FOO, Active, TransitionIn), + Update(FOO, Active), + Render(FOO, Active) + ] + ); + + states.pop()?; + assert_eq!(context.take_log(), vec![]); + // state begins to transition out to 'dead' + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FOO, TransitionOut(TransitionTo::Dead), Active), + Transition(FOO, TransitionOut(TransitionTo::Dead)), + Update(FOO, TransitionOut(TransitionTo::Dead)), + Render(FOO, TransitionOut(TransitionTo::Dead)) + ] + ); + // wait for transition to finish + for _ in 0..4 { + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + Transition(FOO, TransitionOut(TransitionTo::Dead)), + Update(FOO, TransitionOut(TransitionTo::Dead)), + Render(FOO, TransitionOut(TransitionTo::Dead)) + ] + ); + } + // state finished transitioning out, now dies + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(FOO, Dead, TransitionOut(TransitionTo::Dead))]); + + // nothing! no states anymore! + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![]); + + Ok(()) + } + + #[test] + fn push_and_pop_multiple_states() -> Result<(), StateError> { + use LogEntry::*; + use State::*; + + const FIRST: u32 = 1; + const SECOND: u32 = 2; + + let mut states = States::::new(); + let mut context = TestContext::new(); + + // push first state + + states.push(TestState::new(FIRST))?; + assert_eq!(context.take_log(), vec![]); + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(FIRST, Pending, Dead)]); + // first state will transition in + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, TransitionIn, Pending), + Transition(FIRST, TransitionIn), + Update(FIRST, TransitionIn), + Render(FIRST, TransitionIn) + ] + ); + // first state finished transitioning in, now moves to active + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, Active, TransitionIn), + Update(FIRST, Active), + Render(FIRST, Active) + ] + ); + + // push second state + + states.push(TestState::new(SECOND))?; + assert_eq!(context.take_log(), vec![]); + // first state begins to transition out to 'paused' state + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, TransitionOut(TransitionTo::Paused), Active), + Transition(FIRST, TransitionOut(TransitionTo::Paused)), + Update(FIRST, TransitionOut(TransitionTo::Paused)), + Render(FIRST, TransitionOut(TransitionTo::Paused)) + ] + ); + // state finished transitioning out, now is paused + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, Paused, TransitionOut(TransitionTo::Paused)), + StateChange(SECOND, Pending, Dead), + ] + ); + // second state starts up + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![]); + // second state will transition in + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(SECOND, TransitionIn, Pending), + Transition(SECOND, TransitionIn), + Update(SECOND, TransitionIn), + Render(SECOND, TransitionIn) + ] + ); + // second state finished transitioning in, now moves to active + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(SECOND, Active, TransitionIn), + Update(SECOND, Active), + Render(SECOND, Active) + ] + ); + + // pop second state + + states.pop()?; + assert_eq!(context.take_log(), vec![]); + // second state begins to transition out to 'dead' + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(SECOND, TransitionOut(TransitionTo::Dead), Active), + Transition(SECOND, TransitionOut(TransitionTo::Dead)), + Update(SECOND, TransitionOut(TransitionTo::Dead)), + Render(SECOND, TransitionOut(TransitionTo::Dead)) + ] + ); + // second state finished transitioning out, now dies. first state wakes up again. + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(SECOND, Dead, TransitionOut(TransitionTo::Dead))]); + // first state will transition in + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, TransitionIn, Paused), + Transition(FIRST, TransitionIn), + Update(FIRST, TransitionIn), + Render(FIRST, TransitionIn) + ] + ); + // first state finished transitioning in, now moves to active + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, Active, TransitionIn), + Update(FIRST, Active), + Render(FIRST, Active) + ] + ); + + // pop first state + + states.pop()?; + assert_eq!(context.take_log(), vec![]); + // first state begins to transition out to 'dead' + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, TransitionOut(TransitionTo::Dead), Active), + Transition(FIRST, TransitionOut(TransitionTo::Dead)), + Update(FIRST, TransitionOut(TransitionTo::Dead)), + Render(FIRST, TransitionOut(TransitionTo::Dead)) + ] + ); + // first state finished transitioning out, now dies + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(FIRST, Dead, TransitionOut(TransitionTo::Dead))]); + + // nothing! no states anymore! + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![]); + + Ok(()) + } + + #[test] + fn push_and_pop_multiple_states_with_longer_transitions() -> Result<(), StateError> { + use LogEntry::*; + use State::*; + + const FIRST: u32 = 1; + const SECOND: u32 = 2; + + let mut states = States::::new(); + let mut context = TestContext::new(); + + // push first state + + states.push(TestState::new_with_transition_length(FIRST, 3))?; + assert_eq!(context.take_log(), vec![]); + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(FIRST, Pending, Dead)]); + // first state will transition in + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, TransitionIn, Pending), + Transition(FIRST, TransitionIn), + Update(FIRST, TransitionIn), + Render(FIRST, TransitionIn) + ] + ); + // wait for transition to finish + for _ in 0..2 { + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + Transition(FIRST, TransitionIn), + Update(FIRST, TransitionIn), + Render(FIRST, TransitionIn) + ] + ); + } + // first state finished transitioning in, now moves to active + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, Active, TransitionIn), + Update(FIRST, Active), + Render(FIRST, Active) + ] + ); + + // push second state + + states.push(TestState::new_with_transition_length(SECOND, 5))?; + assert_eq!(context.take_log(), vec![]); + // first state begins to transition out to 'paused' state + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, TransitionOut(TransitionTo::Paused), Active), + Transition(FIRST, TransitionOut(TransitionTo::Paused)), + Update(FIRST, TransitionOut(TransitionTo::Paused)), + Render(FIRST, TransitionOut(TransitionTo::Paused)) + ] + ); + // wait for transition to finish + for _ in 0..2 { + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + Transition(FIRST, TransitionOut(TransitionTo::Paused)), + Update(FIRST, TransitionOut(TransitionTo::Paused)), + Render(FIRST, TransitionOut(TransitionTo::Paused)) + ] + ); + } + // first state finished transitioning out, now is paused + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, Paused, TransitionOut(TransitionTo::Paused)), + StateChange(SECOND, Pending, Dead) + ] + ); + // second state starts up + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![]); + // second state will transition in + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(SECOND, TransitionIn, Pending), + Transition(SECOND, TransitionIn), + Update(SECOND, TransitionIn), + Render(SECOND, TransitionIn) + ] + ); + // wait for transition to finish + for _ in 0..4 { + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + Transition(SECOND, TransitionIn), + Update(SECOND, TransitionIn), + Render(SECOND, TransitionIn) + ] + ); + } + // second state finished transitioning in, now moves to active + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(SECOND, Active, TransitionIn), + Update(SECOND, Active), + Render(SECOND, Active) + ] + ); + + // pop second state + + states.pop()?; + assert_eq!(context.take_log(), vec![]); + // second state begins to transition out to 'dead' + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(SECOND, TransitionOut(TransitionTo::Dead), Active), + Transition(SECOND, TransitionOut(TransitionTo::Dead)), + Update(SECOND, TransitionOut(TransitionTo::Dead)), + Render(SECOND, TransitionOut(TransitionTo::Dead)) + ] + ); + // wait for transition to finish + for _ in 0..4 { + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + Transition(SECOND, TransitionOut(TransitionTo::Dead)), + Update(SECOND, TransitionOut(TransitionTo::Dead)), + Render(SECOND, TransitionOut(TransitionTo::Dead)) + ] + ); + } + // second state finished transitioning out, now dies. first state wakes up again. + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(SECOND, Dead, TransitionOut(TransitionTo::Dead))]); + // first state will transition in + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, TransitionIn, Paused), + Transition(FIRST, TransitionIn), + Update(FIRST, TransitionIn), + Render(FIRST, TransitionIn) + ] + ); + // wait for transition to finish + for _ in 0..2 { + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + Transition(FIRST, TransitionIn), + Update(FIRST, TransitionIn), + Render(FIRST, TransitionIn) + ] + ); + } + // first state finished transitioning in, now moves to active + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, Active, TransitionIn), + Update(FIRST, Active), + Render(FIRST, Active) + ] + ); + + // pop first state + + states.pop()?; + assert_eq!(context.take_log(), vec![]); + // first state begins to transition out to 'dead' + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, TransitionOut(TransitionTo::Dead), Active), + Transition(FIRST, TransitionOut(TransitionTo::Dead)), + Update(FIRST, TransitionOut(TransitionTo::Dead)), + Render(FIRST, TransitionOut(TransitionTo::Dead)) + ] + ); + // wait for transition to finish + for _ in 0..2 { + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + Transition(FIRST, TransitionOut(TransitionTo::Dead)), + Update(FIRST, TransitionOut(TransitionTo::Dead)), + Render(FIRST, TransitionOut(TransitionTo::Dead)) + ] + ); + } + // first state finished transitioning out, now dies + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(FIRST, Dead, TransitionOut(TransitionTo::Dead))]); + + // nothing! no states anymore! + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![]); + + Ok(()) + } + + struct SelfPushPopState { + id: u32, + counter: u32, + push_after: Option, + pop_after: u32, + } + + impl SelfPushPopState { + pub fn new(id: u32, push_after: Option, pop_after: u32) -> Self { + SelfPushPopState { + id, + counter: 0, + push_after, + pop_after + } + } + } + + impl GameState for SelfPushPopState { + fn update(&mut self, state: State, context: &mut TestContext) -> Option> { + context.log(LogEntry::Update(self.id, state)); + if state == State::Active { + self.counter += 1; + if self.push_after == Some(self.counter) { + return Some(StateChange::Push(Box::new(SelfPushPopState::new(self.id + 1, None, self.pop_after)))); + } else if self.pop_after == self.counter { + return Some(StateChange::Pop); + } + } + None + } + + fn render(&mut self, state: State, context: &mut TestContext) { + context.log(LogEntry::Render(self.id, state)); + } + + fn transition(&mut self, state: State, context: &mut TestContext) -> bool { + context.log(LogEntry::Transition(self.id, state)); + true + } + + fn state_change(&mut self, new_state: State, old_state: State, context: &mut TestContext) { + context.log(LogEntry::StateChange(self.id, new_state, old_state)); + } + } + + + #[test] + fn state_can_push_and_pop_states_itself() -> Result<(), StateError> { + use LogEntry::*; + use State::*; + + const FIRST: u32 = 1; + const SECOND: u32 = 2; + + let mut states = States::::new(); + let mut context = TestContext::new(); + + // pop first state. it will do the rest this time ... + + states.push(SelfPushPopState::new(FIRST, Some(5), 10))?; + assert_eq!(context.take_log(), vec![]); + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(FIRST, Pending, Dead)]); + // first state will transition in + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, TransitionIn, Pending), + Transition(FIRST, TransitionIn), + Update(FIRST, TransitionIn), + Render(FIRST, TransitionIn) + ] + ); + // first state finished transitioning in, now moves to active + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, Active, TransitionIn), + Update(FIRST, Active), + Render(FIRST, Active) + ] + ); + // wait for first state's counter to count up to where it should push the second state + for _ in 0..4 { + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + Update(FIRST, Active), + Render(FIRST, Active) + ] + ); + } + + // first state begins to transition out to 'paused' state because it pushed the second state + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, TransitionOut(TransitionTo::Paused), Active), + Transition(FIRST, TransitionOut(TransitionTo::Paused)), + Update(FIRST, TransitionOut(TransitionTo::Paused)), + Render(FIRST, TransitionOut(TransitionTo::Paused)) + ] + ); + // first state finished transitioning out, now is paused + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, Paused, TransitionOut(TransitionTo::Paused)), + StateChange(SECOND, Pending, Dead) + ] + ); + // second state starts up + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![]); + // second state will transition in + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(SECOND, TransitionIn, Pending), + Transition(SECOND, TransitionIn), + Update(SECOND, TransitionIn), + Render(SECOND, TransitionIn) + ] + ); + // second state finished transitioning in, now moves to active + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(SECOND, Active, TransitionIn), + Update(SECOND, Active), + Render(SECOND, Active) + ] + ); + // wait for second state's counter to count up to where it should pop itself + for _ in 0..9 { + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + Update(SECOND, Active), + Render(SECOND, Active) + ] + ); + } + + // second state begins to transition out to 'dead' + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(SECOND, TransitionOut(TransitionTo::Dead), Active), + Transition(SECOND, TransitionOut(TransitionTo::Dead)), + Update(SECOND, TransitionOut(TransitionTo::Dead)), + Render(SECOND, TransitionOut(TransitionTo::Dead)) + ] + ); + // second state finished transitioning out, now dies. first state wakes up again. + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(SECOND, Dead, TransitionOut(TransitionTo::Dead))]); + // first state will transition in + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, TransitionIn, Paused), + Transition(FIRST, TransitionIn), + Update(FIRST, TransitionIn), + Render(FIRST, TransitionIn) + ] + ); + // first state finished transitioning in, now moves to active + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, Active, TransitionIn), + Update(FIRST, Active), + Render(FIRST, Active) + ] + ); + // wait for first state's counter to count up to where it should pop itself + for _ in 0..4 { + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + Update(FIRST, Active), + Render(FIRST, Active) + ] + ); + } + + // first state begins to transition out to 'dead' + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FIRST, TransitionOut(TransitionTo::Dead), Active), + Transition(FIRST, TransitionOut(TransitionTo::Dead)), + Update(FIRST, TransitionOut(TransitionTo::Dead)), + Render(FIRST, TransitionOut(TransitionTo::Dead)) + ] + ); + // first state finished transitioning out, now dies + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(FIRST, Dead, TransitionOut(TransitionTo::Dead))]); + + // nothing! no states anymore! + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![]); + + Ok(()) + } + + #[test] + fn cannot_push_or_pop_states_when_current_state_not_active() -> Result<(), StateError> { + use LogEntry::*; + use State::*; + + const FOO: u32 = 1; + + let mut states = States::::new(); + let mut context = TestContext::new(); + + states.push(TestState::new(FOO))?; + assert_eq!(context.take_log(), vec![]); + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(FOO, Pending, Dead)]); + + assert_matches!(states.push(TestState::new(123)), Err(StateError::HasPendingStateChange)); + assert_matches!(states.pop(), Err(StateError::HasPendingStateChange)); + + // state will transition in + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FOO, TransitionIn, Pending), + Transition(FOO, TransitionIn), + Update(FOO, TransitionIn), + Render(FOO, TransitionIn) + ] + ); + + assert_matches!(states.push(TestState::new(123)), Err(StateError::HasPendingStateChange)); + assert_matches!(states.pop(), Err(StateError::HasPendingStateChange)); + + // state finished transitioning in, now moves to active + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FOO, Active, TransitionIn), + Update(FOO, Active), + Render(FOO, Active) + ] + ); + + states.pop()?; + assert_eq!(context.take_log(), vec![]); + // state begins to transition out to 'dead' + tick(&mut states, &mut context)?; + assert_eq!( + context.take_log(), + vec![ + StateChange(FOO, TransitionOut(TransitionTo::Dead), Active), + Transition(FOO, TransitionOut(TransitionTo::Dead)), + Update(FOO, TransitionOut(TransitionTo::Dead)), + Render(FOO, TransitionOut(TransitionTo::Dead)) + ] + ); + + assert_matches!(states.push(TestState::new(123)), Err(StateError::HasPendingStateChange)); + assert_matches!(states.pop(), Err(StateError::HasPendingStateChange)); + + // state finished transitioning out, now dies + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![StateChange(FOO, Dead, TransitionOut(TransitionTo::Dead))]); + + states.pop()?; + + // nothing! no states anymore! + tick(&mut states, &mut context)?; + assert_eq!(context.take_log(), vec![]); + + Ok(()) + } +} diff --git a/libretrogd/src/system/input_devices/keyboard.rs b/libretrogd/src/system/input_devices/keyboard.rs new file mode 100644 index 0000000..63119fc --- /dev/null +++ b/libretrogd/src/system/input_devices/keyboard.rs @@ -0,0 +1,93 @@ +use sdl2::event::Event; +use sdl2::keyboard::Scancode; + +use super::{ButtonState, InputDevice}; + +const MAX_KEYS: usize = 256; + +/// Holds the current state of the keyboard. +/// +/// Must be explicitly updated each frame by calling `handle_event` each frame for all SDL2 events +/// received, as well as calling `do_events` once each frame. Usually, you would accomplish all +/// this house-keeping by simply calling [`System`]'s `do_events` method once per frame. +/// +/// [`System`]: crate::System +pub struct Keyboard { + keyboard: [ButtonState; MAX_KEYS], // Box<[ButtonState]>, +} + +impl Keyboard { + pub fn new() -> Keyboard { + Keyboard { + keyboard: [ButtonState::Idle; MAX_KEYS], + } + /* + Keyboard { + keyboard: vec![ButtonState::Idle; 256].into_boxed_slice(), + } + */ + } + + /// Returns true if the given key was just pressed or is being held down. + #[inline] + pub fn is_key_down(&self, scancode: Scancode) -> bool { + matches!( + self.keyboard[scancode as usize], + ButtonState::Pressed | ButtonState::Held + ) + } + + /// Returns true if the given key was not just pressed and is not being held down. + #[inline] + pub fn is_key_up(&self, scancode: Scancode) -> bool { + matches!( + self.keyboard[scancode as usize], + ButtonState::Released | ButtonState::Idle + ) + } + + /// Returns true if the given key was just pressed (not being held down, yet). + #[inline] + pub fn is_key_pressed(&self, scancode: Scancode) -> bool { + self.keyboard[scancode as usize] == ButtonState::Pressed + } + + /// Returns true if the given key was just released. + #[inline] + pub fn is_key_released(&self, scancode: Scancode) -> bool { + self.keyboard[scancode as usize] == ButtonState::Released + } +} + +impl InputDevice for Keyboard { + fn update(&mut self) { + for state in self.keyboard.iter_mut() { + *state = match *state { + ButtonState::Pressed => ButtonState::Held, + ButtonState::Released => ButtonState::Idle, + otherwise => otherwise, + }; + } + } + + fn handle_event(&mut self, event: &Event) { + match event { + Event::KeyDown { scancode, .. } => { + if let Some(scancode) = scancode { + let state = &mut self.keyboard[*scancode as usize]; + *state = match *state { + ButtonState::Pressed => ButtonState::Held, + ButtonState::Held => ButtonState::Held, + _ => ButtonState::Pressed, + }; + } + } + Event::KeyUp { scancode, .. } => { + if let Some(scancode) = scancode { + self.keyboard[*scancode as usize] = ButtonState::Released; + } + } + _ => (), + } + } +} diff --git a/libretrogd/src/system/input_devices/mod.rs b/libretrogd/src/system/input_devices/mod.rs new file mode 100644 index 0000000..613177e --- /dev/null +++ b/libretrogd/src/system/input_devices/mod.rs @@ -0,0 +1,27 @@ +use sdl2::event::Event; + +pub mod keyboard; +pub mod mouse; + +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub enum ButtonState { + Idle, + Pressed, + Held, + Released, +} + +/// Common trait for input device implementations. +pub trait InputDevice { + /// Performs internal house-keeping necessary for properly reporting the current state of this + /// input device. Normally this should be called on the device after all of this frame's + /// input events have been processed via `handle_event`. + fn update(&mut self); + + /// Processes the data from the given [`Event`] if it is relevant for this input device. You + /// should pass in all events received every frame and let the input device figure out if it + /// is relevant to it or not. + /// + /// [`Event`]: sdl2::event::Event + fn handle_event(&mut self, event: &Event); +} diff --git a/libretrogd/src/system/input_devices/mouse.rs b/libretrogd/src/system/input_devices/mouse.rs new file mode 100644 index 0000000..3967755 --- /dev/null +++ b/libretrogd/src/system/input_devices/mouse.rs @@ -0,0 +1,318 @@ +use sdl2::event::Event; + +use crate::{Bitmap, BlitMethod, Rect}; + +use super::{ButtonState, InputDevice}; + +const MAX_BUTTONS: usize = 32; + +const DEFAULT_MOUSE_CURSOR_HOTSPOT_X: u32 = 0; +const DEFAULT_MOUSE_CURSOR_HOTSPOT_Y: u32 = 0; +const DEFAULT_MOUSE_CURSOR_WIDTH: usize = 16; +const DEFAULT_MOUSE_CURSOR_HEIGHT: usize = 16; + +#[rustfmt::skip] +const DEFAULT_MOUSE_CURSOR: [u8; DEFAULT_MOUSE_CURSOR_WIDTH * DEFAULT_MOUSE_CURSOR_HEIGHT] = [ + 0x00,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0x00,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0x00,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0x00,0x0f,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0x00,0x0f,0x0f,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0x00,0x0f,0x0f,0x0f,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0x00,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0x00,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0x00,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff, + 0x00,0x0f,0x0f,0x0f,0x0f,0x0f,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0xff, + 0x00,0x0f,0x0f,0x00,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0x00,0x0f,0x00,0x00,0x00,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0x00,0x00,0xff,0xff,0x00,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0x00,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0x00,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff, + 0xff,0xff,0xff,0xff,0xff,0xff,0x00,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff +]; + +/// Holds the current state of the mouse. +/// +/// Must be explicitly updated each frame by calling `handle_event` each frame for all SDL2 events +/// received, as well as calling `do_events` once each frame. Usually, you would accomplish all +/// this house-keeping by simply calling [`System`]'s `do_events` method once per frame. +/// +/// [`System`]: crate::System +pub struct Mouse { + x: i32, + y: i32, + x_delta: i32, + y_delta: i32, + buttons: [ButtonState; MAX_BUTTONS], + cursor: Bitmap, + cursor_background: Bitmap, + cursor_hotspot_x: u32, + cursor_hotspot_y: u32, + cursor_enabled: bool, +} + +impl Mouse { + pub fn new() -> Mouse { + let (cursor, cursor_background, cursor_hotspot_x, cursor_hotspot_y) = + Self::get_default_mouse_cursor(); + + Mouse { + x: 0, + y: 0, + x_delta: 0, + y_delta: 0, + buttons: [ButtonState::Idle; MAX_BUTTONS], + cursor, + cursor_background, + cursor_hotspot_x, + cursor_hotspot_y, + cursor_enabled: false, + } + } + + /// Returns the current x coordinate of the mouse cursor. + #[inline] + pub fn x(&self) -> i32 { + self.x + } + + /// Returns the current y coordinate of the mouse cursor. + #[inline] + pub fn y(&self) -> i32 { + self.y + } + + /// Returns the amount of pixels along the x-axis that the mouse cursor moved since the last + /// time that the mouse state was updated. + #[inline] + pub fn x_delta(&self) -> i32 { + self.x_delta + } + + /// Returns the amount of pixels along the y-axis that the mouse cursor moved since the last + /// time that the mouse state was updated. + #[inline] + pub fn y_delta(&self) -> i32 { + self.y_delta + } + + /// Returns true if the given button was just pressed or is being held down. + #[inline] + pub fn is_button_down(&self, button: usize) -> bool { + matches!( + self.buttons[button], + ButtonState::Pressed | ButtonState::Held + ) + } + + /// Returns true if the given button was not just pressed and is not being held down. + #[inline] + pub fn is_button_up(&self, button: usize) -> bool { + matches!( + self.buttons[button], + ButtonState::Released | ButtonState::Idle + ) + } + + /// Returns true if the given button was just pressed (not being held down, yet). + #[inline] + pub fn is_button_pressed(&self, button: usize) -> bool { + self.buttons[button] == ButtonState::Pressed + } + + /// Returns true if the given button was just released. + #[inline] + pub fn is_button_released(&self, button: usize) -> bool { + self.buttons[button] == ButtonState::Released + } + + /// Returns a reference to the current mouse cursor bitmap. + #[inline] + pub fn cursor_bitmap(&self) -> &Bitmap { + &self.cursor + } + + /// Returns the current mouse cursor's "hotspot" x coordinate. + #[inline] + pub fn cursor_hotspot_x(&self) -> u32 { + self.cursor_hotspot_x + } + + /// Returns the current mouse cursor's "hotspot" y coordinate. + #[inline] + pub fn cursor_hotspot_y(&self) -> u32 { + self.cursor_hotspot_y + } + + /// Returns true if mouse cursor bitmap rendering is enabled. + #[inline] + pub fn is_cursor_enabled(&self) -> bool { + self.cursor_enabled + } + + /// Enables or disables mouse cursor bitmap rendering. + #[inline] + pub fn enable_cursor(&mut self, enable: bool) { + self.cursor_enabled = enable; + } + + /// Sets the [`Bitmap`] used to display the mouse cursor and the "hotspot" coordinate. The + /// bitmap provided here should be set up to use color 255 as the transparent color. + /// + /// # Arguments + /// + /// * `cursor`: the bitmap to be used to display the mouse cursor on screen + /// * `hotspot_x`: the "hotspot" x coordinate + /// * `hotspot_y`: the "hotspot" y coordinate. + pub fn set_mouse_cursor(&mut self, cursor: Bitmap, hotspot_x: u32, hotspot_y: u32) { + self.cursor = cursor; + self.cursor_background = Bitmap::new(self.cursor.width(), self.cursor.height()).unwrap(); + self.cursor_hotspot_x = hotspot_x; + self.cursor_hotspot_y = hotspot_y; + } + + /// Resets the mouse cursor bitmap and "hotspot" coordinate back to the default settings. + pub fn set_default_mouse_cursor(&mut self) { + let (cursor, background, hotspot_x, hotspot_y) = Self::get_default_mouse_cursor(); + self.cursor = cursor; + self.cursor_background = background; + self.cursor_hotspot_x = hotspot_x; + self.cursor_hotspot_y = hotspot_y; + } + + fn get_default_mouse_cursor() -> (Bitmap, Bitmap, u32, u32) { + let mut cursor = Bitmap::new( + DEFAULT_MOUSE_CURSOR_WIDTH as u32, + DEFAULT_MOUSE_CURSOR_HEIGHT as u32, + ) + .unwrap(); + cursor.pixels_mut().copy_from_slice(&DEFAULT_MOUSE_CURSOR); + + let cursor_background = Bitmap::new(cursor.width(), cursor.height()).unwrap(); + + ( + cursor, + cursor_background, + DEFAULT_MOUSE_CURSOR_HOTSPOT_X, + DEFAULT_MOUSE_CURSOR_HOTSPOT_Y, + ) + } + + #[inline] + fn get_cursor_render_position(&self) -> (i32, i32) { + ( + self.x - self.cursor_hotspot_x as i32, + self.y - self.cursor_hotspot_y as i32, + ) + } + + /// Renders the mouse cursor bitmap onto the destination bitmap at the mouse's current + /// position. The destination bitmap specified is assumed to be the [`System`]'s video + /// backbuffer bitmap. The background on the destination bitmap is saved internally and a + /// subsequent call to [`Mouse::hide_cursor`] will restore the background. + /// + /// If mouse cursor rendering is not currently enabled, this method does nothing. + /// + /// Applications will not normally need to call this method, as if mouse cursor rendering is + /// enabled, this will be automatically handled by [`System::display`]. + /// + /// [`System`]: crate::System + /// [`System::display`]: crate::System::display + pub fn render_cursor(&mut self, dest: &mut Bitmap) { + if !self.cursor_enabled { + return; + } + + let (x, y) = self.get_cursor_render_position(); + + // preserve existing background first + self.cursor_background.blit_region( + BlitMethod::Solid, + &dest, + &Rect::new(x, y, self.cursor.width(), self.cursor.height()), + 0, + 0, + ); + + dest.blit(BlitMethod::Transparent(255), &self.cursor, x, y); + } + + /// Restores the original destination bitmap contents where the mouse cursor bitmap was + /// rendered to during the previous call to [`Mouse::render_cursor`]. The destination bitmap + /// specified is assumed to be the [`System`]'s video backbuffer bitmap. + /// + /// If mouse cursor rendering is not currently enabled, this method does nothing. + /// + /// Applications will not normally need to call this method, as if mouse cursor rendering is + /// enabled, this will be automatically handled by [`System::display`]. + /// + /// [`System`]: crate::System + /// [`System::display`]: crate::System::display + pub fn hide_cursor(&mut self, dest: &mut Bitmap) { + if !self.cursor_enabled { + return; + } + + let (x, y) = self.get_cursor_render_position(); + dest.blit(BlitMethod::Solid, &self.cursor_background, x, y); + } + + fn update_button_state(&mut self, button: u32, is_pressed: bool) { + let button_state = &mut self.buttons[button as usize]; + *button_state = if is_pressed { + match *button_state { + ButtonState::Pressed => ButtonState::Held, + ButtonState::Held => ButtonState::Held, + _ => ButtonState::Pressed, + } + } else { + match *button_state { + ButtonState::Pressed | ButtonState::Held => ButtonState::Released, + ButtonState::Released => ButtonState::Idle, + ButtonState::Idle => ButtonState::Idle, + } + } + } +} + +impl InputDevice for Mouse { + fn update(&mut self) { + self.x_delta = 0; + self.y_delta = 0; + for state in self.buttons.iter_mut() { + *state = match *state { + ButtonState::Pressed => ButtonState::Held, + ButtonState::Released => ButtonState::Idle, + otherwise => otherwise, + } + } + } + + fn handle_event(&mut self, event: &Event) { + match event { + Event::MouseMotion { + mousestate, + x, + y, + xrel, + yrel, + .. + } => { + self.x = *x; + self.y = *y; + self.x_delta = *xrel; + self.y_delta = *yrel; + for (button, is_pressed) in mousestate.mouse_buttons() { + self.update_button_state(button as u32, is_pressed); + } + } + Event::MouseButtonDown { mouse_btn, .. } => { + self.update_button_state(*mouse_btn as u32, true); + } + Event::MouseButtonUp { mouse_btn, .. } => { + self.update_button_state(*mouse_btn as u32, false); + } + _ => (), + } + } +} diff --git a/libretrogd/src/system/mod.rs b/libretrogd/src/system/mod.rs new file mode 100644 index 0000000..269bec2 --- /dev/null +++ b/libretrogd/src/system/mod.rs @@ -0,0 +1,400 @@ +use byte_slice_cast::AsByteSlice; +use sdl2::event::Event; +use sdl2::pixels::PixelFormatEnum; +use sdl2::render::{Texture, WindowCanvas}; +use sdl2::{EventPump, Sdl, TimerSubsystem, VideoSubsystem}; +use thiserror::Error; + +use crate::{Bitmap, BitmaskFont, InputDevice, Keyboard, Mouse, Palette}; +use crate::{DEFAULT_SCALE_FACTOR, SCREEN_HEIGHT, SCREEN_WIDTH}; + +pub mod input_devices; + +#[derive(Error, Debug)] +pub enum SystemError { + #[error("System init error: {0}")] + InitError(String), + + #[error("System display error: {0}")] + DisplayError(String), +} + +/// Builder for configuring and constructing an instance of [`System`]. +#[derive(Debug)] +pub struct SystemBuilder { + window_title: String, + vsync: bool, + target_framerate: Option, + initial_scale_factor: u32, + resizable: bool, + show_mouse: bool, + relative_mouse_scaling: bool, + integer_scaling: bool, +} + +impl SystemBuilder { + /// Returns a new [`SystemBuilder`] with a default configuration. + pub fn new() -> SystemBuilder { + SystemBuilder { + window_title: String::new(), + vsync: false, + target_framerate: None, + initial_scale_factor: DEFAULT_SCALE_FACTOR, + resizable: true, + show_mouse: false, + relative_mouse_scaling: true, + integer_scaling: false, + } + } + + /// Set the window title for the [`System`] to be built. + pub fn window_title(&mut self, window_title: &str) -> &mut SystemBuilder { + self.window_title = window_title.to_string(); + self + } + + /// Enables or disables V-Sync for the [`System`] to be built. Enabling V-sync automatically + /// disables `target_framerate`. + pub fn vsync(&mut self, enable: bool) -> &mut SystemBuilder { + self.vsync = enable; + self.target_framerate = None; + self + } + + /// Sets a target framerate for the [`System`] being built to run at. This is intended to be + /// used when V-sync is not desired, so setting a target framerate automatically disables + /// `vsync`. + pub fn target_framerate(&mut self, target_framerate: u32) -> &mut SystemBuilder { + self.target_framerate = Some(target_framerate); + self.vsync = false; + self + } + + /// Sets an integer scaling factor for the [`System`] being built to up-scale the virtual + /// framebuffer to when displaying it on screen. + pub fn scale_factor(&mut self, scale_factor: u32) -> &mut SystemBuilder { + self.initial_scale_factor = scale_factor; + self + } + + /// Sets whether the window will be resizable by the user for the [`System`] being built. + pub fn resizable(&mut self, enable: bool) -> &mut SystemBuilder { + self.resizable = enable; + self + } + + /// Enables or disables mouse cursor display by the operating system when the cursor is over + /// the window for the [`System`] being built. Disable this if you intend to render your own + /// custom mouse cursor. + pub fn show_mouse(&mut self, enable: bool) -> &mut SystemBuilder { + self.show_mouse = enable; + self + } + + /// Enables or disables automatic DPI scaling of mouse relative movement values (delta values) + /// available via the [`Mouse`] input device. + pub fn relative_mouse_scaling(&mut self, enable: bool) -> &mut SystemBuilder { + self.relative_mouse_scaling = enable; + self + } + + /// Enables or disables restricting the final rendered output to always be integer scaled, + /// even if that result will not fully fill the area of the window. + pub fn integer_scaling(&mut self, enable: bool) -> &mut SystemBuilder { + self.integer_scaling = enable; + self + } + + /// Builds and returns a [`System`] from the current configuration. + pub fn build(&self) -> Result { + // todo: maybe let this be customized in the future, or at least halved so a 160x120 mode can be available ... ? + let screen_width = SCREEN_WIDTH; + let screen_height = SCREEN_HEIGHT; + let texture_pixel_size = 4; // 32-bit ARGB format + + sdl2::hint::set( + "SDL_MOUSE_RELATIVE_SCALING", + if self.relative_mouse_scaling { + "1" + } else { + "0" + }, + ); + + // build all the individual SDL subsystems + + let sdl_context = match sdl2::init() { + Ok(sdl_context) => sdl_context, + Err(message) => return Err(SystemError::InitError(message)), + }; + + let sdl_timer_subsystem = match sdl_context.timer() { + Ok(timer_subsystem) => timer_subsystem, + Err(message) => return Err(SystemError::InitError(message)), + }; + + let sdl_video_subsystem = match sdl_context.video() { + Ok(video_subsystem) => video_subsystem, + Err(message) => return Err(SystemError::InitError(message)), + }; + + let sdl_event_pump = match sdl_context.event_pump() { + Ok(event_pump) => event_pump, + Err(message) => return Err(SystemError::InitError(message)), + }; + + // create the window + + let window_width = screen_width * self.initial_scale_factor; + let window_height = screen_height * self.initial_scale_factor; + let mut window_builder = &mut (sdl_video_subsystem.window( + self.window_title.as_str(), + window_width, + window_height, + )); + if self.resizable { + window_builder = window_builder.resizable(); + } + let sdl_window = match window_builder.build() { + Ok(window) => window, + Err(error) => return Err(SystemError::InitError(error.to_string())), + }; + + sdl_context.mouse().show_cursor(self.show_mouse); + + // turn the window into a canvas (under the hood, an SDL Renderer that owns the window) + + let mut canvas_builder = sdl_window.into_canvas(); + if self.vsync { + canvas_builder = canvas_builder.present_vsync(); + } + let mut sdl_canvas = match canvas_builder.build() { + Ok(canvas) => canvas, + Err(error) => return Err(SystemError::InitError(error.to_string())), + }; + if let Err(error) = sdl_canvas.set_logical_size(screen_width, screen_height) { + return Err(SystemError::InitError(error.to_string())); + }; + + // TODO: newer versions of rust-sdl2 support this directly off the WindowCanvas struct + unsafe { + sdl2::sys::SDL_RenderSetIntegerScale( + sdl_canvas.raw(), + if self.integer_scaling { + sdl2::sys::SDL_bool::SDL_TRUE + } else { + sdl2::sys::SDL_bool::SDL_FALSE + }, + ); + } + + // create an SDL texture which we will be uploading to every frame to display the + // application's framebuffer + + let sdl_texture = match sdl_canvas.create_texture_streaming( + Some(PixelFormatEnum::ARGB8888), + screen_width, + screen_height, + ) { + Ok(texture) => texture, + Err(error) => return Err(SystemError::InitError(error.to_string())), + }; + let sdl_texture_pitch = (sdl_texture.query().width * texture_pixel_size) as usize; + + // create a raw 32-bit RGBA buffer that will be used as the temporary source for + // SDL texture uploads each frame. necessary as applications are dealing with 8-bit indexed + // bitmaps, not 32-bit RGBA pixels, so this temporary buffer is where we convert the final + // application framebuffer to 32-bit RGBA pixels before it is uploaded to the SDL texture + let texture_pixels_size = (screen_width * screen_height * texture_pixel_size) as usize; + let texture_pixels = vec![0u32; texture_pixels_size].into_boxed_slice(); + + // create the Bitmap object that will be exposed to the application acting as the system + // backbuffer + + let framebuffer = match Bitmap::new(SCREEN_WIDTH, SCREEN_HEIGHT) { + Ok(bmp) => bmp, + Err(error) => return Err(SystemError::InitError(error.to_string())), + }; + + // create the default palette, initialized to the VGA default palette. also exposed to the + // application for manipulation + + let palette = match Palette::new_vga_palette() { + Ok(palette) => palette, + Err(error) => return Err(SystemError::InitError(error.to_string())), + }; + + // create the default font, initialized to the VGA BIOS default font. + + let font = match BitmaskFont::new_vga_font() { + Ok(font) => font, + Err(error) => return Err(SystemError::InitError(error.to_string())), + }; + + // create input device objects, exposed to the application + + let keyboard = Keyboard::new(); + let mouse = Mouse::new(); + + Ok(System { + sdl_context, + sdl_video_subsystem, + sdl_timer_subsystem, + sdl_canvas, + sdl_texture, + sdl_texture_pitch, + sdl_event_pump, + texture_pixels, + video: framebuffer, + palette, + font, + keyboard, + mouse, + target_framerate: self.target_framerate, + target_framerate_delta: None, + next_tick: 0, + }) + } +} + +/// Holds all primary structures necessary for interacting with the operating system and for +/// applications to render to the display, react to input device events, etc. through the +/// "virtual machine" exposed by this library. +#[allow(dead_code)] +pub struct System { + sdl_context: Sdl, + sdl_video_subsystem: VideoSubsystem, + sdl_timer_subsystem: TimerSubsystem, + sdl_canvas: WindowCanvas, + sdl_texture: Texture, + sdl_texture_pitch: usize, + sdl_event_pump: EventPump, + + texture_pixels: Box<[u32]>, + + target_framerate: Option, + target_framerate_delta: Option, + next_tick: i64, + + /// The primary backbuffer [`Bitmap`] that will be rendered to the screen whenever + /// [`System::display`] is called. Regardless of the actual window size, this bitmap is always + /// [`SCREEN_WIDTH`]x[`SCREEN_HEIGHT`] pixels in size. + pub video: Bitmap, + + /// The [`Palette`] that will be used in conjunction with the `video` backbuffer to + /// render the final output to the screen whenever [`System::display`] is called. + pub palette: Palette, + + /// A pre-loaded [`Font`] that can be used for text rendering. + pub font: BitmaskFont, + + /// The current keyboard state. To ensure it is updated each frame, you should call + /// [`System::do_events`] or [`System::do_events_with`] each frame. + pub keyboard: Keyboard, + + /// The current mouse state. To ensure it is updated each frame, you should call + /// [`System::do_events`] or [`System::do_events_with`] each frame. + pub mouse: Mouse, +} + +impl System { + /// Takes the `video` backbuffer bitmap and `palette` and renders it to the window, up-scaled + /// to fill the window (preserving aspect ratio of course). If V-sync is enabled, this method + /// will block to wait for V-sync. Otherwise, if a target framerate was configured a delay + /// might be used to try to meet that framerate. + pub fn display(&mut self) -> Result<(), SystemError> { + self.mouse.render_cursor(&mut self.video); + + // convert application framebuffer to 32-bit RGBA pixels, and then upload it to the SDL + // texture so it will be displayed on screen + + self.video + .copy_as_argb_to(&mut self.texture_pixels, &self.palette); + + let texture_pixels = self.texture_pixels.as_byte_slice(); + if let Err(error) = self + .sdl_texture + .update(None, texture_pixels, self.sdl_texture_pitch) + { + return Err(SystemError::DisplayError(error.to_string())); + } + self.sdl_canvas.clear(); + if let Err(error) = self.sdl_canvas.copy(&self.sdl_texture, None, None) { + return Err(SystemError::DisplayError(error)); + } + self.sdl_canvas.present(); + + self.mouse.hide_cursor(&mut self.video); + + // if a specific target framerate is desired, apply some loop timing/delay to achieve it + // TODO: do this better. delaying when running faster like this is a poor way to do this.. + + if let Some(target_framerate) = self.target_framerate { + if self.target_framerate_delta.is_some() { + // normal path for every other loop iteration except the first + let delay = self.next_tick - self.ticks() as i64; + if delay < 0 { + // this loop iteration took too long, no need to delay + self.next_tick -= delay; + } else { + // this loop iteration completed before next_tick time, delay by the remainder + // time period so we're running at about the desired framerate + self.delay(((delay * 1000) / self.tick_frequency() as i64) as u32); + } + } else { + // this branch will occur on the first main loop iteration. we use the fact that + // target_framerate_delta was not yet set to avoid doing any delay on the first + // loop, just in case there was some other processing between the System struct + // being created and the actual beginning of the first loop ... + self.target_framerate_delta = + Some((self.tick_frequency() / target_framerate as u64) as i64); + } + + // expected time for the next display() call to happen by + self.next_tick = (self.ticks() as i64) + self.target_framerate_delta.unwrap(); + } + + Ok(()) + } + + /// Checks for and responds to all SDL2 events waiting in the queue. Each event is passed to + /// all [`InputDevice`]'s automatically to ensure input device state is up to date. + pub fn do_events(&mut self) { + self.do_events_with(|_event| {}); + } + + /// Same as [`System::do_events`] but also takes a function which will be called for each + /// SDL2 event being processed (after everything else has already processed it), allowing + /// your application to also react to any events received. + pub fn do_events_with(&mut self, mut f: F) + where + F: FnMut(&Event), + { + self.keyboard.update(); + self.mouse.update(); + self.sdl_event_pump.pump_events(); + for event in self.sdl_event_pump.poll_iter() { + self.keyboard.handle_event(&event); + self.mouse.handle_event(&event); + f(&event); + } + } + + pub fn ticks(&self) -> u64 { + self.sdl_timer_subsystem.performance_counter() + } + + pub fn tick_frequency(&self) -> u64 { + self.sdl_timer_subsystem.performance_frequency() + } + + /// Returns the number of milliseconds elapsed since SDL was initialized. + pub fn millis(&self) -> u32 { + self.sdl_timer_subsystem.ticks() + } + + /// Delays (blocks) for about the number of milliseconds specified. + pub fn delay(&mut self, millis: u32) { + self.sdl_timer_subsystem.delay(millis); + } +} diff --git a/libretrogd/src/utils/bytes.rs b/libretrogd/src/utils/bytes.rs new file mode 100644 index 0000000..5e954db --- /dev/null +++ b/libretrogd/src/utils/bytes.rs @@ -0,0 +1,12 @@ +pub trait ReadFixedLengthByteArray { + fn read_bytes(&mut self) -> Result<[u8; N], std::io::Error>; +} + +impl ReadFixedLengthByteArray for T { + fn read_bytes(&mut self) -> Result<[u8; N], std::io::Error> { + assert_ne!(N, 0); + let mut array = [0u8; N]; + self.read_exact(&mut array)?; + Ok(array) + } +} diff --git a/libretrogd/src/utils/mod.rs b/libretrogd/src/utils/mod.rs new file mode 100644 index 0000000..07f93e9 --- /dev/null +++ b/libretrogd/src/utils/mod.rs @@ -0,0 +1,38 @@ +use std::any::Any; + +use num_traits::Unsigned; +use rand::distributions::uniform::SampleUniform; +use rand::Rng; + +pub mod bytes; +pub mod packbits; + +pub fn rnd_value(low: N, high: N) -> N { + rand::thread_rng().gen_range(low..=high) +} + +/// Returns the absolute difference between two unsigned values. This is just here as a temporary +/// alternative to the `abs_diff` method currently provided by Rust but that is marked unstable. +#[inline] +pub fn abs_diff(a: N, b: N) -> N { + if a < b { + b - a + } else { + a - b + } +} + +pub trait AsAny { + fn as_any(&self) -> &dyn Any; + fn as_any_mut(&mut self) -> &mut dyn Any; +} + +impl AsAny for A { + fn as_any(&self) -> &dyn Any { + self as &dyn Any + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self as &mut dyn Any + } +} \ No newline at end of file diff --git a/libretrogd/src/utils/packbits.rs b/libretrogd/src/utils/packbits.rs new file mode 100644 index 0000000..1141e05 --- /dev/null +++ b/libretrogd/src/utils/packbits.rs @@ -0,0 +1,226 @@ +use byteorder::{ReadBytesExt, WriteBytesExt}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum PackBitsError { + #[error("PackBits I/O error")] + IOError(#[from] std::io::Error), +} + +enum PackMode { + Dump, + Run, +} + +pub fn pack_bits(src: &mut S, dest: &mut D, src_length: usize) -> Result<(), PackBitsError> +where + S: ReadBytesExt, + D: WriteBytesExt, +{ + const MIN_RUN: usize = 3; + const MAX_RUN: usize = 128; + const MAX_BUFFER: usize = 128; + + if src_length == 0 { + return Ok(()); + } + let mut bytes_left = src_length; + + let mut buffer = [0u8; (MAX_RUN * 2)]; + + // read the first byte from the source, just to start things off before we get into the loop + buffer[0] = src.read_u8()?; + bytes_left -= 1; + + let mut mode = PackMode::Dump; + let mut run_end = 1; // 1 because we already read the first byte into the buffer + let mut run_start = 0; + let mut previous_byte = buffer[0]; + + while bytes_left > 0 { + let byte = src.read_u8()?; + + buffer[run_end] = byte; + run_end += 1; + + match mode { + // "dump" mode. keep collecting any bytes and write them as-is until we detect the + // start of a run of identical bytes + PackMode::Dump => { + if run_end > MAX_BUFFER { + // we need to flush the temp buffer to the destination + dest.write_u8((run_end - 2) as u8)?; + dest.write_all(&buffer[0..run_end])?; + + buffer[0] = byte; + run_end = 1; + run_start = 0; + } else if byte == previous_byte { + // detected the start of a run of identical bytes + if (run_end - run_start) >= MIN_RUN { + if run_start > 0 { + // we've found a run, flush the buffer we have currently so we can + // start tracking the length of this run + dest.write_u8((run_start - 1) as u8)?; + dest.write_all(&buffer[0..run_start])?; + } + mode = PackMode::Run; + } else if run_start == 0 { + mode = PackMode::Run; + } + } else { + run_start = run_end - 1; + } + } + // "run" mode. keep counting up bytes as long as they are identical to each other. when + // we find the end of a run, write out the run info and switch back to dump mode + PackMode::Run => { + // check for the end of a run of identical bytes + if (byte != previous_byte) || ((run_end - run_start) > MAX_RUN) { + // the identical byte run has ended, write it out to the destination + // (this is just two bytes, the count and the actual byte) + dest.write_i8(-((run_end - run_start - 2) as i8))?; + dest.write_u8(previous_byte)?; + + // clear the temp buffer for our switch back to "dump" mode + buffer[0] = byte; + run_end = 1; + run_start = 0; + mode = PackMode::Dump; + } + } + }; + + previous_byte = byte; + bytes_left -= 1; + } + + // the source bytes have all been read, but we still might have to flush the temp buffer + // out to the destination, or finish writing out a run of identical bytes that was at the very + // end of the source + match mode { + PackMode::Dump => { + dest.write_u8((run_end - 1) as u8)?; + dest.write_all(&buffer[0..run_end])?; + } + PackMode::Run => { + dest.write_i8(-((run_end - run_start - 1) as i8))?; + dest.write_u8(previous_byte)?; + } + }; + + Ok(()) +} + +pub fn unpack_bits( + src: &mut S, + dest: &mut D, + unpacked_length: usize, +) -> Result<(), PackBitsError> +where + S: ReadBytesExt, + D: WriteBytesExt, +{ + let mut buffer = [0u8; 128]; + let mut bytes_written = 0; + + while bytes_written < unpacked_length { + // read the next "code" byte that determines how to process the subsequent byte(s) + let byte = src.read_u8()?; + + if byte > 128 { + // 129-255 = repeat the next byte 257-n times + let run_length = (257 - byte as u32) as usize; + + // read the next byte from the source and repeat it the specified number of times + let byte = src.read_u8()?; + buffer.fill(byte); + dest.write_all(&buffer[0..run_length])?; + bytes_written += run_length; + } else if byte < 128 { + // 0-128 = copy next n-1 bytes from src to dest as-is + let run_length = (byte + 1) as usize; + + src.read_exact(&mut buffer[0..run_length])?; + dest.write_all(&buffer[0..run_length])?; + bytes_written += run_length; + } + + // note that byte == 128 is a no-op (does it even appear in any files ???) + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::io::Cursor; + + use super::*; + + struct TestData<'a> { + packed: &'a [u8], + unpacked: &'a [u8], + } + + static TEST_DATA: &[TestData] = &[ + TestData { + packed: &[ + 0xfe, 0xaa, 0x02, 0x80, 0x00, 0x2a, 0xfd, 0xaa, 0x03, 0x80, 0x00, 0x2a, 0x22, 0xf7, + 0xaa, + ], + unpacked: &[ + 0xaa, 0xaa, 0xaa, 0x80, 0x00, 0x2a, 0xaa, 0xaa, 0xaa, 0xaa, 0x80, 0x00, 0x2a, 0x22, + 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, + ], + }, + TestData { + packed: &[0x00, 0xaa], + unpacked: &[0xaa], + }, + TestData { + packed: &[0xf9, 0xaa], + unpacked: &[0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa], + }, + TestData { + packed: &[0xf9, 0xaa, 0x00, 0xbb], + unpacked: &[0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xbb], + }, + TestData { + packed: &[0x07, 0xa0, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8], + unpacked: &[0xa0, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8], + }, + TestData { + packed: &[0x08, 0xa0, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa8], + unpacked: &[0xa0, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa8], + }, + TestData { + packed: &[0x06, 0xa0, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xfe, 0xa8], + unpacked: &[0xa0, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa8, 0xa8], + }, + ]; + + #[test] + fn packs() -> Result<(), PackBitsError> { + for TestData { packed, unpacked } in TEST_DATA { + let mut src = Cursor::new(*unpacked); + let mut dest = vec![0u8; 0]; + pack_bits(&mut src, &mut dest, unpacked.len())?; + assert_eq!(dest, *packed); + } + + Ok(()) + } + + #[test] + fn unpacks() -> Result<(), PackBitsError> { + for TestData { packed, unpacked } in TEST_DATA { + let mut src = Cursor::new(*packed); + let mut dest = vec![0u8; 0]; + unpack_bits(&mut src, &mut dest, unpacked.len())?; + assert_eq!(dest, *unpacked); + } + + Ok(()) + } +} diff --git a/libretrogd/test-assets/dp2.pal b/libretrogd/test-assets/dp2.pal new file mode 100644 index 0000000..47099d7 Binary files /dev/null and b/libretrogd/test-assets/dp2.pal differ diff --git a/libretrogd/test-assets/test-tiles.lbm b/libretrogd/test-assets/test-tiles.lbm new file mode 100644 index 0000000..daa7ca7 Binary files /dev/null and b/libretrogd/test-assets/test-tiles.lbm differ diff --git a/libretrogd/test-assets/test.pcx b/libretrogd/test-assets/test.pcx new file mode 100755 index 0000000..af81243 Binary files /dev/null and b/libretrogd/test-assets/test.pcx differ diff --git a/libretrogd/test-assets/test_bmp_pixels_raw.bin b/libretrogd/test-assets/test_bmp_pixels_raw.bin new file mode 100644 index 0000000..a864fe4 --- /dev/null +++ b/libretrogd/test-assets/test_bmp_pixels_raw.bin @@ -0,0 +1 @@ +~~~~~~~~~~~~~}}~}~}~}~~~}}~}~}~}}}|}}}|}|||}}||}|}|}}|{|||{|{|||{|{|{|{|||{z{z{z{z{z{zz{{z{{z{z{z{zyzzzyzzzzzzyzyzyzyzyzyyyyxyyyxyyyxyyyxyyxyxyxyxyxyxyxyxxxxxxxxxxxxxxxxx \ No newline at end of file diff --git a/libretrogd/test-assets/test_ilbm.lbm b/libretrogd/test-assets/test_ilbm.lbm new file mode 100755 index 0000000..94d7cd2 Binary files /dev/null and b/libretrogd/test-assets/test_ilbm.lbm differ diff --git a/libretrogd/test-assets/test_image.lbm b/libretrogd/test-assets/test_image.lbm new file mode 100644 index 0000000..8407341 Binary files /dev/null and b/libretrogd/test-assets/test_image.lbm differ diff --git a/libretrogd/test-assets/test_image.pcx b/libretrogd/test-assets/test_image.pcx new file mode 100644 index 0000000..41ca0b6 Binary files /dev/null and b/libretrogd/test-assets/test_image.pcx differ diff --git a/libretrogd/test-assets/test_pbm.lbm b/libretrogd/test-assets/test_pbm.lbm new file mode 100755 index 0000000..f2029a0 Binary files /dev/null and b/libretrogd/test-assets/test_pbm.lbm differ