use std::collections::{hash_map::Entry::*, HashMap, HashSet};
use std::fmt;
use std::num::NonZeroU32;
use std::sync::{Arc, Mutex, Weak};
use cgmath::{EuclideanSpace, Point3};
use fnv::{FnvHashMap, FnvHashSet};
use indoc::indoc;
use instant::{Duration, Instant};
use crate::block::{EvaluatedBlock, Resolution};
use crate::camera::{Camera, Flaws};
use crate::chunking::{cube_to_chunk, point_to_chunk, ChunkChart, ChunkPos, OctantMask};
use crate::listen::Listener;
use crate::math::{FreeCoordinate, GridCoordinate, GridPoint};
use crate::mesh::{
BlockMesh, BlockMeshProvider, GfxVertex, MeshOptions, SpaceMesh, TextureAllocator, TextureTile,
};
use crate::space::{BlockIndex, Space, SpaceChange};
use crate::universe::URef;
use crate::util::{ConciseDebug, CustomFormat, StatusText, TimeStats};
const LOG_CHUNK_UPDATES: bool = false;
#[derive(Debug)]
pub struct ChunkedSpaceMesh<D, Vert, Tex, const CHUNK_SIZE: GridCoordinate>
where
Tex: TextureAllocator,
{
space: URef<Space>,
todo: Arc<Mutex<CsmTodo<CHUNK_SIZE>>>,
block_meshes: VersionedBlockMeshes<Vert, Tex::Tile>,
chunks: FnvHashMap<ChunkPos<CHUNK_SIZE>, ChunkMesh<D, Vert, Tex, CHUNK_SIZE>>,
chunk_chart: ChunkChart<CHUNK_SIZE>,
view_chunk: ChunkPos<CHUNK_SIZE>,
did_not_finish_chunks: bool,
last_mesh_options: Option<MeshOptions>,
zero_time: Instant,
complete_time: Option<Instant>,
}
impl<D, Vert, Tex, const CHUNK_SIZE: GridCoordinate> ChunkedSpaceMesh<D, Vert, Tex, CHUNK_SIZE>
where
D: Default,
Vert: GfxVertex<TexPoint = <<Tex as TextureAllocator>::Tile as TextureTile>::Point> + PartialEq,
Tex: TextureAllocator,
Tex::Tile: PartialEq,
{
pub fn new(space: URef<Space>) -> Self {
let space_borrowed = space.read().unwrap();
let todo = CsmTodo::initially_dirty();
let todo_rc = Arc::new(Mutex::new(todo));
space_borrowed.listen(TodoListener(Arc::downgrade(&todo_rc)));
Self {
space,
todo: todo_rc,
block_meshes: VersionedBlockMeshes::new(),
chunks: FnvHashMap::default(),
chunk_chart: ChunkChart::new(0.0),
view_chunk: ChunkPos(Point3::new(0, 0, 0)),
did_not_finish_chunks: true,
last_mesh_options: None,
zero_time: Instant::now(),
complete_time: None,
}
}
pub fn space(&self) -> &URef<Space> {
&self.space
}
pub fn chunk_chart(&self) -> &ChunkChart<CHUNK_SIZE> {
&self.chunk_chart
}
pub fn iter_chunks(&self) -> impl Iterator<Item = &ChunkMesh<D, Vert, Tex, CHUNK_SIZE>> {
self.chunks.values()
}
pub fn iter_in_view<'a>(
&'a self,
camera: &'a Camera,
) -> impl Iterator<Item = &'a ChunkMesh<D, Vert, Tex, CHUNK_SIZE>> + DoubleEndedIterator + 'a
{
self.chunk_chart
.chunks(self.view_chunk(), camera.view_direction_mask())
.filter_map(|pos| self.chunk(pos))
.filter(|chunk| {
!camera.options().use_frustum_culling
|| camera.aab_in_view(chunk.position.bounds().into())
})
}
pub fn chunk(
&self,
position: ChunkPos<CHUNK_SIZE>,
) -> Option<&ChunkMesh<D, Vert, Tex, CHUNK_SIZE>> {
self.chunks.get(&position)
}
pub fn update_blocks_and_some_chunks<F>(
&mut self,
camera: &Camera,
block_texture_allocator: &Tex,
deadline: Instant,
mut chunk_render_updater: F,
) -> CsmUpdateInfo
where
F: FnMut(ChunkMeshUpdate<'_, D, Vert, Tex::Tile, CHUNK_SIZE>),
{
let update_start_time = Instant::now();
let graphics_options = camera.options();
let mesh_options = MeshOptions::new(graphics_options);
let view_point = camera.view_position();
let view_chunk = point_to_chunk(view_point);
let view_chunk_is_different = self.view_chunk != view_chunk;
self.view_chunk = view_chunk;
let mut todo = self.todo.lock().unwrap();
let space = &*if let Ok(space) = self.space.read() {
space
} else {
return CsmUpdateInfo {
prep_time: Instant::now().duration_since(update_start_time),
..CsmUpdateInfo::default()
};
};
if Some(&mesh_options) != self.last_mesh_options.as_ref() {
todo.all_blocks_and_chunks = true;
self.last_mesh_options = Some(mesh_options);
}
let mesh_options = self.last_mesh_options.as_ref().unwrap();
if todo.all_blocks_and_chunks {
todo.all_blocks_and_chunks = false;
todo.blocks
.extend(0..(space.block_data().len() as BlockIndex));
self.block_meshes.clear();
self.zero_time = Instant::now();
self.complete_time = None;
}
self.chunk_chart.resize_if_needed(camera.view_distance());
let prep_to_update_meshes_time = Instant::now();
let block_updates = self.block_meshes.update(
&mut todo.blocks,
space,
block_texture_allocator,
mesh_options,
deadline - Duration::from_micros(500),
);
let all_done_with_blocks = todo.blocks.is_empty();
let block_update_to_chunk_scan_time = Instant::now();
if view_chunk_is_different {
let cache_distance = FreeCoordinate::from(CHUNK_SIZE);
let retention_distance_squared =
(camera.view_distance().ceil() + cache_distance).powi(2) as i32;
self.chunks.retain(|pos, _| {
pos.min_distance_squared_from(view_chunk) <= retention_distance_squared
});
todo.chunks.retain(|pos, _| {
pos.min_distance_squared_from(view_chunk) <= retention_distance_squared
});
}
let chunk_bounds = space.bounds().divide(CHUNK_SIZE);
let mut chunk_mesh_generation_times = TimeStats::default();
let mut chunk_mesh_callback_times = TimeStats::default();
let mut did_not_finish = false;
for p in self.chunk_chart.chunks(view_chunk, OctantMask::ALL) {
if !chunk_bounds.contains_cube(p.0) {
continue;
}
let this_chunk_start_time = Instant::now();
if this_chunk_start_time > deadline {
did_not_finish = true;
break;
}
let chunk_entry = self.chunks.entry(p);
if (todo
.chunks
.get(&p)
.map(|ct| ct.recompute_mesh)
.unwrap_or(false)
&& !self.did_not_finish_chunks)
|| matches!(chunk_entry, Vacant(_))
|| matches!(
chunk_entry,
Occupied(ref oe) if oe.get().stale_blocks(&self.block_meshes))
{
let chunk = chunk_entry.or_insert_with(|| {
todo.chunks.insert(p, ChunkTodo::CLEAN);
ChunkMesh::new(p)
});
chunk.recompute_mesh(
todo.chunks.get_mut(&p).unwrap(), space,
mesh_options,
&self.block_meshes,
);
let compute_end_update_start = Instant::now();
chunk_render_updater(chunk.borrow_for_update(false));
chunk_mesh_generation_times +=
TimeStats::one(compute_end_update_start.duration_since(this_chunk_start_time));
chunk_mesh_callback_times +=
TimeStats::one(Instant::now().duration_since(compute_end_update_start));
}
}
self.did_not_finish_chunks = did_not_finish;
let chunk_scan_end_time = Instant::now();
let depth_sort_end_time = if let Some(chunk) = self.chunks.get_mut(&view_chunk) {
if chunk.depth_sort_for_view(view_point.cast::<Vert::Coordinate>().unwrap()) {
chunk_render_updater(chunk.borrow_for_update(true));
Some(Instant::now())
} else {
None
}
} else {
None
};
let complete = all_done_with_blocks && !did_not_finish;
if complete && self.complete_time.is_none() {
let t = Instant::now();
log::debug!(
"SpaceRenderer({space}): all meshes done in {time}",
space = self.space().name(),
time = t.duration_since(self.zero_time).custom_format(StatusText)
);
self.complete_time = Some(t);
}
let mut flaws = Flaws::empty();
if !complete {
flaws |= Flaws::UNFINISHED;
}
CsmUpdateInfo {
flaws,
total_time: depth_sort_end_time
.unwrap_or(chunk_scan_end_time)
.duration_since(update_start_time),
prep_time: prep_to_update_meshes_time.duration_since(update_start_time),
chunk_scan_time: chunk_scan_end_time
.saturating_duration_since(block_update_to_chunk_scan_time)
.saturating_sub(chunk_mesh_generation_times.sum + chunk_mesh_callback_times.sum),
chunk_mesh_generation_times,
chunk_mesh_callback_times,
depth_sort_time: depth_sort_end_time.map(|t| t.duration_since(chunk_scan_end_time)),
block_updates,
}
}
pub fn view_chunk(&self) -> ChunkPos<CHUNK_SIZE> {
self.view_chunk
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub struct CsmUpdateInfo {
pub flaws: Flaws,
pub total_time: Duration,
pub prep_time: Duration,
pub chunk_scan_time: Duration,
pub chunk_mesh_generation_times: TimeStats,
pub chunk_mesh_callback_times: TimeStats,
depth_sort_time: Option<Duration>,
pub block_updates: TimeStats,
}
impl CustomFormat<StatusText> for CsmUpdateInfo {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>, _: StatusText) -> fmt::Result {
let CsmUpdateInfo {
flaws,
total_time: _,
prep_time,
chunk_scan_time,
chunk_mesh_generation_times,
chunk_mesh_callback_times,
depth_sort_time,
block_updates,
} = self;
write!(
fmt,
indoc! {"
Space prep {prep_time} Mesh flaws: {flaws:?}
Block mesh gen {block_updates}
Chunk scan {chunk_scan_time}
mesh gen {chunk_mesh_generation_times}
upload {chunk_mesh_callback_times}
depthsort {depth_sort_time}\
"},
flaws = flaws,
prep_time = prep_time.custom_format(StatusText),
block_updates = block_updates,
chunk_scan_time = chunk_scan_time.custom_format(StatusText),
chunk_mesh_generation_times = chunk_mesh_generation_times,
chunk_mesh_callback_times = chunk_mesh_callback_times,
depth_sort_time = depth_sort_time
.unwrap_or(Duration::ZERO)
.custom_format(StatusText),
)
}
}
#[derive(Debug)]
struct VersionedBlockMeshes<Vert, Tile> {
meshes: Vec<VersionedBlockMesh<Vert, Tile>>,
last_version_counter: NonZeroU32,
}
impl<Vert, Tile> VersionedBlockMeshes<Vert, Tile>
where
Vert: GfxVertex<TexPoint = <Tile as TextureTile>::Point> + PartialEq,
Tile: TextureTile + PartialEq,
{
fn new() -> Self {
Self {
meshes: Vec::new(),
last_version_counter: NonZeroU32::new(u32::MAX).unwrap(),
}
}
fn clear(&mut self) {
self.meshes.clear();
}
fn update<A>(
&mut self,
todo: &mut FnvHashSet<BlockIndex>,
space: &Space,
block_texture_allocator: &A,
mesh_options: &MeshOptions,
deadline: Instant,
) -> TimeStats
where
A: TextureAllocator<Tile = Tile>,
{
if todo.is_empty() {
return TimeStats::default();
}
self.last_version_counter = match self.last_version_counter.get().checked_add(1) {
None => NonZeroU32::new(1).unwrap(),
Some(n) => NonZeroU32::new(n).unwrap(),
};
let current_version_number = BlockMeshVersion::Numbered(self.last_version_counter);
let block_data = space.block_data();
{
let old_len = self.meshes.len();
let new_len = block_data.len();
if old_len > new_len {
self.meshes.truncate(new_len);
} else {
let mut fast_options = mesh_options.clone();
fast_options.ignore_voxels = true;
self.meshes.reserve(new_len);
for bd in &block_data[self.meshes.len()..new_len] {
let evaluated = bd.evaluated();
self.meshes.push(if evaluated.resolution > Resolution::R1 {
VersionedBlockMesh {
mesh: BlockMesh::new(evaluated, block_texture_allocator, &fast_options),
version: BlockMeshVersion::NotReady,
}
} else {
VersionedBlockMesh {
mesh: BlockMesh::new(evaluated, block_texture_allocator, mesh_options),
version: current_version_number,
}
});
}
}
}
let mut last_start_time = Instant::now();
let mut stats = TimeStats::default();
while last_start_time < deadline && !todo.is_empty() {
let index: BlockIndex = todo.iter().next().copied().unwrap();
todo.remove(&index);
let index: usize = index.into();
let bd = &block_data[index];
let new_evaluated_block: &EvaluatedBlock = bd.evaluated();
let current_mesh_entry: &mut VersionedBlockMesh<_, _> = &mut self.meshes[index];
if current_mesh_entry
.mesh
.try_update_texture_only(new_evaluated_block)
{
} else {
let new_block_mesh =
BlockMesh::new(new_evaluated_block, block_texture_allocator, mesh_options);
if new_block_mesh != current_mesh_entry.mesh
|| current_mesh_entry.version == BlockMeshVersion::NotReady
{
*current_mesh_entry = VersionedBlockMesh {
mesh: new_block_mesh,
version: current_version_number,
};
} else {
}
}
let duration = stats.record_consecutive_interval(&mut last_start_time, Instant::now());
if duration > Duration::from_millis(4) {
log::trace!(
"Block mesh took {}: {:?} {:?}",
duration.custom_format(StatusText),
new_evaluated_block.attributes.display_name,
bd.block(),
);
}
}
stats
}
}
impl<'a, Vert, Tile> BlockMeshProvider<'a, Vert, Tile> for &'a VersionedBlockMeshes<Vert, Tile> {
fn get(&mut self, index: BlockIndex) -> Option<&'a BlockMesh<Vert, Tile>> {
Some(&self.meshes.get(usize::from(index))?.mesh)
}
}
#[derive(Debug)]
struct VersionedBlockMesh<Vert, Tile> {
mesh: BlockMesh<Vert, Tile>,
version: BlockMeshVersion,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum BlockMeshVersion {
NotReady,
Numbered(NonZeroU32),
}
#[derive(Debug, Eq, PartialEq)]
pub struct ChunkMesh<D, Vert, Tex, const CHUNK_SIZE: GridCoordinate>
where
Tex: TextureAllocator,
{
position: ChunkPos<CHUNK_SIZE>,
mesh: SpaceMesh<Vert, Tex::Tile>,
pub render_data: D,
block_dependencies: Vec<(BlockIndex, BlockMeshVersion)>,
}
impl<D, Vert, Tex, const CHUNK_SIZE: GridCoordinate> ChunkMesh<D, Vert, Tex, CHUNK_SIZE>
where
D: Default, Vert: GfxVertex,
Tex: TextureAllocator,
{
fn new(position: ChunkPos<CHUNK_SIZE>) -> Self {
Self {
position,
mesh: SpaceMesh::default(),
render_data: D::default(),
block_dependencies: Vec::new(),
}
}
#[inline]
pub fn mesh(&self) -> &SpaceMesh<Vert, Tex::Tile> {
&self.mesh
}
#[inline]
pub fn position(&self) -> ChunkPos<CHUNK_SIZE> {
self.position
}
fn borrow_for_update(
&mut self,
indices_only: bool,
) -> ChunkMeshUpdate<'_, D, Vert, Tex::Tile, CHUNK_SIZE> {
ChunkMeshUpdate {
position: self.position,
mesh: &self.mesh,
render_data: &mut self.render_data,
indices_only,
}
}
fn recompute_mesh(
&mut self,
chunk_todo: &mut ChunkTodo,
space: &Space,
options: &MeshOptions,
block_meshes: &VersionedBlockMeshes<Vert, Tex::Tile>,
) {
let compute_start: Option<Instant> = LOG_CHUNK_UPDATES.then(Instant::now);
let bounds = self.position.bounds();
self.mesh.compute(space, bounds, options, block_meshes);
if let Some(start) = compute_start {
let duration_ms = Instant::now().duration_since(start).as_secs_f32() * 1000.0;
let chunk_origin = bounds.lower_bounds();
let vertices = self.mesh.vertices().len();
if vertices == 0 {
log::trace!(
"meshed {:?}+ in {:.3} ms, 0",
chunk_origin.custom_format(ConciseDebug),
duration_ms,
);
} else {
log::trace!(
"meshed {:?}+ in {:.3} ms, {} in {:.3} µs/v",
chunk_origin.custom_format(ConciseDebug),
duration_ms,
vertices,
duration_ms * (1000.0 / vertices as f32),
);
}
}
self.block_dependencies.clear();
self.block_dependencies.extend(
self.mesh
.blocks_used_iter()
.map(|index| (index, block_meshes.meshes[usize::from(index)].version)),
);
chunk_todo.recompute_mesh = false;
}
pub fn depth_sort_for_view(&mut self, view_position: Point3<Vert::Coordinate>) -> bool {
self.mesh.depth_sort_for_view(
view_position
- self
.position
.bounds()
.lower_bounds()
.to_vec()
.cast()
.unwrap(),
)
}
fn stale_blocks(&self, block_meshes: &VersionedBlockMeshes<Vert, Tex::Tile>) -> bool {
self.block_dependencies
.iter()
.any(|&(index, version)| block_meshes.meshes[usize::from(index)].version != version)
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct ChunkMeshUpdate<'a, D, V, T, const CHUNK_SIZE: GridCoordinate> {
pub position: ChunkPos<CHUNK_SIZE>,
pub mesh: &'a SpaceMesh<V, T>,
pub render_data: &'a mut D,
pub indices_only: bool,
}
#[derive(Debug, Default)]
struct CsmTodo<const CHUNK_SIZE: GridCoordinate> {
all_blocks_and_chunks: bool,
blocks: FnvHashSet<BlockIndex>,
chunks: FnvHashMap<ChunkPos<CHUNK_SIZE>, ChunkTodo>,
}
impl<const CHUNK_SIZE: GridCoordinate> CsmTodo<CHUNK_SIZE> {
fn initially_dirty() -> Self {
Self {
all_blocks_and_chunks: true,
blocks: HashSet::default(),
chunks: HashMap::default(),
}
}
fn modify_block_and_adjacent<F>(&mut self, cube: GridPoint, mut f: F)
where
F: FnMut(&mut ChunkTodo),
{
for axis in 0..3 {
for offset in &[-1, 1] {
let mut adjacent = cube;
adjacent[axis] += offset;
if let Some(chunk) = self.chunks.get_mut(&cube_to_chunk(adjacent)) {
f(chunk);
}
}
}
}
}
#[derive(Clone, Debug)]
struct TodoListener<const CHUNK_SIZE: GridCoordinate>(Weak<Mutex<CsmTodo<CHUNK_SIZE>>>);
impl<const CHUNK_SIZE: GridCoordinate> Listener<SpaceChange> for TodoListener<CHUNK_SIZE> {
fn receive(&self, message: SpaceChange) {
if let Some(cell) = self.0.upgrade() {
if let Ok(mut todo) = cell.lock() {
match message {
SpaceChange::EveryBlock => {
todo.all_blocks_and_chunks = true;
todo.blocks.clear();
todo.chunks.clear();
}
SpaceChange::Block(p) => {
todo.modify_block_and_adjacent(p, |chunk_todo| {
chunk_todo.recompute_mesh = true;
});
}
SpaceChange::Lighting(_p) => {
}
SpaceChange::Number(index) => {
if !todo.all_blocks_and_chunks {
todo.blocks.insert(index);
}
}
SpaceChange::BlockValue(index) => {
if !todo.all_blocks_and_chunks {
todo.blocks.insert(index);
}
}
}
}
}
}
fn alive(&self) -> bool {
self.0.strong_count() > 0
}
}
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
struct ChunkTodo {
recompute_mesh: bool,
}
impl ChunkTodo {
const CLEAN: Self = Self {
recompute_mesh: false,
};
}
#[cfg(test)]
mod tests {
use cgmath::EuclideanSpace as _;
use ordered_float::NotNan;
use super::*;
use crate::block::Block;
use crate::camera::{GraphicsOptions, TransparencyOption, Viewport};
use crate::math::{FreeCoordinate, GridAab, GridCoordinate};
use crate::mesh::{BlockVertex, NoTexture, NoTextures};
use crate::space::SpaceTransaction;
use crate::universe::Universe;
const CHUNK_SIZE: GridCoordinate = 16;
const LARGE_VIEW_DISTANCE: f64 = 200.0;
fn read_todo_chunks(
todo: &Mutex<CsmTodo<CHUNK_SIZE>>,
) -> Vec<(ChunkPos<CHUNK_SIZE>, ChunkTodo)> {
let mut v = todo
.lock()
.unwrap()
.chunks
.iter()
.map(|(&p, &ct)| (p, ct))
.collect::<Vec<_>>();
v.sort_by_key(|(p, _): &(ChunkPos<CHUNK_SIZE>, _)| {
<_ as Into<[GridCoordinate; 3]>>::into(p.0)
});
v
}
#[test]
fn update_adjacent_chunk_positive() {
let todo: Arc<Mutex<CsmTodo<CHUNK_SIZE>>> = Default::default();
let listener = TodoListener(Arc::downgrade(&todo));
todo.lock().unwrap().chunks.extend(vec![
(ChunkPos::new(-1, 0, 0), ChunkTodo::CLEAN),
(ChunkPos::new(0, 0, 0), ChunkTodo::CLEAN),
(ChunkPos::new(1, 0, 0), ChunkTodo::CLEAN),
]);
listener.receive(SpaceChange::Block(GridPoint::new(
CHUNK_SIZE - 1,
CHUNK_SIZE / 2,
CHUNK_SIZE / 2,
)));
assert_eq!(
read_todo_chunks(&todo),
vec![
(ChunkPos::new(-1, 0, 0), ChunkTodo::CLEAN),
(
ChunkPos::new(0, 0, 0),
ChunkTodo {
recompute_mesh: true,
..ChunkTodo::CLEAN
}
),
(
ChunkPos::new(1, 0, 0),
ChunkTodo {
recompute_mesh: true,
..ChunkTodo::CLEAN
}
),
],
);
}
#[test]
fn update_adjacent_chunk_negative() {
let todo: Arc<Mutex<CsmTodo<CHUNK_SIZE>>> = Default::default();
let listener = TodoListener(Arc::downgrade(&todo));
todo.lock().unwrap().chunks.extend(vec![
(ChunkPos::new(-1, 0, 0), ChunkTodo::CLEAN),
(ChunkPos::new(0, 0, 0), ChunkTodo::CLEAN),
(ChunkPos::new(1, 0, 0), ChunkTodo::CLEAN),
]);
listener.receive(SpaceChange::Block(GridPoint::new(
0,
CHUNK_SIZE / 2,
CHUNK_SIZE / 2,
)));
assert_eq!(
read_todo_chunks(&todo),
vec![
(
ChunkPos::new(-1, 0, 0),
ChunkTodo {
recompute_mesh: true,
..ChunkTodo::CLEAN
}
),
(
ChunkPos::new(0, 0, 0),
ChunkTodo {
recompute_mesh: true,
..ChunkTodo::CLEAN
}
),
(ChunkPos::new(1, 0, 0), ChunkTodo::CLEAN),
],
);
}
#[test]
fn todo_ignores_absent_chunks() {
let todo: Arc<Mutex<CsmTodo<CHUNK_SIZE>>> = Default::default();
let listener = TodoListener(Arc::downgrade(&todo));
let p = GridPoint::new(1, 1, 1) * (CHUNK_SIZE / 2);
listener.receive(SpaceChange::Block(p));
assert_eq!(read_todo_chunks(&todo), vec![]);
todo.lock()
.unwrap()
.chunks
.insert(ChunkPos::new(0, 0, 0), ChunkTodo::CLEAN);
listener.receive(SpaceChange::Block(p));
assert_eq!(
read_todo_chunks(&todo),
vec![(
ChunkPos::new(0, 0, 0),
ChunkTodo {
recompute_mesh: true,
..ChunkTodo::CLEAN
}
),],
);
}
#[derive(Debug)]
struct CsmTester {
#[allow(dead_code)] universe: Universe,
space: URef<Space>,
camera: Camera,
csm: ChunkedSpaceMesh<(), BlockVertex<NoTexture>, NoTextures, 16>,
}
impl CsmTester {
fn new(space: Space, view_distance: f64) -> Self {
let mut universe = Universe::new();
let space_ref = universe.insert_anonymous(space);
let csm = ChunkedSpaceMesh::<(), BlockVertex<NoTexture>, NoTextures, CHUNK_SIZE>::new(
space_ref.clone(),
);
let camera = Camera::new(
GraphicsOptions {
view_distance: NotNan::new(view_distance).unwrap(),
..GraphicsOptions::default()
},
Viewport::ARBITRARY,
);
Self {
universe,
space: space_ref,
camera,
csm,
}
}
fn update<F>(&mut self, chunk_render_updater: F) -> CsmUpdateInfo
where
F: FnMut(ChunkMeshUpdate<'_, (), BlockVertex<NoTexture>, NoTexture, 16>),
{
self.csm.update_blocks_and_some_chunks(
&self.camera,
&NoTextures,
Instant::now() + Duration::from_secs(1_000_000),
chunk_render_updater,
)
}
fn move_camera_to(&mut self, position: impl Into<Point3<FreeCoordinate>>) {
let mut view_transform = self.camera.get_view_transform();
view_transform.disp = position.into().to_vec() * f64::from(CHUNK_SIZE);
self.camera.set_view_transform(view_transform);
}
}
#[test]
fn basic_chunk_presence() {
let mut tester = CsmTester::new(Space::empty_positive(1, 1, 1), LARGE_VIEW_DISTANCE);
tester.update(|_| {});
assert_ne!(None, tester.csm.chunk(ChunkPos::new(0, 0, 0)));
assert_eq!(None, tester.csm.chunk(ChunkPos::new(1, 0, 0)));
}
#[test]
fn sort_view_every_frame_only_if_transparent() {
let mut tester = CsmTester::new(Space::empty_positive(1, 1, 1), LARGE_VIEW_DISTANCE);
tester.update(|u| {
assert!(!u.indices_only);
});
tester
.space
.execute(&SpaceTransaction::set_cube(
[0, 0, 0],
None,
Some(Block::from(rgba_const!(1.0, 1.0, 1.0, 0.5))),
))
.unwrap();
let mut did_call = false;
tester.update(|u| {
if u.indices_only {
did_call = true;
}
});
assert!(did_call, "Expected indices_only_updater");
did_call = false;
tester.update(|u| {
if u.indices_only {
did_call = true;
}
});
assert!(did_call, "Expected indices_only_updater #2");
}
#[test]
fn graphics_options_change() {
let mut options = GraphicsOptions {
view_distance: NotNan::from(1),
transparency: TransparencyOption::Volumetric,
..Default::default()
};
let mut space = Space::empty_positive(1, 1, 1);
space
.set([0, 0, 0], Block::from(rgba_const!(1., 1., 1., 0.25)))
.unwrap();
let mut tester = CsmTester::new(space, 200.0);
tester.camera.set_options(options.clone());
let mut vertices = None;
tester.update(|u| vertices = Some(u.mesh.vertices().len()));
assert_eq!(vertices, Some(24));
options.transparency = TransparencyOption::Threshold(notnan!(0.5));
tester.camera.set_options(options.clone());
vertices = None;
tester.update(|u| vertices = Some(u.mesh.vertices().len()));
assert_eq!(vertices, Some(0));
}
#[test]
fn drop_chunks_when_moving() {
let mut tester = CsmTester::new(
Space::builder(GridAab::from_lower_upper(
[-1000, -100, -100],
[1000, 100, 100],
))
.build(),
f64::from(CHUNK_SIZE) / 2.0,
);
tester.move_camera_to([0.5, 0.5, 0.5]);
tester.update(|_| {});
let initial_chunk_count = tester.csm.iter_chunks().count();
assert_eq!(initial_chunk_count, 3usize.pow(3));
for i in 1..30 {
let position = Point3::new(1.5 + f64::from(i), 0.5, 0.5);
tester.move_camera_to(position);
tester.update(|_| {});
let count = tester.csm.iter_chunks().count();
println!("{i}: {position:?}, {count} chunks");
}
assert!(tester.csm.iter_chunks().count() < initial_chunk_count * 3);
}
#[test]
fn did_not_finish_detection() {
let mut tester = CsmTester::new(Space::empty_positive(1000, 1, 1), LARGE_VIEW_DISTANCE);
eprintln!("--- timing out update");
let info = tester.csm.update_blocks_and_some_chunks(
&tester.camera,
&NoTextures,
Instant::now() - Duration::from_secs(1),
|_| {},
);
assert_eq!(
(
info.flaws,
tester.csm.did_not_finish_chunks,
tester.csm.complete_time
),
(Flaws::UNFINISHED, true, None)
);
eprintln!("--- normal update");
let info = tester.update(|_| {});
assert_eq!(
(
info.flaws,
tester.csm.did_not_finish_chunks,
tester.csm.complete_time.is_some(),
),
(Flaws::empty(), false, true)
);
}
}