Skip to content

Commit b670b8e

Browse files
committed
Fix late mesh updates were applied to blocks that no longer need them.
For example, blocks can still be present if they need only collision, but when a visual mesh update arrives late, it needs to be ignored, instead of just be applied. Moving viewer A through viewer B's region very fast could make this happen.
1 parent fe5d704 commit b670b8e

4 files changed

+77
-23
lines changed

terrain/variable_lod/voxel_lod_terrain.cpp

+25-14
Original file line numberDiff line numberDiff line change
@@ -1366,6 +1366,11 @@ void VoxelLodTerrain::apply_main_thread_update_tasks() {
13661366
}
13671367
block->drop_visuals();
13681368
remove_shader_material_from_block(*block, _shader_material_pool);
1369+
// Also update the state in the threaded representation
1370+
auto it = lod.mesh_map_state.map.find(bpos);
1371+
if (it != lod.mesh_map_state.map.end()) {
1372+
it->second.visual_loaded = false;
1373+
}
13691374
}
13701375
lod.mesh_blocks_to_drop_visual.clear();
13711376

@@ -1678,6 +1683,8 @@ void VoxelLodTerrain::apply_mesh_update(VoxelEngine::BlockMeshOutput &ob) {
16781683
bool collision_active;
16791684
bool first_collision_load = false;
16801685
bool first_visual_load = false;
1686+
bool visual_expected = false;
1687+
bool collision_expected = false;
16811688
{
16821689
VoxelLodTerrainUpdateData::Lod &lod = update_data.state.lods[ob.lod];
16831690
RWLockRead rlock(lod.mesh_map_state.map_lock);
@@ -1707,21 +1714,23 @@ void VoxelLodTerrain::apply_mesh_update(VoxelEngine::BlockMeshOutput &ob) {
17071714
visual_active = mesh_block_state.visual_active;
17081715
collision_active = mesh_block_state.collision_active;
17091716

1710-
if (ob.visual_was_required) {
1711-
if (!mesh_block_state.visual_loaded) {
1712-
// First mesh load (note, no mesh being present counts as load too. Before that we would not know)
1713-
mesh_block_state.visual_loaded = true;
1714-
first_visual_load = true;
1715-
}
1717+
visual_expected = mesh_block_state.mesh_viewers.get() > 0;
1718+
collision_expected = mesh_block_state.collision_viewers.get() > 0;
1719+
1720+
if (visual_expected && ob.visual_was_required) {
1721+
// Mark visuals loaded for the streaming system to subdivide LODs.
1722+
// First mesh load? (note, no mesh being present counts as load too. Before that we would not know)
1723+
first_visual_load = (mesh_block_state.visual_loaded.exchange(true) == false);
17161724
}
1717-
if (!mesh_block_state.collision_loaded) {
1718-
// First mesh load (note, no mesh being present counts as load too. Before that we would not know)
1719-
mesh_block_state.collision_loaded = true;
1720-
first_collision_load = true;
1725+
if (collision_expected) {
1726+
// Mark collisions loaded for the streaming system to subdivide LODs.
1727+
// First mesh load? (note, no mesh being present counts as load too. Before that we would not know)
1728+
first_collision_load = (mesh_block_state.collision_loaded.exchange(true) == false);
17211729
}
17221730
}
17231731
if ((first_visual_load || first_collision_load) &&
17241732
_update_data->settings.streaming_system == VoxelLodTerrainUpdateData::STREAMING_SYSTEM_CLIPBOX) {
1733+
// Notify streaming system so it can subdivide LODs as they load
17251734
VoxelLodTerrainUpdateData::ClipboxStreamingState &cs = _update_data->state.clipbox_streaming;
17261735
MutexLock mlock(cs.loaded_mesh_blocks_mutex);
17271736
cs.loaded_mesh_blocks.push_back(VoxelLodTerrainUpdateData::LoadedMeshBlockEvent{
@@ -1737,7 +1746,7 @@ void VoxelLodTerrain::apply_mesh_update(VoxelEngine::BlockMeshOutput &ob) {
17371746
VoxelMesher::Output &mesh_data = ob.surfaces;
17381747

17391748
Ref<ArrayMesh> mesh;
1740-
if (ob.visual_was_required) {
1749+
if (ob.visual_was_required && visual_expected) {
17411750
if (ob.has_mesh_resource) {
17421751
// The mesh was already built as part of the threaded task
17431752
mesh = ob.mesh;
@@ -1797,7 +1806,7 @@ void VoxelLodTerrain::apply_mesh_update(VoxelEngine::BlockMeshOutput &ob) {
17971806
}
17981807
#endif
17991808

1800-
if (ob.visual_was_required) {
1809+
if (ob.visual_was_required && visual_expected) {
18011810
// We consider a block having a "rendering" mesh as having loaded visuals.
18021811
if (!block->has_mesh()) {
18031812
// Setup visuals
@@ -1864,7 +1873,7 @@ void VoxelLodTerrain::apply_mesh_update(VoxelEngine::BlockMeshOutput &ob) {
18641873
has_collision = ob.lod < _collision_lod_count;
18651874
}
18661875

1867-
if (has_collision) {
1876+
if (has_collision && collision_expected) {
18681877
const uint64_t now = get_ticks_msec();
18691878

18701879
if (_collision_update_delay == 0 ||
@@ -1889,9 +1898,11 @@ void VoxelLodTerrain::apply_mesh_update(VoxelEngine::BlockMeshOutput &ob) {
18891898
}
18901899
}
18911900

1901+
// This is done regardless in case a MeshInstance or collision body is created, because it will then set its
1902+
// position
18921903
block->set_parent_transform(get_global_transform());
18931904

1894-
if (ob.detail_textures != nullptr) {
1905+
if (ob.detail_textures != nullptr && visual_expected) {
18951906
if (ob.detail_textures->valid) {
18961907
apply_detail_texture_update_to_block(*block, *ob.detail_textures, ob.lod);
18971908
} else {

terrain/variable_lod/voxel_lod_terrain_update_clipbox_streaming.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,7 @@ void unview_mesh_box(const Box3i out_of_range_box, VoxelLodTerrainUpdateData::Lo
783783
} else if (mesh_block.mesh_viewers.get() == 0) {
784784
// Unload graphics to save memory
785785
lod.mesh_blocks_to_drop_visual.push_back(bpos);
786+
// Note, `visuals_loaded` will remain true until they are actually unloaded.
786787
}
787788
}
788789
});

terrain/variable_lod/voxel_lod_terrain_update_data.h

+15-9
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
#include "../../generators/voxel_generator.h"
77
#include "../../streams/voxel_stream.h"
88
#include "../../util/containers/fixed_array.h"
9-
#include "../../util/ref_count.h"
9+
#include "../../util/safe_ref_count.h"
1010
#include "../../util/tasks/cancellation_token.h"
1111
#include "../voxel_mesh_map.h"
1212
#include "lod_octree.h"
@@ -102,27 +102,32 @@ struct VoxelLodTerrainUpdateData {
102102
std::atomic<DetailTextureState> detail_texture_state;
103103

104104
// Refcount here to support multiple viewers, we can't do it on the main thread's mesh map since the
105-
// streaming logic is in the update task
106-
RefCount mesh_viewers;
107-
RefCount collision_viewers;
105+
// streaming logic is in the update task.
106+
// TODO Optimize: this could almost not need to be atomic.
107+
// This is an atomic refcount only because the main thread needs to read it when receiving mesh updates. It
108+
// could be made non-atomic if mesh updates were handled in the threaded update, but that has a more
109+
// implications than making this atomic for now.
110+
SafeRefCount mesh_viewers;
111+
SafeRefCount collision_viewers;
108112

109113
// Cancelled when this mesh block is removed, so if tasks are still queued to do work for that block, they
110114
// will be cancelled
111115
TaskCancellationToken cancellation_token;
112116

113117
// Index within the list of meshes to update during one update of the terrain. Used to avoid putting the same
114-
// mesh more than once in the list, while allowing to change options after it's been added to the list. Not
115-
// relevant outside of the update.
118+
// mesh more than once in the list, while allowing to change options after it's been added to the list. Should
119+
// reset to -1 after each update (since the list is consumed)
116120
int update_list_index;
117121

118122
uint8_t transition_mask;
119123
bool visual_active;
120124
bool collision_active;
121125

122126
// Tells whether the first meshing was done since this block was added.
123-
// Written by the main thread only, since the main thread receives mesh updates
124-
bool visual_loaded;
125-
bool collision_loaded;
127+
// Written by the main thread only, when it receives mesh updates or when it unloads resources.
128+
// Read by threaded update to decide when to subdivide LODs.
129+
std::atomic_bool visual_loaded;
130+
std::atomic_bool collision_loaded;
126131

127132
// bool pending_update_has_visuals;
128133
// bool pending_update_has_collision;
@@ -183,6 +188,7 @@ struct VoxelLodTerrainUpdateData {
183188
int last_view_distance_mesh_blocks = 0;
184189

185190
// Deferred outputs to main thread. Should only be read once the task is finished, so no need to lock.
191+
// TODO These output actions are not particularly serialized, that might cause issues (havent so far).
186192
std::vector<Vector3i> mesh_blocks_to_unload;
187193
std::vector<TransitionUpdate> mesh_blocks_to_update_transitions;
188194
std::vector<Vector3i> mesh_blocks_to_activate_visuals;

util/safe_ref_count.h

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#ifndef ZN_SAFE_REF_COUNT_H
2+
#define ZN_SAFE_REF_COUNT_H
3+
4+
#include "errors.h"
5+
#include <atomic>
6+
7+
namespace zylann {
8+
9+
// Thread-safe reference counter.
10+
class SafeRefCount {
11+
public:
12+
SafeRefCount() {}
13+
SafeRefCount(int initial_count) : _count(initial_count) {}
14+
15+
inline int add() {
16+
return _count.fetch_add(1, std::memory_order_acq_rel);
17+
}
18+
19+
inline int remove() {
20+
const int previous_count = _count.fetch_sub(1, std::memory_order_acq_rel);
21+
ZN_ASSERT_RETURN_V_MSG(
22+
previous_count != 0, previous_count, "Trying to decrease refcount when it's already zero");
23+
return previous_count;
24+
}
25+
26+
inline int get() const {
27+
return _count;
28+
}
29+
30+
private:
31+
std::atomic_int32_t _count = { 0 };
32+
};
33+
34+
} // namespace zylann
35+
36+
#endif // ZN_SAFE_REF_COUNT_H

0 commit comments

Comments
 (0)