Skip to content

Commit

Permalink
Initial implementation of "clipbox" streaming.
Browse files Browse the repository at this point in the history
The name is made up and might not make much sense. It's inspired from
"clipmaps" in heightmap terrains, due to the hierarchical 3D boxes being
used.

It still uses logic similar to octrees, but block allocation and request
logic follows hierarchical boxes instead of directly being driven by an
explicit octree data structure. Octree logic only determines which mesh
chunks should be active as they load in and out, so that LODs dont
overlap and don't leave holes.

Expected advantages are lower CPU usage due only reacting on loads instead
of polling, and far less hashmap lookups due to neighbors not having to be
polled, because loading boxes are padded and all LODs can load in one go,
and in parallel (no need to wait for parent LODs).
Its simplicity also allows more predictable and controllable chunk
loading management, for multiplayer and signals notably.

Needs more work and testing, some features are missing, there might be
some bugs and some downsides to improve.
  • Loading branch information
Zylann committed Dec 8, 2023
1 parent 229a0e9 commit e385eeb
Show file tree
Hide file tree
Showing 8 changed files with 878 additions and 24 deletions.
40 changes: 25 additions & 15 deletions storage/voxel_data.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -924,13 +924,15 @@ bool VoxelData::has_blocks_with_voxels_in_area_broad_mip_test(Box3i box_in_voxel
return true;
}

