feat: updated engine

This commit is contained in:
Sara Gerretsen 2026-07-10 17:04:34 +02:00
parent cbe99774ff
commit f4cf6b3999
6607 changed files with 910135 additions and 430025 deletions

View file

@ -30,11 +30,15 @@
#include "resource_loader.h"
#include "core/config/engine.h"
#include "core/config/project_settings.h"
#include "core/core_bind.h"
#include "core/io/dir_access.h"
#include "core/io/file_access.h"
#include "core/io/resource_importer.h"
#include "core/object/callable_mp.h"
#include "core/object/class_db.h"
#include "core/object/message_queue.h"
#include "core/object/script_language.h"
#include "core/os/condition_variable.h"
#include "core/os/os.h"
@ -218,26 +222,6 @@ void ResourceFormatLoader::_bind_methods() {
GDVIRTUAL_BIND(_load, "path", "original_path", "use_sub_threads", "cache_mode");
}
///////////////////////////////////
// These are used before and after a wait for a WorkerThreadPool task
// because that can lead to another load started in the same thread,
// something we must treat as a different stack for the purposes
// of tracking nesting.
#define PREPARE_FOR_WTP_WAIT \
int load_nesting_backup = ResourceLoader::load_nesting; \
Vector<String> load_paths_stack_backup = ResourceLoader::load_paths_stack; \
ResourceLoader::load_nesting = 0; \
ResourceLoader::load_paths_stack.clear();
#define RESTORE_AFTER_WTP_WAIT \
DEV_ASSERT(ResourceLoader::load_nesting == 0); \
DEV_ASSERT(ResourceLoader::load_paths_stack.is_empty()); \
ResourceLoader::load_nesting = load_nesting_backup; \
ResourceLoader::load_paths_stack = load_paths_stack_backup; \
load_paths_stack_backup.clear();
// This should be robust enough to be called redundantly without issues.
void ResourceLoader::LoadToken::clear() {
WorkerThreadPool::TaskID task_to_await = 0;
@ -276,9 +260,11 @@ void ResourceLoader::LoadToken::clear() {
// If task is unused, await it here, locally, now the token data is consistent.
if (task_to_await) {
PREPARE_FOR_WTP_WAIT
int load_nesting_backup = load_nesting;
load_nesting = 0;
WorkerThreadPool::get_singleton()->wait_for_task_completion(task_to_await);
RESTORE_AFTER_WTP_WAIT
DEV_ASSERT(load_nesting == 0);
load_nesting = load_nesting_backup;
}
}
@ -286,22 +272,11 @@ ResourceLoader::LoadToken::~LoadToken() {
clear();
}
Ref<Resource> ResourceLoader::_load(const String &p_path, const String &p_original_path, const String &p_type_hint, ResourceFormatLoader::CacheMode p_cache_mode, Error *r_error, bool p_use_sub_threads, float *r_progress) {
Ref<Resource> ResourceLoader::_load(const String &p_path, const String &p_original_path, const String &p_type_hint, CacheMode p_cache_mode, Error *r_error, bool p_use_sub_threads, float *r_progress) {
const String &original_path = p_original_path.is_empty() ? p_path : p_original_path;
load_nesting++;
if (load_paths_stack.size()) {
MutexLock thread_load_lock(thread_load_mutex);
const String &parent_task_path = load_paths_stack.get(load_paths_stack.size() - 1);
HashMap<String, ThreadLoadTask>::Iterator E = thread_load_tasks.find(parent_task_path);
// Avoid double-tracking, for progress reporting, resources that boil down to a remapped path containing the real payload (e.g., imported resources).
bool is_remapped_load = original_path == parent_task_path;
if (E && !is_remapped_load) {
E->value.sub_tasks.insert(original_path);
}
}
load_paths_stack.push_back(original_path);
print_verbose(vformat("Loading resource: %s", p_path));
print_verbose(vformat("Loading resource: %s remapped: %s", p_path, _path_remap(p_path)));
// Try all loaders and pick the first match for the type hint
bool found = false;
@ -317,7 +292,6 @@ Ref<Resource> ResourceLoader::_load(const String &p_path, const String &p_origin
}
}
load_paths_stack.resize(load_paths_stack.size() - 1);
res_ref_overrides.erase(load_nesting);
load_nesting--;
@ -362,22 +336,193 @@ Ref<Resource> ResourceLoader::_load(const String &p_path, const String &p_origin
// The load task token must be manually re-referenced before this is called, which includes threaded runs.
void ResourceLoader::_run_load_task(void *p_userdata) {
ThreadLoadTask &load_task = *(ThreadLoadTask *)p_userdata;
int thread_index = WorkerThreadPool::get_singleton()->get_thread_index();
String thread_waiting_on_backup;
bool wait = false;
{
MutexLock thread_load_lock(thread_load_mutex);
if (cleaning_tasks) {
load_task.status = THREAD_LOAD_FAILED;
return;
}
if (load_task.started_load && load_task.thread_index != thread_index) {
// If we were already waiting for task completion in a previous
// step make sure we don't clobber the old wait.
if (thread_waiting_on.has(thread_index)) {
thread_waiting_on_backup = thread_waiting_on[thread_index];
}
thread_waiting_on[thread_index] = load_task.local_path;
wait = true;
} else {
load_task.started_load = true;
load_task.thread_index = thread_index;
}
}
ThreadLoadTask *curr_load_task_backup = curr_load_task;
curr_load_task = &load_task;
if (wait) {
// There are a couple of reasons why we got here:
// 1) We re-started the task in _load_complete_inner but we also
// got started via the original task in the WorkerThreadPool
// 2) There's a race between multiple threads in _load_complete_inner
// and more than one thread thought they had to restart
ThreadLoadStatus status;
LocalVector<int> chain;
do {
chain.clear();
thread_load_mutex.lock();
int waiting_on_thread = load_task.thread_index;
int current_thread = waiting_on_thread;
bool progress_blocked = thread_waiting_on.has(waiting_on_thread);
ThreadLoadTask *waiting_on_task = nullptr;
bool cycle_detected = false;
// Try to figure out if we're in a dependency cycle, and what asset
// we are ultimately waiting for.
if (progress_blocked && thread_index != -1) {
while (true) {
String *waiting_on_path = thread_waiting_on.getptr(current_thread);
if (!waiting_on_path) {
break;
}
waiting_on_task = thread_load_tasks.getptr(*waiting_on_path);
if (!waiting_on_task) {
// Path might be remapped, and someone might be waiting on the
// remapped path.
waiting_on_task = thread_load_tasks.getptr(_path_remap(*waiting_on_path));
if (!waiting_on_task) {
break;
}
}
// Record the cycle so we can determine whether we should be the one
// to break it or not.
int next_thread = waiting_on_task->thread_index;
chain.push_back(current_thread);
// We made it back to us, we're in a cycle.
if (next_thread == thread_index || chain.has(next_thread)) {
cycle_detected = true;
break;
}
if (chain.size() > thread_load_tasks.size()) {
ERR_PRINT(vformat("chain.size() > thread_load_tasks.size() for '%s' on thread %d",
load_task.local_path, thread_index));
cycle_detected = true;
break;
}
current_thread = next_thread;
}
}
status = load_task.status;
if (status == THREAD_LOAD_IN_PROGRESS) {
if (cycle_detected) {
// Only do something if we're the lowest thread ID waiting,
// if we didn't we'd run the risk of concurrently running
// the resource load again.
int lowest_waiting = thread_index;
for (const int &link : chain) {
if (link < lowest_waiting) {
lowest_waiting = link;
}
}
if (lowest_waiting == thread_index) {
print_verbose(
vformat("CYCLE: Stealing on thread %d for resource '%s' originally on thread %d",
thread_index, load_task.local_path, waiting_on_task->thread_index));
// Take over the task. The original thread was definitely
// not going to make progress.
load_task.thread_index = thread_index;
thread_waiting_on.erase(thread_index);
wait = false;
}
}
} else {
wait = false;
}
// Only yield if we ultimately found a task that we are waiting on.
bool should_yield = progress_blocked && wait && waiting_on_task && thread_index != -1;
if (should_yield) {
// We need to make sure we yield on our actual current task. If we are
// waiting we are certainly not the the task being ran.
yielders.push_back(WorkerThreadPool::get_singleton()->get_caller_task_id());
}
thread_load_mutex.unlock();
if (should_yield) {
// We are blocked on some upstream task in our dependency chain. We
// don't know how long it will take or what is needed to unblock it.
// If we yield we give the WTP a free thread to solve the problem.
int load_nesting_backup = load_nesting;
load_nesting = 0;
WorkerThreadPool::get_singleton()->yield();
DEV_ASSERT(load_nesting == 0);
load_nesting = load_nesting_backup;
thread_load_mutex.lock();
yielders.erase(WorkerThreadPool::get_singleton()->get_caller_task_id());
status = load_task.status;
thread_load_mutex.unlock();
} else if (wait) {
// Forward progress is being made, just wait for task completion.
// If we are not currently blocked more dependencies might block later,
// so we cannot yield or wait on a task.
// This is not the most optimal thing to do, but it is safe. Either the
// dependency will complete soon, or will block soon when we can safely
// yield.
OS::get_singleton()->delay_usec(1000);
}
} while (wait && status == THREAD_LOAD_IN_PROGRESS && !cleaning_tasks);
if (cleaning_tasks || status != THREAD_LOAD_IN_PROGRESS) {
curr_load_task = curr_load_task_backup;
}
if (cleaning_tasks) {
load_task.status = THREAD_LOAD_FAILED;
// Do not attempt to unreference the load token. Many things are
// tearing down concurrently and our task might be dead already. If it is
// the load token is already released.
return;
}
if (status != THREAD_LOAD_IN_PROGRESS) {
load_task.load_token->unreference();
thread_load_mutex.lock();
if (thread_waiting_on_backup.is_empty()) {
thread_waiting_on.erase(thread_index);
} else {
thread_waiting_on[thread_index] = thread_waiting_on_backup;
}
thread_load_mutex.unlock();
return;
}
// do it ourselves anyway
}
// Thread-safe either if it's the current thread or a brand new one.
CallQueue *own_mq_override = nullptr;
if (load_nesting == 0) {
DEV_ASSERT(load_paths_stack.is_empty());
if (!Thread::is_main_thread()) {
// Let the caller thread use its own, for added flexibility. Provide one otherwise.
if (MessageQueue::get_singleton() == MessageQueue::get_main_singleton()) {
@ -399,36 +544,41 @@ void ResourceLoader::_run_load_task(void *p_userdata) {
}
thread_load_mutex.lock();
bool thread_load_mutex_held = true;
bool was_finished = load_task.finished_load;
if (load_task.resource.is_valid()) {
load_task.finished_load = true;
}
load_task.resource = res;
load_task.progress = 1.0; // It was fully loaded at this point, so force progress to 1.0.
load_task.error = load_err;
if (load_task.error != OK) {
load_task.status = THREAD_LOAD_FAILED;
} else {
load_task.status = THREAD_LOAD_LOADED;
}
if (load_task.cond_var && load_task.need_wait) {
load_task.cond_var->notify_all();
}
load_task.need_wait = false;
bool ignoring = load_task.cache_mode == ResourceFormatLoader::CACHE_MODE_IGNORE || load_task.cache_mode == ResourceFormatLoader::CACHE_MODE_IGNORE_DEEP;
bool replacing = load_task.cache_mode == ResourceFormatLoader::CACHE_MODE_REPLACE || load_task.cache_mode == ResourceFormatLoader::CACHE_MODE_REPLACE_DEEP;
bool unlock_pending = true;
bool ignoring = load_task.cache_mode == CACHE_MODE_IGNORE || load_task.cache_mode == CACHE_MODE_IGNORE_DEEP;
bool replacing = load_task.cache_mode == CACHE_MODE_REPLACE || load_task.cache_mode == CACHE_MODE_REPLACE_DEEP;
if (load_task.resource.is_valid()) {
// From now on, no critical section needed as no one will write to the task anymore.
// Moreover, the mutex being unlocked is a requirement if some of the calls below
// that set the resource up invoke code that in turn requests resource loading.
thread_load_mutex.unlock();
unlock_pending = false;
thread_load_mutex_held = false;
if (!ignoring) {
ResourceCache::lock.lock(); // Check and operations must happen atomically.
bool pending_unlock = true;
Ref<Resource> old_res = ResourceCache::get_ref(load_task.local_path);
if (was_finished) {
// If another thread already finished the entire load wait for it to complete
// cache registration, then use their instance.
while (!old_res.is_valid()) {
ResourceCache::lock.unlock();
OS::get_singleton()->delay_usec(1000);
ResourceCache::lock.lock();
old_res = ResourceCache::get_ref(load_task.local_path);
}
}
if (old_res.is_valid()) {
if (old_res != load_task.resource) {
// Resource can already exists at this point for two reasons:
@ -476,7 +626,7 @@ void ResourceLoader::_run_load_task(void *p_userdata) {
load_task.progress = 1.0;
thread_load_mutex.unlock();
unlock_pending = false;
thread_load_mutex_held = false;
if (_loaded_callback) {
_loaded_callback(load_task.resource, load_task.local_path);
@ -484,22 +634,56 @@ void ResourceLoader::_run_load_task(void *p_userdata) {
}
}
if (!thread_load_mutex_held) {
thread_load_mutex.lock();
}
if (cleaning_tasks) {
// If we are cleaning don't wake up yielders here.
// And don't unreference the load token, it will get destroyed
// with the task later.
load_task.status = THREAD_LOAD_FAILED;
thread_load_mutex.unlock();
return;
}
if (load_task.error != OK) {
load_task.status = THREAD_LOAD_FAILED;
} else {
load_task.status = THREAD_LOAD_LOADED;
}
if (load_task.cond_var && load_task.need_wait) {
load_task.cond_var->notify_all();
}
load_task.need_wait = false;
if (!thread_waiting_on_backup.is_empty()) {
thread_waiting_on[thread_index] = thread_waiting_on_backup;
}
for (int tid : yielders) {
// Thundering herd, but not really an issue in practice. The
// number of threads in the WorkerThreadPool is bound and
// low.
WorkerThreadPool::get_singleton()->notify_yield_over(tid);
}
thread_load_mutex.unlock();
// It's safe now to let the task go in case no one else was grabbing the token.
load_task.load_token->unreference();
if (unlock_pending) {
thread_load_mutex.unlock();
}
if (load_nesting == 0) {
if (own_mq_override) {
MessageQueue::set_thread_singleton_override(nullptr);
memdelete(own_mq_override);
}
DEV_ASSERT(load_paths_stack.is_empty());
}
curr_load_task = curr_load_task_backup;
print_verbose(vformat("Completed load for: '%s' remapped '%s' at thread %d", load_task.local_path, remapped_path, thread_index));
}
String ResourceLoader::_validate_local_path(const String &p_path) {
@ -513,7 +697,7 @@ String ResourceLoader::_validate_local_path(const String &p_path) {
}
}
Error ResourceLoader::load_threaded_request(const String &p_path, const String &p_type_hint, bool p_use_sub_threads, ResourceFormatLoader::CacheMode p_cache_mode) {
Error ResourceLoader::load_threaded_request(const String &p_path, const String &p_type_hint, bool p_use_sub_threads, CacheMode p_cache_mode) {
Ref<ResourceLoader::LoadToken> token = _load_start(p_path, p_type_hint, p_use_sub_threads ? LOAD_THREAD_DISTRIBUTE : LOAD_THREAD_SPAWN_SINGLE, p_cache_mode, true);
return token.is_valid() ? OK : FAILED;
}
@ -538,7 +722,7 @@ void ResourceLoader::_load_threaded_request_setup_user_token(LoadToken *p_token,
print_lt("REQUEST: user load tokens: " + itos(user_load_tokens.size()));
}
Ref<Resource> ResourceLoader::load(const String &p_path, const String &p_type_hint, ResourceFormatLoader::CacheMode p_cache_mode, Error *r_error) {
Ref<Resource> ResourceLoader::load(const String &p_path, const String &p_type_hint, CacheMode p_cache_mode, Error *r_error) {
if (r_error) {
*r_error = OK;
}
@ -563,11 +747,11 @@ Ref<Resource> ResourceLoader::load(const String &p_path, const String &p_type_hi
return res;
}
Ref<ResourceLoader::LoadToken> ResourceLoader::_load_start(const String &p_path, const String &p_type_hint, LoadThreadMode p_thread_mode, ResourceFormatLoader::CacheMode p_cache_mode, bool p_for_user) {
Ref<ResourceLoader::LoadToken> ResourceLoader::_load_start(const String &p_path, const String &p_type_hint, LoadThreadMode p_thread_mode, CacheMode p_cache_mode, bool p_for_user) {
String local_path = _validate_local_path(p_path);
ERR_FAIL_COND_V(local_path.is_empty(), Ref<ResourceLoader::LoadToken>());
bool ignoring_cache = p_cache_mode == ResourceFormatLoader::CACHE_MODE_IGNORE || p_cache_mode == ResourceFormatLoader::CACHE_MODE_IGNORE_DEEP;
bool ignoring_cache = p_cache_mode == CACHE_MODE_IGNORE || p_cache_mode == CACHE_MODE_IGNORE_DEEP;
Ref<LoadToken> load_token;
bool must_not_register = false;
@ -613,7 +797,7 @@ Ref<ResourceLoader::LoadToken> ResourceLoader::_load_start(const String &p_path,
load_task.type_hint = p_type_hint;
load_task.cache_mode = p_cache_mode;
load_task.use_sub_threads = p_thread_mode == LOAD_THREAD_DISTRIBUTE;
if (p_cache_mode == ResourceFormatLoader::CACHE_MODE_REUSE) {
if (p_cache_mode == CACHE_MODE_REUSE) {
Ref<Resource> existing = ResourceCache::get_ref(local_path);
if (existing.is_valid()) {
//referencing is fine
@ -626,6 +810,12 @@ Ref<ResourceLoader::LoadToken> ResourceLoader::_load_start(const String &p_path,
}
}
// Task hierarchy
if (curr_load_task) {
load_task.parent_task = curr_load_task;
curr_load_task->sub_tasks.insert(load_task.local_path);
}
// If we want to ignore cache, but there's another task loading it, we can't add this one to the map.
must_not_register = ignoring_cache && thread_load_tasks.has(local_path);
if (must_not_register) {
@ -781,6 +971,10 @@ Ref<Resource> ResourceLoader::load_threaded_get(const String &p_path, Error *r_e
thread_load_lock.temp_unlock();
bool exit = !_ensure_load_progress();
OS::get_singleton()->delay_usec(1000);
if (MessageQueue::get_singleton()) {
MessageQueue::get_singleton()->flush();
}
thread_load_lock.temp_relock();
if (exit) {
break;
@ -815,6 +1009,22 @@ void ResourceLoader::set_is_import_thread(bool p_import_thread) {
import_thread = p_import_thread;
}
void ResourceLoader::notify_load_error(const String &p_err) {
if (err_notify) {
MessageQueue::get_main_singleton()->push_callable(callable_mp_static(err_notify).bind(p_err));
}
}
void ResourceLoader::notify_dependency_error(const String &p_path, const String &p_dependency, const String &p_type) {
if (dep_err_notify) {
if (Thread::get_caller_id() == Thread::get_main_id()) {
dep_err_notify(p_path, p_dependency, p_type);
} else {
MessageQueue::get_main_singleton()->push_callable(callable_mp_static(dep_err_notify).bind(p_path, p_dependency, p_type));
}
}
}
Ref<Resource> ResourceLoader::_load_complete_inner(LoadToken &p_load_token, Error *r_error, MutexLock<SafeBinaryMutex<BINARY_MUTEX_TAG>> &p_thread_load_lock) {
if (r_error) {
*r_error = OK;
@ -851,20 +1061,10 @@ Ref<Resource> ResourceLoader::_load_complete_inner(LoadToken &p_load_token, Erro
// Loading thread is in the worker pool.
p_thread_load_lock.temp_unlock();
PREPARE_FOR_WTP_WAIT
Error wait_err = WorkerThreadPool::get_singleton()->wait_for_task_completion(load_task.task_id);
RESTORE_AFTER_WTP_WAIT
DEV_ASSERT(!wait_err || wait_err == ERR_BUSY);
if (wait_err == ERR_BUSY) {
// The WorkerThreadPool has reported that the current task wants to await on an older one.
// That't not allowed for safety, to avoid deadlocks. Fortunately, though, in the context of
// resource loading that means that the task to wait for can be restarted here to break the
// cycle, with as much recursion into this process as needed.
// When the stack is eventually unrolled, the original load will have been notified to go on.
load_task.load_token->reference();
_run_load_task(&load_task);
}
// The wtp won't let us wait on tasks that are older than us. But ResourceLoader has its own
// deadlock detection and prevention in _run_load_task(), rely on that instead.
load_task.load_token->reference();
_run_load_task(&load_task);
p_thread_load_lock.temp_relock();
load_task.awaited = true;
@ -904,21 +1104,24 @@ Ref<Resource> ResourceLoader::_load_complete_inner(LoadToken &p_load_token, Erro
load_task_ptr = &load_task;
}
p_thread_load_lock.temp_unlock();
Ref<Resource> resource = load_task_ptr->resource;
if (r_error) {
*r_error = load_task_ptr->error;
}
if (resource.is_valid()) {
if (curr_load_task) {
// A task awaiting another => Let the awaiter accumulate the resource changed connections.
DEV_ASSERT(curr_load_task != load_task_ptr);
for (const ThreadLoadTask::ResourceChangedConnection &rcc : load_task_ptr->resource_changed_connections) {
curr_load_task->resource_changed_connections.push_back(rcc);
if (load_task_ptr->parent_task) {
if (!load_task_ptr->connections_propagated) {
// A task awaiting another => Let the awaiter accumulate the resource changed connections.
DEV_ASSERT(load_task_ptr->parent_task != load_task_ptr);
for (const ThreadLoadTask::ResourceChangedConnection &rcc : load_task_ptr->resource_changed_connections) {
load_task_ptr->parent_task->resource_changed_connections.push_back(rcc);
}
load_task_ptr->connections_propagated = true;
}
} else {
p_thread_load_lock.temp_unlock();
// A leaf task being awaited => Propagate the resource changed connections.
if (Thread::is_main_thread()) {
// On the main thread it's safe to migrate the connections to the standard signal mechanism.
@ -942,11 +1145,11 @@ Ref<Resource> ResourceLoader::_load_complete_inner(LoadToken &p_load_token, Erro
}
}
}
p_thread_load_lock.temp_relock();
}
}
p_thread_load_lock.temp_relock();
return resource;
}
@ -977,6 +1180,7 @@ void ResourceLoader::resource_changed_connect(Resource *p_source, const Callable
rcc.callable = p_callable;
rcc.flags = p_flags;
curr_load_task->resource_changed_connections.push_back(rcc);
curr_load_task->resource_dependencies.push_back(p_source);
}
void ResourceLoader::resource_changed_disconnect(Resource *p_source, const Callable &p_callable) {
@ -987,6 +1191,7 @@ void ResourceLoader::resource_changed_disconnect(Resource *p_source, const Calla
for (uint32_t i = 0; i < curr_load_task->resource_changed_connections.size(); ++i) {
const ThreadLoadTask::ResourceChangedConnection &rcc = curr_load_task->resource_changed_connections[i];
if (unlikely(rcc.source == p_source && rcc.callable == p_callable)) {
curr_load_task->resource_dependencies.erase(p_source);
curr_load_task->resource_changed_connections.remove_at_unordered(i);
return;
}
@ -1046,8 +1251,7 @@ bool ResourceLoader::exists(const String &p_path, const String &p_type_hint) {
return true; // If cached, it probably exists
}
bool xl_remapped = false;
String path = _path_remap(local_path, &xl_remapped);
String path = _path_remap(local_path);
// Try all loaders and pick the first match for the type hint
for (int i = 0; i < loader_count; i++) {
@ -1389,6 +1593,9 @@ void ResourceLoader::clear_thread_load_tasks() {
while (true) {
bool none_running = true;
for (int tid : yielders) {
WorkerThreadPool::get_singleton()->notify_yield_over(tid);
}
if (thread_load_tasks.size()) {
for (KeyValue<String, ResourceLoader::ThreadLoadTask> &E : thread_load_tasks) {
if (E.value.status == THREAD_LOAD_IN_PROGRESS) {
@ -1403,8 +1610,15 @@ void ResourceLoader::clear_thread_load_tasks() {
if (none_running) {
break;
}
thread_load_lock.temp_unlock();
if (MessageQueue::get_singleton()) {
MessageQueue::get_singleton()->flush();
}
OS::get_singleton()->delay_usec(1000);
thread_load_lock.temp_relock();
}
@ -1418,6 +1632,8 @@ void ResourceLoader::clear_thread_load_tasks() {
}
thread_load_tasks.clear();
thread_waiting_on.clear();
// yielders is already guaranteed to be empty now
cleaning_tasks = false;
}
@ -1568,7 +1784,6 @@ bool ResourceLoader::timestamp_on_load = false;
thread_local bool ResourceLoader::import_thread = false;
thread_local int ResourceLoader::load_nesting = 0;
thread_local Vector<String> ResourceLoader::load_paths_stack;
thread_local HashMap<int, HashMap<String, Ref<Resource>>> ResourceLoader::res_ref_overrides;
thread_local ResourceLoader::ThreadLoadTask *ResourceLoader::curr_load_task = nullptr;
@ -1580,6 +1795,9 @@ template <>
thread_local SafeBinaryMutex<ResourceLoader::BINARY_MUTEX_TAG>::TLSData SafeBinaryMutex<ResourceLoader::BINARY_MUTEX_TAG>::tls_data(_get_res_loader_mutex());
SafeBinaryMutex<ResourceLoader::BINARY_MUTEX_TAG> ResourceLoader::thread_load_mutex;
HashMap<String, ResourceLoader::ThreadLoadTask> ResourceLoader::thread_load_tasks;
HashMap<int, String> ResourceLoader::thread_waiting_on;
LocalVector<int> ResourceLoader::yielders;
bool ResourceLoader::cleaning_tasks = false;
HashMap<String, ResourceLoader::LoadToken *> ResourceLoader::user_load_tokens;