Merge branch 'zesterer/perf2' into 'master'

Various terrain-related performance improvements

See merge request veloren/veloren!3655
This commit is contained in:
Joshua Barretto 2022-10-12 14:46:13 +00:00
commit b677051ef8
12 changed files with 133 additions and 20 deletions

View File

@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- HQX upscaling shader for people playing on low internal resolutions
- Pets can now be traded with.
- Crafting recipe for black lantern
- Added redwood and dead trees
### Changed
- Use fluent for translations

1
Cargo.lock generated
View File

@ -6729,6 +6729,7 @@ dependencies = [
"ordered-float 2.10.0",
"petgraph 0.6.2",
"rand 0.8.5",
"rand_chacha 0.3.1",
"rayon",
"ron 0.7.1",
"roots",

View File

@ -82,6 +82,7 @@ specs = { version = "0.18", features = ["serde", "storage-event-control", "night
[dev-dependencies]
#bench
criterion = "0.3"
rand_chacha = "0.3"
#test
tracing-subscriber = { version = "0.3.7", default-features = false, features = ["fmt", "time", "ansi", "smallvec", "env-filter"] }

View File

@ -124,6 +124,27 @@ fn criterion_benchmark(c: &mut Criterion) {
}
})
});
c.bench_function("for_each_in", |b| {
use rand::prelude::*;
let mut rng = rand_chacha::ChaChaRng::seed_from_u64(thread_rng().gen());
b.iter(|| {
let pos = Vec3::new(
rng.gen_range(0..TerrainChunk::RECT_SIZE.x as i32 - 3),
rng.gen_range(0..TerrainChunk::RECT_SIZE.x as i32 - 3),
rng.gen_range(MIN_Z..MAX_Z - 6),
);
chunk.for_each_in(
Aabb {
min: pos,
max: pos + Vec3::new(3, 3, 6),
},
|pos, vox| {
black_box((pos, vox));
},
);
})
});
black_box(chunk);
}

View File

