godot: Godot 3.2.4rc3 mono crashes on exit with specific combination of packed scene and dynamic font

Godot version:

v3.2.4.rc3.mono.official

OS/device including version: MacOS 11.2.2

OpenGL ES 3.0 Renderer: Intel® Iris™ Plus Graphics 650 OpenGL ES Batching: ON

Issue description:

Sorry the title is such a mess. I couldn’t figure out a way to summarize this succinctly.

Godot segfaults on exit if the following sequence occurs:

  • Load a scene with a monoscript containing an exported PackedScene variable
  • Change scenes to that packed scene.
  • If the packed scene uses a control with a DynamicFont Godot will now segfault when quitting producing a traceback such as the following.

TRACEBACK.txt

Concretely if I have a button with this script

using Godot;

public class AButton : Button
{
    [Export] private PackedScene Other;
    public override void _Pressed()
    {
        base._Pressed();
        GetTree().ChangeSceneTo(Other);
    }
}

and the scene Other contains a dynamic font, then the game crashes on exit if the scene Other has been loaded.

Steps to reproduce: The conditions for this are somewhat specific but I’ve been able to reproduce them reliably with the following.

  • In the example project below. Run the scene AScene.tscn
  • Click the button
  • Quit At this point the game crashes on exit and dumps a stacktrace.

Note that no crash occurs if:

  • The second scene isn’t loaded
  • The second scene isn’t loaded from the packed scene variable
  • The second scene does not use a DynamicFont

Minimal reproduction project:

SampleCrash.zip

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Comments: 21 (19 by maintainers)

Most upvoted comments

I meant the Godot debugger, if it some day it gets features to debug GDScript threads. But, again, that’s not relevant now. If that happens at some point, we will just decide how to deal with it then.

For Godot debugger we can add get/set_thread_name() and Map<ID, String> to make it even more readable.

Indeed, see #46411 (comment) where @neikeq has come to the same conclusion (he’s preparing a fix).

In case it’s useful, here’s my patch, which fixes crash on macOS (I have not tested it on any other platforms).

diff --git a/core/os/thread.cpp b/core/os/thread.cpp
index 58b29b9802..61cc31e154 100644
--- a/core/os/thread.cpp
+++ b/core/os/thread.cpp
@@ -41,9 +41,13 @@ void (*Thread::set_priority_func)(Thread::Priority) = nullptr;
 void (*Thread::init_func)() = nullptr;
 void (*Thread::term_func)() = nullptr;
 
-Thread::ID Thread::main_thread_id = 1;
-SafeNumeric<Thread::ID> Thread::last_thread_id{ 1 };
-thread_local Thread::ID Thread::caller_id = 1;
+uint64_t _thread_id_hash(const std::thread::id &p_t) {
+	static std::hash<std::thread::id> hasher;
+	return hasher(p_t);
+}
+
+Thread::ID Thread::main_thread_id = _thread_id_hash(std::this_thread::get_id());
+thread_local Thread::ID Thread::caller_id = _thread_id_hash(std::this_thread::get_id());
 
 void Thread::_set_platform_funcs(
 		Error (*p_set_name_func)(const String &),
@@ -57,7 +61,7 @@ void Thread::_set_platform_funcs(
 }
 
 void Thread::callback(Thread *p_self, const Settings &p_settings, Callback p_callback, void *p_userdata) {
-	Thread::caller_id = p_self->id;
+	Thread::caller_id = _thread_id_hash(p_self->thread.get_id());
 	if (set_priority_func) {
 		set_priority_func(p_settings.priority);
 	}
@@ -73,7 +77,7 @@ void Thread::callback(Thread *p_self, const Settings &p_settings, Callback p_cal
 }
 
 void Thread::start(Thread::Callback p_callback, void *p_user, const Settings &p_settings) {
-	if (id != 0) {
+	if (id != _thread_id_hash(std::thread::id())) {
 #ifdef DEBUG_ENABLED
 		WARN_PRINT("A Thread object has been re-started without wait_to_finish() having been called on it. Please do so to ensure correct cleanup of the thread.");
 #endif
@@ -81,22 +85,22 @@ void Thread::start(Thread::Callback p_callback, void *p_user, const Settings &p_
 		std::thread empty_thread;
 		thread.swap(empty_thread);
 	}
-	id = last_thread_id.increment();
 	std::thread new_thread(&Thread::callback, this, p_settings, p_callback, p_user);
 	thread.swap(new_thread);
+	id = _thread_id_hash(thread.get_id());
 }
 
 bool Thread::is_started() const {
-	return id != 0;
+	return id != _thread_id_hash(std::thread::id());
 }
 
 void Thread::wait_to_finish() {
-	if (id != 0) {
+	if (id != _thread_id_hash(std::thread::id())) {
 		ERR_FAIL_COND_MSG(id == get_caller_id(), "A Thread can't wait for itself to finish.");
 		thread.join();
 		std::thread empty_thread;
 		thread.swap(empty_thread);
-		id = 0;
+		id = _thread_id_hash(std::thread::id());
 	}
 }
 
@@ -109,10 +113,10 @@ Error Thread::set_name(const String &p_name) {
 }
 
 Thread::Thread() :
-		id(0) {}
+		id(_thread_id_hash(std::thread::id())) {}
 
 Thread::~Thread() {
-	if (id != 0) {
+	if (id != _thread_id_hash(std::thread::id())) {
 #ifdef DEBUG_ENABLED
 		WARN_PRINT("A Thread object has been destroyed without wait_to_finish() having been called on it. Please do so to ensure correct cleanup of the thread.");
 #endif
diff --git a/core/os/thread.h b/core/os/thread.h
index 837f9323c3..38763e2ebc 100644
--- a/core/os/thread.h
+++ b/core/os/thread.h
@@ -62,7 +62,6 @@ private:
 	friend class Main;
 
 	static ID main_thread_id;
-	static SafeNumeric<ID> last_thread_id;
 
 	ID id;
 	static thread_local ID caller_id;

Edit: Added default std::thread::id() values instead of zeros.