Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved RoomCoordinate and RoomXY, new RoomOffset for optimized arithmetic #543

Merged
merged 27 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9902299
feat: add indexing impls for `RoomCoordinate`
khoover Oct 9, 2024
9293f72
feat: add `ROOM_USIZE` constant
khoover Oct 9, 2024
3cf8c14
feat: create x/y-major wrappers for `RoomXY` indexing
khoover Oct 9, 2024
e7cad72
feat: allow taking `LocalCostMatrix` as `XMajor<u8>` ref
khoover Oct 9, 2024
3271f97
refactor: use `RoomCoordinate` indexing for `LocalRoomTerrain`
khoover Oct 9, 2024
1f0ff51
refactor: use ROOM_USIZE instead of ROOM_SIZE as usize
khoover Oct 9, 2024
2324310
style: import ROOM_AREA from constants too
khoover Oct 9, 2024
2e69bd9
refactor: Replaced match with ?
khoover Oct 13, 2024
1257f7c
feat: Added size assumption function, removed unsafe
khoover Oct 13, 2024
f149aa7
fix: Fix clippy lints
khoover Oct 13, 2024
934be55
perf: better wasm emission for RoomCoordinate arithmetic
khoover Oct 13, 2024
469cdc4
perf: Added RoomCoord tests, improved checked add
khoover Oct 14, 2024
494f7fd
cleanup: Use copied and all in tests
khoover Oct 14, 2024
619c188
test: refine the room area test
khoover Oct 14, 2024
ada73d8
test: replace .min().max() with clamp
khoover Oct 14, 2024
b27b803
perf: faster saturating add for RoomCoordinate
khoover Oct 14, 2024
fb107cd
fix: eliminate all ROOM_SIZE-related literals
khoover Oct 14, 2024
bd2cfde
fix: fmt
khoover Oct 14, 2024
8f7ef85
fix: missing parens
khoover Oct 14, 2024
34b23db
feat: Add RoomOffset and more arithmetic
khoover Oct 14, 2024
7dba3a5
refactor: rename assume_size_constraint
khoover Oct 14, 2024
5bff038
More docs, `RoomOffset::abs`
khoover Oct 15, 2024
7701804
refactor: Add doc comments, return Result over Option
khoover Oct 29, 2024
9568dc7
fix: added correct import for doctest
khoover Oct 29, 2024
394a303
fix: fmt
khoover Oct 29, 2024
2f10f6f
fix: Fix wrapping arithmetic for overflowing_add.
khoover Oct 29, 2024
63ca176
Changelog
shanemadden Oct 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/constants/extra.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,10 +270,15 @@ pub const ROOM_VISUAL_PER_ROOM_SIZE_LIMIT: u32 = 500 * 1024;
/// [`Room`]: crate::objects::Room
pub const ROOM_SIZE: u8 = 50;

/// ['ROOM_SIZE'] cast into a `usize`, for indexing convenience.
///
/// ['ROOM_SIZE']: self::ROOM_SIZE
pub const ROOM_USIZE: usize = ROOM_SIZE as usize;

/// The number of total tiles in each [`Room`] in the game
///
/// [`Room`]: crate::objects::Room
pub const ROOM_AREA: usize = (ROOM_SIZE as usize) * (ROOM_SIZE as usize);
pub const ROOM_AREA: usize = ROOM_USIZE * ROOM_USIZE;

/// Owner username of hostile non-player structures and creeps which occupy
/// sector center rooms.
Expand Down
20 changes: 15 additions & 5 deletions src/local/cost_matrix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::{
traits::{CostMatrixGet, CostMatrixSet},
};

use super::{linear_index_to_xy, xy_to_linear_index, Position, RoomXY};
use super::{linear_index_to_xy, Position, RoomXY, XMajor};

/// A matrix of pathing costs for a room, stored in Rust memory.
///
Expand Down Expand Up @@ -121,19 +121,29 @@ impl From<&CostMatrix> for LocalCostMatrix {
}
}

impl AsRef<XMajor<u8>> for LocalCostMatrix {
fn as_ref(&self) -> &XMajor<u8> {
XMajor::from_flat_ref(&self.bits)
}
}

impl AsMut<XMajor<u8>> for LocalCostMatrix {
fn as_mut(&mut self) -> &mut XMajor<u8> {
XMajor::from_flat_mut(&mut self.bits)
}
}

impl Index<RoomXY> for LocalCostMatrix {
type Output = u8;

fn index(&self, xy: RoomXY) -> &Self::Output {
// SAFETY: RoomXY is always a valid coordinate.
unsafe { self.bits.get_unchecked(xy_to_linear_index(xy)) }
&self.bits[xy.x][xy.y]
}
}