@ -181,6 +181,49 @@ impl<V, S: RectVolSize, M: Clone> ReadVol for Chonk<V, S, M> {
.map_err(Self::Error::SubChunkError)
}
}
#[inline(always)]
fn get_unchecked(&self, pos: Vec3<i32>) -> &V {
if pos.z < self.get_min_z() {
// Below the terrain
&self.below
} else if pos.z >= self.get_max_z() {
// Above the terrain
&self.above
} else {
// Within the terrain
let sub_chunk_idx = self.sub_chunk_idx(pos.z);
let rpos = pos
- Vec3::unit_z()
* (self.z_offset + sub_chunk_idx * SubChunkSize::<S>::SIZE.z as i32);
self.sub_chunks[sub_chunk_idx as usize]
.get_unchecked(rpos)
}
}
fn for_each_in(&self, aabb: Aabb<i32>, mut f: impl FnMut(Vec3<i32>, Self::Vox))
where
Self::Vox: Copy,
{
let idx = self.sub_chunk_idx(aabb.min.z);
// Special-case for the AABB being entirely within a single sub-chunk as this is very common.
if idx == self.sub_chunk_idx(aabb.max.z) && idx >= 0 && idx < self.sub_chunks.len() as i32 {
let sub_chunk = &self.sub_chunks[idx as usize];
let z_off = self.z_offset + idx * SubChunkSize::<S>::SIZE.z as i32;
sub_chunk.for_each_in(
Aabb { min: aabb.min.with_z(aabb.min.z - z_off), max: aabb.max.with_z(aabb.max.z - z_off) },
|pos, vox| f(pos.with_z(pos.z + z_off), vox),
);
} else {
for z in aabb.min.z..aabb.max.z + 1 {
for y in aabb.min.y..aabb.max.y + 1 {
for x in aabb.min.x..aabb.max.x + 1 {
f(Vec3::new(x, y, z), *self.get_unchecked(Vec3::new(x, y, z)));
}
}
}
}
}
}
impl<V: Clone + PartialEq, S: RectVolSize, M: Clone> WriteVol for Chonk<V, S, M> {

View File

@ -0,0 +1,20 @@
use std::hash::{Hasher, BuildHasher};
#[derive(Copy, Clone, Default)]
pub struct GridHasher(u64);
// It's extremely unlikely that the spatial grid can be used to viably DOS the server given that clients only have
// control over their player and a handful of entities in their near vicinity. For this reason, we just use an xor
// hash, which should keep collisions relatively low since the spatial coherence of the grid is distributed fairly
// evenly with the output of the hash function.
impl Hasher for GridHasher {
fn finish(&self) -> u64 { self.0 }
fn write(&mut self, _: &[u8]) { panic!("Hashing arbitrary bytes is unimplemented"); }
fn write_i32(&mut self, x: i32) { self.0 = self.0.wrapping_mul(113989) ^ self.0 ^ x as u64; }
}
impl BuildHasher for GridHasher {
type Hasher = Self;
fn build_hasher(&self) -> Self::Hasher { *self }
}

View File

@ -7,6 +7,7 @@ pub mod projection;
/// Contains [`SpatialGrid`] which is useful for accelerating queries of nearby
/// entities
mod spatial_grid;
mod grid_hasher;
pub const GIT_VERSION_BUILD: &str = include_str!(concat!(env!("OUT_DIR"), "/githash"));
pub const GIT_TAG_BUILD: &str = include_str!(concat!(env!("OUT_DIR"), "/gittag"));
@ -39,3 +40,4 @@ pub use option::either_with;
pub use plane::Plane;
pub use projection::Projection;
pub use spatial_grid::SpatialGrid;
pub use grid_hasher::GridHasher;

View File

@ -1,11 +1,12 @@
use vek::*;
use super::GridHasher;
#[derive(Debug)]
pub struct SpatialGrid {
// Uses two scales of grids so that we can have a hard limit on how far to search in the
// smaller grid
grid: hashbrown::HashMap<Vec2<i32>, Vec<specs::Entity>>,
large_grid: hashbrown::HashMap<Vec2<i32>, Vec<specs::Entity>>,
grid: hashbrown::HashMap<Vec2<i32>, Vec<specs::Entity>, GridHasher>,
large_grid: hashbrown::HashMap<Vec2<i32>, Vec<specs::Entity>, GridHasher>,
// Log base 2 of the cell size of the spatial grid
lg2_cell_size: usize,
// Log base 2 of the cell size of the large spatial grid
@ -53,7 +54,7 @@ impl SpatialGrid {
/// NOTE: for best optimization of the iterator use
/// `for_each` rather than a for loop.
pub fn in_aabr<'a>(&'a self, aabr: Aabr<i32>) -> impl Iterator<Item = specs::Entity> + 'a {
let iter = |max_entity_radius, grid: &'a hashbrown::HashMap<_, _>, lg2_cell_size| {
let iter = |max_entity_radius, grid: &'a hashbrown::HashMap<_, _, _>, lg2_cell_size| {
// Add buffer for other entity radius
let min = aabr.min - max_entity_radius as i32;
let max = aabr.max + max_entity_radius as i32;

View File

@ -98,6 +98,10 @@ pub trait ReadVol: BaseVol {
/// Get a reference to the voxel at the provided position in the volume.
fn get(&self, pos: Vec3<i32>) -> Result<&Self::Vox, Self::Error>;
/// Get a reference to the voxel at the provided position in the volume. Many volumes provide a fast path,
/// provided the position is always in-bounds. Note that this function is still safe.
fn get_unchecked(&self, pos: Vec3<i32>) -> &Self::Vox { self.get(pos).unwrap() }
/// NOTE: By default, this ray will simply run from `from` to `to` without
/// stopping. To make something interesting happen, call `until` or
/// `for_each`.

View File

@ -307,6 +307,28 @@ impl<V, S: VolSize, M> ReadVol for Chunk<V, S, M> {
Ok(self.get_unchecked(pos))
}
}
#[inline(always)]
fn get_unchecked(&self, pos: Vec3<i32>) -> &Self::Vox {
self.get_unchecked(pos)
}
fn for_each_in(&self, mut aabb: Aabb<i32>, mut f: impl FnMut(Vec3<i32>, Self::Vox))
where
Self::Vox: Copy,
{
aabb.intersect(Aabb {
min: Vec3::zero(),
max: S::SIZE.map(|e| e as i32) - 1,
});
for z in aabb.min.z..aabb.max.z + 1 {
for y in aabb.min.y..aabb.max.y + 1 {
for x in aabb.min.x..aabb.max.x + 1 {
f(Vec3::new(x, y, z), *self.get_unchecked(Vec3::new(x, y, z)));
}
}
}
}
}
impl<V: Clone + PartialEq, S: VolSize, M> WriteVol for Chunk<V, S, M> {

View File

@ -2,6 +2,7 @@ use crate::{
terrain::MapSizeLg,
vol::{BaseVol, ReadVol, RectRasterableVol, SampleVol, WriteVol},
volumes::dyna::DynaError,
util::GridHasher,
};
use hashbrown::{hash_map, HashMap};
use std::{fmt::Debug, ops::Deref, sync::Arc};
@ -24,7 +25,7 @@ pub struct VolGrid2d<V: RectRasterableVol> {
map_size_lg: MapSizeLg,
/// Default voxel for use outside of max map bounds.
default: Arc<V>,
chunks: HashMap<Vec2<i32>, Arc<V>>,
chunks: HashMap<Vec2<i32>, Arc<V>, GridHasher>,
}
impl<V: RectRasterableVol> VolGrid2d<V> {
@ -64,9 +65,10 @@ impl<V: RectRasterableVol + ReadVol + Debug> ReadVol for VolGrid2d<V> {
let ck = Self::chunk_key(pos);
self.get_key(ck)
.ok_or(VolGrid2dError::NoSuchChunk)
.and_then(|chunk| {
.map(|chunk| {
let co = Self::chunk_offs(pos);
chunk.get(co).map_err(VolGrid2dError::ChunkError)
// Always within bounds of the chunk, so we can use the get_unchecked form
chunk.get_unchecked(co)
})
}
@ -271,7 +273,7 @@ impl<'a, V: RectRasterableVol + ReadVol> CachedVolGrid2d<'a, V> {
chunk
};
let co = VolGrid2d::<V>::chunk_offs(pos);
chunk.get(co).map_err(VolGrid2dError::ChunkError)
Ok(chunk.get_unchecked(co))
}
}

View File

@ -1460,11 +1460,9 @@ fn box_voxel_collision<'a, T: BaseVol<Vox = Block> + ReadVol>(
// Calculate the world space near aabb
let near_aabb = move_aabb(near_aabb, pos.0);
let player_overlap = |block_aabb: Aabb<f32>| {
ordered_float::OrderedFloat(
(block_aabb.center() - player_aabb.center() - Vec3::unit_z() * 0.5)
.map(f32::abs)
.sum(),
)
(block_aabb.center() - player_aabb.center() - Vec3::unit_z() * 0.5)
.map(f32::abs)
.sum()
};
terrain.for_each_in(near_aabb, |block_pos, block| {
@ -1479,19 +1477,16 @@ fn box_voxel_collision<'a, T: BaseVol<Vox = Block> + ReadVol>(
// Determine whether the block's AABB collides with the player's AABB
if block_aabb.collides_with_aabb(player_aabb) {
most_colliding = match most_colliding {
match &most_colliding {
// Select the minimum of the value from `player_overlap`
other @ Some((_, other_block_aabb, _))
Some((_, other_block_aabb, _))
if {
// TODO: comment below is outdated (as of ~1 year ago)
// Find the maximum of the minimum collision axes (this bit
// is weird, trust me that it works)
player_overlap(block_aabb) >= player_overlap(other_block_aabb)
} =>
{
other
},
_ => Some((block_pos, block_aabb, block)),
player_overlap(block_aabb) >= player_overlap(*other_block_aabb)
} => {},
_ => most_colliding = Some((block_pos, block_aabb, block)),
}
}
}