Merge branch 'imbris/server-start-faster' into 'master'

Improve server startup times, in particular, finding paths between sites.

See merge request veloren/veloren!3888
This commit is contained in:
Imbris 2023-05-04 18:06:38 +00:00
commit 60dbcf86f9
20 changed files with 518 additions and 319 deletions

View File

@ -4,12 +4,13 @@ use core::{
fmt, fmt,
hash::{BuildHasher, Hash}, hash::{BuildHasher, Hash},
}; };
use hashbrown::{HashMap, HashSet}; use hashbrown::HashMap;
use std::collections::BinaryHeap; use std::collections::BinaryHeap;
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub struct PathEntry<S> { pub struct PathEntry<S> {
cost: f32, // cost so far + heursitic
cost_estimate: f32,
node: S, node: S,
} }
@ -23,25 +24,50 @@ impl<S: Eq> Ord for PathEntry<S> {
// This method implements reverse ordering, so that the lowest cost // This method implements reverse ordering, so that the lowest cost
// will be ordered first // will be ordered first
fn cmp(&self, other: &PathEntry<S>) -> Ordering { fn cmp(&self, other: &PathEntry<S>) -> Ordering {
other.cost.partial_cmp(&self.cost).unwrap_or(Equal) other
.cost_estimate
.partial_cmp(&self.cost_estimate)
.unwrap_or(Equal)
} }
} }
impl<S: Eq> PartialOrd for PathEntry<S> { impl<S: Eq> PartialOrd for PathEntry<S> {
fn partial_cmp(&self, other: &PathEntry<S>) -> Option<Ordering> { Some(self.cmp(other)) } fn partial_cmp(&self, other: &PathEntry<S>) -> Option<Ordering> { Some(self.cmp(other)) }
// This is particularily hot in `BinaryHeap::pop`, so we provide this
// implementation.
//
// NOTE: This probably doesn't handle edge cases like `NaNs` in a consistent
// manner with `Ord`, but I don't think we need to care about that here(?)
//
// See note about reverse ordering above.
fn le(&self, other: &PathEntry<S>) -> bool { other.cost_estimate <= self.cost_estimate }
} }
pub enum PathResult<T> { pub enum PathResult<T> {
/// No reachable nodes were satisfactory.
///
/// Contains path to node with the lowest heuristic value (out of the
/// explored nodes).
None(Path<T>), None(Path<T>),
/// Either max_iters or max_cost was reached.
///
/// Contains path to node with the lowest heuristic value (out of the
/// explored nodes).
Exhausted(Path<T>), Exhausted(Path<T>),
Path(Path<T>), /// Path succefully found.
///
/// Second field is cost.
Path(Path<T>, f32),
Pending, Pending,
} }
impl<T> PathResult<T> { impl<T> PathResult<T> {
pub fn into_path(self) -> Option<Path<T>> { /// Returns `Some((path, cost))` if a path reaching the target was
/// successfully found.
pub fn into_path(self) -> Option<(Path<T>, f32)> {
match self { match self {
PathResult::Path(path) => Some(path), PathResult::Path(path, cost) => Some((path, cost)),
_ => None, _ => None,
} }
} }
@ -50,23 +76,38 @@ impl<T> PathResult<T> {
match self { match self {
PathResult::None(p) => PathResult::None(f(p)), PathResult::None(p) => PathResult::None(f(p)),
PathResult::Exhausted(p) => PathResult::Exhausted(f(p)), PathResult::Exhausted(p) => PathResult::Exhausted(f(p)),
PathResult::Path(p) => PathResult::Path(f(p)), PathResult::Path(p, cost) => PathResult::Path(f(p), cost),
PathResult::Pending => PathResult::Pending, PathResult::Pending => PathResult::Pending,
} }
} }
} }
// If node entry exists, this was visited!
#[derive(Clone, Debug)]
struct NodeEntry<S> {
/// Previous node in the cheapest path (known so far) that goes from the
/// start to this node.
///
/// If `came_from == self` this is the start node! (to avoid inflating the
/// size with `Option`)
came_from: S,
/// Cost to reach this node from the start by following the cheapest path
/// known so far. This is the sum of the transition costs between all the
/// nodes on this path.
cost: f32,
}
#[derive(Clone)] #[derive(Clone)]
pub struct Astar<S, Hasher> { pub struct Astar<S, Hasher> {
iter: usize, iter: usize,
max_iters: usize, max_iters: usize,
potential_nodes: BinaryHeap<PathEntry<S>>, max_cost: f32,
came_from: HashMap<S, S, Hasher>, potential_nodes: BinaryHeap<PathEntry<S>>, // cost, node pairs
cheapest_scores: HashMap<S, f32, Hasher>, visited_nodes: HashMap<S, NodeEntry<S>, Hasher>,
final_scores: HashMap<S, f32, Hasher>, /// Node with the lowest heuristic value so far.
visited: HashSet<S, Hasher>, ///
cheapest_node: Option<S>, /// (node, heuristic value)
cheapest_cost: Option<f32>, closest_node: Option<(S, f32)>,
} }
/// NOTE: Must manually derive since Hasher doesn't implement it. /// NOTE: Must manually derive since Hasher doesn't implement it.
@ -76,12 +117,8 @@ impl<S: Clone + Eq + Hash + fmt::Debug, H: BuildHasher> fmt::Debug for Astar<S,
.field("iter", &self.iter) .field("iter", &self.iter)
.field("max_iters", &self.max_iters) .field("max_iters", &self.max_iters)
.field("potential_nodes", &self.potential_nodes) .field("potential_nodes", &self.potential_nodes)
.field("came_from", &self.came_from) .field("visited_nodes", &self.visited_nodes)
.field("cheapest_scores", &self.cheapest_scores) .field("closest_node", &self.closest_node)
.field("final_scores", &self.final_scores)
.field("visited", &self.visited)
.field("cheapest_node", &self.cheapest_node)
.field("cheapest_cost", &self.cheapest_cost)
.finish() .finish()
} }
} }
@ -90,72 +127,110 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
pub fn new(max_iters: usize, start: S, hasher: H) -> Self { pub fn new(max_iters: usize, start: S, hasher: H) -> Self {
Self { Self {
max_iters, max_iters,
max_cost: f32::MAX,
iter: 0, iter: 0,
potential_nodes: core::iter::once(PathEntry { potential_nodes: core::iter::once(PathEntry {
cost: 0.0, cost_estimate: 0.0,
node: start.clone(), node: start.clone(),
}) })
.collect(), .collect(),
came_from: HashMap::with_hasher(hasher.clone()), visited_nodes: {
cheapest_scores: { let mut s = HashMap::with_capacity_and_hasher(1, hasher);
let mut h = HashMap::with_capacity_and_hasher(1, hasher.clone()); s.extend(core::iter::once((start.clone(), NodeEntry {
h.extend(core::iter::once((start.clone(), 0.0))); came_from: start,
h cost: 0.0,
}, })));
final_scores: {
let mut h = HashMap::with_capacity_and_hasher(1, hasher.clone());
h.extend(core::iter::once((start.clone(), 0.0)));
h
},
visited: {
let mut s = HashSet::with_capacity_and_hasher(1, hasher);
s.extend(core::iter::once(start));
s s
}, },
cheapest_node: None, closest_node: None,
cheapest_cost: None,
} }
} }
pub fn with_max_cost(mut self, max_cost: f32) -> Self {
self.max_cost = max_cost;
self
}
pub fn poll<I>( pub fn poll<I>(
&mut self, &mut self,
iters: usize, iters: usize,
// Estimate how far we are from the target? but we are given two nodes...
// (current, previous)
mut heuristic: impl FnMut(&S, &S) -> f32, mut heuristic: impl FnMut(&S, &S) -> f32,
// get neighboring nodes
mut neighbors: impl FnMut(&S) -> I, mut neighbors: impl FnMut(&S) -> I,
mut transition: impl FnMut(&S, &S) -> f32, // have we reached target?
mut satisfied: impl FnMut(&S) -> bool, mut satisfied: impl FnMut(&S) -> bool,
) -> PathResult<S> ) -> PathResult<S>
where where
I: Iterator<Item = S>, I: Iterator<Item = (S, f32)>, // (node, transition cost)
{ {
let iter_limit = self.max_iters.min(self.iter + iters); let iter_limit = self.max_iters.min(self.iter + iters);
while self.iter < iter_limit { while self.iter < iter_limit {
if let Some(PathEntry { node, .. }) = self.potential_nodes.pop() { if let Some(PathEntry {
node,
cost_estimate,
}) = self.potential_nodes.pop()
{
let (node_cost, came_from) = self
.visited_nodes
.get(&node)
.map(|n| (n.cost, n.came_from.clone()))
.expect("All nodes in the queue should be included in visisted_nodes");
if satisfied(&node) { if satisfied(&node) {
return PathResult::Path(self.reconstruct_path_to(node)); return PathResult::Path(self.reconstruct_path_to(node), node_cost);
// Note, we assume that cost_estimate isn't an overestimation
// (i.e. that `heuristic` doesn't overestimate).
} else if cost_estimate > self.max_cost {
return PathResult::Exhausted(
self.closest_node
.clone()
.map(|(lc, _)| self.reconstruct_path_to(lc))
.unwrap_or_default(),
);
} else { } else {
for neighbor in neighbors(&node) { for (neighbor, transition_cost) in neighbors(&node) {
let node_cheapest = self.cheapest_scores.get(&node).unwrap_or(&f32::MAX); if neighbor == came_from {
let neighbor_cheapest = continue;
self.cheapest_scores.get(&neighbor).unwrap_or(&f32::MAX); }
let neighbor_cost = self
.visited_nodes
.get(&neighbor)
.map_or(f32::MAX, |n| n.cost);
let cost = node_cheapest + transition(&node, &neighbor); // compute cost to traverse to each neighbor
if cost < *neighbor_cheapest { let cost = node_cost + transition_cost;
self.came_from.insert(neighbor.clone(), node.clone());
self.cheapest_scores.insert(neighbor.clone(), cost); if cost < neighbor_cost {
let previously_visited = self
.visited_nodes
.insert(neighbor.clone(), NodeEntry {
came_from: node.clone(),
cost,
})
.is_some();
let h = heuristic(&neighbor, &node); let h = heuristic(&neighbor, &node);
let neighbor_cost = cost + h; // note that `cost` field does not include the heuristic
self.final_scores.insert(neighbor.clone(), neighbor_cost); // priority queue does include heuristic
let cost_estimate = cost + h;
if self.cheapest_cost.map(|cc| h < cc).unwrap_or(true) { if self
self.cheapest_node = Some(node.clone()); .closest_node
self.cheapest_cost = Some(h); .as_ref()
.map(|&(_, ch)| h < ch)
.unwrap_or(true)
{
self.closest_node = Some((node.clone(), h));
}; };
if self.visited.insert(neighbor.clone()) { // TODO: I think the if here should be removed
// if we hadn't already visited, add this to potential nodes, what about
// its neighbors, wouldn't they need to be revisted???
if !previously_visited {
self.potential_nodes.push(PathEntry { self.potential_nodes.push(PathEntry {
cost_estimate,
node: neighbor, node: neighbor,
cost: neighbor_cost,
}); });
} }
} }
@ -163,9 +238,9 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
} }
} else { } else {
return PathResult::None( return PathResult::None(
self.cheapest_node self.closest_node
.clone() .clone()
.map(|lc| self.reconstruct_path_to(lc)) .map(|(lc, _)| self.reconstruct_path_to(lc))
.unwrap_or_default(), .unwrap_or_default(),
); );
} }
@ -175,9 +250,9 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
if self.iter >= self.max_iters { if self.iter >= self.max_iters {
PathResult::Exhausted( PathResult::Exhausted(
self.cheapest_node self.closest_node
.clone() .clone()
.map(|lc| self.reconstruct_path_to(lc)) .map(|(lc, _)| self.reconstruct_path_to(lc))
.unwrap_or_default(), .unwrap_or_default(),
) )
} else { } else {
@ -185,12 +260,15 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
} }
} }
pub fn get_cheapest_cost(&self) -> Option<f32> { self.cheapest_cost }
fn reconstruct_path_to(&mut self, end: S) -> Path<S> { fn reconstruct_path_to(&mut self, end: S) -> Path<S> {
let mut path = vec![end.clone()]; let mut path = vec![end.clone()];
let mut cnode = &end; let mut cnode = &end;
while let Some(node) = self.came_from.get(cnode) { while let Some(node) = self
.visited_nodes
.get(cnode)
.map(|n| &n.came_from)
.filter(|n| *n != cnode)
{
path.push(node.clone()); path.push(node.clone());
cnode = node; cnode = node;
} }

View File

@ -537,6 +537,17 @@ where
}; };
let heuristic = |pos: &Vec3<i32>, _: &Vec3<i32>| (pos.distance_squared(end) as f32).sqrt(); let heuristic = |pos: &Vec3<i32>, _: &Vec3<i32>| (pos.distance_squared(end) as f32).sqrt();
let transition = |a: Vec3<i32>, b: Vec3<i32>| {
let crow_line = LineSegment2 {
start: startf.xy(),
end: endf.xy(),
};
// Modify the heuristic a little in order to prefer paths that take us on a
// straight line toward our target. This means we get smoother movement.
1.0 + crow_line.distance_to_point(b.xy().map(|e| e as f32)) * 0.025
+ (b.z - a.z - 1).max(0) as f32 * 10.0
};
let neighbors = |pos: &Vec3<i32>| { let neighbors = |pos: &Vec3<i32>| {
let pos = *pos; let pos = *pos;
const DIRS: [Vec3<i32>; 17] = [ const DIRS: [Vec3<i32>; 17] = [
@ -616,7 +627,10 @@ where
.map(|b| !b.is_solid()) .map(|b| !b.is_solid())
.unwrap_or(true))) .unwrap_or(true)))
}) })
.map(move |(pos, dir)| pos + dir) .map(|(pos, dir)| {
let destination = pos + dir;
(destination, transition(pos, destination))
})
// .chain( // .chain(
// DIAGONALS // DIAGONALS
// .iter() // .iter()
@ -627,17 +641,6 @@ where
// ) // )
}; };
let transition = |a: &Vec3<i32>, b: &Vec3<i32>| {
let crow_line = LineSegment2 {
start: startf.xy(),
end: endf.xy(),
};
// Modify the heuristic a little in order to prefer paths that take us on a
// straight line toward our target. This means we get smoother movement.
1.0 + crow_line.distance_to_point(b.xy().map(|e| e as f32)) * 0.025
+ (b.z - a.z - 1).max(0) as f32 * 10.0
};
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() {
@ -645,12 +648,12 @@ where
Some(astar) => astar, Some(astar) => astar,
}; };
let path_result = new_astar.poll(100, heuristic, neighbors, transition, satisfied); let path_result = new_astar.poll(100, heuristic, neighbors, satisfied);
*astar = Some(new_astar); *astar = Some(new_astar);
match path_result { match path_result {
PathResult::Path(path) => { PathResult::Path(path, _cost) => {
*astar = None; *astar = None;
(Some(path), true) (Some(path), true)
}, },

View File

@ -942,6 +942,12 @@ pub fn handle_manipulate_loadout(
BuildHasherDefault::<FxHasher64>::default(), BuildHasherDefault::<FxHasher64>::default(),
); );
// Transition uses manhattan distance as the cost, with a slightly lower cost
// for z transitions
let transition = |a: Vec3<i32>, b: Vec3<i32>| {
let (a, b) = (a.map(|x| x as f32), b.map(|x| x as f32));
((a - b) * Vec3::new(1.0, 1.0, 0.9)).map(|e| e.abs()).sum()
};
// Neighbors are all neighboring blocks that are air // Neighbors are all neighboring blocks that are air
let neighbors = |pos: &Vec3<i32>| { let neighbors = |pos: &Vec3<i32>| {
const DIRS: [Vec3<i32>; 6] = [ const DIRS: [Vec3<i32>; 6] = [
@ -953,24 +959,23 @@ pub fn handle_manipulate_loadout(
Vec3::new(0, 0, -1), Vec3::new(0, 0, -1),
]; ];
let pos = *pos; let pos = *pos;
DIRS.iter().map(move |dir| dir + pos).filter(|pos| { DIRS.iter()
data.terrain .map(move |dir| {
.get(*pos) let dest = dir + pos;
.ok() (dest, transition(pos, dest))
.map_or(false, |block| !block.is_filled()) })
}) .filter(|(pos, _)| {
}; data.terrain
// Transition uses manhattan distance as the cost, with a slightly lower cost .get(*pos)
// for z transitions .ok()
let transition = |a: &Vec3<i32>, b: &Vec3<i32>| { .map_or(false, |block| !block.is_filled())
let (a, b) = (a.map(|x| x as f32), b.map(|x| x as f32)); })
((a - b) * Vec3::new(1.0, 1.0, 0.9)).map(|e| e.abs()).sum()
}; };
// Pathing satisfied when it reaches the sprite position // Pathing satisfied when it reaches the sprite position
let satisfied = |pos: &Vec3<i32>| *pos == sprite_pos; let satisfied = |pos: &Vec3<i32>| *pos == sprite_pos;
let not_blocked_by_terrain = astar let not_blocked_by_terrain = astar
.poll(iters, heuristic, neighbors, transition, satisfied) .poll(iters, heuristic, neighbors, satisfied)
.into_path() .into_path()
.is_some(); .is_some();

View File

@ -3,6 +3,7 @@ use super::{
vec2_as_uniform_idx, TerrainChunkSize, NEIGHBOR_DELTA, TERRAIN_CHUNK_BLOCKS_LG, vec2_as_uniform_idx, TerrainChunkSize, NEIGHBOR_DELTA, TERRAIN_CHUNK_BLOCKS_LG,
}; };
use crate::vol::RectVolSize; use crate::vol::RectVolSize;
use common_base::prof_span;
use core::{f32, f64, iter, ops::RangeInclusive}; use core::{f32, f64, iter, ops::RangeInclusive};
use vek::*; use vek::*;
@ -451,6 +452,7 @@ impl<'a> MapConfig<'a> {
sample_wpos: impl Fn(Vec2<i32>) -> f32, sample_wpos: impl Fn(Vec2<i32>) -> f32,
mut write_pixel: impl FnMut(Vec2<usize>, (u8, u8, u8, u8)), mut write_pixel: impl FnMut(Vec2<usize>, (u8, u8, u8, u8)),
) -> MapDebug { ) -> MapDebug {
prof_span!("MapConfig::generate");
let MapConfig { let MapConfig {
map_size_lg, map_size_lg,
dimensions, dimensions,

View File

@ -24,7 +24,7 @@ use common::{
vol::{ReadVol, WriteVol}, vol::{ReadVol, WriteVol},
weather::{Weather, WeatherGrid}, weather::{Weather, WeatherGrid},
}; };
use common_base::span; use common_base::{prof_span, span};
use common_ecs::{PhysicsMetrics, SysMetrics}; use common_ecs::{PhysicsMetrics, SysMetrics};
use common_net::sync::{interpolation as sync_interp, WorldSyncExt}; use common_net::sync::{interpolation as sync_interp, WorldSyncExt};
use core::{convert::identity, time::Duration}; use core::{convert::identity, time::Duration};
@ -179,6 +179,7 @@ impl State {
map_size_lg: MapSizeLg, map_size_lg: MapSizeLg,
default_chunk: Arc<TerrainChunk>, default_chunk: Arc<TerrainChunk>,
) -> specs::World { ) -> specs::World {
prof_span!("State::setup_ecs_world");
let mut ecs = specs::World::new(); let mut ecs = specs::World::new();
// Uids for sync // Uids for sync
ecs.register_sync_marker(); ecs.register_sync_marker();

View File

@ -52,10 +52,10 @@ fn path_in_site(start: Vec2<i32>, end: Vec2<i32>, site: &site2::Site) -> PathRes
let heuristic = |tile: &Vec2<i32>, _: &Vec2<i32>| tile.as_::<f32>().distance(end.as_()); let heuristic = |tile: &Vec2<i32>, _: &Vec2<i32>| tile.as_::<f32>().distance(end.as_());
let mut astar = Astar::new(1000, start, BuildHasherDefault::<FxHasher64>::default()); let mut astar = Astar::new(1000, start, BuildHasherDefault::<FxHasher64>::default());
let transition = |a: &Vec2<i32>, b: &Vec2<i32>| { let transition = |a: Vec2<i32>, b: Vec2<i32>| {
let distance = a.as_::<f32>().distance(b.as_()); let distance = a.as_::<f32>().distance(b.as_());
let a_tile = site.tiles.get(*a); let a_tile = site.tiles.get(a);
let b_tile = site.tiles.get(*b); let b_tile = site.tiles.get(b);
let terrain = match &b_tile.kind { let terrain = match &b_tile.kind {
TileKind::Empty => 3.0, TileKind::Empty => 3.0,
@ -79,12 +79,12 @@ fn path_in_site(start: Vec2<i32>, end: Vec2<i32>, site: &site2::Site) -> PathRes
let building = if a_tile.is_building() && b_tile.is_road() { let building = if a_tile.is_building() && b_tile.is_road() {
a_tile a_tile
.plot .plot
.and_then(|plot| is_door_tile(plot, *a).then_some(1.0)) .and_then(|plot| is_door_tile(plot, a).then_some(1.0))
.unwrap_or(10000.0) .unwrap_or(10000.0)
} else if b_tile.is_building() && a_tile.is_road() { } else if b_tile.is_building() && a_tile.is_road() {
b_tile b_tile
.plot .plot
.and_then(|plot| is_door_tile(plot, *b).then_some(1.0)) .and_then(|plot| is_door_tile(plot, b).then_some(1.0))
.unwrap_or(10000.0) .unwrap_or(10000.0)
} else if (a_tile.is_building() || b_tile.is_building()) && a_tile.plot != b_tile.plot { } else if (a_tile.is_building() || b_tile.is_building()) && a_tile.plot != b_tile.plot {
10000.0 10000.0
@ -97,10 +97,13 @@ fn path_in_site(start: Vec2<i32>, end: Vec2<i32>, site: &site2::Site) -> PathRes
let neighbors = |tile: &Vec2<i32>| { let neighbors = |tile: &Vec2<i32>| {
let tile = *tile; let tile = *tile;
CARDINALS.iter().map(move |c| tile + *c) CARDINALS.iter().map(move |c| {
let n = tile + *c;
(n, transition(tile, n))
})
}; };
astar.poll(1000, heuristic, neighbors, transition, |tile| { astar.poll(1000, heuristic, neighbors, |tile| {
*tile == end || site.tiles.get_known(*tile).is_none() *tile == end || site.tiles.get_known(*tile).is_none()
}) })
} }
@ -135,17 +138,22 @@ fn path_between_sites(
let mut astar = Astar::new(250, start, BuildHasherDefault::<FxHasher64>::default()); let mut astar = Astar::new(250, start, BuildHasherDefault::<FxHasher64>::default());
let neighbors = |site: &Id<civ::Site>| world.civs().neighbors(*site); let transition = |a: Id<civ::Site>, b: Id<civ::Site>| {
let transition = |a: &Id<civ::Site>, b: &Id<civ::Site>| {
world world
.civs() .civs()
.track_between(*a, *b) .track_between(a, b)
.map(|(id, _)| world.civs().tracks.get(id).cost) .map(|(id, _)| world.civs().tracks.get(id).cost)
.unwrap_or(f32::INFINITY) .unwrap_or(f32::INFINITY)
}; };
let neighbors = |site: &Id<civ::Site>| {
let site = *site;
world
.civs()
.neighbors(site)
.map(move |n| (n, transition(n, site)))
};
let path = astar.poll(250, heuristic, neighbors, transition, |site| *site == end); let path = astar.poll(250, heuristic, neighbors, |site| *site == end);
path.map(|path| { path.map(|path| {
let path = path let path = path
@ -170,7 +178,7 @@ fn path_site(
let end = site.wpos_tile_pos(end.as_()); let end = site.wpos_tile_pos(end.as_());
let nodes = match path_in_site(start, end, site) { let nodes = match path_in_site(start, end, site) {
PathResult::Path(p) => p.nodes, PathResult::Path(p, _c) => p.nodes,
PathResult::Exhausted(p) => p.nodes, PathResult::Exhausted(p) => p.nodes,
PathResult::None(_) | PathResult::Pending => return None, PathResult::None(_) | PathResult::Pending => return None,
}; };
@ -198,7 +206,7 @@ fn path_towns(
path: p.nodes.into(), path: p.nodes.into(),
repoll: true, repoll: true,
}), }),
PathResult::Path(p) => Some(PathData { PathResult::Path(p, _c) => Some(PathData {
end, end,
path: p.nodes.into(), path: p.nodes.into(),
repoll: false, repoll: false,

View File

@ -83,6 +83,7 @@ use common::{
terrain::{TerrainChunk, TerrainChunkSize}, terrain::{TerrainChunk, TerrainChunkSize},
vol::RectRasterableVol, vol::RectRasterableVol,
}; };
use common_base::prof_span;
use common_ecs::run_now; use common_ecs::run_now;
use common_net::{ use common_net::{
msg::{ClientType, DisconnectReason, ServerGeneral, ServerInfo, ServerMsg}, msg::{ClientType, DisconnectReason, ServerGeneral, ServerInfo, ServerMsg},
@ -221,6 +222,7 @@ impl Server {
data_dir: &std::path::Path, data_dir: &std::path::Path,
runtime: Arc<Runtime>, runtime: Arc<Runtime>,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
prof_span!("Server::new");
info!("Server data dir is: {}", data_dir.display()); info!("Server data dir is: {}", data_dir.display());
if settings.auth_server_address.is_none() { if settings.auth_server_address.is_none() {
info!("Authentication is disabled"); info!("Authentication is disabled");
@ -282,6 +284,8 @@ impl Server {
default_chunk: Arc::new(world.generate_oob_chunk()), default_chunk: Arc::new(world.generate_oob_chunk()),
}; };
let lod = lod::Lod::from_world(&world, index.as_index_ref(), &pools);
let mut state = State::server( let mut state = State::server(
pools, pools,
world.sim().map_size_lg(), world.sim().map_size_lg(),
@ -451,9 +455,7 @@ impl Server {
// Insert the world into the ECS (todo: Maybe not an Arc?) // Insert the world into the ECS (todo: Maybe not an Arc?)
let world = Arc::new(world); let world = Arc::new(world);
state.ecs_mut().insert(Arc::clone(&world)); state.ecs_mut().insert(Arc::clone(&world));
state state.ecs_mut().insert(lod);
.ecs_mut()
.insert(lod::Lod::from_world(&world, index.as_index_ref()));
state.ecs_mut().insert(index.clone()); state.ecs_mut().insert(index.clone());
// Set starting time for the server. // Set starting time for the server.

View File

@ -17,19 +17,23 @@ pub struct Lod {
impl Lod { impl Lod {
#[cfg(feature = "worldgen")] #[cfg(feature = "worldgen")]
pub fn from_world(world: &World, index: IndexRef) -> Self { pub fn from_world(world: &World, index: IndexRef, threadpool: &rayon::ThreadPool) -> Self {
let mut zones = HashMap::new(); common_base::prof_span!("Lod::from_world");
threadpool.install(|| {
let zone_sz = (world.sim().get_size() + lod::ZONE_SIZE - 1) / lod::ZONE_SIZE;
let zone_sz = (world.sim().get_size() + lod::ZONE_SIZE - 1) / lod::ZONE_SIZE; use rayon::prelude::*;
let zones = (0..zone_sz.x)
.into_par_iter()
.flat_map(|i| (0..zone_sz.y).into_par_iter().map(move |j| (i, j)))
.map(|(i, j)| {
let zone_pos = Vec2::new(i, j).map(|e| e as i32);
(zone_pos, world.get_lod_zone(zone_pos, index))
})
.collect();
for i in 0..zone_sz.x { Self { zones }
for j in 0..zone_sz.y { })
let zone_pos = Vec2::new(i, j).map(|e| e as i32);
zones.insert(zone_pos, world.get_lod_zone(zone_pos, index));
}
}
Self { zones }
} }
#[cfg(not(feature = "worldgen"))] #[cfg(not(feature = "worldgen"))]

View File

@ -72,6 +72,9 @@ name = "tree"
name = "chunk_compression_benchmarks" name = "chunk_compression_benchmarks"
required-features = ["bin_compression"] required-features = ["bin_compression"]
[[example]]
name = "world_generate_time"
[[example]] [[example]]
name = "world_block_statistics" name = "world_block_statistics"
required-features = ["bin_compression"] required-features = ["bin_compression"]

View File

@ -0,0 +1,23 @@
use std::time::Instant;
use veloren_world::{
sim::{FileOpts, WorldOpts, DEFAULT_WORLD_MAP},
World,
};
fn main() {
let threadpool = rayon::ThreadPoolBuilder::new().build().unwrap();
let start = Instant::now();
let (world, index) = World::generate(
0,
WorldOpts {
seed_elements: true,
// Load default map from assets.
world_file: FileOpts::LoadAsset(DEFAULT_WORLD_MAP.into()),
calendar: None,
},
&threadpool,
);
core::hint::black_box((world, index));
println!("{} ms", start.elapsed().as_nanos() / 1_000_000);
}

View File

@ -21,6 +21,7 @@ use common::{
}, },
vol::RectVolSize, vol::RectVolSize,
}; };
use common_base::prof_span;
use core::{fmt, hash::BuildHasherDefault, ops::Range}; use core::{fmt, hash::BuildHasherDefault, ops::Range};
use fxhash::FxHasher64; use fxhash::FxHasher64;
use rand::prelude::*; use rand::prelude::*;
@ -160,7 +161,7 @@ impl<'a, R: Rng> GenCtx<'a, R> {
impl Civs { impl Civs {
pub fn generate(seed: u32, sim: &mut WorldSim, index: &mut Index) -> Self { pub fn generate(seed: u32, sim: &mut WorldSim, index: &mut Index) -> Self {
common_base::prof_span!("Civs::generate"); prof_span!("Civs::generate");
let mut this = Self::default(); let mut this = Self::default();
let rng = ChaChaRng::from_seed(seed_expan::rng_state(seed)); let rng = ChaChaRng::from_seed(seed_expan::rng_state(seed));
let name_rng = rng.clone(); let name_rng = rng.clone();
@ -181,14 +182,18 @@ impl Civs {
// this.generate_caves(&mut ctx); // this.generate_caves(&mut ctx);
info!("starting civilisation creation"); info!("starting civilisation creation");
prof_span!(guard, "create civs");
for _ in 0..initial_civ_count { for _ in 0..initial_civ_count {
prof_span!("create civ");
debug!("Creating civilisation..."); debug!("Creating civilisation...");
if this.birth_civ(&mut ctx.reseed()).is_none() { if this.birth_civ(&mut ctx.reseed()).is_none() {
warn!("Failed to find starting site for civilisation."); warn!("Failed to find starting site for civilisation.");
} }
} }
drop(guard);
info!(?initial_civ_count, "all civilisations created"); info!(?initial_civ_count, "all civilisations created");
prof_span!(guard, "find locations and establish sites");
for _ in 0..initial_civ_count * 3 { for _ in 0..initial_civ_count * 3 {
attempt(5, || { attempt(5, || {
let (loc, kind) = match ctx.rng.gen_range(0..64) { let (loc, kind) = match ctx.rng.gen_range(0..64) {
@ -260,11 +265,13 @@ impl Civs {
})) }))
}); });
} }
drop(guard);
// Tick // Tick
//=== old economy is gone //=== old economy is gone
// Flatten ground around sites // Flatten ground around sites
prof_span!(guard, "Flatten ground around sites");
for site in this.sites.values() { for site in this.sites.values() {
let wpos = site.center * TerrainChunkSize::RECT_SIZE.map(|e: u32| e as i32); let wpos = site.center * TerrainChunkSize::RECT_SIZE.map(|e: u32| e as i32);
@ -329,8 +336,10 @@ impl Civs {
} }
} }
} }
drop(guard);
// Place sites in world // Place sites in world
prof_span!(guard, "Place sites in world");
let mut cnt = 0; let mut cnt = 0;
for sim_site in this.sites.values_mut() { for sim_site in this.sites.values_mut() {
cnt += 1; cnt += 1;
@ -426,6 +435,7 @@ impl Civs {
} }
debug!(?sim_site.center, "Placed site at location"); debug!(?sim_site.center, "Placed site at location");
} }
drop(guard);
info!(?cnt, "all sites placed"); info!(?cnt, "all sites placed");
//this.display_info(); //this.display_info();
@ -455,23 +465,25 @@ impl Civs {
} }
} }
// TODO: this looks optimizable
// collect natural resources // collect natural resources
prof_span!(guard, "collect natural resources");
let sites = &mut index.sites; let sites = &mut index.sites;
(0..ctx.sim.map_size_lg().chunks_len()) (0..ctx.sim.map_size_lg().chunks_len()).for_each(|posi| {
.into_iter() let chpos = uniform_idx_as_vec2(ctx.sim.map_size_lg(), posi);
.for_each(|posi| { let wpos = chpos.map(|e| e as i64) * TerrainChunkSize::RECT_SIZE.map(|e| e as i64);
let chpos = uniform_idx_as_vec2(ctx.sim.map_size_lg(), posi); let closest_site = (*sites)
let wpos = chpos.map(|e| e as i64) * TerrainChunkSize::RECT_SIZE.map(|e| e as i64); .iter_mut()
let closest_site = (*sites) .filter(|s| !matches!(s.1.kind, crate::site::SiteKind::Dungeon(_)))
.iter_mut() .min_by_key(|(_id, s)| s.get_origin().map(|e| e as i64).distance_squared(wpos));
.filter(|s| !matches!(s.1.kind, crate::site::SiteKind::Dungeon(_))) if let Some((_id, s)) = closest_site {
.min_by_key(|(_id, s)| s.get_origin().map(|e| e as i64).distance_squared(wpos)); let distance_squared = s.get_origin().map(|e| e as i64).distance_squared(wpos);
if let Some((_id, s)) = closest_site { s.economy
let distance_squared = s.get_origin().map(|e| e as i64).distance_squared(wpos); .add_chunk(ctx.sim.get(chpos).unwrap(), distance_squared);
s.economy }
.add_chunk(ctx.sim.get(chpos).unwrap(), distance_squared); });
} drop(guard);
});
sites sites
.iter_mut() .iter_mut()
.for_each(|(_, s)| s.economy.cache_economy()); .for_each(|(_, s)| s.economy.cache_economy());
@ -671,9 +683,12 @@ impl Civs {
.distance_squared(self.sites.get(b).center) as f32) .distance_squared(self.sites.get(b).center) as f32)
.sqrt() .sqrt()
}; };
let neighbors = |p: &Id<Site>| self.neighbors(*p); let transition =
let transition = |a: &Id<Site>, b: &Id<Site>| { |a: Id<Site>, b: Id<Site>| self.tracks.get(self.track_between(a, b).unwrap().0).cost;
self.tracks.get(self.track_between(*a, *b).unwrap().0).cost let neighbors = |p: &Id<Site>| {
let p = *p;
self.neighbors(p)
.map(move |neighbor| (neighbor, transition(p, neighbor)))
}; };
let satisfied = |p: &Id<Site>| *p == b; let satisfied = |p: &Id<Site>| *p == b;
// We use this hasher (FxHasher64) because // We use this hasher (FxHasher64) because
@ -681,10 +696,7 @@ impl Civs {
// (2) we care about determinism across computers (ruling out AAHash); // (2) we care about determinism across computers (ruling out AAHash);
// (3) we have 8-byte keys (for which FxHash is fastest). // (3) we have 8-byte keys (for which FxHash is fastest).
let mut astar = Astar::new(100, a, BuildHasherDefault::<FxHasher64>::default()); let mut astar = Astar::new(100, a, BuildHasherDefault::<FxHasher64>::default());
astar astar.poll(100, heuristic, neighbors, satisfied).into_path()
.poll(100, heuristic, neighbors, transition, satisfied)
.into_path()
.and_then(|path| astar.get_cheapest_cost().map(|cost| (path, cost)))
} }
fn birth_civ(&mut self, ctx: &mut GenCtx<impl Rng>) -> Option<Id<Civ>> { fn birth_civ(&mut self, ctx: &mut GenCtx<impl Rng>) -> Option<Id<Civ>> {
@ -730,7 +742,7 @@ impl Civs {
/// Adds lake POIs and names them /// Adds lake POIs and names them
fn name_biomes(&mut self, ctx: &mut GenCtx<impl Rng>) { fn name_biomes(&mut self, ctx: &mut GenCtx<impl Rng>) {
common_base::prof_span!("name_biomes"); prof_span!("name_biomes");
let map_size_lg = ctx.sim.map_size_lg(); let map_size_lg = ctx.sim.map_size_lg();
let world_size = map_size_lg.chunks(); let world_size = map_size_lg.chunks();
let mut biomes: Vec<(common::terrain::BiomeKind, Vec<usize>)> = Vec::new(); let mut biomes: Vec<(common::terrain::BiomeKind, Vec<usize>)> = Vec::new();
@ -769,7 +781,7 @@ impl Civs {
biomes.push((biome, filled)); biomes.push((biome, filled));
} }
common_base::prof_span!("after flood fill"); prof_span!("after flood fill");
let mut biome_count = 0; let mut biome_count = 0;
for biome in biomes { for biome in biomes {
let name = match biome.0 { let name = match biome.0 {
@ -1013,7 +1025,7 @@ impl Civs {
/// Adds mountain POIs and name them /// Adds mountain POIs and name them
fn name_peaks(&mut self, ctx: &mut GenCtx<impl Rng>) { fn name_peaks(&mut self, ctx: &mut GenCtx<impl Rng>) {
common_base::prof_span!("name_peaks"); prof_span!("name_peaks");
let map_size_lg = ctx.sim.map_size_lg(); let map_size_lg = ctx.sim.map_size_lg();
const MIN_MOUNTAIN_ALT: f32 = 600.0; const MIN_MOUNTAIN_ALT: f32 = 600.0;
const MIN_MOUNTAIN_CHAOS: f32 = 0.35; const MIN_MOUNTAIN_CHAOS: f32 = 0.35;
@ -1093,6 +1105,7 @@ impl Civs {
loc: Vec2<i32>, loc: Vec2<i32>,
site_fn: impl FnOnce(Id<Place>) -> Site, site_fn: impl FnOnce(Id<Place>) -> Site,
) -> Id<Site> { ) -> Id<Site> {
prof_span!("establish_site");
const SITE_AREA: Range<usize> = 1..4; //64..256; const SITE_AREA: Range<usize> = 1..4; //64..256;
fn establish_site( fn establish_site(
@ -1112,10 +1125,17 @@ impl Civs {
let site = establish_site(self, ctx, loc, site_fn); let site = establish_site(self, ctx, loc, site_fn);
// Find neighbors // Find neighbors
const MAX_NEIGHBOR_DISTANCE: f32 = 2000.0; // Note, the maximum distance that I have so far observed not hitting the
// iteration limit in `find_path` is 364. So I think this is a reasonable
// limit (although the relationship between distance and pathfinding iterations
// can be a bit variable). Note, I have seen paths reach the iteration limit
// with distances as small as 137, so this certainly doesn't catch all
// cases that would fail.
const MAX_NEIGHBOR_DISTANCE: f32 = 400.0;
let mut nearby = self let mut nearby = self
.sites .sites
.iter() .iter()
.filter(|&(id, _)| id != site)
.filter(|(_, p)| { .filter(|(_, p)| {
matches!( matches!(
p.kind, p.kind,
@ -1139,99 +1159,93 @@ impl Civs {
| SiteKind::DesertCity | SiteKind::DesertCity
| SiteKind::Castle = self.sites[site].kind | SiteKind::Castle = self.sites[site].kind
{ {
for (nearby, _) in nearby.into_iter().take(5) { for (nearby, _) in nearby.into_iter().take(4) {
// Find a novel path prof_span!("for nearby");
if let Some((path, cost)) = find_path( // Find a route using existing paths
ctx, //
|start| self.bridges.get(&start).map(|(end, _)| *end), // If the novel path isn't efficient compared to this, don't use it
loc, let max_novel_cost = self
self.sites.get(nearby).center, .route_between(site, nearby)
) { .map_or(f32::MAX, |(_, route_cost)| route_cost / 3.0);
// Find a path using existing paths
if self
.route_between(site, nearby)
// If the novel path isn't efficient compared to existing routes, don't use it
.filter(|(_, route_cost)| *route_cost < cost * 3.0)
.is_none()
{
// Write the track to the world as a path
for locs in path.nodes().windows(3) {
let mut randomize_offset = false;
if let Some((i, _)) = NEIGHBORS
.iter()
.enumerate()
.find(|(_, dir)| **dir == locs[0] - locs[1])
{
ctx.sim.get_mut(locs[0]).unwrap().path.0.neighbors |=
1 << ((i as u8 + 4) % 8);
ctx.sim.get_mut(locs[1]).unwrap().path.0.neighbors |=
1 << (i as u8);
randomize_offset = true;
}
if let Some((i, _)) = NEIGHBORS
.iter()
.enumerate()
.find(|(_, dir)| **dir == locs[2] - locs[1])
{
ctx.sim.get_mut(locs[2]).unwrap().path.0.neighbors |=
1 << ((i as u8 + 4) % 8);
ctx.sim.get_mut(locs[1]).unwrap().path.0.neighbors |=
1 << (i as u8);
randomize_offset = true;
} else if !self.bridges.contains_key(&locs[1]) {
let center = (locs[1] + locs[2]) / 2;
let id =
establish_site(self, &mut ctx.reseed(), center, move |place| {
Site {
kind: SiteKind::Bridge(locs[1], locs[2]),
site_tmp: None,
center,
place,
}
});
self.bridges.insert(locs[1], (locs[2], id));
self.bridges.insert(locs[2], (locs[1], id));
}
/*
let to_prev_idx = NEIGHBORS
.iter()
.enumerate()
.find(|(_, dir)| **dir == (locs[0] - locs[1]).map(|e| e.signum()))
.expect("Track locations must be neighbors")
.0;
let to_next_idx = NEIGHBORS
.iter()
.enumerate()
.find(|(_, dir)| **dir == (locs[2] - locs[1]).map(|e| e.signum()))
.expect("Track locations must be neighbors")
.0;
let start = loc;
let end = self.sites.get(nearby).center;
// Find a novel path.
let get_bridge = |start| self.bridges.get(&start).map(|(end, _)| *end);
if let Some((path, cost)) = find_path(ctx, get_bridge, start, end, max_novel_cost) {
// Write the track to the world as a path
for locs in path.nodes().windows(3) {
let mut randomize_offset = false;
if let Some((i, _)) = NEIGHBORS
.iter()
.enumerate()
.find(|(_, dir)| **dir == locs[0] - locs[1])
{
ctx.sim.get_mut(locs[0]).unwrap().path.0.neighbors |= ctx.sim.get_mut(locs[0]).unwrap().path.0.neighbors |=
1 << ((to_prev_idx as u8 + 4) % 8); 1 << ((i as u8 + 4) % 8);
ctx.sim.get_mut(locs[2]).unwrap().path.0.neighbors |= ctx.sim.get_mut(locs[1]).unwrap().path.0.neighbors |= 1 << (i as u8);
1 << ((to_next_idx as u8 + 4) % 8); randomize_offset = true;
let mut chunk = ctx.sim.get_mut(locs[1]).unwrap();
chunk.path.0.neighbors |=
(1 << (to_prev_idx as u8)) | (1 << (to_next_idx as u8));
*/
if randomize_offset {
let mut chunk = ctx.sim.get_mut(locs[1]).unwrap();
chunk.path.0.offset = Vec2::new(
ctx.rng.gen_range(-16..17),
ctx.rng.gen_range(-16..17),
);
}
} }
// Take note of the track if let Some((i, _)) = NEIGHBORS
let track = self.tracks.insert(Track { cost, path }); .iter()
self.track_map .enumerate()
.entry(site) .find(|(_, dir)| **dir == locs[2] - locs[1])
.or_default() {
.insert(nearby, track); ctx.sim.get_mut(locs[2]).unwrap().path.0.neighbors |=
1 << ((i as u8 + 4) % 8);
ctx.sim.get_mut(locs[1]).unwrap().path.0.neighbors |= 1 << (i as u8);
randomize_offset = true;
} else if !self.bridges.contains_key(&locs[1]) {
let center = (locs[1] + locs[2]) / 2;
let id =
establish_site(self, &mut ctx.reseed(), center, move |place| {
Site {
kind: SiteKind::Bridge(locs[1], locs[2]),
site_tmp: None,
center,
place,
}
});
self.bridges.insert(locs[1], (locs[2], id));
self.bridges.insert(locs[2], (locs[1], id));
}
/*
let to_prev_idx = NEIGHBORS
.iter()
.enumerate()
.find(|(_, dir)| **dir == (locs[0] - locs[1]).map(|e| e.signum()))
.expect("Track locations must be neighbors")
.0;
let to_next_idx = NEIGHBORS
.iter()
.enumerate()
.find(|(_, dir)| **dir == (locs[2] - locs[1]).map(|e| e.signum()))
.expect("Track locations must be neighbors")
.0;
ctx.sim.get_mut(locs[0]).unwrap().path.0.neighbors |=
1 << ((to_prev_idx as u8 + 4) % 8);
ctx.sim.get_mut(locs[2]).unwrap().path.0.neighbors |=
1 << ((to_next_idx as u8 + 4) % 8);
let mut chunk = ctx.sim.get_mut(locs[1]).unwrap();
chunk.path.0.neighbors |=
(1 << (to_prev_idx as u8)) | (1 << (to_next_idx as u8));
*/
if randomize_offset {
let mut chunk = ctx.sim.get_mut(locs[1]).unwrap();
chunk.path.0.offset =
Vec2::new(ctx.rng.gen_range(-16..17), ctx.rng.gen_range(-16..17));
}
} }
// Take note of the track
let track = self.tracks.insert(Track { cost, path });
self.track_map
.entry(site)
.or_default()
.insert(nearby, track);
} }
} }
} }
@ -1302,21 +1316,25 @@ fn find_path(
get_bridge: impl Fn(Vec2<i32>) -> Option<Vec2<i32>>, get_bridge: impl Fn(Vec2<i32>) -> Option<Vec2<i32>>,
a: Vec2<i32>, a: Vec2<i32>,
b: Vec2<i32>, b: Vec2<i32>,
max_path_cost: f32,
) -> Option<(Path<Vec2<i32>>, f32)> { ) -> Option<(Path<Vec2<i32>>, f32)> {
prof_span!("find_path");
const MAX_PATH_ITERS: usize = 100_000; const MAX_PATH_ITERS: usize = 100_000;
let sim = &ctx.sim; let sim = &ctx.sim;
// NOTE: If heuristic overestimates the actual cost, then A* is not guaranteed
// to produce the least-cost path (since it will explore partially based on
// the heuristic).
// TODO: heuristic can be larger than actual cost, since existing bridges cost
// 1.0 (after the 1.0 that is added to everthting), but they can cover
// multiple chunks.
let heuristic = move |l: &Vec2<i32>, _: &Vec2<i32>| (l.distance_squared(b) as f32).sqrt(); let heuristic = move |l: &Vec2<i32>, _: &Vec2<i32>| (l.distance_squared(b) as f32).sqrt();
let get_bridge = &get_bridge;
let neighbors = |l: &Vec2<i32>| { let neighbors = |l: &Vec2<i32>| {
let l = *l; let l = *l;
NEIGHBORS let bridge = get_bridge(l);
.iter() let potential = walk_in_all_dirs(sim, bridge, l);
.filter_map(move |dir| walk_in_dir(sim, get_bridge, l, *dir)) potential
.map(move |(p, _)| p) .into_iter()
}; .filter_map(|p| p.map(|(node, cost)| (node, cost + 1.0)))
let transition = |a: &Vec2<i32>, b: &Vec2<i32>| {
1.0 + walk_in_dir(sim, get_bridge, *a, (*b - *a).map(|e| e.signum()))
.map_or(10000.0, |(_, cost)| cost)
}; };
let satisfied = |l: &Vec2<i32>| *l == b; let satisfied = |l: &Vec2<i32>| *l == b;
// We use this hasher (FxHasher64) because // We use this hasher (FxHasher64) because
@ -1327,73 +1345,103 @@ fn find_path(
MAX_PATH_ITERS, MAX_PATH_ITERS,
a, a,
BuildHasherDefault::<FxHasher64>::default(), BuildHasherDefault::<FxHasher64>::default(),
); )
.with_max_cost(max_path_cost);
astar astar
.poll(MAX_PATH_ITERS, heuristic, neighbors, transition, satisfied) .poll(MAX_PATH_ITERS, heuristic, neighbors, satisfied)
.into_path() .into_path()
.and_then(|path| astar.get_cheapest_cost().map(|cost| (path, cost)))
} }
/// Return Some if travel between a location and a chunk next to it is permitted /// Return Some if travel between a location and a chunk next to it is permitted
/// If permitted, the approximate relative const of traversal is given /// If permitted, the approximate relative const of traversal is given
// (TODO: by whom?) // (TODO: by whom?)
fn walk_in_dir( /// Return tuple: (final location, cost)
///
/// For efficiency, this computes for all 8 directions at once.
fn walk_in_all_dirs(
sim: &WorldSim, sim: &WorldSim,
get_bridge: impl Fn(Vec2<i32>) -> Option<Vec2<i32>>, bridge: Option<Vec2<i32>>,
a: Vec2<i32>, a: Vec2<i32>,
dir: Vec2<i32>, ) -> [Option<(Vec2<i32>, f32)>; 8] {
) -> Option<(Vec2<i32>, f32)> { let mut potential = [None; 8];
if let Some(p) = get_bridge(a).filter(|p| (p - a).map(|e| e.signum()) == dir) {
// Traversing an existing bridge has no cost. let adjacents = NEIGHBORS.map(|dir| a + dir);
Some((p, 0.0))
} else if loc_suitable_for_walking(sim, a + dir) { let Some(a_chunk) = sim.get(a) else { return potential };
let a_chunk = sim.get(a)?; let mut chunks = [None; 8];
let b_chunk = sim.get(a + dir)?; for i in 0..8 {
if loc_suitable_for_walking(sim, adjacents[i]) {
chunks[i] = sim.get(adjacents[i]);
}
}
for i in 0..8 {
let Some(b_chunk) = chunks[i] else { continue };
let hill_cost = ((b_chunk.alt - a_chunk.alt).abs() / 5.0).powi(2); let hill_cost = ((b_chunk.alt - a_chunk.alt).abs() / 5.0).powi(2);
let water_cost = (b_chunk.water_alt - b_chunk.alt + 8.0).clamped(0.0, 8.0) * 3.0; // Try not to path swamps / tidal areas let water_cost = (b_chunk.water_alt - b_chunk.alt + 8.0).clamped(0.0, 8.0) * 3.0; // Try not to path swamps / tidal areas
let wild_cost = if b_chunk.path.0.is_way() { let wild_cost = if b_chunk.path.0.is_way() {
0.0 // Traversing existing paths has no additional cost! 0.0 // Traversing existing paths has no additional cost!
} else { } else {
3.0 // + (1.0 - b_chunk.tree_density) * 20.0 // Prefer going through forests, for aesthetics 3.0 // + (1.0 - b_chunk.tree_density) * 20.0 // Prefer going through forests, for aesthetics
}; };
Some((a + dir, 1.0 + hill_cost + water_cost + wild_cost))
} else if dir.x == 0 || dir.y == 0 { let cost = 1.0 + hill_cost + water_cost + wild_cost;
(4..=5).find_map(|i| { potential[i] = Some((adjacents[i], cost));
loc_suitable_for_walking(sim, a + dir * i)
.then(|| (a + dir * i, 120.0 + (i - 4) as f32 * 10.0))
})
} else {
None
} }
// Look for potential bridge spots in the cardinal directions if
// `loc_suitable_for_wallking` was false for the adjacent chunk.
for (i, &dir) in NEIGHBORS.iter().enumerate() {
let is_cardinal_dir = dir.x == 0 || dir.y == 0;
if is_cardinal_dir && potential[i].is_none() {
// if we can skip over unsuitable area with a bridge
potential[i] = (4..=5).find_map(|i| {
loc_suitable_for_walking(sim, a + dir * i)
.then(|| (a + dir * i, 120.0 + (i - 4) as f32 * 10.0))
});
}
}
// If current position is a bridge, skip to its destination.
if let Some(p) = bridge {
let dir = (p - a).map(|e| e.signum());
if let Some((dir_index, _)) = NEIGHBORS
.iter()
.enumerate()
.find(|(_, n_dir)| **n_dir == dir)
{
potential[dir_index] = Some((p, 0.0));
}
}
potential
} }
/// Return true if a position is suitable for walking on /// Return true if a position is suitable for walking on
fn loc_suitable_for_walking(sim: &WorldSim, loc: Vec2<i32>) -> bool { fn loc_suitable_for_walking(sim: &WorldSim, loc: Vec2<i32>) -> bool {
if sim.get(loc).is_some() { if sim.get(loc).is_some() {
!NEIGHBORS.iter().any(|n| { NEIGHBORS.iter().all(|n| {
sim.get(loc + *n) sim.get(loc + *n)
.map_or(false, |chunk| chunk.river.near_water()) .map_or(false, |chunk| !chunk.river.near_water())
}) })
} else { } else {
false false
} }
} }
/// Return true if a site could be constructed between a location and a chunk
/// next to it is permitted (TODO: by whom?)
fn site_in_dir(sim: &WorldSim, a: Vec2<i32>, dir: Vec2<i32>, site_kind: SiteKind) -> bool {
loc_suitable_for_site(sim, a, site_kind) && loc_suitable_for_site(sim, a + dir, site_kind)
}
/// Return true if a position is suitable for site construction (TODO: /// Return true if a position is suitable for site construction (TODO:
/// criteria?) /// criteria?)
fn loc_suitable_for_site(sim: &WorldSim, loc: Vec2<i32>, site_kind: SiteKind) -> bool { fn loc_suitable_for_site(
sim: &WorldSim,
loc: Vec2<i32>,
site_kind: SiteKind,
is_suitable_loc: bool,
) -> bool {
fn check_chunk_occupation(sim: &WorldSim, loc: Vec2<i32>, radius: i32) -> bool { fn check_chunk_occupation(sim: &WorldSim, loc: Vec2<i32>, radius: i32) -> bool {
for x in (-radius)..radius { for x in (-radius)..radius {
for y in (-radius)..radius { for y in (-radius)..radius {
let check_loc = loc + Vec2::new(x, y).cpos_to_wpos(); let check_loc = loc + Vec2::new(x, y);
if sim.get(check_loc).map_or(false, |c| !c.sites.is_empty()) { if sim.get(check_loc).map_or(false, |c| !c.sites.is_empty()) {
return false; return false;
} }
@ -1401,8 +1449,9 @@ fn loc_suitable_for_site(sim: &WorldSim, loc: Vec2<i32>, site_kind: SiteKind) ->
} }
true true
} }
let not_occupied = check_chunk_occupation(sim, loc, site_kind.exclusion_radius()); let not_occupied = || check_chunk_occupation(sim, loc, site_kind.exclusion_radius());
site_kind.is_suitable_loc(loc, sim) && not_occupied // only check occupation if the location is suitable
is_suitable_loc && not_occupied()
} }
/// Attempt to search for a location that's suitable for site construction /// Attempt to search for a location that's suitable for site construction
@ -1411,6 +1460,7 @@ fn find_site_loc(
proximity_reqs: &ProximityRequirements, proximity_reqs: &ProximityRequirements,
site_kind: SiteKind, site_kind: SiteKind,
) -> Option<Vec2<i32>> { ) -> Option<Vec2<i32>> {
prof_span!("find_site_loc");
const MAX_ATTEMPTS: usize = 10000; const MAX_ATTEMPTS: usize = 10000;
let mut loc = None; let mut loc = None;
for _ in 0..MAX_ATTEMPTS { for _ in 0..MAX_ATTEMPTS {
@ -1421,16 +1471,15 @@ fn find_site_loc(
) )
}); });
if proximity_reqs.satisfied_by(test_loc) { let is_suitable_loc = site_kind.is_suitable_loc(test_loc, ctx.sim);
if loc_suitable_for_site(ctx.sim, test_loc, site_kind) { if is_suitable_loc && proximity_reqs.satisfied_by(test_loc) {
if loc_suitable_for_site(ctx.sim, test_loc, site_kind, is_suitable_loc) {
return Some(test_loc); return Some(test_loc);
} }
loc = ctx.sim.get(test_loc).and_then(|c| { // If the current location is suitable and meets proximity requirements,
site_kind // try nearby spot downhill.
.is_suitable_loc(test_loc, ctx.sim) loc = ctx.sim.get(test_loc).and_then(|c| c.downhill);
.then_some(c.downhill?.wpos_to_cpos())
});
} }
} }

View File

@ -55,11 +55,11 @@ use common::{
}, },
vol::{ReadVol, RectVolSize, WriteVol}, vol::{ReadVol, RectVolSize, WriteVol},
}; };
use common_base::prof_span;
use common_net::msg::{world_msg, WorldMapMsg}; use common_net::msg::{world_msg, WorldMapMsg};
use enum_map::EnumMap; use enum_map::EnumMap;
use rand::{prelude::*, Rng}; use rand::{prelude::*, Rng};
use rand_chacha::ChaCha8Rng; use rand_chacha::ChaCha8Rng;
use rayon::iter::ParallelIterator;
use serde::Deserialize; use serde::Deserialize;
use std::time::Duration; use std::time::Duration;
use vek::*; use vek::*;
@ -110,6 +110,7 @@ impl World {
opts: sim::WorldOpts, opts: sim::WorldOpts,
threadpool: &rayon::ThreadPool, threadpool: &rayon::ThreadPool,
) -> (Self, IndexOwned) { ) -> (Self, IndexOwned) {
prof_span!("World::generate");
// NOTE: Generating index first in order to quickly fail if the color manifest // NOTE: Generating index first in order to quickly fail if the color manifest
// is broken. // is broken.
threadpool.install(|| { threadpool.install(|| {
@ -136,6 +137,7 @@ impl World {
} }
pub fn get_map_data(&self, index: IndexRef, threadpool: &rayon::ThreadPool) -> WorldMapMsg { pub fn get_map_data(&self, index: IndexRef, threadpool: &rayon::ThreadPool) -> WorldMapMsg {
prof_span!("World::get_map_data");
threadpool.install(|| { threadpool.install(|| {
// we need these numbers to create unique ids for cave ends // we need these numbers to create unique ids for cave ends
let num_sites = self.civs().sites().count() as u64; let num_sites = self.civs().sites().count() as u64;
@ -562,6 +564,7 @@ impl World {
let mut objects = Vec::new(); let mut objects = Vec::new();
// Add trees // Add trees
prof_span!(guard, "add trees");
objects.append( objects.append(
&mut self &mut self
.sim() .sim()
@ -600,6 +603,7 @@ impl World {
}) })
.collect(), .collect(),
); );
drop(guard);
// Add buildings // Add buildings
objects.extend( objects.extend(

View File

@ -51,6 +51,7 @@ use common::{
}, },
vol::RectVolSize, vol::RectVolSize,
}; };
use common_base::prof_span;
use common_net::msg::WorldMapMsg; use common_net::msg::WorldMapMsg;
use noise::{ use noise::{
BasicMulti, Billow, Fbm, HybridMulti, MultiFractal, NoiseFn, RangeFunction, RidgedMulti, BasicMulti, Billow, Fbm, HybridMulti, MultiFractal, NoiseFn, RangeFunction, RidgedMulti,
@ -668,6 +669,7 @@ pub struct WorldSim {
impl WorldSim { impl WorldSim {
pub fn generate(seed: u32, opts: WorldOpts, threadpool: &rayon::ThreadPool) -> Self { pub fn generate(seed: u32, opts: WorldOpts, threadpool: &rayon::ThreadPool) -> Self {
prof_span!("WorldSim::generate");
let calendar = opts.calendar; // separate lifetime of elements let calendar = opts.calendar; // separate lifetime of elements
let world_file = opts.world_file; let world_file = opts.world_file;
@ -1586,6 +1588,7 @@ impl WorldSim {
/// Draw a map of the world based on chunk information. Returns a buffer of /// Draw a map of the world based on chunk information. Returns a buffer of
/// u32s. /// u32s.
pub fn get_map(&self, index: IndexRef, calendar: Option<&Calendar>) -> WorldMapMsg { pub fn get_map(&self, index: IndexRef, calendar: Option<&Calendar>) -> WorldMapMsg {
prof_span!("WorldSim::get_map");
let mut map_config = MapConfig::orthographic( let mut map_config = MapConfig::orthographic(
self.map_size_lg(), self.map_size_lg(),
core::ops::RangeInclusive::new(CONFIG.sea_level, CONFIG.sea_level + self.max_height), core::ops::RangeInclusive::new(CONFIG.sea_level, CONFIG.sea_level + self.max_height),
@ -1603,6 +1606,7 @@ impl WorldSim {
}; };
let samples_data = { let samples_data = {
prof_span!("samples data");
let column_sample = ColumnGen::new(self); let column_sample = ColumnGen::new(self);
(0..self.map_size_lg().chunks_len()) (0..self.map_size_lg().chunks_len())
.into_par_iter() .into_par_iter()
@ -2293,10 +2297,10 @@ impl WorldSim {
&self, &self,
wpos_min: Vec2<i32>, wpos_min: Vec2<i32>,
wpos_max: Vec2<i32>, wpos_max: Vec2<i32>,
) -> impl ParallelIterator<Item = TreeAttr> + '_ { ) -> impl Iterator<Item = TreeAttr> + '_ {
self.gen_ctx self.gen_ctx
.structure_gen .structure_gen
.par_iter(wpos_min, wpos_max) .iter(wpos_min, wpos_max)
.filter_map(move |(wpos, seed)| { .filter_map(move |(wpos, seed)| {
let lottery = self.make_forest_lottery(wpos); let lottery = self.make_forest_lottery(wpos);
Some(TreeAttr { Some(TreeAttr {

View File

@ -3,6 +3,7 @@ use common::{
terrain::{neighbors, uniform_idx_as_vec2, vec2_as_uniform_idx, MapSizeLg, TerrainChunkSize}, terrain::{neighbors, uniform_idx_as_vec2, vec2_as_uniform_idx, MapSizeLg, TerrainChunkSize},
vol::RectVolSize, vol::RectVolSize,
}; };
use common_base::prof_span;
use noise::{MultiFractal, NoiseFn, Perlin, Seedable}; use noise::{MultiFractal, NoiseFn, Perlin, Seedable};
use num::Float; use num::Float;
use rayon::prelude::*; use rayon::prelude::*;
@ -374,6 +375,7 @@ pub fn get_horizon_map<F: Float + Sync, A: Send, H: Send>(
to_angle: impl Fn(F) -> A + Sync, to_angle: impl Fn(F) -> A + Sync,
to_height: impl Fn(F) -> H + Sync, to_height: impl Fn(F) -> H + Sync,
) -> Result<[HorizonMap<A, H>; 2], ()> { ) -> Result<[HorizonMap<A, H>; 2], ()> {
prof_span!("get_horizon_map");
if maxh < minh { if maxh < minh {
// maxh must be greater than minh // maxh must be greater than minh
return Err(()); return Err(());

View File

@ -1,3 +1,7 @@
use crate::{sim::WorldSim, site::economy::simulate_economy, Index}; use crate::{sim::WorldSim, site::economy::simulate_economy, Index};
use common_base::prof_span;
pub fn simulate(index: &mut Index, _world: &mut WorldSim) { simulate_economy(index); } pub fn simulate(index: &mut Index, _world: &mut WorldSim) {
prof_span!("sim2::simulate");
simulate_economy(index);
}

View File

@ -1347,15 +1347,19 @@ impl Land {
&self, &self,
origin: Vec2<i32>, origin: Vec2<i32>,
dest: Vec2<i32>, dest: Vec2<i32>,
mut path_cost_fn: impl FnMut(Option<&Tile>, Option<&Tile>) -> f32, path_cost_fn: impl Fn(Option<&Tile>, Option<&Tile>) -> f32,
) -> Option<Path<Vec2<i32>>> { ) -> Option<Path<Vec2<i32>>> {
let heuristic = |pos: &Vec2<i32>, _: &Vec2<i32>| (pos - dest).map(|e| e as f32).magnitude(); let heuristic = |pos: &Vec2<i32>, _: &Vec2<i32>| (pos - dest).map(|e| e as f32).magnitude();
let transition =
|from: Vec2<i32>, to: Vec2<i32>| path_cost_fn(self.tile_at(from), self.tile_at(to));
let neighbors = |pos: &Vec2<i32>| { let neighbors = |pos: &Vec2<i32>| {
let pos = *pos; let pos = *pos;
CARDINALS.iter().map(move |dir| pos + *dir) let transition = &transition;
CARDINALS.iter().map(move |dir| {
let to = pos + *dir;
(to, transition(pos, to))
})
}; };
let transition =
|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;
// We use this hasher (FxHasher64) because // We use this hasher (FxHasher64) because
@ -1363,8 +1367,9 @@ impl Land {
// (2) we don't care about determinism across computers (we could use AAHash); // (2) we don't care about determinism across computers (we could use AAHash);
// (3) we have 8-byte keys (for which FxHash is fastest). // (3) we have 8-byte keys (for which FxHash is fastest).
Astar::new(250, origin, BuildHasherDefault::<FxHasher64>::default()) Astar::new(250, origin, BuildHasherDefault::<FxHasher64>::default())
.poll(250, heuristic, neighbors, transition, satisfied) .poll(250, heuristic, neighbors, satisfied)
.into_path() .into_path()
.map(|(p, _c)| p)
} }
/// We use this hasher (FxHasher64) because /// We use this hasher (FxHasher64) because

View File

@ -160,18 +160,23 @@ impl Site {
} }
max_cost + (dir != old_dir) as i32 as f32 * 35.0 max_cost + (dir != old_dir) as i32 as f32 * 35.0
}; };
let path = Astar::new(MAX_ITERS, (a, Vec2::zero()), DefaultHashBuilder::default()) let (path, _cost) = Astar::new(MAX_ITERS, (a, Vec2::zero()), DefaultHashBuilder::default())
.poll( .poll(
MAX_ITERS, MAX_ITERS,
&heuristic, &heuristic,
|(tile, _)| { |(tile, _)| {
let tile = *tile; let tile = *tile;
CARDINALS.iter().map(move |dir| (tile + *dir, *dir)) let this = &self;
}, CARDINALS.iter().map(move |dir| {
|(a, _), (b, _)| { let neighbor = (tile + *dir, *dir);
let alt_a = land.get_alt_approx(self.tile_center_wpos(*a));
let alt_b = land.get_alt_approx(self.tile_center_wpos(*b)); // Transition cost
(alt_a - alt_b).abs() / TILE_SIZE as f32 let alt_a = land.get_alt_approx(this.tile_center_wpos(tile));
let alt_b = land.get_alt_approx(this.tile_center_wpos(neighbor.0));
let cost = (alt_a - alt_b).abs() / TILE_SIZE as f32;
(neighbor, cost)
})
}, },
|(tile, _)| *tile == b, |(tile, _)| *tile == b,
) )

View File

@ -545,31 +545,33 @@ impl Floor {
fn create_route(&mut self, _ctx: &mut GenCtx<impl Rng>, a: Vec2<i32>, b: Vec2<i32>) { fn create_route(&mut self, _ctx: &mut GenCtx<impl Rng>, a: Vec2<i32>, b: Vec2<i32>) {
let heuristic = let heuristic =
move |l: &Vec2<i32>, _: &Vec2<i32>| (l - b).map(|e| e.abs()).reduce_max() as f32; move |l: &Vec2<i32>, _: &Vec2<i32>| (l - b).map(|e| e.abs()).reduce_max() as f32;
let neighbors = |l: &Vec2<i32>| { let transition = |_a: Vec2<i32>, b: Vec2<i32>| match self.tiles.get(b) {
let l = *l;
CARDINALS
.iter()
.map(move |dir| l + dir)
.filter(|pos| self.tiles.get(*pos).is_some())
};
let transition = |_a: &Vec2<i32>, b: &Vec2<i32>| match self.tiles.get(*b) {
Some(Tile::Room(_)) | Some(Tile::Tunnel) => 1.0, Some(Tile::Room(_)) | Some(Tile::Tunnel) => 1.0,
Some(Tile::Solid) => 25.0, Some(Tile::Solid) => 25.0,
Some(Tile::UpStair(_, _)) | Some(Tile::DownStair(_)) => 0.0, Some(Tile::UpStair(_, _)) | Some(Tile::DownStair(_)) => 0.0,
_ => 100000.0, _ => 100000.0,
}; };
let neighbors = |l: &Vec2<i32>| {
let l = *l;
CARDINALS
.iter()
.map(move |dir| {
let dest = l + dir;
(dest, transition(l, dest))
})
.filter(|&(pos, _)| self.tiles.get(pos).is_some())
};
let satisfied = |l: &Vec2<i32>| *l == b; let satisfied = |l: &Vec2<i32>| *l == b;
// We use this hasher (FxHasher64) because // We use this hasher (FxHasher64) because
// (1) we don't care about DDOS attacks (ruling out SipHash); // (1) we don't care about DDOS attacks (ruling out SipHash);
// (2) we don't care about determinism across computers (we could use AAHash); // (2) we don't care about determinism across computers (we could use AAHash);
// (3) we have 8-byte keys (for which FxHash is fastest). // (3) we have 8-byte keys (for which FxHash is fastest).
let mut astar = Astar::new(20000, a, BuildHasherDefault::<FxHasher64>::default()); let mut astar = Astar::new(20000, a, BuildHasherDefault::<FxHasher64>::default());
let path = astar let (path, _cost) = astar
.poll( .poll(
FLOOR_SIZE.product() as usize + 1, FLOOR_SIZE.product() as usize + 1,
heuristic, heuristic,
neighbors, neighbors,
transition,
satisfied, satisfied,
) )
.into_path() .into_path()

View File

@ -23,13 +23,13 @@ pub use self::{
pub use common::grid::Grid; pub use common::grid::Grid;
use fxhash::FxHasher32; use fxhash::{FxHasher32, FxHasher64};
use hashbrown::{HashMap, HashSet}; use hashbrown::{HashMap, HashSet};
use std::hash::BuildHasherDefault; use std::hash::BuildHasherDefault;
use vek::*; use vek::*;
// Deterministic HashMap and HashSet // Deterministic HashMap and HashSet
pub type DHashMap<K, V> = HashMap<K, V, BuildHasherDefault<FxHasher32>>; pub type DHashMap<K, V> = HashMap<K, V, BuildHasherDefault<FxHasher64>>;
pub type DHashSet<T> = HashSet<T, BuildHasherDefault<FxHasher32>>; pub type DHashSet<T> = HashSet<T, BuildHasherDefault<FxHasher32>>;
pub fn attempt<T>(max_iters: usize, mut f: impl FnMut() -> Option<T>) -> Option<T> { pub fn attempt<T>(max_iters: usize, mut f: impl FnMut() -> Option<T>) -> Option<T> {

View File

@ -1,5 +1,4 @@
use super::{RandomField, Sampler}; use super::{RandomField, Sampler};
use rayon::prelude::*;
use vek::*; use vek::*;
#[derive(Clone)] #[derive(Clone)]
@ -69,11 +68,7 @@ impl StructureGen2d {
/// Note: Generates all possible closest samples for elements in the range /// Note: Generates all possible closest samples for elements in the range
/// of min to max, *exclusive.* /// of min to max, *exclusive.*
pub fn par_iter( pub fn iter(&self, min: Vec2<i32>, max: Vec2<i32>) -> impl Iterator<Item = StructureField> {
&self,
min: Vec2<i32>,
max: Vec2<i32>,
) -> impl ParallelIterator<Item = StructureField> {
let freq = self.freq; let freq = self.freq;
let spread = self.spread; let spread = self.spread;
let spread_mul = Self::spread_mul(spread); let spread_mul = Self::spread_mul(spread);
@ -102,7 +97,7 @@ impl StructureGen2d {
let x_field = self.x_field; let x_field = self.x_field;
let y_field = self.y_field; let y_field = self.y_field;
let seed_field = self.seed_field; let seed_field = self.seed_field;
(0..len).into_par_iter().map(move |xy| { (0..len).map(move |xy| {
let index = min_index + Vec2::new((xy % xlen as u64) as i32, (xy / xlen as u64) as i32); let index = min_index + Vec2::new((xy % xlen as u64) as i32, (xy / xlen as u64) as i32);
Self::index_to_sample_internal( Self::index_to_sample_internal(
freq, freq,