godot_voxel/terrain/voxel_mesh_updater.cpp

272 lines
6.4 KiB
C++

#include "voxel_mesh_updater.h"
#include "../util/utility.h"
#include <core/os/os.h>
VoxelMeshUpdater::VoxelMeshUpdater(Ref<VoxelLibrary> library, MeshingParams params) {
if (library.is_valid()) {
_blocky_mesher.instance();
_blocky_mesher->set_library(library);
_blocky_mesher->set_occlusion_enabled(params.baked_ao);
_blocky_mesher->set_occlusion_darkness(params.baked_ao_darkness);
}
if (params.smooth_surface) {
_dmc_mesher.instance();
_dmc_mesher->set_geometric_error(0.05);
_dmc_mesher->set_octree_mode(VoxelMesherDMC::OCTREE_NONE);
}
_input_mutex = Mutex::create();
_output_mutex = Mutex::create();
_thread_exit = false;
_semaphore = Semaphore::create();
_thread = Thread::create(_thread_func, this);
_needs_sort = true;
}
VoxelMeshUpdater::~VoxelMeshUpdater() {
_thread_exit = true;
_semaphore->post();
Thread::wait_to_finish(_thread);
memdelete(_thread);
memdelete(_semaphore);
memdelete(_input_mutex);
memdelete(_output_mutex);
}
void VoxelMeshUpdater::push(const Input &input) {
bool should_run = false;
int replaced_blocks = 0;
{
MutexLock lock(_input_mutex);
for (int i = 0; i < input.blocks.size(); ++i) {
Vector3i pos = input.blocks[i].position;
// If a block is exactly on the priority position, update it instantly on the main thread
// This is to eliminate latency for player's actions, assuming updating a block isn't slower than a frame
/*if (pos == _shared_input.priority_position) {
OutputBlock ob;
process_block(_shared_input.blocks[i], ob);
{
MutexLock lock2(_output_mutex);
_shared_output.blocks.push_back(ob);
}
continue;
}*/
int *index = _block_indexes.getptr(pos);
if (index) {
// The block is already in the update queue, replace it
++replaced_blocks;
_shared_input.blocks.write[*index] = input.blocks[i];
} else {
int j = _shared_input.blocks.size();
_shared_input.blocks.push_back(input.blocks[i]);
_block_indexes[pos] = j;
}
}
if (_shared_input.priority_position != input.priority_position || input.blocks.size() > 0) {
_needs_sort = true;
}
_shared_input.priority_position = input.priority_position;
should_run = !_shared_input.is_empty();
}
if (replaced_blocks > 0) {
print_line(String("VoxelMeshUpdater: {0} blocks already in queue were replaced").format(varray(replaced_blocks)));
}
if (should_run) {
_semaphore->post();
}
}
void VoxelMeshUpdater::pop(Output &output) {
MutexLock lock(_output_mutex);
output.blocks.append_array(_shared_output.blocks);
output.stats = _shared_output.stats;
_shared_output.blocks.clear();
}
int VoxelMeshUpdater::get_required_padding() const {
int padding = 0;
if (_blocky_mesher.is_valid()) {
padding = max(padding, _blocky_mesher->get_minimum_padding());
}
if (_dmc_mesher.is_valid()) {
padding = max(padding, _dmc_mesher->get_minimum_padding());
}
return padding;
}
void VoxelMeshUpdater::_thread_func(void *p_self) {
VoxelMeshUpdater *self = reinterpret_cast<VoxelMeshUpdater *>(p_self);
self->thread_func();
}
void VoxelMeshUpdater::thread_func() {
while (!_thread_exit) {
uint32_t sync_interval = 50.0; // milliseconds
uint32_t sync_time = OS::get_singleton()->get_ticks_msec() + sync_interval;
int queue_index = 0;
Stats stats;
thread_sync(queue_index, stats);
while (!_input.blocks.empty() && !_thread_exit) {
if (!_input.blocks.empty()) {
InputBlock block = _input.blocks[queue_index];
++queue_index;
if (queue_index >= _input.blocks.size()) {
_input.blocks.clear();
}
uint64_t time_before = OS::get_singleton()->get_ticks_usec();
OutputBlock ob;
process_block(block, ob);
uint64_t time_taken = OS::get_singleton()->get_ticks_usec() - time_before;
// Do some stats
if (stats.first) {
stats.first = false;
stats.min_time = time_taken;
stats.max_time = time_taken;
} else {
if (time_taken < stats.min_time)
stats.min_time = time_taken;
if (time_taken > stats.max_time)
stats.max_time = time_taken;
}
_output.blocks.push_back(ob);
}
uint32_t time = OS::get_singleton()->get_ticks_msec();
if (time >= sync_time || _input.blocks.empty()) {
thread_sync(queue_index, stats);
sync_time = OS::get_singleton()->get_ticks_msec() + sync_interval;
queue_index = 0;
stats = Stats();
}
}
if (_thread_exit) {
break;
}
// Wait for future wake-up
_semaphore->wait();
}
}
void VoxelMeshUpdater::process_block(const InputBlock &block, OutputBlock &output) {
CRASH_COND(block.voxels.is_null());
int padding = get_required_padding();
if (_blocky_mesher.is_valid()) {
_blocky_mesher->build(output.blocky_surfaces, **block.voxels, padding);
}
if (_dmc_mesher.is_valid()) {
_dmc_mesher->build(output.smooth_surfaces, **block.voxels, padding);
}
output.position = block.position;
}
// Sorts distance to viewer
// The closest block will be the first one in the array
struct BlockUpdateComparator {
Vector3i center;
inline bool operator()(const VoxelMeshUpdater::InputBlock &a, const VoxelMeshUpdater::InputBlock &b) const {
return a.position.distance_sq(center) < b.position.distance_sq(center);
}
};
void VoxelMeshUpdater::thread_sync(int queue_index, Stats stats) {
if (!_input.blocks.empty()) {
// Cleanup input vector
if (queue_index >= _input.blocks.size()) {
_input.blocks.clear();
} else if (queue_index > 0) {
// Shift up remaining items since we use a Vector
shift_up(_input.blocks, queue_index);
}
}
stats.remaining_blocks = _input.blocks.size();
bool needs_sort;
{
// Get input
MutexLock lock(_input_mutex);
_input.blocks.append_array(_shared_input.blocks);
_input.priority_position = _shared_input.priority_position;
_shared_input.blocks.clear();
_block_indexes.clear();
needs_sort = _needs_sort;
_needs_sort = false;
}
if (!_output.blocks.empty()) {
// print_line(String("VoxelMeshUpdater: posting {0} blocks, {1} remaining ; cost [{2}..{3}] usec")
// .format(varray(_output.blocks.size(), _input.blocks.size(), stats.min_time, stats.max_time)));
// Post output
MutexLock lock(_output_mutex);
_shared_output.blocks.append_array(_output.blocks);
_shared_output.stats = stats;
_output.blocks.clear();
}
if (!_input.blocks.empty() && needs_sort) {
// Re-sort priority
SortArray<VoxelMeshUpdater::InputBlock, BlockUpdateComparator> sorter;
sorter.compare.center = _input.priority_position;
sorter.sort(_input.blocks.ptrw(), _input.blocks.size());
}
}