From 8a9fd16112e68d79faadfb9df2f2e0b7371321ff Mon Sep 17 00:00:00 2001 From: Miguel de Icaza Date: Mon, 23 Mar 2026 21:42:53 -0400 Subject: [PATCH] Fixes a crash in viewport.cpp in the wild while running a game (#117775) I believe the crash is a stale-lifetime bug in 3D picking. Viewport::_process_picking() caches last_object / last_id for multiple queued input events at the same pointer position. If the first _input_event callback removes the picked CollisionObject3D or current Camera3D from the tree, the next queued event can still reuse that cached target and call _collision_object_3d_input_event(), which immediately does get_global_transform on an object that is no longer safe to query. The patch does four things: 1. It rejects captured 3D pick targets that are no longer is_inside_tree() before reuse at viewport.cpp (similar changes exist in the code): 2. It re-fetches the cached same-position object from ObjectDB, verifies it still matches the cached pointer, and verifies it is still in the tree before dispatching. 3. It skips _mouse_exit() for a hovered 3D object that has already left the tree at viewport.cpp. 4. It adds a final guard inside _collision_object_3d_input_event() so even a future caller cannot query transforms on removed nodes or cameras at viewport.cpp. --- scene/main/viewport.cpp | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/scene/main/viewport.cpp b/scene/main/viewport.cpp index 964ae08bab..42cd651efc 100644 --- a/scene/main/viewport.cpp +++ b/scene/main/viewport.cpp @@ -1001,7 +1001,7 @@ void Viewport::_process_picking() { CollisionObject3D *capture_object = nullptr; if (physics_object_capture.is_valid()) { capture_object = ObjectDB::get_instance(physics_object_capture); - if (!capture_object || !camera_3d || (mb.is_valid() && mb->get_button_index() == MouseButton::LEFT && !mb->is_pressed())) { + if (!capture_object || !capture_object->is_inside_tree() || !camera_3d || (mb.is_valid() && mb->get_button_index() == MouseButton::LEFT && !mb->is_pressed())) { physics_object_capture = ObjectID(); } else { last_id = physics_object_capture; @@ -1011,12 +1011,15 @@ void Viewport::_process_picking() { if (pos == last_pos) { if (last_id.is_valid()) { - if (ObjectDB::get_instance(last_id) && last_object) { - // Good, exists. - _collision_object_3d_input_event(last_object, camera_3d, ev, result.position, result.normal, result.shape); - if (last_object->get_capture_input_on_drag() && mb.is_valid() && mb->get_button_index() == MouseButton::LEFT && mb->is_pressed()) { + CollisionObject3D *current_last_object = ObjectDB::get_instance(last_id); + if (current_last_object && current_last_object == last_object && current_last_object->is_inside_tree()) { + _collision_object_3d_input_event(current_last_object, camera_3d, ev, result.position, result.normal, result.shape); + if (current_last_object->get_capture_input_on_drag() && mb.is_valid() && mb->get_button_index() == MouseButton::LEFT && mb->is_pressed()) { physics_object_capture = last_id; } + } else { + last_id = ObjectID(); + last_object = nullptr; } } } else { @@ -1050,7 +1053,7 @@ void Viewport::_process_picking() { if (is_mouse && new_collider != physics_object_over) { if (physics_object_over.is_valid()) { CollisionObject3D *previous_co = ObjectDB::get_instance(physics_object_over); - if (previous_co) { + if (previous_co && previous_co->is_inside_tree()) { previous_co->_mouse_exit(); } } @@ -4723,6 +4726,13 @@ void Viewport::_audio_listener_3d_make_next_current(AudioListener3D *p_exclude) #ifndef PHYSICS_3D_DISABLED void Viewport::_collision_object_3d_input_event(CollisionObject3D *p_object, Camera3D *p_camera, const Ref &p_input_event, const Vector3 &p_pos, const Vector3 &p_normal, int p_shape) { + ERR_FAIL_NULL(p_object); + ERR_FAIL_NULL(p_camera); + if (!p_object->is_inside_tree() || !p_camera->is_inside_tree()) { + physics_last_id = ObjectID(); + return; + } + Transform3D object_transform = p_object->get_global_transform(); Transform3D camera_transform = p_camera->get_global_transform(); ObjectID id = p_object->get_instance_id();