void VoxelData::view_area(Box3i blocks_box, std::vector<Vector3i> &missing_blocks,
std::vector<Vector3i> &found_blocks_positions, std::vector<VoxelDataBlock> &found_blocks) {
void VoxelData::view_area(Box3i blocks_box, unsigned int lod_index, std::vector<Vector3i> *missing_blocks,
std::vector<Vector3i> *found_blocks_positions, std::vector<VoxelDataBlock> *found_blocks) {
ZN_PROFILE_SCOPE();
ZN_ASSERT_RETURN(lod_index < _lods.size());

const Box3i bounds_in_blocks = get_bounds().downscaled(get_block_size());
blocks_box = blocks_box.clipped(bounds_in_blocks);

Lod &lod = _lods[0];
Lod &lod = _lods[lod_index];

// Locking for write because we are modifying states on blocks.
// TODO Could use atomics if contention is too much?
Expand All @@ -939,25 +941,31 @@ void VoxelData::view_area(Box3i blocks_box, std::vector<Vector3i> &missing_block
// Locking for read because we don't add or remove blocks.
RWLockRead rlock(lod.map_lock);

blocks_box.for_each_cell_zxy([&lod, &found_blocks_positions, &found_blocks, &missing_blocks](Vector3i bpos) {
blocks_box.for_each_cell_zxy([&lod, found_blocks_positions, found_blocks, &missing_blocks](Vector3i bpos) {
VoxelDataBlock *block = lod.map.get_block(bpos);
if (block != nullptr) {
block->viewers.add();
found_blocks.push_back(*block);
found_blocks_positions.push_back(bpos);
} else {
missing_blocks.push_back(bpos);
if (found_blocks != nullptr) {
found_blocks->push_back(*block);
}
if (found_blocks_positions != nullptr) {
found_blocks_positions->push_back(bpos);
}
} else if (missing_blocks != nullptr) {
missing_blocks->push_back(bpos);
}
});
}

void VoxelData::unview_area(Box3i blocks_box, std::vector<Vector3i> &missing_blocks,
std::vector<Vector3i> &removed_blocks, std::vector<BlockToSave> *to_save) {
void VoxelData::unview_area(Box3i blocks_box, unsigned int lod_index, std::vector<Vector3i> *removed_blocks,
std::vector<Vector3i> *missing_blocks, std::vector<BlockToSave> *to_save) {
ZN_PROFILE_SCOPE();
ZN_ASSERT_RETURN(lod_index < _lods.size());

const Box3i bounds_in_blocks = get_bounds().downscaled(get_block_size());
blocks_box = blocks_box.clipped(bounds_in_blocks);

Lod &lod = _lods[0];
Lod &lod = _lods[lod_index];

// Locking for write because we are modifying states on blocks.
// TODO Could use atomics if contention is too much? However if we do, we need to ensure no other thread is holding
Expand All @@ -967,7 +975,7 @@ void VoxelData::unview_area(Box3i blocks_box, std::vector<Vector3i> &missing_blo
// Locking for write because we are potentially going to remove blocks from the map.
RWLockWrite wlock(lod.map_lock);

blocks_box.for_each_cell_zxy([&lod, &missing_blocks, &removed_blocks, to_save](Vector3i bpos) {
blocks_box.for_each_cell_zxy([&lod, missing_blocks, removed_blocks, to_save](Vector3i bpos) {
VoxelDataBlock *block = lod.map.get_block(bpos);
if (block != nullptr) {
block->viewers.remove();
Expand All @@ -977,10 +985,12 @@ void VoxelData::unview_area(Box3i blocks_box, std::vector<Vector3i> &missing_blo
} else {
lod.map.remove_block(bpos, BeforeUnloadSaveAction{ to_save, bpos, 0 });
}
removed_blocks.push_back(bpos);
if (removed_blocks != nullptr) {
removed_blocks->push_back(bpos);
}
}
} else {
missing_blocks.push_back(bpos);
} else if (missing_blocks != nullptr) {
missing_blocks->push_back(bpos);
}
});
}
Expand Down
21 changes: 18 additions & 3 deletions storage/voxel_data.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ class VoxelData {

void set_full_load_completed(bool complete);

inline bool is_full_load_completed() const {
return _full_load_completed;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Voxel queries.
// When not specified, the used LOD index is 0.
Expand Down Expand Up @@ -282,13 +286,20 @@ class VoxelData {
// Increases the reference count of loaded blocks in the area.
// Returns positions where blocks were loaded, and where they were missing.
// Shallow copies of found blocks are returned (voxel data is referenced).
void view_area(Box3i blocks_box, std::vector<Vector3i> &missing_blocks,
std::vector<Vector3i> &found_blocks_positions, std::vector<VoxelDataBlock> &found_blocks);
// Should only be used if refcounting is used, may fail otherwise.
void view_area(Box3i blocks_box, unsigned int lod_index, std::vector<Vector3i> *missing_blocks,
std::vector<Vector3i> *found_blocks_positions, std::vector<VoxelDataBlock> *found_blocks);

// Decreases the reference count of loaded blocks in the area. Blocks reaching zero will be unloaded.
// Returns positions where blocks were unloaded, and where they were missing.
// If `to_save` is not null and some unloaded blocks contained modifications, their data will be returned too.
void unview_area(Box3i blocks_box, std::vector<Vector3i> &missing_blocks, std::vector<Vector3i> &removed_blocks,
// Should only be used if refcounting is used, may fail otherwise.
void unview_area(Box3i blocks_box, unsigned int lod_index,
// Blocks that actually got removed (some areas can have no block)
std::vector<Vector3i> *removed_blocks,
// Missing blocks are used in case the caller has a collection of loading blocks, so it can cancel them
std::vector<Vector3i> *missing_blocks,
// Blocks to save are those that had unsaved modifications
std::vector<BlockToSave> *to_save);

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -329,8 +340,12 @@ class VoxelData {
RWLockRead rlock(data_lod.map_lock);
const VoxelDataBlock *block = data_lod.map.get_block(block_pos);
if (block == nullptr) {
// The block is not there, so unless streaming is not enabled, we don't know if it has edits or not.
return nullptr;
}

// The block is there, so we know if it has edits or not.

// TODO Thread-safety: this checking presence of voxels is not safe.
// It can change while meshing takes place if a modifier is moved in the same area,
// because it invalidates cached data (that doesn't require locking the map, and doesn't lock a VoxelBuffer,
Expand Down
5 changes: 3 additions & 2 deletions terrain/fixed_lod/voxel_terrain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1330,13 +1330,14 @@ void VoxelTerrain::process_viewer_data_box_change(
// Decrement refcounts from loaded blocks, and unload them
prev_data_box.difference(new_data_box, [this, may_save](Box3i out_of_range_box) {
// ZN_PRINT_VERBOSE(format("Unview data box {}", out_of_range_box));
_data->unview_area(out_of_range_box, tls_missing_blocks, tls_found_blocks_positions,
_data->unview_area(out_of_range_box, 0, &tls_found_blocks_positions, &tls_missing_blocks,
may_save ? &_blocks_to_save : nullptr);
});

// Remove loading blocks (those were loaded and had their refcount reach zero)
for (const Vector3i bpos : tls_found_blocks_positions) {
emit_data_block_unloaded(bpos);
// TODO If they were loaded, why would they be in loading blocks?
_loading_blocks.erase(bpos);
}

Expand Down Expand Up @@ -1384,7 +1385,7 @@ void VoxelTerrain::process_viewer_data_box_change(

new_data_box.difference(prev_data_box, [this](Box3i box_to_load) {
// ZN_PRINT_VERBOSE(format("View data box {}", box_to_load));
_data->view_area(box_to_load, tls_missing_blocks, tls_found_blocks_positions, tls_found_blocks);
_data->view_area(box_to_load, 0, &tls_missing_blocks, &tls_found_blocks_positions, &tls_found_blocks);
});

// Schedule loading of missing blocks
Expand Down
20 changes: 20 additions & 0 deletions terrain/variable_lod/voxel_lod_terrain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,8 @@ void VoxelLodTerrain::reset_mesh_maps() {
// Reset previous state caches to force rebuilding the view area
state.octree_streaming.last_octree_region_box = Box3i();
state.octree_streaming.lod_octrees.clear();
state.clipbox_streaming.lod_distance_in_data_chunks_previous_update = 0;
state.clipbox_streaming.lod_distance_in_mesh_chunks_previous_update = 0;
}

int VoxelLodTerrain::get_lod_count() const {
Expand Down Expand Up @@ -1441,6 +1443,12 @@ void VoxelLodTerrain::apply_data_block_response(VoxelEngine::BlockDataOutput &ob
lod.loading_blocks.erase(ob.position);
}

if (_data->is_streaming_enabled()) {
VoxelLodTerrainUpdateData::ClipboxStreamingState &cs = _update_data->state.clipbox_streaming;
MutexLock mlock(cs.loaded_data_blocks_mutex);
cs.loaded_data_blocks.push_back(VoxelLodTerrainUpdateData::BlockLocation{ ob.position, ob.lod_index });
}

if (_instancer != nullptr && ob.instances != nullptr) {
_instancer->on_data_block_loaded(ob.position, ob.lod_index, std::move(ob.instances));
}
Expand Down Expand Up @@ -1471,6 +1479,7 @@ void VoxelLodTerrain::apply_mesh_update(VoxelEngine::BlockMeshOutput &ob) {

uint8_t transition_mask;
bool active;
bool first_load = false;
{
VoxelLodTerrainUpdateData::Lod &lod = update_data.state.lods[ob.lod];
RWLockRead rlock(lod.mesh_map_state.map_lock);
Expand All @@ -1497,6 +1506,17 @@ void VoxelLodTerrain::apply_mesh_update(VoxelEngine::BlockMeshOutput &ob) {
VoxelLodTerrainUpdateData::MeshState expected = VoxelLodTerrainUpdateData::MESH_UPDATE_SENT;
mesh_block_state.state.compare_exchange_strong(expected, VoxelLodTerrainUpdateData::MESH_UP_TO_DATE);
active = mesh_block_state.active;

if (!mesh_block_state.loaded) {
// First mesh load (note, no mesh being present counts as load too. Before that we would not know)
mesh_block_state.loaded = true;
first_load = true;
}
}
if (first_load) {
VoxelLodTerrainUpdateData::ClipboxStreamingState &cs = _update_data->state.clipbox_streaming;
MutexLock mlock(cs.loaded_mesh_blocks_mutex);
cs.loaded_mesh_blocks.push_back(VoxelLodTerrainUpdateData::BlockLocation{ ob.position, ob.lod });
}

// -------- Part where we invoke Godot functions ---------
Expand Down
Loading

0 comments on commit e385eeb

Please sign in to comment.