impl IndexMut<RoomXY> for LocalCostMatrix {
fn index_mut(&mut self, xy: RoomXY) -> &mut Self::Output {
// SAFETY: RoomXY is always a valid coordinate.
unsafe { self.bits.get_unchecked_mut(xy_to_linear_index(xy)) }
&mut self.bits[xy.x][xy.y]
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/local/object_id/raw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ impl Serialize for RawObjectId {

struct RawObjectIdVisitor;

impl<'de> Visitor<'de> for RawObjectIdVisitor {
impl Visitor<'_> for RawObjectIdVisitor {
type Value = RawObjectId;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
Expand Down
182 changes: 159 additions & 23 deletions src/local/room_coordinate.rs
khoover marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
use std::{error::Error, fmt};
use std::{
error::Error,
fmt,
hint::assert_unchecked,
ops::{Index, IndexMut},
};

use serde::{Deserialize, Serialize};
use wasm_bindgen::UnwrapThrowExt;

use crate::constants::ROOM_SIZE;
use crate::constants::{ROOM_AREA, ROOM_SIZE, ROOM_USIZE};

#[derive(Debug, Clone, Copy)]
pub struct OutOfBoundsError(pub u8);
Expand All @@ -16,14 +22,18 @@ impl fmt::Display for OutOfBoundsError {
impl Error for OutOfBoundsError {}

/// An X or Y coordinate in a room, restricted to the valid range of
/// coordinates.
/// coordinates. This restriction can be used in safety constraints, and is
/// enforced by all safe `RoomCoordinate` constructors.
#[derive(
Debug, Hash, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize,
)]
#[serde(try_from = "u8", into = "u8")]
pub struct RoomCoordinate(u8);

impl RoomCoordinate {
pub const MAX: Self = Self(ROOM_SIZE - 1);
pub const MIN: Self = Self(0);

/// Create a `RoomCoordinate` from a `u8`, returning an error if the
/// coordinate is not in the valid room size range
#[inline]
Expand All @@ -50,6 +60,18 @@ impl RoomCoordinate {
RoomCoordinate(coord)
}

/// Provides a hint to the compiler that the contained `u8` is smaller than
/// `ROOM_SIZE`. Allows for better optimized safe code that uses this
/// property.
pub fn assume_size_constraint(self) {
debug_assert!(self.0 < ROOM_SIZE);
// SAFETY: It is only safe to construct `RoomCoordinate` when self.0 <
// ROOM_SIZE.
unsafe {
assert_unchecked(self.0 < ROOM_SIZE);
}
}

/// Get the integer value of this coordinate
pub const fn u8(self) -> u8 {
self.0
Expand Down Expand Up @@ -77,17 +99,15 @@ impl RoomCoordinate {
/// assert_eq!(forty_nine.checked_add(1), None);
/// ```
pub fn checked_add(self, rhs: i8) -> Option<RoomCoordinate> {
match (self.0 as i8).checked_add(rhs) {
Some(result) => match result {
// less than 0
i8::MIN..=-1 => None,
// greater than 49
50..=i8::MAX => None,
// SAFETY: we've checked that this coord is in the valid range
c => Some(unsafe { RoomCoordinate::unchecked_new(c as u8) }),
},
None => None,
}
self.assume_size_constraint();
// Why this works, assuming ROOM_SIZE < i8::MAX + 1 == 128 and ignoring the
// test:
// - if rhs < 0: the smallest value this can produce is -128, which casted to
// u8 is 128. The closer rhs is to 0, the larger the cast sum is. So if
// ROOM_SIZE <= i8::MAX, any underflow will fail the x < ROOM_SIZE check.
// - if rhs > 0: as long as self.0 <= i8::MAX, self.0 + rhs <= 2 * i8::MAX <
// 256, so there isn't unsigned overflow.
RoomCoordinate::new(self.0.wrapping_add_signed(rhs)).ok()
}

/// Get the coordinate adjusted by a certain value, saturating at the edges
Expand All @@ -108,15 +128,14 @@ impl RoomCoordinate {
/// assert_eq!(forty_nine.saturating_add(i8::MIN), zero);
/// ```
pub fn saturating_add(self, rhs: i8) -> RoomCoordinate {
let result = match (self.0 as i8).saturating_add(rhs) {
// less than 0, saturate to 0
i8::MIN..=-1 => 0,
// greater than 49, saturate to 49
50..=i8::MAX => ROOM_SIZE - 1,
c => c as u8,
};
// SAFETY: we've ensured that this coord is in the valid range
unsafe { RoomCoordinate::unchecked_new(result) }
self.assume_size_constraint();
let (res, overflow) = self.0.overflowing_add_signed(rhs);
if overflow {
RoomCoordinate::MIN
} else {
// Optimizer will see the return is always Ok
RoomCoordinate::new(res.min(ROOM_SIZE - 1)).unwrap_throw()
}
}
}

Expand All @@ -139,3 +158,120 @@ impl TryFrom<u8> for RoomCoordinate {
RoomCoordinate::new(coord)
}
}

impl<T> Index<RoomCoordinate> for [T; ROOM_USIZE] {
type Output = T;

fn index(&self, index: RoomCoordinate) -> &Self::Output {
index.assume_size_constraint();
&self[index.0 as usize]
}
}

impl<T> IndexMut<RoomCoordinate> for [T; ROOM_USIZE] {
fn index_mut(&mut self, index: RoomCoordinate) -> &mut Self::Output {
index.assume_size_constraint();
&mut self[index.0 as usize]
}
}

impl<T> Index<RoomCoordinate> for [T; ROOM_AREA] {
type Output = [T; ROOM_USIZE];

fn index(&self, index: RoomCoordinate) -> &Self::Output {
// SAFETY: ROOM_USIZE * ROOM_USIZE = ROOM_AREA, so [T; ROOM_AREA] and [[T;
// ROOM_USIZE]; ROOM_USIZE] have the same layout.
let this =
unsafe { &*(self as *const [T; ROOM_AREA] as *const [[T; ROOM_USIZE]; ROOM_USIZE]) };
&this[index]
}
}

impl<T> IndexMut<RoomCoordinate> for [T; ROOM_AREA] {
fn index_mut(&mut self, index: RoomCoordinate) -> &mut Self::Output {
// SAFETY: ROOM_USIZE * ROOM_USIZE = ROOM_AREA, so [T; ROOM_AREA] and [[T;
// ROOM_USIZE]; ROOM_USIZE] have the same layout.
let this =
unsafe { &mut *(self as *mut [T; ROOM_AREA] as *mut [[T; ROOM_USIZE]; ROOM_USIZE]) };
&mut this[index]
}
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn checked_add() {
for coord_inner in 0..ROOM_SIZE {
let coord = RoomCoordinate::new(coord_inner).unwrap();
for rhs in i8::MIN..=i8::MAX {
let sum = coord.checked_add(rhs);
assert_eq!(
sum.is_some(),
(0..ROOM_SIZE as i16).contains(&(coord_inner as i16 + rhs as i16))
);
if let Some(res) = sum {
assert_eq!(res.u8(), (coord_inner as i16 + rhs as i16) as u8);
}
}
}
}

#[test]
fn saturating_add() {
for coord_inner in 0..ROOM_SIZE {
let coord = RoomCoordinate::new(coord_inner).unwrap();
for rhs in i8::MIN..=i8::MAX {
assert_eq!(
coord.saturating_add(rhs).u8(),
(coord_inner as i16 + rhs as i16).clamp(0, ROOM_SIZE as i16 - 1) as u8
)
}
}
}

#[test]
fn index_room_size() {
let mut base: Box<[u8; ROOM_USIZE]> = (0..50)
.collect::<Vec<u8>>()
.into_boxed_slice()
.try_into()
.unwrap();
for i in 0..ROOM_SIZE {
let coord = RoomCoordinate::new(i).unwrap();
assert_eq!(base[coord], i);
base[coord] += 1;
}
base.iter()
.copied()
.zip(1..(ROOM_SIZE + 1))
.for_each(|(actual, expected)| assert_eq!(actual, expected));
}

#[test]
fn index_room_area() {
let mut base: Box<[u16; ROOM_AREA]> = Box::new([0; ROOM_AREA]);
for i in 0..ROOM_USIZE {
for j in 0..ROOM_USIZE {
base[i * ROOM_USIZE + j] = i as u16 * ROOM_SIZE as u16;
}
}

for i in 0..ROOM_SIZE {
let coord = RoomCoordinate::new(i).unwrap();
assert!(base[coord]
.iter()
.copied()
.all(|val| val == i as u16 * ROOM_SIZE as u16));
for j in 0..ROOM_USIZE {
base[coord][j] += j as u16;
}
}

assert_eq!(
(0..ROOM_AREA as u16).collect::<Vec<u16>>().as_slice(),
base.as_slice()
);
}
}
2 changes: 1 addition & 1 deletion src/local/room_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,7 @@ mod serde {

struct RoomNameVisitor;

impl<'de> Visitor<'de> for RoomNameVisitor {
impl Visitor<'_> for RoomNameVisitor {
type Value = RoomName;

fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
Expand Down
Loading