Skip to content

Commit a2d23de

Browse files
committed
Cache autoloads once resolved
1 parent bbc95a6 commit a2d23de

File tree

4 files changed

+115
-5
lines changed

4 files changed

+115
-5
lines changed

godot-core/src/init/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ fn gdext_on_level_deinit(level: InitLevel) {
232232
// No business logic by itself, but ensures consistency if re-initialization (hot-reload on Linux) occurs.
233233

234234
crate::task::cleanup();
235+
crate::tools::cleanup();
235236

236237
// Garbage-collect various statics.
237238
// SAFETY: this is the last time meta APIs are used.

godot-core/src/tools/autoload.rs

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,24 @@
55
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
66
*/
77

8+
use std::cell::RefCell;
9+
use std::collections::HashMap;
10+
11+
use sys::is_main_thread;
12+
813
use crate::builtin::NodePath;
914
use crate::classes::{Engine, Node, SceneTree};
1015
use crate::meta::error::ConvertError;
1116
use crate::obj::{Gd, Inherits, Singleton};
17+
use crate::sys;
1218

13-
/// Retrieves an autoload by its name.
19+
/// Retrieves an autoload by name.
1420
///
1521
/// See [Godot docs] for an explanation of the autoload concept. Godot sometimes uses the term "autoload" interchangeably with "singleton";
1622
/// we strictly refer to the former to separate from [`Singleton`][crate::obj::Singleton] objects.
1723
///
24+
/// If the autoload can be resolved, it will be cached and returned very quickly the second time.
25+
///
1826
/// [Godot docs]: https://docs.godotengine.org/en/stable/tutorials/scripting/singletons_autoload.html
1927
///
2028
/// # Panics
@@ -48,6 +56,8 @@ where
4856
/// Autoloads are accessed via the `/root/{name}` path in the scene tree. The name is the one you used to register the autoload in
4957
/// `project.godot`. By convention, it often corresponds to the class name, but does not have to.
5058
///
59+
/// If the autoload can be resolved, it will be cached and returned very quickly the second time.
60+
///
5161
/// See also [`get_autoload_by_name()`] for simpler function expecting the class name and non-fallible invocation.
5262
///
5363
/// This function returns `Err` if:
@@ -76,6 +86,16 @@ pub fn try_get_autoload_by_name<T>(autoload_name: &str) -> Result<Gd<T>, Convert
7686
where
7787
T: Inherits<Node>,
7888
{
89+
ensure_main_thread()?;
90+
91+
// Check cache first.
92+
let cached = AUTOLOAD_CACHE.with(|cache| cache.borrow().get(autoload_name).cloned());
93+
94+
if let Some(cached_node) = cached {
95+
return cast_autoload(cached_node, autoload_name);
96+
}
97+
98+
// Cache miss - fetch from scene tree.
7999
let main_loop = Engine::singleton()
80100
.get_main_loop()
81101
.ok_or_else(|| ConvertError::new("main loop not available"))?;
@@ -90,10 +110,68 @@ where
90110
.get_root()
91111
.ok_or_else(|| ConvertError::new("scene tree root not available"))?;
92112

93-
root.try_get_node_as::<T>(&autoload_path).ok_or_else(|| {
94-
let class = T::class_id();
113+
let autoload_node = root
114+
.try_get_node_as::<Node>(&autoload_path)
115+
.ok_or_else(|| ConvertError::new(format!("autoload `{autoload_name}` not found")))?;
116+
117+
// Store in cache as Gd<Node>.
118+
AUTOLOAD_CACHE.with(|cache| {
119+
cache
120+
.borrow_mut()
121+
.insert(autoload_name.to_string(), autoload_node.clone());
122+
});
123+
124+
// Cast to requested type.
125+
cast_autoload(autoload_node, autoload_name)
126+
}
127+
128+
// ----------------------------------------------------------------------------------------------------------------------------------------------
129+
// Cache implementation
130+
131+
thread_local! {
132+
/// Cache for autoloads. Maps autoload name to `Gd<Node>`.
133+
///
134+
/// Uses `thread_local!` because `Gd<T>` is not `Send`/`Sync`. Since all Godot objects must be accessed
135+
/// from the main thread, this is safe. We enforce main-thread access via `ensure_main_thread()`.
136+
static AUTOLOAD_CACHE: RefCell<HashMap<String, Gd<Node>>> = RefCell::new(HashMap::new());
137+
}
138+
139+
/// Verifies that the current thread is the main thread.
140+
///
141+
/// Returns an error if called from a thread other than the main thread. This is necessary because `Gd<T>` is not thread-safe.
142+
fn ensure_main_thread() -> Result<(), ConvertError> {
143+
if is_main_thread() {
144+
Ok(())
145+
} else {
146+
Err(ConvertError::new(
147+
"Autoloads must be fetched from main thread, as Gd<T> is not thread-safe",
148+
))
149+
}
150+
}
151+
152+
/// Casts an autoload node to the requested type, with descriptive error message on failure.
153+
fn cast_autoload<T>(node: Gd<Node>, autoload_name: &str) -> Result<Gd<T>, ConvertError>
154+
where
155+
T: Inherits<Node>,
156+
{
157+
node.try_cast::<T>().map_err(|node| {
158+
let expected = T::class_id();
159+
let actual = node.get_class();
160+
95161
ConvertError::new(format!(
96-
"autoload `{autoload_name}` not found or has wrong type (expected {class})",
162+
"autoload `{autoload_name}` has wrong type (expected {expected}, got {actual})",
97163
))
98164
})
99165
}
166+
167+
/// Clears the autoload cache (called during shutdown).
168+
///
169+
/// # Panics
170+
/// Panics if called from a thread other than the main thread.
171+
pub(crate) fn clear_autoload_cache() {
172+
ensure_main_thread().expect("clear_autoload_cache() must be called from the main thread");
173+
174+
AUTOLOAD_CACHE.with(|cache| {
175+
cache.borrow_mut().clear();
176+
});
177+
}

godot-core/src/tools/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,9 @@ pub use autoload::*;
1919
pub use gfile::*;
2020
pub use save_load::*;
2121
pub use translate::*;
22+
23+
// ----------------------------------------------------------------------------------------------------------------------------------------------
24+
25+
pub(crate) fn cleanup() {
26+
clear_autoload_cache();
27+
}

itest/rust/src/engine_tests/autoload_test.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use godot::classes::Node;
99
use godot::prelude::*;
1010
use godot::tools::{get_autoload_by_name, try_get_autoload_by_name};
1111

12-
use crate::framework::itest;
12+
use crate::framework::{itest, quick_thread};
1313

1414
#[derive(GodotClass)]
1515
#[class(init, base=Node)]
@@ -65,3 +65,28 @@ fn autoload_try_get_named_bad_type() {
6565
let result = try_get_autoload_by_name::<Node2D>("MyAutoload");
6666
result.expect_err("autoload of incompatible node type");
6767
}
68+
69+
#[itest]
70+
fn autoload_from_other_thread() {
71+
use std::sync::{Arc, Mutex};
72+
73+
// We can't return the Result from the thread because Gd<T> is not Send, so we extract the error message instead.
74+
let outer_error = Arc::new(Mutex::new(String::new()));
75+
let inner_error = Arc::clone(&outer_error);
76+
77+
quick_thread(move || {
78+
let result = try_get_autoload_by_name::<AutoloadClass>("MyAutoload");
79+
match result {
80+
Ok(_) => panic!("autoload access from non-main thread should fail"),
81+
Err(err) => {
82+
*inner_error.lock().unwrap() = err.to_string();
83+
}
84+
}
85+
});
86+
87+
let msg = outer_error.lock().unwrap();
88+
assert_eq!(
89+
*msg,
90+
"Autoloads must be fetched from main thread, as Gd<T> is not thread-safe"
91+
);
92+
}

0 commit comments

Comments
 (0)