Skip to content

Commit 92e0a04

Browse files
committed
Prevent crashes on extension reload by ensuring signal teardown
1 parent 777454d commit 92e0a04

4 files changed

Lines changed: 89 additions & 7 deletions

File tree

stagehand/nodes/instanced_renderer_3d.cpp

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -185,28 +185,44 @@ void InstancedRenderer3D::set_prefabs_rendered(const godot::PackedStringArray &p
185185
update_configuration_warnings();
186186
}
187187

188-
void InstancedRenderer3D::set_lod_levels(const godot::TypedArray<InstancedRenderer3DLODConfiguration> &p_lod_levels) {
188+
void InstancedRenderer3D::connect_configuration_change_signals() {
189189
const godot::Callable warnings_callable = callable_mp(static_cast<godot::Node *>(this), &godot::Node::update_configuration_warnings);
190190

191191
for (int i = 0; i < lod_levels.size(); ++i) {
192-
godot::Ref<InstancedRenderer3DLODConfiguration> previous_lod_level = lod_levels[i];
193-
if (previous_lod_level.is_valid() && previous_lod_level->is_connected("changed", warnings_callable)) {
194-
previous_lod_level->disconnect("changed", warnings_callable);
192+
godot::Ref<InstancedRenderer3DLODConfiguration> lod_level = lod_levels[i];
193+
if (lod_level.is_valid() && !lod_level->is_connected("changed", warnings_callable)) {
194+
lod_level->connect("changed", warnings_callable);
195195
}
196196
}
197+
}
197198

198-
lod_levels = p_lod_levels;
199+
void InstancedRenderer3D::disconnect_configuration_change_signals() {
200+
const godot::Callable warnings_callable = callable_mp(static_cast<godot::Node *>(this), &godot::Node::update_configuration_warnings);
199201

200202
for (int i = 0; i < lod_levels.size(); ++i) {
201203
godot::Ref<InstancedRenderer3DLODConfiguration> lod_level = lod_levels[i];
202-
if (lod_level.is_valid() && !lod_level->is_connected("changed", warnings_callable)) {
203-
lod_level->connect("changed", warnings_callable);
204+
if (lod_level.is_valid() && lod_level->is_connected("changed", warnings_callable)) {
205+
lod_level->disconnect("changed", warnings_callable);
204206
}
205207
}
208+
}
209+
210+
void InstancedRenderer3D::set_lod_levels(const godot::TypedArray<InstancedRenderer3DLODConfiguration> &p_lod_levels) {
211+
disconnect_configuration_change_signals();
212+
213+
lod_levels = p_lod_levels;
214+
215+
connect_configuration_change_signals();
206216

207217
update_configuration_warnings();
208218
}
209219

220+
void InstancedRenderer3D::_notification(int p_what) {
221+
if (p_what == godot::Node::NOTIFICATION_EXIT_TREE || p_what == godot::Object::NOTIFICATION_PREDELETE) {
222+
disconnect_configuration_change_signals();
223+
}
224+
}
225+
210226
godot::PackedStringArray InstancedRenderer3D::_get_configuration_warnings() const {
211227
godot::PackedStringArray warnings;
212228

stagehand/nodes/instanced_renderer_3d.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,12 @@ class InstancedRenderer3D : public godot::Node3D {
9292

9393
protected:
9494
static void _bind_methods();
95+
void _notification(int p_what);
9596

9697
private:
98+
void connect_configuration_change_signals();
99+
void disconnect_configuration_change_signals();
100+
97101
godot::PackedStringArray prefabs_rendered;
98102
godot::TypedArray<InstancedRenderer3DLODConfiguration> lod_levels;
99103
godot::Ref<godot::Material> material;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
extends Node3D
2+
3+
4+
func _ready() -> void:
5+
print("Test: InstancedRenderer3D LOD signal cleanup")
6+
7+
var renderer = $InstancedRenderer3D
8+
var lod_levels = renderer.get_lod_levels()
9+
assert_eq(lod_levels.size(), 1, "Single LOD level configured")
10+
11+
renderer.set_lod_levels(lod_levels)
12+
13+
var lod_resource = lod_levels[0]
14+
assert_true(lod_resource.get_signal_connection_list("changed").size() > 0, "LOD resource changed signal connected while renderer is alive")
15+
16+
renderer.queue_free()
17+
await get_tree().process_frame
18+
await get_tree().process_frame
19+
20+
assert_true(not is_instance_valid(renderer), "Renderer freed cleanly")
21+
assert_eq(lod_resource.get_signal_connection_list("changed").size(), 0, "LOD resource changed signal disconnected after renderer teardown")
22+
23+
lod_resource.set_mesh(SphereMesh.new())
24+
print(" PASS: LOD resource updates remain stable after renderer teardown")
25+
26+
print("InstancedRenderer3D LOD signal cleanup test passed!")
27+
get_tree().quit(0)
28+
29+
30+
func assert_eq(actual, expected, label: String) -> void:
31+
if actual != expected:
32+
_fail("%s: expected %s, got %s" % [label, str(expected), str(actual)])
33+
else:
34+
print(" PASS: %s" % label)
35+
36+
37+
func assert_true(value: bool, label: String) -> void:
38+
if not value:
39+
_fail(label)
40+
else:
41+
print(" PASS: %s" % label)
42+
43+
44+
func _fail(msg: String) -> void:
45+
print("FAIL: %s" % msg)
46+
get_tree().quit(1)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[gd_scene format=3]
2+
3+
[ext_resource type="Script" path="res://tests/instanced_renderer_lod_cleanup/instanced_renderer_lod_cleanup.gd" id="1_script"]
4+
5+
[sub_resource type="SphereMesh" id="SphereMesh_lod0"]
6+
7+
[sub_resource type="InstancedRenderer3DLODConfiguration" id="LODResource_0"]
8+
mesh = SubResource("SphereMesh_lod0")
9+
visibility_range_begin = 0.0
10+
visibility_range_end = 50.0
11+
12+
[node name="InstancedRendererLODSignalCleanup" type="Node3D"]
13+
script = ExtResource("1_script")
14+
15+
[node name="InstancedRenderer3D" type="InstancedRenderer3D" parent="."]
16+
lod_levels = [SubResource("LODResource_0")]

0 commit comments

Comments
 (0)