From 811970a306500ad7b26d2086fd4931a06ac54cf5 Mon Sep 17 00:00:00 2001 From: Sara Date: Wed, 25 Feb 2026 18:57:06 +0100 Subject: [PATCH] chore: massively sped up mesh gen performance --- modules/terrain/terrain.cpp | 132 +++++++++++++++++++-------- modules/terrain/terrain.h | 8 +- modules/terrain/terrain_chunk.cpp | 18 ++-- modules/terrain/terrain_chunk.h | 6 +- modules/terrain/terrain_modifier.cpp | 83 ++++++++++++----- modules/terrain/terrain_modifier.h | 19 +++- project/scenes/terrain_test.tscn | 14 +-- 7 files changed, 201 insertions(+), 79 deletions(-) diff --git a/modules/terrain/terrain.cpp b/modules/terrain/terrain.cpp index ac4f8fc8..e593dfee 100644 --- a/modules/terrain/terrain.cpp +++ b/modules/terrain/terrain.cpp @@ -6,50 +6,62 @@ void Terrain::_bind_methods() { BIND_PROPERTY(Variant::INT, side_length); BIND_PROPERTY(Variant::INT, chunk_size); BIND_PROPERTY(Variant::INT, detail); + BIND_PROPERTY(Variant::INT, thread_count); } void Terrain::ready() { - threads.resize_initialized(22); construct_chunk_grid(); generate_meshes(); } -void Terrain::update_modifier_list() { - bool editor{ Engine::get_singleton()->is_editor_hint() }; - this->modifiers.clear(); - for (Variant var : get_children()) { - if (TerrainModifier * mod{ cast_to(var) }) { - if (editor && !mod->is_connected(TerrainModifier::sig_changed, callable_mp(this, &self_type::on_terrain_changed))) { - mod->connect(TerrainModifier::sig_changed, callable_mp(this, &self_type::on_terrain_changed)); - } - this->modifiers.push_back(mod); - } +void Terrain::process() { + if (!is_inside_tree()) { + return; + } + if (this->pending_tasks == 0 && this->dirty) { + this->workload_lock.lock(); + if (this->workload.is_empty()) { + this->dirty = false; + this->workload.append_array(this->meshes); + this->pending_tasks = this->workload.size(); + } + this->workload_lock.unlock(); } - on_terrain_changed(); } void Terrain::child_order_changed() { - // TODO: check if the order of _modifiers_ actually changed + Callable on_changed_callable{ callable_mp(this, &self_type::on_terrain_changed) }; + // check if the modifiers actually changed if (is_ready()) { - update_modifier_list(); - } -} - -void Terrain::child_entered(Node *node) { - if (cast_to(node) != nullptr && is_ready()) { - update_modifier_list(); + bool modified{ false }; + size_t idx{ 0 }; + for (Variant var : get_children()) { + if (TerrainModifier * mod{ cast_to(var) }) { + if (modified) { + this->modifiers.push_back(mod); + } else if (idx >= this->modifiers.size() || mod != this->modifiers[idx]) { + modified = true; + this->modifiers.resize(idx); + this->modifiers.push_back(mod); + } else { + idx++; + } + if (!mod->is_connected(TerrainModifier::sig_changed, on_changed_callable)) { + mod->connect(TerrainModifier::sig_changed, on_changed_callable); + } + } + } + if (modified && is_ready()) { + on_terrain_changed(); + } } } void Terrain::child_exiting(Node *node) { if (TerrainModifier * mod{ cast_to(node) }) { - this->modifiers.erase(mod); if (Engine::get_singleton()->is_editor_hint() && !is_queued_for_deletion()) { mod->disconnect(TerrainModifier::sig_changed, callable_mp(this, &self_type::on_terrain_changed)); } - if (is_ready()) { - on_terrain_changed(); - } } } @@ -66,36 +78,62 @@ void Terrain::_notification(int what) { return; case NOTIFICATION_ENTER_TREE: if (!is_ready()) { - connect("child_entered_tree", callable_mp(this, &self_type::child_entered)); connect("child_exiting_tree", callable_mp(this, &self_type::child_exiting)); connect("child_order_changed", callable_mp(this, &self_type::child_order_changed)); } return; case NOTIFICATION_READY: + set_process(true); ready(); return; + case NOTIFICATION_PROCESS: + process(); + return; + case NOTIFICATION_EXIT_TREE: + this->workload_lock.lock(); + this->threads_stop = true; + this->workload_lock.unlock(); + for (Thread &thread : this->threads) { + thread.wait_to_finish(); + } + return; } } void Terrain::generate_meshes_thread(void *terrain) { Terrain *self{ static_cast(terrain) }; + print_line("thread", Thread::get_caller_id(), "start"); for (;;) { self->workload_lock.lock(); + if (self->threads_stop) { + self->workload_lock.unlock(); + print_line(Thread::get_caller_id(), "exiting"); + break; + } if (self->workload.is_empty()) { self->workload_lock.unlock(); - return; + Thread::yield(); + continue; } TerrainChunkMesh *mesh{ self->workload[0] }; self->workload.remove_at(0); self->workload_lock.unlock(); if (!mesh->is_inside_tree()) { - return; + print_line(Thread::get_caller_id(), "mesh is outside tree, exiting"); + break; } mesh->update_mesh(); + Thread::yield(); } + print_line(Thread::get_caller_id(), "done"); + return; } void Terrain::construct_chunk_grid() { + for (TerrainChunkMesh *mesh : this->meshes) { + mesh->queue_free(); + } + this->meshes.clear(); size_t const chunks_per_side{ this->side_length / this->chunk_size }; Vector3 const origin{ (float)this->chunk_size / 2.f, 0.f, (float)this->chunk_size / 2.f }; for (size_t y{ 0 }; y < chunks_per_side; ++y) { @@ -113,18 +151,7 @@ void Terrain::construct_chunk_grid() { } void Terrain::generate_meshes() { - if (!is_inside_tree()) { - return; - } - this->workload.append_array(this->meshes); - for (Thread &thread : this->threads) { - thread.start(&self_type::generate_meshes_thread, this); - } - for (Thread &thread : this->threads) { - if (thread.is_started()) { - thread.wait_to_finish(); - } - } + dirty = true; } float Terrain::height_at(Vector2 world_coordinate) { @@ -173,3 +200,30 @@ void Terrain::set_detail(size_t detail) { size_t Terrain::get_detail() const { return this->detail; } + +void Terrain::set_thread_count(size_t num) { + this->workload_lock.lock(); + this->threads_stop = true; + this->workload_lock.unlock(); + for (Thread &thread : this->threads) { + thread.wait_to_finish(); + } + this->threads_stop = false; + this->threads.resize_initialized(num); + for (Thread &thread : this->threads) { + thread.start(&self_type::generate_meshes_thread, this); + } +} + +size_t Terrain::get_thread_count() const { + return this->threads.size(); +} + +void Terrain::mesh_task_complete() { + this->pending_tasks--; + if (this->pending_tasks == 0) { + for (TerrainModifier *mod : this->modifiers) { + mod->set_dirty(false); + } + } +} diff --git a/modules/terrain/terrain.h b/modules/terrain/terrain.h index 594dc810..a8cc9fa0 100644 --- a/modules/terrain/terrain.h +++ b/modules/terrain/terrain.h @@ -13,7 +13,7 @@ class Terrain : public Node { GDCLASS(Terrain, Node); static void _bind_methods(); void ready(); - void update_modifier_list(); + void process(); void child_order_changed(); void child_entered(Node *node); @@ -29,12 +29,15 @@ public: void construct_chunk_grid(); void generate_meshes(); float height_at(Vector2 world_coordinate); + void mesh_task_complete(); private: Timer *timer{ nullptr }; size_t side_length{ 200 }; size_t chunk_size{ 50 }; size_t detail{ 1 }; + bool dirty{ false }; + size_t pending_tasks{ 0 }; public: void set_side_length(size_t length); @@ -43,10 +46,13 @@ public: size_t get_chunk_size() const; void set_detail(size_t detail); size_t get_detail() const; + void set_thread_count(size_t num); + size_t get_thread_count() const; private: Vector workload{}; Mutex workload_lock; + bool threads_stop{ false }; Vector meshes{}; Vector modifiers{}; LocalVector threads{}; diff --git a/modules/terrain/terrain_chunk.cpp b/modules/terrain/terrain_chunk.cpp index a429a1c4..fcad2d4a 100644 --- a/modules/terrain/terrain_chunk.cpp +++ b/modules/terrain/terrain_chunk.cpp @@ -5,6 +5,11 @@ void TerrainChunkMesh::_bind_methods() {} +void TerrainChunkMesh::apply_new_mesh() { + set_mesh(this->new_mesh); + this->terrain->mesh_task_complete(); +} + void TerrainChunkMesh::generate_vertices() { ERR_FAIL_COND_EDMSG(this->terrain == nullptr, "TerrainChunkMesh::generate_vertices: no terrain assigned"); ERR_FAIL_COND_EDMSG(this->size <= 0.f, "TerrainChunkMesh::generate_vertices: size <= 0"); @@ -56,9 +61,7 @@ void TerrainChunkMesh::_notification(int what) { default: return; case NOTIFICATION_READY: - if (!get_mesh().is_valid()) { - set_mesh(memnew(ArrayMesh)); - } + set_process_thread_group(ProcessThreadGroup::PROCESS_THREAD_GROUP_SUB_THREAD); this->safe_position = get_global_position(); return; } @@ -67,14 +70,17 @@ void TerrainChunkMesh::_notification(int what) { void TerrainChunkMesh::update_mesh() { ERR_FAIL_COND_EDMSG(this->size <= 0.f, "TerrainChunkMesh::generate: size <= 0"); ERR_FAIL_COND_EDMSG(points_per_side() <= 0, "TerrainChunkMesh::generate: points per side <= 0"); - this->surface->clear(); + this->lock.lock(); + this->surface = memnew(SurfaceTool); this->surface->begin(Mesh::PRIMITIVE_TRIANGLES); generate_vertices(); generate_faces(); this->surface->generate_normals(); this->surface->generate_tangents(); - callable_mp(Ref(this->mesh).ptr(), &ArrayMesh::clear_surfaces).call_deferred(); - callable_mp(Ref(this->mesh).ptr(), &ArrayMesh::add_surface_from_arrays).call_deferred(Mesh::PRIMITIVE_TRIANGLES, this->surface->commit_to_arrays(), TypedArray(), Dictionary(), 0); + this->new_mesh = memnew(ArrayMesh); + this->surface->commit(this->new_mesh); + callable_mp(this, &self_type::apply_new_mesh).call_deferred(); + this->lock.unlock(); } size_t TerrainChunkMesh::points_per_side() const { diff --git a/modules/terrain/terrain_chunk.h b/modules/terrain/terrain_chunk.h index 86fa47f1..e1105444 100644 --- a/modules/terrain/terrain_chunk.h +++ b/modules/terrain/terrain_chunk.h @@ -2,12 +2,14 @@ #include "macros.h" #include "scene/3d/mesh_instance_3d.h" +#include "scene/resources/mesh.h" #include "scene/resources/surface_tool.h" class Terrain; class TerrainChunkMesh : public MeshInstance3D { GDCLASS(TerrainChunkMesh, MeshInstance3D); static void _bind_methods(); + void apply_new_mesh(); void generate_vertices(); void generate_faces(); @@ -19,8 +21,10 @@ public: size_t points_per_side() const; private: + Mutex lock{}; Vector3 safe_position{}; - Ref surface{ memnew(SurfaceTool) }; + Ref surface{}; + Ref new_mesh{}; Terrain *terrain{ nullptr }; size_t detail{ 1 }; size_t size{ 1 }; diff --git a/modules/terrain/terrain_modifier.cpp b/modules/terrain/terrain_modifier.cpp index d85ac271..fbb897bc 100644 --- a/modules/terrain/terrain_modifier.cpp +++ b/modules/terrain/terrain_modifier.cpp @@ -57,6 +57,7 @@ float TerrainModifier::blend(float under, float over) { } void TerrainModifier::changed() { + this->dirty = true; emit_signal(sig_changed); } @@ -94,15 +95,46 @@ TerrainModifier::BlendMode TerrainModifier::get_blend_mode() const { String const TerrainModifier::sig_changed{ "changed" }; +void ReadWriteMutex::lock_read() { + this->read_lock.lock(); + this->read_count++; + this->read_lock.unlock(); +} + +void ReadWriteMutex::unlock_read() { + this->read_lock.lock(); + this->read_count--; + this->read_lock.unlock(); +} + +void ReadWriteMutex::lock_write() { + while (true) { + this->read_lock.lock(); + if (this->read_count == 0) { + return; + } + this->read_lock.unlock(); + } +} + +void ReadWriteMutex::unlock_write() { + this->read_lock.unlock(); +} + void TerrainModifierDistance::_bind_methods() { BIND_HPROPERTY(Variant::OBJECT, distance_weight_curve, PROPERTY_HINT_RESOURCE_TYPE, "Curve"); BIND_HPROPERTY(Variant::OBJECT, distance_height_curve, PROPERTY_HINT_RESOURCE_TYPE, "Curve"); } void TerrainModifierDistance::curves_changed() { - this->mtx.lock(); - this->safe_curves_dirty = true; - this->mtx.unlock(); + this->lock.lock_write(); + if (this->distance_height_curve.is_valid()) { + this->distance_height_curve->bake(); + } + if (this->distance_weight_curve.is_valid()) { + this->distance_weight_curve->bake(); + } + this->lock.unlock_write(); changed(); } @@ -112,27 +144,34 @@ float TerrainModifierDistance::distance_at(Vector2 const &world_coordinate) { } float TerrainModifierDistance::evaluate_at(Vector2 world_coordinate, float before) { - this->mtx.lock(); - if (this->safe_curves_dirty) { - this->safe_weight_curve = this->distance_weight_curve->duplicate_deep(); - this->safe_weight_curve->bake(); - this->safe_height_curve = this->distance_height_curve->duplicate_deep(); - this->safe_height_curve->bake(); - this->safe_curves_dirty = false; - } - this->mtx.unlock(); - if (this->safe_weight_curve.is_null() || this->safe_height_curve.is_null()) { + this->lock.lock_read(); + if (this->distance_weight_curve.is_null() || this->distance_height_curve.is_null()) { + this->lock.unlock_read(); return before; } float const distance{ distance_at(world_coordinate) }; + if (distance >= this->distance_weight_curve->get_max_domain()) { + this->lock.unlock_read(); + return before; + } float const weight_offset{ - std::clamp(distance, this->safe_weight_curve->get_min_domain(), this->safe_weight_curve->get_max_domain()) + std::clamp(distance, this->distance_weight_curve->get_min_domain(), this->distance_weight_curve->get_max_domain()) }; - float const weight{ this->safe_weight_curve->sample_baked(weight_offset) }; float const height_offset{ - std::clamp(distance, this->safe_height_curve->get_min_domain(), this->safe_height_curve->get_max_domain()) + std::clamp(distance, this->distance_height_curve->get_min_domain(), this->distance_height_curve->get_max_domain()) }; - return weight <= 0.f ? before : Math::lerp(before, blend(before, this->safe_height_curve->sample_baked(height_offset) + get_thread_safe_global_position().y), weight); + this->lock.unlock_read(); + + this->lock.lock_write(); + float const weight{ this->distance_weight_curve->sample_baked(weight_offset) }; + float const height{ this->distance_height_curve->sample_baked(height_offset) }; + this->lock.unlock_write(); + + this->lock.lock_read(); + float out{ weight <= 0.f ? before : Math::lerp(before, blend(before, height + get_thread_safe_global_position().y), weight) }; + this->lock.unlock_read(); + + return out; } PackedStringArray TerrainModifierDistance::get_configuration_warnings() const { @@ -147,6 +186,7 @@ PackedStringArray TerrainModifierDistance::get_configuration_warnings() const { } void TerrainModifierDistance::set_distance_weight_curve(Ref curve) { + this->lock.lock_write(); if (Engine::get_singleton()->is_editor_hint()) { if (this->distance_weight_curve.is_valid()) { this->distance_weight_curve->disconnect_changed(callable_mp(this, &self_type::curves_changed)); @@ -155,10 +195,10 @@ void TerrainModifierDistance::set_distance_weight_curve(Ref curve) { curve->connect_changed(callable_mp(this, &self_type::curves_changed)); } } - curves_changed(); this->distance_weight_curve = curve; + this->lock.unlock_write(); + curves_changed(); update_configuration_warnings(); - changed(); } Ref TerrainModifierDistance::get_distance_weight_curve() const { @@ -166,6 +206,7 @@ Ref TerrainModifierDistance::get_distance_weight_curve() const { } void TerrainModifierDistance::set_distance_height_curve(Ref curve) { + this->lock.lock_write(); if (Engine::get_singleton()->is_editor_hint()) { if (this->distance_height_curve.is_valid()) { this->distance_height_curve->disconnect_changed(callable_mp(this, &self_type::curves_changed)); @@ -174,10 +215,10 @@ void TerrainModifierDistance::set_distance_height_curve(Ref curve) { curve->connect_changed(callable_mp(this, &self_type::curves_changed)); } } - curves_changed(); this->distance_height_curve = curve; + this->lock.unlock_write(); + curves_changed(); update_configuration_warnings(); - changed(); } Ref TerrainModifierDistance::get_distance_height_curve() const { diff --git a/modules/terrain/terrain_modifier.h b/modules/terrain/terrain_modifier.h index 333f0306..1b433d72 100644 --- a/modules/terrain/terrain_modifier.h +++ b/modules/terrain/terrain_modifier.h @@ -26,6 +26,7 @@ private: float blend_distance{ 10.0 }; BlendMode blend_mode{ Add }; Vector3 thread_safe_global_position{}; + bool dirty{ false }; public: Vector3 get_thread_safe_global_position() const; @@ -33,12 +34,24 @@ public: float get_blend_distance() const; void set_blend_mode(BlendMode mode); BlendMode get_blend_mode() const; + GET_SET_FNS(bool, dirty); static String const sig_changed; }; MAKE_TYPE_INFO(TerrainModifier::BlendMode, Variant::INT); +struct ReadWriteMutex { + void lock_read(); + void unlock_read(); + void lock_write(); + void unlock_write(); + +private: + Mutex read_lock{}; + int read_count{}; +}; + class TerrainModifierDistance : public TerrainModifier { GDCLASS(TerrainModifierDistance, TerrainModifier); static void _bind_methods(); @@ -52,14 +65,10 @@ public: PackedStringArray get_configuration_warnings() const override; private: - Mutex mtx{}; + ReadWriteMutex lock{}; Ref distance_weight_curve{}; Ref distance_height_curve{}; - bool safe_curves_dirty{ false }; - Ref safe_weight_curve{}; - Ref safe_height_curve{}; - public: void set_distance_weight_curve(Ref curve); Ref get_distance_weight_curve() const; diff --git a/project/scenes/terrain_test.tscn b/project/scenes/terrain_test.tscn index 808568a5..3022edec 100644 --- a/project/scenes/terrain_test.tscn +++ b/project/scenes/terrain_test.tscn @@ -14,14 +14,14 @@ sky = SubResource("Sky_w3uoq") [sub_resource type="Curve" id="Curve_kbmr5"] _limits = [0.0, 1.0, 0.0, 200.0] -_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(62.138725, 0.41686416), -0.016318396, -0.016318396, 0, 0, Vector2(99.24809, 0), 0.0, 0.0, 0, 0] +_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(123.87009, 0.38367254), -0.016318396, -0.016318396, 0, 0, Vector2(200, 0), 0.0, 0.0, 0, 0] point_count = 3 [sub_resource type="Curve" id="Curve_w3uoq"] [sub_resource type="Curve" id="Curve_chm2y"] _limits = [0.0, 1.0, 0.0, 200.0] -_data = [Vector2(0, 1), 0.0, -0.010352392, 0, 0, Vector2(200, 0), -0.0016109183, -0.05797184, 0, 0] +_data = [Vector2(0, 1), 0.0, -0.010352392, 0, 0, Vector2(200, 0), 0.00017739221, -0.05797184, 0, 0] point_count = 2 [sub_resource type="Curve" id="Curve_o3i6r"] @@ -47,22 +47,24 @@ point_count = 1 environment = SubResource("Environment_o3i6r") [node name="Terrain" type="Terrain" parent="." unique_id=1169843565] -chunk_size = 25 +side_length = 1000 +chunk_size = 100 +thread_count = 4 [node name="TerrainModifierDistance" type="TerrainModifierDistance" parent="Terrain" unique_id=1885116624] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 155.48662, 58.282597, 65.78725) +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 701.1817, 109.88881, 615.8812) blend_distance = 4.0 distance_weight_curve = SubResource("Curve_kbmr5") distance_height_curve = SubResource("Curve_w3uoq") [node name="TerrainModifierDistance3" type="TerrainModifierDistance" parent="Terrain" unique_id=1846439541] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 39.480015, 95.0672, 37.15439) +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 138.05537, 108.75946, 327.30096) blend_distance = 4.0 distance_weight_curve = SubResource("Curve_chm2y") distance_height_curve = SubResource("Curve_o3i6r") [node name="TerrainModifierDistance2" type="TerrainModifierDistance" parent="Terrain" unique_id=2110821264] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 183.81352, -21.613277, 195.71535) +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 183.81352, -28.866524, 195.71535) blend_mode = 1 distance_weight_curve = SubResource("Curve_nonsf") distance_height_curve = SubResource("Curve_4kj3c")