From 34427373ef243f0b9bde7b78bf5049dc8e544903 Mon Sep 17 00:00:00 2001 From: Joshua Yanovski Date: Thu, 21 May 2020 21:20:01 +0200 Subject: [PATCH] Make civsim and sites deterministic. For anything in worldgen where you use a HashMap, *please* think carefully about which hasher you are going to use! This is especially true if (for some reason) you are depending on hashmap iteration order remaining stable for some aspect of worldgen. --- CHANGELOG.md | 1 + Cargo.lock | 2 + common/Cargo.toml | 1 + common/src/astar.rs | 63 +++++++--- common/src/path.rs | 16 ++- common/src/store.rs | 25 ++-- world/Cargo.toml | 1 + world/src/civ/mod.rs | 195 +++++++++++++++++++++---------- world/src/sim/location.rs | 8 +- world/src/sim/mod.rs | 5 +- world/src/site/dungeon/mod.rs | 15 ++- world/src/site/settlement/mod.rs | 38 ++++-- world/src/util/hash_cache.rs | 45 ------- world/src/util/mod.rs | 2 - 14 files changed, 269 insertions(+), 148 deletions(-) delete mode 100644 world/src/util/hash_cache.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b21edd215..725ea1c17a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Improved camera aiming +- Made civsim, sites, etc. deterministic from the same seed. ### Removed diff --git a/Cargo.lock b/Cargo.lock index a5dbad11c5..2728ac9b7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4993,6 +4993,7 @@ dependencies = [ "crossbeam", "dot_vox", "find_folder", + "fxhash", "hashbrown", "image", "indexmap", @@ -5115,6 +5116,7 @@ dependencies = [ "arr_macro", "bincode", "bitvec", + "fxhash", "hashbrown", "image", "itertools", diff --git a/common/Cargo.toml b/common/Cargo.toml index d55ba5e696..3a50a4e9f4 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -13,6 +13,7 @@ specs-idvs = { git = "https://gitlab.com/veloren/specs-idvs.git" } specs = { version = "0.15.1", features = ["serde", "nightly", "storage-event-control"] } vek = { version = "0.10.0", features = ["serde"] } dot_vox = "4.0.0" +fxhash = "0.2.1" image = "0.22.3" mio = "0.6.19" mio-extras = "2.0.5" diff --git a/common/src/astar.rs b/common/src/astar.rs index 3ad4912733..920aeeee88 100644 --- a/common/src/astar.rs +++ b/common/src/astar.rs @@ -1,7 +1,11 @@ use crate::path::Path; -use core::cmp::Ordering::Equal; +use core::{ + cmp::Ordering::{self, Equal}, + f32, fmt, + hash::{BuildHasher, Hash}, +}; use hashbrown::{HashMap, HashSet}; -use std::{cmp::Ordering, collections::BinaryHeap, f32, hash::Hash}; +use std::collections::BinaryHeap; #[derive(Copy, Clone, Debug)] pub struct PathEntry { @@ -43,33 +47,62 @@ impl PathResult { } } -#[derive(Clone, Debug)] -pub struct Astar { +#[derive(Clone)] +pub struct Astar { iter: usize, max_iters: usize, potential_nodes: BinaryHeap>, - came_from: HashMap, - cheapest_scores: HashMap, - final_scores: HashMap, - visited: HashSet, + came_from: HashMap, + cheapest_scores: HashMap, + final_scores: HashMap, + visited: HashSet, cheapest_node: Option, cheapest_cost: Option, } -impl Astar { - pub fn new(max_iters: usize, start: S, heuristic: impl FnOnce(&S) -> f32) -> Self { +/// NOTE: Must manually derive since Hasher doesn't implement it. +impl fmt::Debug for Astar { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Astar") + .field("iter", &self.iter) + .field("max_iters", &self.max_iters) + .field("potential_nodes", &self.potential_nodes) + .field("came_from", &self.came_from) + .field("cheapest_scores", &self.cheapest_scores) + .field("final_scores", &self.final_scores) + .field("visited", &self.visited) + .field("cheapest_node", &self.cheapest_node) + .field("cheapest_cost", &self.cheapest_cost) + .finish() + } +} + +impl Astar { + pub fn new(max_iters: usize, start: S, heuristic: impl FnOnce(&S) -> f32, hasher: H) -> Self { Self { max_iters, iter: 0, - potential_nodes: std::iter::once(PathEntry { + potential_nodes: core::iter::once(PathEntry { cost: 0.0, node: start.clone(), }) .collect(), - came_from: HashMap::default(), - cheapest_scores: std::iter::once((start.clone(), 0.0)).collect(), - final_scores: std::iter::once((start.clone(), heuristic(&start))).collect(), - visited: std::iter::once(start).collect(), + came_from: HashMap::with_hasher(hasher.clone()), + cheapest_scores: { + let mut h = HashMap::with_capacity_and_hasher(1, hasher.clone()); + h.extend(core::iter::once((start.clone(), 0.0))); + h + }, + final_scores: { + let mut h = HashMap::with_capacity_and_hasher(1, hasher.clone()); + h.extend(core::iter::once((start.clone(), heuristic(&start)))); + h + }, + visited: { + let mut s = HashSet::with_capacity_and_hasher(1, hasher); + s.extend(core::iter::once(start)); + s + }, cheapest_node: None, cheapest_cost: None, } diff --git a/common/src/path.rs b/common/src/path.rs index a3c649fc57..b64b74e669 100644 --- a/common/src/path.rs +++ b/common/src/path.rs @@ -3,6 +3,7 @@ use crate::{ terrain::Block, vol::{BaseVol, ReadVol}, }; +use hashbrown::hash_map::DefaultHashBuilder; use rand::{thread_rng, Rng}; use std::iter::FromIterator; use vek::*; @@ -92,7 +93,11 @@ impl Route { pub struct Chaser { last_search_tgt: Option>, route: Route, - astar: Option>>, + /// We use this hasher (AAHasher) because: + /// (1) we care about DDOS attacks (ruling out FxHash); + /// (2) we don't care about determinism across computers (we can use + /// AAHash). + astar: Option, DefaultHashBuilder>>, } impl Chaser { @@ -147,7 +152,7 @@ impl Chaser { } fn find_path( - astar: &mut Option>>, + astar: &mut Option, DefaultHashBuilder>>, vol: &V, startf: Vec3, endf: Vec3, @@ -263,7 +268,12 @@ where let satisfied = |pos: &Vec3| pos == &end; let mut new_astar = match astar.take() { - None => Astar::new(20_000, start, heuristic.clone()), + None => Astar::new( + 20_000, + start, + heuristic.clone(), + DefaultHashBuilder::default(), + ), Some(astar) => astar, }; diff --git a/common/src/store.rs b/common/src/store.rs index 983d6a156b..8942e4a722 100644 --- a/common/src/store.rs +++ b/common/src/store.rs @@ -5,10 +5,12 @@ use std::{ ops::{Index, IndexMut}, }; -pub struct Id(usize, PhantomData); +// NOTE: We use u64 to make sure we are consistent across all machines. We +// assume that usize fits into 8 bytes. +pub struct Id(u64, PhantomData); impl Id { - pub fn id(&self) -> usize { self.0 } + pub fn id(&self) -> u64 { self.0 } } impl Copy for Id {} @@ -37,12 +39,19 @@ impl Default for Store { } impl Store { - pub fn get(&self, id: Id) -> &T { self.items.get(id.0).unwrap() } + pub fn get(&self, id: Id) -> &T { + // NOTE: Safe conversion, because it came from usize. + self.items.get(id.0 as usize).unwrap() + } - pub fn get_mut(&mut self, id: Id) -> &mut T { self.items.get_mut(id.0).unwrap() } + pub fn get_mut(&mut self, id: Id) -> &mut T { + // NOTE: Safe conversion, because it came from usize. + self.items.get_mut(id.0 as usize).unwrap() + } pub fn ids(&self) -> impl Iterator> { - (0..self.items.len()).map(|i| Id(i, PhantomData)) + // NOTE: Assumes usize fits into 8 bytes. + (0..self.items.len() as u64).map(|i| Id(i, PhantomData)) } pub fn iter(&self) -> impl Iterator { self.items.iter() } @@ -53,11 +62,13 @@ impl Store { self.items .iter() .enumerate() - .map(|(i, item)| (Id(i, PhantomData), item)) + // NOTE: Assumes usize fits into 8 bytes. + .map(|(i, item)| (Id(i as u64, PhantomData), item)) } pub fn insert(&mut self, item: T) -> Id { - let id = Id(self.items.len(), PhantomData); + // NOTE: Assumes usize fits into 8 bytes. + let id = Id(self.items.len() as u64, PhantomData); self.items.push(item); id } diff --git a/world/Cargo.toml b/world/Cargo.toml index b8bacc991d..24c424845f 100644 --- a/world/Cargo.toml +++ b/world/Cargo.toml @@ -8,6 +8,7 @@ edition = "2018" bincode = "1.2.0" common = { package = "veloren-common", path = "../common" } bitvec = "0.17.4" +fxhash = "0.2.1" image = "0.22.3" itertools = "0.8.2" vek = "0.10.0" diff --git a/world/src/civ/mod.rs b/world/src/civ/mod.rs index 32199f46b4..b1b79bc610 100644 --- a/world/src/civ/mod.rs +++ b/world/src/civ/mod.rs @@ -2,6 +2,7 @@ mod econ; +use self::{Occupation::*, Stock::*}; use crate::{ sim::WorldSim, site::{Dungeon, Settlement, Site as WorldSite}, @@ -15,10 +16,15 @@ use common::{ terrain::TerrainChunkSize, vol::RectVolSize, }; +use core::{ + fmt, + hash::{BuildHasherDefault, Hash}, + ops::Range, +}; +use fxhash::{FxHasher32, FxHasher64}; use hashbrown::{HashMap, HashSet}; use rand::prelude::*; use rand_chacha::ChaChaRng; -use std::{fmt, hash::Hash, ops::Range}; use vek::*; const INITIAL_CIV_COUNT: usize = 64; @@ -29,7 +35,15 @@ pub struct Civs { places: Store, tracks: Store, - track_map: HashMap, HashMap, Id>>, + /// We use this hasher (FxHasher64) because + /// (1) we don't care about DDOS attacks (ruling out SipHash); + /// (2) we care about determinism across computers (ruling out AAHash); + /// (3) we have 8-byte keys (for which FxHash is fastest). + track_map: HashMap< + Id, + HashMap, Id, BuildHasherDefault>, + BuildHasherDefault, + >, sites: Store, } @@ -235,7 +249,16 @@ impl Civs { let transition = |a: &Id, b: &Id| self.tracks.get(self.track_between(*a, *b).unwrap()).cost; let satisfied = |p: &Id| *p == b; - let mut astar = Astar::new(100, a, heuristic); + // We use this hasher (FxHasher64) because + // (1) we don't care about DDOS attacks (ruling out SipHash); + // (2) we care about determinism across computers (ruling out AAHash); + // (3) we have 8-byte keys (for which FxHash is fastest). + let mut astar = Astar::new( + 100, + a, + heuristic, + BuildHasherDefault::::default(), + ); astar .poll(100, heuristic, neighbors, transition, satisfied) .into_path() @@ -281,8 +304,12 @@ impl Civs { loc: Vec2, area: Range, ) -> Option> { - let mut dead = HashSet::new(); - let mut alive = HashSet::new(); + // We use this hasher (FxHasher64) because + // (1) we don't care about DDOS attacks (ruling out SipHash); + // (2) we care about determinism across computers (ruling out AAHash); + // (3) we have 8-byte keys (for which FxHash is fastest). + let mut dead = HashSet::with_hasher(BuildHasherDefault::::default()); + let mut alive = HashSet::with_hasher(BuildHasherDefault::::default()); alive.insert(loc); // Fill the surrounding area @@ -495,7 +522,16 @@ fn find_path( let transition = |a: &Vec2, b: &Vec2| 1.0 + walk_in_dir(sim, *a, *b - *a).unwrap_or(10000.0); let satisfied = |l: &Vec2| *l == b; - let mut astar = Astar::new(20000, a, heuristic); + // We use this hasher (FxHasher64) because + // (1) we don't care about DDOS attacks (ruling out SipHash); + // (2) we care about determinism across computers (ruling out AAHash); + // (3) we have 8-byte keys (for which FxHash is fastest). + let mut astar = Astar::new( + 20000, + a, + heuristic, + BuildHasherDefault::::default(), + ); astar .poll(20000, heuristic, neighbors, transition, satisfied) .into_path() @@ -688,13 +724,13 @@ impl fmt::Display for Site { writeln!(f, "- coin: {}", self.coin.floor() as u32)?; writeln!(f, "Stocks")?; for (stock, q) in self.stocks.iter() { - writeln!(f, "- {}: {}", stock, q.floor())?; + writeln!(f, "- {:?}: {}", stock, q.floor())?; } writeln!(f, "Values")?; for stock in TRADE_STOCKS.iter() { writeln!( f, - "- {}: {}", + "- {:?}: {}", stock, self.values[*stock] .map(|x| x.to_string()) @@ -703,11 +739,16 @@ impl fmt::Display for Site { } writeln!(f, "Laborers")?; for (labor, n) in self.labors.iter() { - writeln!(f, "- {}: {}", labor, (*n * self.population).floor() as u32)?; + writeln!( + f, + "- {:?}: {}", + labor, + (*n * self.population).floor() as u32 + )?; } writeln!(f, "Export targets")?; for (stock, n) in self.export_targets.iter() { - writeln!(f, "- {}: {}", stock, n)?; + writeln!(f, "- {:?}: {}", stock, n)?; } Ok(()) @@ -723,43 +764,50 @@ pub enum SiteKind { impl Site { pub fn simulate(&mut self, years: f32, nat_res: &NaturalResources) { // Insert natural resources into the economy - if self.stocks[FISH] < nat_res.river { - self.stocks[FISH] = nat_res.river; + if self.stocks[Fish] < nat_res.river { + self.stocks[Fish] = nat_res.river; } - if self.stocks[WHEAT] < nat_res.farmland { - self.stocks[WHEAT] = nat_res.farmland; + if self.stocks[Wheat] < nat_res.farmland { + self.stocks[Wheat] = nat_res.farmland; } - if self.stocks[LOGS] < nat_res.wood { - self.stocks[LOGS] = nat_res.wood; + if self.stocks[Logs] < nat_res.wood { + self.stocks[Logs] = nat_res.wood; } - if self.stocks[GAME] < nat_res.wood { - self.stocks[GAME] = nat_res.wood; + if self.stocks[Game] < nat_res.wood { + self.stocks[Game] = nat_res.wood; } - if self.stocks[ROCK] < nat_res.rock { - self.stocks[ROCK] = nat_res.rock; + if self.stocks[Rock] < nat_res.rock { + self.stocks[Rock] = nat_res.rock; } + // We use this hasher (FxHasher32) because + // (1) we don't care about DDOS attacks (ruling out SipHash); + // (2) we care about determinism across computers (ruling out AAHash); + // (3) we have 1-byte keys (for which FxHash is supposedly fastest). let orders = vec![ - (None, vec![(FOOD, 0.5)]), - (Some(COOK), vec![(FLOUR, 16.0), (MEAT, 4.0), (WOOD, 3.0)]), - (Some(LUMBERJACK), vec![(LOGS, 4.5)]), - (Some(MINER), vec![(ROCK, 7.5)]), - (Some(FISHER), vec![(FISH, 4.0)]), - (Some(HUNTER), vec![(GAME, 4.0)]), - (Some(FARMER), vec![(WHEAT, 4.0)]), + (None, vec![(Food, 0.5)]), + (Some(Cook), vec![(Flour, 16.0), (Meat, 4.0), (Wood, 3.0)]), + (Some(Lumberjack), vec![(Logs, 4.5)]), + (Some(Miner), vec![(Rock, 7.5)]), + (Some(Fisher), vec![(Fish, 4.0)]), + (Some(Hunter), vec![(Game, 4.0)]), + (Some(Farmer), vec![(Wheat, 4.0)]), ] .into_iter() - .collect::>>(); + .collect::, BuildHasherDefault>>(); // Per labourer, per year - let production = Stocks::from_list(&[ - (FARMER, (FLOUR, 2.0)), - (LUMBERJACK, (WOOD, 1.5)), - (MINER, (STONE, 0.6)), - (FISHER, (MEAT, 3.0)), - (HUNTER, (MEAT, 0.25)), - (COOK, (FOOD, 20.0)), - ]); + let production = MapVec::from_list( + &[ + (Farmer, (Flour, 2.0)), + (Lumberjack, (Wood, 1.5)), + (Miner, (Stone, 0.6)), + (Fisher, (Meat, 3.0)), + (Hunter, (Meat, 0.25)), + (Cook, (Food, 20.0)), + ], + (Rock, 0.0), + ); let mut demand = Stocks::from_default(0.0); for (labor, orders) in &orders { @@ -881,7 +929,7 @@ impl Site { // Births/deaths const NATURAL_BIRTH_RATE: f32 = 0.15; const DEATH_RATE: f32 = 0.05; - let birth_rate = if self.surplus[FOOD] > 0.0 { + let birth_rate = if self.surplus[Food] > 0.0 { NATURAL_BIRTH_RATE } else { 0.0 @@ -890,26 +938,33 @@ impl Site { } } -type Occupation = &'static str; -const FARMER: Occupation = "farmer"; -const LUMBERJACK: Occupation = "lumberjack"; -const MINER: Occupation = "miner"; -const FISHER: Occupation = "fisher"; -const HUNTER: Occupation = "hunter"; -const COOK: Occupation = "cook"; +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +enum Occupation { + Farmer = 0, + Lumberjack = 1, + Miner = 2, + Fisher = 3, + Hunter = 4, + Cook = 5, +} -type Stock = &'static str; -const WHEAT: Stock = "wheat"; -const FLOUR: Stock = "flour"; -const MEAT: Stock = "meat"; -const FISH: Stock = "fish"; -const GAME: Stock = "game"; -const FOOD: Stock = "food"; -const LOGS: Stock = "logs"; -const WOOD: Stock = "wood"; -const ROCK: Stock = "rock"; -const STONE: Stock = "stone"; -const TRADE_STOCKS: [Stock; 5] = [FLOUR, MEAT, FOOD, WOOD, STONE]; +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum Stock { + Wheat = 0, + Flour = 1, + Meat = 2, + Fish = 3, + Game = 4, + Food = 5, + Logs = 6, + Wood = 7, + Rock = 8, + Stone = 9, +} + +const TRADE_STOCKS: [Stock; 5] = [Flour, Meat, Food, Wood, Stone]; #[derive(Debug, Clone)] struct TradeState { @@ -934,21 +989,35 @@ impl Default for TradeState { pub type Stocks = MapVec; -#[derive(Default, Clone, Debug)] +#[derive(Clone, Debug)] pub struct MapVec { - entries: HashMap, + /// We use this hasher (FxHasher32) because + /// (1) we don't care about DDOS attacks (ruling out SipHash); + /// (2) we care about determinism across computers (ruling out AAHash); + /// (3) we have 1-byte keys (for which FxHash is supposedly fastest). + entries: HashMap>, default: T, } -impl MapVec { - pub fn from_list<'a>(i: impl IntoIterator) -> Self +/// Need manual implementation of Default since K doesn't need that bound. +impl Default for MapVec { + fn default() -> Self { + Self { + entries: Default::default(), + default: Default::default(), + } + } +} + +impl MapVec { + pub fn from_list<'a>(i: impl IntoIterator, default: T) -> Self where K: 'a, T: 'a, { Self { entries: i.into_iter().cloned().collect(), - default: T::default(), + default, } } @@ -986,12 +1055,12 @@ impl MapVec { } } -impl std::ops::Index for MapVec { +impl std::ops::Index for MapVec { type Output = T; fn index(&self, entry: K) -> &Self::Output { self.get(entry) } } -impl std::ops::IndexMut for MapVec { +impl std::ops::IndexMut for MapVec { fn index_mut(&mut self, entry: K) -> &mut Self::Output { self.get_mut(entry) } } diff --git a/world/src/sim/location.rs b/world/src/sim/location.rs index 2eeb1c7a3d..5f9efa0e36 100644 --- a/world/src/sim/location.rs +++ b/world/src/sim/location.rs @@ -1,3 +1,5 @@ +use core::hash::BuildHasherDefault; +use fxhash::FxHasher64; use hashbrown::HashSet; use rand::{seq::SliceRandom, Rng}; use vek::*; @@ -7,7 +9,11 @@ pub struct Location { pub(crate) name: String, pub(crate) center: Vec2, pub(crate) kingdom: Option, - pub(crate) neighbours: HashSet, + // We use this hasher (FxHasher64) because + // (1) we don't care about DDOS attacks (ruling out SipHash); + // (2) we care about determinism across computers (ruling out AAHash); + // (3) we have 8-byte keys (for which FxHash is fastest). + pub(crate) neighbours: HashSet>, } impl Location { diff --git a/world/src/sim/mod.rs b/world/src/sim/mod.rs index 131eabac01..570c7c58d0 100644 --- a/world/src/sim/mod.rs +++ b/world/src/sim/mod.rs @@ -1353,14 +1353,15 @@ impl WorldSim { .map(|l| l.center) .enumerate() .collect::>(); + // NOTE: We assume that usize is 8 or fewer bytes. (0..locations.len()).for_each(|i| { let pos = locations[i].center.map(|e| e as i64); loc_clone.sort_by_key(|(_, l)| l.map(|e| e as i64).distance_squared(pos)); loc_clone.iter().skip(1).take(2).for_each(|(j, _)| { - locations[i].neighbours.insert(*j); - locations[*j].neighbours.insert(i); + locations[i].neighbours.insert(*j as u64); + locations[*j].neighbours.insert(i as u64); }); }); diff --git a/world/src/site/dungeon/mod.rs b/world/src/site/dungeon/mod.rs index 16dd5ed58b..a379b38209 100644 --- a/world/src/site/dungeon/mod.rs +++ b/world/src/site/dungeon/mod.rs @@ -16,9 +16,11 @@ use common::{ terrain::{Block, BlockKind, Structure, TerrainChunkSize}, vol::{BaseVol, ReadVol, RectSizedVol, RectVolSize, Vox, WriteVol}, }; +use core::{f32, hash::BuildHasherDefault}; +use fxhash::FxHasher64; use lazy_static::lazy_static; use rand::prelude::*; -use std::{f32, sync::Arc}; +use std::sync::Arc; use vek::*; impl WorldSim { @@ -381,7 +383,16 @@ impl Floor { _ => 100000.0, }; let satisfied = |l: &Vec2| *l == b; - let mut astar = Astar::new(20000, a, heuristic); + // We use this hasher (FxHasher64) because + // (1) we don't care about DDOS attacks (ruling out SipHash); + // (2) we don't care about determinism across computers (we could use AAHash); + // (3) we have 8-byte keys (for which FxHash is fastest). + let mut astar = Astar::new( + 20000, + a, + heuristic, + BuildHasherDefault::::default(), + ); let path = astar .poll( FLOOR_SIZE.product() as usize + 1, diff --git a/world/src/site/settlement/mod.rs b/world/src/site/settlement/mod.rs index 3fafa933d7..c274cff062 100644 --- a/world/src/site/settlement/mod.rs +++ b/world/src/site/settlement/mod.rs @@ -18,9 +18,10 @@ use common::{ terrain::{Block, BlockKind, TerrainChunkSize}, vol::{BaseVol, ReadVol, RectSizedVol, RectVolSize, Vox, WriteVol}, }; +use fxhash::FxHasher64; use hashbrown::{HashMap, HashSet}; use rand::prelude::*; -use std::{collections::VecDeque, f32}; +use std::{collections::VecDeque, f32, hash::BuildHasherDefault}; use vek::*; #[allow(dead_code)] @@ -967,7 +968,11 @@ pub struct Sample<'a> { } pub struct Land { - tiles: HashMap, Tile>, + /// We use this hasher (FxHasher64) because + /// (1) we need determinism across computers (ruling out AAHash); + /// (2) we don't care about DDOS attacks (ruling out SipHash); + /// (3) we have 8-byte keys (for which FxHash is fastest). + tiles: HashMap, Tile, BuildHasherDefault>, plots: Store, sampler_warp: StructureGen2d, hazard: Id, @@ -978,7 +983,7 @@ impl Land { let mut plots = Store::default(); let hazard = plots.insert(Plot::Hazard); Self { - tiles: HashMap::new(), + tiles: HashMap::default(), plots, sampler_warp: StructureGen2d::new(rng.gen(), AREA_SIZE, AREA_SIZE * 2 / 5), hazard, @@ -1089,21 +1094,38 @@ impl Land { |from: &Vec2, to: &Vec2| path_cost_fn(self.tile_at(*from), self.tile_at(*to)); let satisfied = |pos: &Vec2| *pos == dest; - Astar::new(250, origin, heuristic) - .poll(250, heuristic, neighbors, transition, satisfied) - .into_path() + // We use this hasher (FxHasher64) because + // (1) we don't care about DDOS attacks (ruling out SipHash); + // (2) we don't care about determinism across computers (we could use AAHash); + // (3) we have 8-byte keys (for which FxHash is fastest). + Astar::new( + 250, + origin, + heuristic, + BuildHasherDefault::::default(), + ) + .poll(250, heuristic, neighbors, transition, satisfied) + .into_path() } + /// We use this hasher (FxHasher64) because + /// (1) we don't care about DDOS attacks (ruling out SipHash); + /// (2) we care about determinism across computers (ruling out AAHash); + /// (3) we have 8-byte keys (for which FxHash is fastest). fn grow_from( &self, start: Vec2, max_size: usize, _rng: &mut impl Rng, mut match_fn: impl FnMut(Option<&Plot>) -> bool, - ) -> HashSet> { + ) -> HashSet, BuildHasherDefault> { let mut open = VecDeque::new(); open.push_back(start); - let mut closed = HashSet::new(); + // We use this hasher (FxHasher64) because + // (1) we don't care about DDOS attacks (ruling out SipHash); + // (2) we care about determinism across computers (ruling out AAHash); + // (3) we have 8-byte keys (for which FxHash is fastest). + let mut closed = HashSet::with_hasher(BuildHasherDefault::::default()); while open.len() + closed.len() < max_size { let next_pos = if let Some(next_pos) = open.pop_front() { diff --git a/world/src/util/hash_cache.rs b/world/src/util/hash_cache.rs deleted file mode 100644 index ae669f74c3..0000000000 --- a/world/src/util/hash_cache.rs +++ /dev/null @@ -1,45 +0,0 @@ -use hashbrown::HashMap; -use std::hash::Hash; - -pub struct HashCache { - capacity: usize, - map: HashMap, - counter: usize, -} - -impl Default for HashCache { - fn default() -> Self { Self::with_capacity(1024) } -} - -impl HashCache { - pub fn with_capacity(capacity: usize) -> Self { - Self { - capacity, - map: HashMap::with_capacity(1024), - counter: 0, - } - } - - pub fn maintain(&mut self) { - const CACHE_BLOAT_RATE: usize = 2; - - if self.map.len() > self.capacity * CACHE_BLOAT_RATE { - let (capacity, counter) = (self.capacity, self.counter); - self.map.retain(|_, (c, _)| *c + capacity > counter); - } - } - - pub fn get V>(&mut self, key: K, f: F) -> &V { - self.maintain(); - - let counter = &mut self.counter; - &self - .map - .entry(key.clone()) - .or_insert_with(|| { - *counter += 1; - (*counter, f(key)) - }) - .1 - } -} diff --git a/world/src/util/mod.rs b/world/src/util/mod.rs index ce4dbd3971..9ab6f91dce 100644 --- a/world/src/util/mod.rs +++ b/world/src/util/mod.rs @@ -1,6 +1,5 @@ pub mod fast_noise; pub mod grid; -pub mod hash_cache; pub mod random; pub mod sampler; pub mod seed_expan; @@ -12,7 +11,6 @@ pub mod unit_chooser; pub use self::{ fast_noise::FastNoise, grid::Grid, - hash_cache::HashCache, random::{RandomField, RandomPerm}, sampler::{Sampler, SamplerMut}, small_cache::SmallCache,