Skip to content

Commit 9d48303

Browse files
committed
Improve init-level tests; fix RenderingServer singleton error
1 parent 07c34e7 commit 9d48303

File tree

4 files changed

+101
-43
lines changed

4 files changed

+101
-43
lines changed

godot-codegen/src/special_cases/special_cases.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -985,7 +985,7 @@ pub fn get_derived_virtual_method_presence(class_name: &TyName, godot_method_nam
985985
}
986986
}
987987

988-
/// Initialization order for Godot (see https://github.com/godotengine/godot/blob/master/main/main.cpp)
988+
/// Initialization order for Godot (see https://github.com/godotengine/godot/blob/master/main/main.cpp).
989989
/// - Main::setup()
990990
/// - register_core_types()
991991
/// - register_early_core_singletons()
@@ -1002,13 +1002,25 @@ pub fn get_derived_virtual_method_presence(class_name: &TyName, godot_method_nam
10021002
/// - initialize_extensions(GDExtension::INITIALIZATION_LEVEL_EDITOR)
10031003
/// - register_server_singletons() ...another weird one.
10041004
/// - Autoloads, etc.
1005+
///
1006+
/// ## Singleton availability by initialization level
1007+
/// - **Core level**: Basic singletons like `Engine`, `OS`, `ProjectSettings`, `Time` are available.
1008+
/// - **Servers level**: Server singletons like `RenderingServer` are NOT yet available due to GDExtension timing issues.
1009+
/// - **Scene level**: All singletons including `RenderingServer` are available.
1010+
/// - **Editor level**: Editor-specific functionality is available.
1011+
///
1012+
/// GDExtension singletons are generally not available during *any* level initialization, with the exception of a few core singletons
1013+
/// (see above). This is different from how modules work, where servers are available at _Servers_ level.
1014+
///
1015+
/// See also:
1016+
/// - Singletons not accessible in Scene (godot-cpp): <https://github.com/godotengine/godot-cpp/issues/1180>
1017+
/// - `global_get_singleton` not returning singletons: <https://github.com/godotengine/godot/issues/64975>
1018+
/// - PR to make singletons available: <https://github.com/godotengine/godot/pull/98862>
10051019
#[rustfmt::skip]
10061020
pub fn classify_codegen_level(class_name: &str) -> Option<ClassCodegenLevel> {
10071021
let level = match class_name {
10081022
// See register_core_types() in https://github.com/godotengine/godot/blob/master/core/register_core_types.cpp,
1009-
// which is called before Core level is initialized.
1010-
// Currently only promoting super basic classes to Core level since this is a brittle hardcoded list (feel free expand as
1011-
// necessary based on careful evaluation of register_core_types).
1023+
// which is called before Core level is initialized. Only a small list is promoted to Core; carefully evaluate if more are added.
10121024
| "Object" | "RefCounted" | "Resource" | "MainLoop" | "GDExtension"
10131025
=> ClassCodegenLevel::Core,
10141026

@@ -1035,6 +1047,7 @@ pub fn classify_codegen_level(class_name: &str) -> Option<ClassCodegenLevel> {
10351047
| "RenderData" | "RenderDataExtension"
10361048
| "RenderSceneData" | "RenderSceneDataExtension"
10371049
=> ClassCodegenLevel::Servers,
1050+
10381051
// Declared final (un-inheritable) in Rust, but those are still servers.
10391052
| "AudioServer" | "CameraServer" | "NavigationServer2D" | "NavigationServer3D" | "RenderingServer" | "TranslationServer" | "XRServer" | "DisplayServer"
10401053
=> ClassCodegenLevel::Servers,
@@ -1045,6 +1058,7 @@ pub fn classify_codegen_level(class_name: &str) -> Option<ClassCodegenLevel> {
10451058
| "OpenXRInteractionProfileEditor"
10461059
| "OpenXRBindingModifierEditor" if cfg!(before_api = "4.5")
10471060
=> ClassCodegenLevel::Editor,
1061+
10481062
// https://github.com/godotengine/godot/issues/86206
10491063
"ResourceImporterOggVorbis" | "ResourceImporterMP3" if cfg!(before_api = "4.3")
10501064
=> ClassCodegenLevel::Editor,

godot-core/src/init/mod.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,23 @@ fn gdext_on_level_deinit(level: InitLevel) {
244244
/// Note that this only changes the name. You cannot provide your own function -- use the [`on_level_init()`][ExtensionLibrary::on_level_init]
245245
/// hook for custom startup logic.
246246
///
247+
/// # Availability of Godot APIs during init and deinit
248+
// Init order: see also special_cases.rs > classify_codegen_level().
249+
/// Godot loads functionality gradually during its startup routines, and unloads it during shutdown. As a result, Godot classes are only
250+
/// available above a certain level. Trying to access a class API when it's not available will panic (if not, please report it as a bug).
251+
///
252+
/// A few singletons (`Engine`, `Os`, `Time`, `ProjectSettings`) are available from the `Core` level onward and can be used inside
253+
/// this method. Most other singletons are **not available during init** at all, and will only become accessible once the first frame has
254+
/// run.
255+
///
256+
/// The exact time a class is available depends on the Godot initialization logic, which is quite complex and may change between versions.
257+
/// To get an up-to-date view, inspect the Godot source code of [main.cpp], particularly `Main::setup()`, `Main::setup2()` and
258+
/// `Main::cleanup()` methods. Make sure to look at the correct version of the file.
259+
///
260+
/// In case of doubt, do not rely on classes being available during init/deinit.
261+
///
262+
/// [main.cpp]: https://github.com/godotengine/godot/blob/master/main/main.cpp
263+
///
247264
/// # Safety
248265
/// The library cannot enforce any safety guarantees outside Rust code, which means that **you as a user** are
249266
/// responsible to uphold them: namely in GDScript code or other GDExtension bindings loaded by the engine.
@@ -269,6 +286,8 @@ pub unsafe trait ExtensionLibrary {
269286
/// Custom logic when a certain init-level of Godot is loaded.
270287
///
271288
/// This will only be invoked for levels >= [`Self::min_level()`], in ascending order. Use `if` or `match` to hook to specific levels.
289+
///
290+
/// If the overridden method panics, an error will be printed, but GDExtension loading is **not** aborted.
272291
#[allow(unused_variables)]
273292
fn on_level_init(level: InitLevel) {
274293
// Nothing by default.
@@ -277,6 +296,8 @@ pub unsafe trait ExtensionLibrary {
277296
/// Custom logic when a certain init-level of Godot is unloaded.
278297
///
279298
/// This will only be invoked for levels >= [`Self::min_level()`], in descending order. Use `if` or `match` to hook to specific levels.
299+
///
300+
/// If the overridden method panics, an error will be printed, but GDExtension unloading is **not** aborted.
280301
#[allow(unused_variables)]
281302
fn on_level_deinit(level: InitLevel) {
282303
// Nothing by default.

itest/rust/src/lib.rs

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
*/
77

88
use godot::init::{gdextension, ExtensionLibrary, InitLevel};
9-
use godot::sys::Global;
109

1110
mod benchmarks;
1211
mod builtin_tests;
@@ -19,39 +18,13 @@ mod register_tests;
1918
// ----------------------------------------------------------------------------------------------------------------------------------------------
2019
// Entry point
2120

22-
static LEVELS_SEEN: Global<Vec<InitLevel>> = Global::default();
23-
2421
#[gdextension(entry_symbol = itest_init)]
2522
unsafe impl ExtensionLibrary for framework::IntegrationTests {
2623
fn min_level() -> InitLevel {
2724
InitLevel::Core
2825
}
29-
fn on_level_init(level: InitLevel) {
30-
LEVELS_SEEN.lock().push(level);
31-
match level {
32-
InitLevel::Core => {
33-
// Make sure we can access early core singletons.
34-
object_tests::test_early_core_singletons();
35-
}
36-
InitLevel::Servers => {
37-
// Make sure we can access server singletons by now.
38-
object_tests::test_server_singletons();
39-
}
40-
InitLevel::Scene => {}
41-
InitLevel::Editor => {}
42-
}
43-
}
44-
}
4526

46-
// Ensure that we saw all the init levels expected.
47-
#[crate::framework::itest]
48-
fn observed_all_init_levels() {
49-
let levels_seen = LEVELS_SEEN.lock().clone();
50-
assert_eq!(levels_seen[0], InitLevel::Core);
51-
assert_eq!(levels_seen[1], InitLevel::Servers);
52-
assert_eq!(levels_seen[2], InitLevel::Scene);
53-
// NOTE: some tests don't see editor mode
54-
if let Some(level_3) = levels_seen.get(3) {
55-
assert_eq!(*level_3, InitLevel::Editor);
27+
fn on_level_init(level: InitLevel) {
28+
object_tests::on_level_init(level);
5629
}
5730
}

itest/rust/src/object_tests/init_level_test.rs

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@
77

88
use std::sync::atomic::{AtomicBool, Ordering};
99

10+
use godot::init::InitLevel;
1011
use godot::obj::NewAlloc;
1112
use godot::register::{godot_api, GodotClass};
13+
use godot::sys::Global;
1214

13-
use crate::framework::itest;
15+
use crate::framework::{expect_panic, itest, runs_release, suppress_godot_print};
1416

1517
static OBJECT_CALL_HAS_RUN: AtomicBool = AtomicBool::new(false);
18+
static LEVELS_SEEN: Global<Vec<InitLevel>> = Global::default();
1619

1720
#[derive(GodotClass)]
1821
#[class(base = Object, init)]
@@ -34,8 +37,49 @@ impl SomeObject {
3437
}
3538
}
3639

40+
// Ensure that the above function has actually run and succeeded.
41+
#[itest]
42+
fn init_level_all_initialized() {
43+
assert!(
44+
OBJECT_CALL_HAS_RUN.load(Ordering::Relaxed),
45+
"Object call function did not run during Core init level"
46+
);
47+
}
48+
49+
// Ensure that we saw all the init levels expected.
50+
#[itest]
51+
fn init_level_observed_all() {
52+
let levels_seen = LEVELS_SEEN.lock().clone();
53+
54+
assert_eq!(levels_seen[0], InitLevel::Core);
55+
assert_eq!(levels_seen[1], InitLevel::Servers);
56+
assert_eq!(levels_seen[2], InitLevel::Scene);
57+
58+
// In Debug/Editor builds, Editor level is loaded; otherwise not.
59+
let level_3 = levels_seen.get(3);
60+
if runs_release() {
61+
assert_eq!(level_3, None);
62+
} else {
63+
assert_eq!(level_3, Some(&InitLevel::Editor));
64+
}
65+
}
66+
67+
// ----------------------------------------------------------------------------------------------------------------------------------------------
68+
// Level-specific callbacks
69+
70+
pub fn on_level_init(level: InitLevel) {
71+
LEVELS_SEEN.lock().push(level);
72+
73+
match level {
74+
InitLevel::Core => on_init_core(),
75+
InitLevel::Servers => on_init_servers(),
76+
InitLevel::Scene => on_init_scene(),
77+
InitLevel::Editor => on_init_editor(),
78+
}
79+
}
80+
3781
// Runs during core init level to ensure we can access core singletons.
38-
pub fn test_early_core_singletons() {
82+
fn on_init_core() {
3983
// Ensure we can create and use an Object-derived class during Core init level.
4084
SomeObject::test();
4185

@@ -59,14 +103,20 @@ pub fn test_early_core_singletons() {
59103
assert!(time.get_ticks_usec() <= time.get_ticks_usec());
60104
}
61105

62-
// Runs during scene init level to ensure we can access general singletons in the Scene init call for the extension as a whole.
63-
pub fn test_server_singletons() {
64-
let mut rendering = godot::classes::RenderingServer::singleton();
65-
assert!(rendering.get_test_cube() != godot::builtin::Rid::Invalid);
106+
fn on_init_servers() {
107+
// Nothing yet.
66108
}
67109

68-
// Ensure that the above function actually ran.
69-
#[itest]
70-
fn class_run_during_servers_init() {
71-
assert!(OBJECT_CALL_HAS_RUN.load(Ordering::Acquire));
110+
fn on_init_scene() {
111+
// Known limitation that singletons only become available later:
112+
// https://github.com/godotengine/godot-cpp/issues/1180#issuecomment-3074351805
113+
expect_panic("Singletons not loaded during Scene init level", || {
114+
suppress_godot_print(|| {
115+
let _ = godot::classes::RenderingServer::singleton();
116+
});
117+
});
118+
}
119+
120+
pub fn on_init_editor() {
121+
// Nothing yet.
72122
}

0 commit comments

Comments
 (0)