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.
This commit is contained in:
Joshua Yanovski 2020-05-21 21:20:01 +02:00
parent f8376fd5dc
commit 2670184954
13 changed files with 268 additions and 148 deletions

2
Cargo.lock generated
View File

@ -4996,6 +4996,7 @@ dependencies = [
"crossbeam", "crossbeam",
"dot_vox", "dot_vox",
"find_folder", "find_folder",
"fxhash",
"hashbrown", "hashbrown",
"image", "image",
"indexmap", "indexmap",
@ -5118,6 +5119,7 @@ dependencies = [
"arr_macro", "arr_macro",
"bincode", "bincode",
"bitvec", "bitvec",
"fxhash",
"hashbrown", "hashbrown",
"image", "image",
"itertools", "itertools",

View File

@ -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"] } specs = { version = "0.15.1", features = ["serde", "nightly", "storage-event-control"] }
vek = { version = "0.10.0", features = ["serde"] } vek = { version = "0.10.0", features = ["serde"] }
dot_vox = "4.0.0" dot_vox = "4.0.0"
fxhash = "0.2.1"
image = "0.22.3" image = "0.22.3"
mio = "0.6.19" mio = "0.6.19"
mio-extras = "2.0.5" mio-extras = "2.0.5"

View File

@ -1,7 +1,11 @@
use crate::path::Path; 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 hashbrown::{HashMap, HashSet};
use std::{cmp::Ordering, collections::BinaryHeap, f32, hash::Hash}; use std::collections::BinaryHeap;
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub struct PathEntry<S> { pub struct PathEntry<S> {
@ -43,33 +47,62 @@ impl<T> PathResult<T> {
} }
} }
#[derive(Clone, Debug)] #[derive(Clone)]
pub struct Astar<S: Clone + Eq + Hash> { pub struct Astar<S, Hasher> {
iter: usize, iter: usize,
max_iters: usize, max_iters: usize,
potential_nodes: BinaryHeap<PathEntry<S>>, potential_nodes: BinaryHeap<PathEntry<S>>,
came_from: HashMap<S, S>, came_from: HashMap<S, S, Hasher>,
cheapest_scores: HashMap<S, f32>, cheapest_scores: HashMap<S, f32, Hasher>,
final_scores: HashMap<S, f32>, final_scores: HashMap<S, f32, Hasher>,
visited: HashSet<S>, visited: HashSet<S, Hasher>,
cheapest_node: Option<S>, cheapest_node: Option<S>,
cheapest_cost: Option<f32>, cheapest_cost: Option<f32>,
} }
impl<S: Clone + Eq + Hash> Astar<S> { /// NOTE: Must manually derive since Hasher doesn't implement it.
pub fn new(max_iters: usize, start: S, heuristic: impl FnOnce(&S) -> f32) -> Self { impl<S: Clone + Eq + Hash + fmt::Debug, H: BuildHasher> fmt::Debug for Astar<S, H> {
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<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
pub fn new(max_iters: usize, start: S, heuristic: impl FnOnce(&S) -> f32, hasher: H) -> Self {
Self { Self {
max_iters, max_iters,
iter: 0, iter: 0,
potential_nodes: std::iter::once(PathEntry { potential_nodes: core::iter::once(PathEntry {
cost: 0.0, cost: 0.0,
node: start.clone(), node: start.clone(),
}) })
.collect(), .collect(),
came_from: HashMap::default(), came_from: HashMap::with_hasher(hasher.clone()),
cheapest_scores: std::iter::once((start.clone(), 0.0)).collect(), cheapest_scores: {
final_scores: std::iter::once((start.clone(), heuristic(&start))).collect(), let mut h = HashMap::with_capacity_and_hasher(1, hasher.clone());
visited: std::iter::once(start).collect(), 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_node: None,
cheapest_cost: None, cheapest_cost: None,
} }

View File

@ -3,6 +3,7 @@ use crate::{
terrain::Block, terrain::Block,
vol::{BaseVol, ReadVol}, vol::{BaseVol, ReadVol},
}; };
use hashbrown::hash_map::DefaultHashBuilder;
use rand::{thread_rng, Rng}; use rand::{thread_rng, Rng};
use std::iter::FromIterator; use std::iter::FromIterator;
use vek::*; use vek::*;
@ -92,7 +93,11 @@ impl Route {
pub struct Chaser { pub struct Chaser {
last_search_tgt: Option<Vec3<f32>>, last_search_tgt: Option<Vec3<f32>>,
route: Route, route: Route,
astar: Option<Astar<Vec3<i32>>>, /// 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<Astar<Vec3<i32>, DefaultHashBuilder>>,
} }
impl Chaser { impl Chaser {
@ -147,7 +152,7 @@ impl Chaser {
} }
fn find_path<V>( fn find_path<V>(
astar: &mut Option<Astar<Vec3<i32>>>, astar: &mut Option<Astar<Vec3<i32>, DefaultHashBuilder>>,
vol: &V, vol: &V,
startf: Vec3<f32>, startf: Vec3<f32>,
endf: Vec3<f32>, endf: Vec3<f32>,
@ -263,7 +268,12 @@ where
let satisfied = |pos: &Vec3<i32>| pos == &end; let satisfied = |pos: &Vec3<i32>| pos == &end;
let mut new_astar = match astar.take() { 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, Some(astar) => astar,
}; };

View File

@ -5,10 +5,12 @@ use std::{
ops::{Index, IndexMut}, ops::{Index, IndexMut},
}; };
pub struct Id<T>(usize, PhantomData<T>); // NOTE: We use u64 to make sure we are consistent across all machines. We
// assume that usize fits into 8 bytes.
pub struct Id<T>(u64, PhantomData<T>);
impl<T> Id<T> { impl<T> Id<T> {
pub fn id(&self) -> usize { self.0 } pub fn id(&self) -> u64 { self.0 }
} }
impl<T> Copy for Id<T> {} impl<T> Copy for Id<T> {}
@ -37,12 +39,19 @@ impl<T> Default for Store<T> {
} }
impl<T> Store<T> { impl<T> Store<T> {
pub fn get(&self, id: Id<T>) -> &T { self.items.get(id.0).unwrap() } pub fn get(&self, id: Id<T>) -> &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<T>) -> &mut T { self.items.get_mut(id.0).unwrap() } pub fn get_mut(&mut self, id: Id<T>) -> &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<Item = Id<T>> { pub fn ids(&self) -> impl Iterator<Item = Id<T>> {
(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<Item = &T> { self.items.iter() } pub fn iter(&self) -> impl Iterator<Item = &T> { self.items.iter() }
@ -53,11 +62,13 @@ impl<T> Store<T> {
self.items self.items
.iter() .iter()
.enumerate() .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<T> { pub fn insert(&mut self, item: T) -> Id<T> {
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); self.items.push(item);
id id
} }

View File

@ -8,6 +8,7 @@ edition = "2018"
bincode = "1.2.0" bincode = "1.2.0"
common = { package = "veloren-common", path = "../common" } common = { package = "veloren-common", path = "../common" }
bitvec = "0.17.4" bitvec = "0.17.4"
fxhash = "0.2.1"
image = "0.22.3" image = "0.22.3"
itertools = "0.8.2" itertools = "0.8.2"
vek = "0.10.0" vek = "0.10.0"

View File

@ -2,6 +2,7 @@
mod econ; mod econ;
use self::{Occupation::*, Stock::*};
use crate::{ use crate::{
config::CONFIG, config::CONFIG,
sim::WorldSim, sim::WorldSim,
@ -16,10 +17,15 @@ use common::{
terrain::TerrainChunkSize, terrain::TerrainChunkSize,
vol::RectVolSize, vol::RectVolSize,
}; };
use core::{
fmt,
hash::{BuildHasherDefault, Hash},
ops::Range,
};
use fxhash::FxHasher32;
use hashbrown::{HashMap, HashSet}; use hashbrown::{HashMap, HashSet};
use rand::prelude::*; use rand::prelude::*;
use rand_chacha::ChaChaRng; use rand_chacha::ChaChaRng;
use std::{fmt, hash::Hash, ops::Range};
use vek::*; use vek::*;
const INITIAL_CIV_COUNT: usize = 64; const INITIAL_CIV_COUNT: usize = 64;
@ -30,7 +36,15 @@ pub struct Civs {
places: Store<Place>, places: Store<Place>,
tracks: Store<Track>, tracks: Store<Track>,
track_map: HashMap<Id<Site>, HashMap<Id<Site>, Id<Track>>>, /// 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 8-byte keys (for which FxHash is fastest).
track_map: HashMap<
Id<Site>,
HashMap<Id<Site>, Id<Track>, BuildHasherDefault<FxHasher32>>,
BuildHasherDefault<FxHasher32>,
>,
sites: Store<Site>, sites: Store<Site>,
} }
@ -241,7 +255,16 @@ impl Civs {
let transition = let transition =
|a: &Id<Site>, b: &Id<Site>| self.tracks.get(self.track_between(*a, *b).unwrap()).cost; |a: &Id<Site>, b: &Id<Site>| self.tracks.get(self.track_between(*a, *b).unwrap()).cost;
let satisfied = |p: &Id<Site>| *p == b; let satisfied = |p: &Id<Site>| *p == b;
let mut astar = Astar::new(100, a, heuristic); // 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 8-byte keys (for which FxHash is fastest).
let mut astar = Astar::new(
100,
a,
heuristic,
BuildHasherDefault::<FxHasher32>::default(),
);
astar astar
.poll(100, heuristic, neighbors, transition, satisfied) .poll(100, heuristic, neighbors, transition, satisfied)
.into_path() .into_path()
@ -287,8 +310,12 @@ impl Civs {
loc: Vec2<i32>, loc: Vec2<i32>,
area: Range<usize>, area: Range<usize>,
) -> Option<Id<Place>> { ) -> Option<Id<Place>> {
let mut dead = HashSet::new(); // We use this hasher (FxHasher32) because
let mut alive = HashSet::new(); // (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::<FxHasher32>::default());
let mut alive = HashSet::with_hasher(BuildHasherDefault::<FxHasher32>::default());
alive.insert(loc); alive.insert(loc);
// Fill the surrounding area // Fill the surrounding area
@ -501,7 +528,16 @@ fn find_path(
let transition = let transition =
|a: &Vec2<i32>, b: &Vec2<i32>| 1.0 + walk_in_dir(sim, *a, *b - *a).unwrap_or(10000.0); |a: &Vec2<i32>, b: &Vec2<i32>| 1.0 + walk_in_dir(sim, *a, *b - *a).unwrap_or(10000.0);
let satisfied = |l: &Vec2<i32>| *l == b; let satisfied = |l: &Vec2<i32>| *l == b;
let mut astar = Astar::new(20000, a, heuristic); // 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 8-byte keys (for which FxHash is fastest).
let mut astar = Astar::new(
20000,
a,
heuristic,
BuildHasherDefault::<FxHasher32>::default(),
);
astar astar
.poll(20000, heuristic, neighbors, transition, satisfied) .poll(20000, heuristic, neighbors, transition, satisfied)
.into_path() .into_path()
@ -694,13 +730,13 @@ impl fmt::Display for Site {
writeln!(f, "- coin: {}", self.coin.floor() as u32)?; writeln!(f, "- coin: {}", self.coin.floor() as u32)?;
writeln!(f, "Stocks")?; writeln!(f, "Stocks")?;
for (stock, q) in self.stocks.iter() { for (stock, q) in self.stocks.iter() {
writeln!(f, "- {}: {}", stock, q.floor())?; writeln!(f, "- {:?}: {}", stock, q.floor())?;
} }
writeln!(f, "Values")?; writeln!(f, "Values")?;
for stock in TRADE_STOCKS.iter() { for stock in TRADE_STOCKS.iter() {
writeln!( writeln!(
f, f,
"- {}: {}", "- {:?}: {}",
stock, stock,
self.values[*stock] self.values[*stock]
.map(|x| x.to_string()) .map(|x| x.to_string())
@ -709,11 +745,16 @@ impl fmt::Display for Site {
} }
writeln!(f, "Laborers")?; writeln!(f, "Laborers")?;
for (labor, n) in self.labors.iter() { 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")?; writeln!(f, "Export targets")?;
for (stock, n) in self.export_targets.iter() { for (stock, n) in self.export_targets.iter() {
writeln!(f, "- {}: {}", stock, n)?; writeln!(f, "- {:?}: {}", stock, n)?;
} }
Ok(()) Ok(())
@ -729,43 +770,50 @@ pub enum SiteKind {
impl Site { impl Site {
pub fn simulate(&mut self, years: f32, nat_res: &NaturalResources) { pub fn simulate(&mut self, years: f32, nat_res: &NaturalResources) {
// Insert natural resources into the economy // Insert natural resources into the economy
if self.stocks[FISH] < nat_res.river { if self.stocks[Fish] < nat_res.river {
self.stocks[FISH] = nat_res.river; self.stocks[Fish] = nat_res.river;
} }
if self.stocks[WHEAT] < nat_res.farmland { if self.stocks[Wheat] < nat_res.farmland {
self.stocks[WHEAT] = nat_res.farmland; self.stocks[Wheat] = nat_res.farmland;
} }
if self.stocks[LOGS] < nat_res.wood { if self.stocks[Logs] < nat_res.wood {
self.stocks[LOGS] = nat_res.wood; self.stocks[Logs] = nat_res.wood;
} }
if self.stocks[GAME] < nat_res.wood { if self.stocks[Game] < nat_res.wood {
self.stocks[GAME] = nat_res.wood; self.stocks[Game] = nat_res.wood;
} }
if self.stocks[ROCK] < nat_res.rock { if self.stocks[Rock] < nat_res.rock {
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![ let orders = vec![
(None, vec![(FOOD, 0.5)]), (None, vec![(Food, 0.5)]),
(Some(COOK), vec![(FLOUR, 16.0), (MEAT, 4.0), (WOOD, 3.0)]), (Some(Cook), vec![(Flour, 16.0), (Meat, 4.0), (Wood, 3.0)]),
(Some(LUMBERJACK), vec![(LOGS, 4.5)]), (Some(Lumberjack), vec![(Logs, 4.5)]),
(Some(MINER), vec![(ROCK, 7.5)]), (Some(Miner), vec![(Rock, 7.5)]),
(Some(FISHER), vec![(FISH, 4.0)]), (Some(Fisher), vec![(Fish, 4.0)]),
(Some(HUNTER), vec![(GAME, 4.0)]), (Some(Hunter), vec![(Game, 4.0)]),
(Some(FARMER), vec![(WHEAT, 4.0)]), (Some(Farmer), vec![(Wheat, 4.0)]),
] ]
.into_iter() .into_iter()
.collect::<HashMap<_, Vec<(Stock, f32)>>>(); .collect::<HashMap<_, Vec<(Stock, f32)>, BuildHasherDefault<FxHasher32>>>();
// Per labourer, per year // Per labourer, per year
let production = Stocks::from_list(&[ let production = MapVec::from_list(
(FARMER, (FLOUR, 2.0)), &[
(LUMBERJACK, (WOOD, 1.5)), (Farmer, (Flour, 2.0)),
(MINER, (STONE, 0.6)), (Lumberjack, (Wood, 1.5)),
(FISHER, (MEAT, 3.0)), (Miner, (Stone, 0.6)),
(HUNTER, (MEAT, 0.25)), (Fisher, (Meat, 3.0)),
(COOK, (FOOD, 20.0)), (Hunter, (Meat, 0.25)),
]); (Cook, (Food, 20.0)),
],
(Rock, 0.0),
);
let mut demand = Stocks::from_default(0.0); let mut demand = Stocks::from_default(0.0);
for (labor, orders) in &orders { for (labor, orders) in &orders {
@ -887,7 +935,7 @@ impl Site {
// Births/deaths // Births/deaths
const NATURAL_BIRTH_RATE: f32 = 0.15; const NATURAL_BIRTH_RATE: f32 = 0.15;
const DEATH_RATE: f32 = 0.05; 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 NATURAL_BIRTH_RATE
} else { } else {
0.0 0.0
@ -896,26 +944,33 @@ impl Site {
} }
} }
type Occupation = &'static str; #[repr(u8)]
const FARMER: Occupation = "farmer"; #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
const LUMBERJACK: Occupation = "lumberjack"; enum Occupation {
const MINER: Occupation = "miner"; Farmer = 0,
const FISHER: Occupation = "fisher"; Lumberjack = 1,
const HUNTER: Occupation = "hunter"; Miner = 2,
const COOK: Occupation = "cook"; Fisher = 3,
Hunter = 4,
Cook = 5,
}
type Stock = &'static str; #[repr(u8)]
const WHEAT: Stock = "wheat"; #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
const FLOUR: Stock = "flour"; pub enum Stock {
const MEAT: Stock = "meat"; Wheat = 0,
const FISH: Stock = "fish"; Flour = 1,
const GAME: Stock = "game"; Meat = 2,
const FOOD: Stock = "food"; Fish = 3,
const LOGS: Stock = "logs"; Game = 4,
const WOOD: Stock = "wood"; Food = 5,
const ROCK: Stock = "rock"; Logs = 6,
const STONE: Stock = "stone"; Wood = 7,
const TRADE_STOCKS: [Stock; 5] = [FLOUR, MEAT, FOOD, WOOD, STONE]; Rock = 8,
Stone = 9,
}
const TRADE_STOCKS: [Stock; 5] = [Flour, Meat, Food, Wood, Stone];
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct TradeState { struct TradeState {
@ -940,21 +995,35 @@ impl Default for TradeState {
pub type Stocks<T> = MapVec<Stock, T>; pub type Stocks<T> = MapVec<Stock, T>;
#[derive(Default, Clone, Debug)] #[derive(Clone, Debug)]
pub struct MapVec<K, T> { pub struct MapVec<K, T> {
entries: HashMap<K, T>, /// 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<K, T, BuildHasherDefault<FxHasher32>>,
default: T, default: T,
} }
impl<K: Copy + Eq + Hash, T: Default + Clone> MapVec<K, T> { /// Need manual implementation of Default since K doesn't need that bound.
pub fn from_list<'a>(i: impl IntoIterator<Item = &'a (K, T)>) -> Self impl<K, T: Default> Default for MapVec<K, T> {
fn default() -> Self {
Self {
entries: Default::default(),
default: Default::default(),
}
}
}
impl<K: Copy + Eq + Hash, T: Clone> MapVec<K, T> {
pub fn from_list<'a>(i: impl IntoIterator<Item = &'a (K, T)>, default: T) -> Self
where where
K: 'a, K: 'a,
T: 'a, T: 'a,
{ {
Self { Self {
entries: i.into_iter().cloned().collect(), entries: i.into_iter().cloned().collect(),
default: T::default(), default,
} }
} }
@ -992,12 +1061,12 @@ impl<K: Copy + Eq + Hash, T: Default + Clone> MapVec<K, T> {
} }
} }
impl<K: Copy + Eq + Hash, T: Default + Clone> std::ops::Index<K> for MapVec<K, T> { impl<K: Copy + Eq + Hash, T: Clone> std::ops::Index<K> for MapVec<K, T> {
type Output = T; type Output = T;
fn index(&self, entry: K) -> &Self::Output { self.get(entry) } fn index(&self, entry: K) -> &Self::Output { self.get(entry) }
} }
impl<K: Copy + Eq + Hash, T: Default + Clone> std::ops::IndexMut<K> for MapVec<K, T> { impl<K: Copy + Eq + Hash, T: Clone> std::ops::IndexMut<K> for MapVec<K, T> {
fn index_mut(&mut self, entry: K) -> &mut Self::Output { self.get_mut(entry) } fn index_mut(&mut self, entry: K) -> &mut Self::Output { self.get_mut(entry) }
} }

View File

@ -1,3 +1,5 @@
use core::hash::BuildHasherDefault;
use fxhash::FxHasher32;
use hashbrown::HashSet; use hashbrown::HashSet;
use rand::{seq::SliceRandom, Rng}; use rand::{seq::SliceRandom, Rng};
use vek::*; use vek::*;
@ -7,7 +9,11 @@ pub struct Location {
pub(crate) name: String, pub(crate) name: String,
pub(crate) center: Vec2<i32>, pub(crate) center: Vec2<i32>,
pub(crate) kingdom: Option<Kingdom>, pub(crate) kingdom: Option<Kingdom>,
pub(crate) neighbours: HashSet<usize>, // 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 8-byte keys (for which FxHash is fastest).
pub(crate) neighbours: HashSet<u64, BuildHasherDefault<FxHasher32>>,
} }
impl Location { impl Location {

View File

@ -1448,14 +1448,15 @@ impl WorldSim {
.map(|l| l.center) .map(|l| l.center)
.enumerate() .enumerate()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
// NOTE: We assume that usize is 8 or fewer bytes.
(0..locations.len()).for_each(|i| { (0..locations.len()).for_each(|i| {
let pos = locations[i].center.map(|e| e as i64); 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.sort_by_key(|(_, l)| l.map(|e| e as i64).distance_squared(pos));
loc_clone.iter().skip(1).take(2).for_each(|(j, _)| { loc_clone.iter().skip(1).take(2).for_each(|(j, _)| {
locations[i].neighbours.insert(*j); locations[i].neighbours.insert(*j as u64);
locations[*j].neighbours.insert(i); locations[*j].neighbours.insert(i as u64);
}); });
}); });

View File

@ -16,9 +16,11 @@ use common::{
terrain::{Block, BlockKind, Structure, TerrainChunkSize}, terrain::{Block, BlockKind, Structure, TerrainChunkSize},
vol::{BaseVol, ReadVol, RectSizedVol, RectVolSize, Vox, WriteVol}, vol::{BaseVol, ReadVol, RectSizedVol, RectVolSize, Vox, WriteVol},
}; };
use core::{f32, hash::BuildHasherDefault};
use fxhash::FxHasher32;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use rand::prelude::*; use rand::prelude::*;
use std::{f32, sync::Arc}; use std::sync::Arc;
use vek::*; use vek::*;
impl WorldSim { impl WorldSim {
@ -381,7 +383,16 @@ impl Floor {
_ => 100000.0, _ => 100000.0,
}; };
let satisfied = |l: &Vec2<i32>| *l == b; let satisfied = |l: &Vec2<i32>| *l == b;
let mut astar = Astar::new(20000, a, heuristic); // We use this hasher (FxHasher32) 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 4-byte keys (for which FxHash is fastest).
let mut astar = Astar::new(
20000,
a,
heuristic,
BuildHasherDefault::<FxHasher32>::default(),
);
let path = astar let path = astar
.poll( .poll(
FLOOR_SIZE.product() as usize + 1, FLOOR_SIZE.product() as usize + 1,

View File

@ -18,9 +18,10 @@ use common::{
terrain::{Block, BlockKind, TerrainChunkSize}, terrain::{Block, BlockKind, TerrainChunkSize},
vol::{BaseVol, ReadVol, RectSizedVol, RectVolSize, Vox, WriteVol}, vol::{BaseVol, ReadVol, RectSizedVol, RectVolSize, Vox, WriteVol},
}; };
use fxhash::FxHasher32;
use hashbrown::{HashMap, HashSet}; use hashbrown::{HashMap, HashSet};
use rand::prelude::*; use rand::prelude::*;
use std::{collections::VecDeque, f32}; use std::{collections::VecDeque, f32, hash::BuildHasherDefault};
use vek::*; use vek::*;
#[allow(dead_code)] #[allow(dead_code)]
@ -967,7 +968,11 @@ pub struct Sample<'a> {
} }
pub struct Land { pub struct Land {
tiles: HashMap<Vec2<i32>, Tile>, /// We use this hasher (FxHasher32) because
/// (1) we need determinism across computers (ruling out AAHash);
/// (2) we don't care about DDOS attacks (ruling out SipHash);
/// (3) we have 4-byte keys (for which FxHash is fastest).
tiles: HashMap<Vec2<i32>, Tile, BuildHasherDefault<FxHasher32>>,
plots: Store<Plot>, plots: Store<Plot>,
sampler_warp: StructureGen2d, sampler_warp: StructureGen2d,
hazard: Id<Plot>, hazard: Id<Plot>,
@ -978,7 +983,7 @@ impl Land {
let mut plots = Store::default(); let mut plots = Store::default();
let hazard = plots.insert(Plot::Hazard); let hazard = plots.insert(Plot::Hazard);
Self { Self {
tiles: HashMap::new(), tiles: HashMap::default(),
plots, plots,
sampler_warp: StructureGen2d::new(rng.gen(), AREA_SIZE, AREA_SIZE * 2 / 5), sampler_warp: StructureGen2d::new(rng.gen(), AREA_SIZE, AREA_SIZE * 2 / 5),
hazard, hazard,
@ -1089,21 +1094,38 @@ impl Land {
|from: &Vec2<i32>, to: &Vec2<i32>| path_cost_fn(self.tile_at(*from), self.tile_at(*to)); |from: &Vec2<i32>, to: &Vec2<i32>| path_cost_fn(self.tile_at(*from), self.tile_at(*to));
let satisfied = |pos: &Vec2<i32>| *pos == dest; let satisfied = |pos: &Vec2<i32>| *pos == dest;
Astar::new(250, origin, heuristic) // We use this hasher (FxHasher32) 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 4-byte keys (for which FxHash is fastest).
Astar::new(
250,
origin,
heuristic,
BuildHasherDefault::<FxHasher32>::default(),
)
.poll(250, heuristic, neighbors, transition, satisfied) .poll(250, heuristic, neighbors, transition, satisfied)
.into_path() .into_path()
} }
/// 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 8-byte keys (for which FxHash is fastest).
fn grow_from( fn grow_from(
&self, &self,
start: Vec2<i32>, start: Vec2<i32>,
max_size: usize, max_size: usize,
_rng: &mut impl Rng, _rng: &mut impl Rng,
mut match_fn: impl FnMut(Option<&Plot>) -> bool, mut match_fn: impl FnMut(Option<&Plot>) -> bool,
) -> HashSet<Vec2<i32>> { ) -> HashSet<Vec2<i32>, BuildHasherDefault<FxHasher32>> {
let mut open = VecDeque::new(); let mut open = VecDeque::new();
open.push_back(start); open.push_back(start);
let mut closed = HashSet::new(); // 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 8-byte keys (for which FxHash is fastest).
let mut closed = HashSet::with_hasher(BuildHasherDefault::<FxHasher32>::default());
while open.len() + closed.len() < max_size { while open.len() + closed.len() < max_size {
let next_pos = if let Some(next_pos) = open.pop_front() { let next_pos = if let Some(next_pos) = open.pop_front() {

View File

@ -1,45 +0,0 @@
use hashbrown::HashMap;
use std::hash::Hash;
pub struct HashCache<K: Hash + Eq + Clone, V> {
capacity: usize,
map: HashMap<K, (usize, V)>,
counter: usize,
}
impl<K: Hash + Eq + Clone, V> Default for HashCache<K, V> {
fn default() -> Self { Self::with_capacity(1024) }
}
impl<K: Hash + Eq + Clone, V> HashCache<K, V> {
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<F: FnOnce(K) -> 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
}
}

View File

@ -1,6 +1,5 @@
pub mod fast_noise; pub mod fast_noise;
pub mod grid; pub mod grid;
pub mod hash_cache;
pub mod random; pub mod random;
pub mod sampler; pub mod sampler;
pub mod seed_expan; pub mod seed_expan;
@ -12,7 +11,6 @@ pub mod unit_chooser;
pub use self::{ pub use self::{
fast_noise::FastNoise, fast_noise::FastNoise,
grid::Grid, grid::Grid,
hash_cache::HashCache,
random::{RandomField, RandomPerm}, random::{RandomField, RandomPerm},
sampler::{Sampler, SamplerMut}, sampler::{Sampler, SamplerMut},
small_cache::SmallCache, small_cache::SmallCache,