From 25425747c196adee5568d3dbebfaa9dff2cee587 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Wed, 22 Oct 2025 18:20:21 +0200 Subject: [PATCH 1/4] ExtensionLibrary: on_main_loop_* -> on_stage_[de]init + on_main_loop_frame Add InitStage as a superset of InitLevel, with added MainLoop variant. Integrates into the layered approach of on_level_[de]init, but with a new pair of methods due to backwards compatibility. --- godot-core/src/init/mod.rs | 103 +++++++++++++---- godot-ffi/src/lib.rs | 36 ++++++ godot/src/prelude.rs | 2 +- itest/hot-reload/rust/src/lib.rs | 8 +- itest/rust/src/lib.rs | 16 +-- ...{init_level_test.rs => init_stage_test.rs} | 107 +++++++++++------- itest/rust/src/object_tests/mod.rs | 4 +- 7 files changed, 190 insertions(+), 86 deletions(-) rename itest/rust/src/object_tests/{init_level_test.rs => init_stage_test.rs} (65%) diff --git a/godot-core/src/init/mod.rs b/godot-core/src/init/mod.rs index 8aefef1cb..3377031fa 100644 --- a/godot-core/src/init/mod.rs +++ b/godot-core/src/init/mod.rs @@ -16,7 +16,7 @@ use crate::out; mod reexport_pub { #[cfg(not(wasm_nothreads))] pub use super::sys::main_thread_id; - pub use super::sys::{is_main_thread, GdextBuild}; + pub use super::sys::{is_main_thread, GdextBuild, InitStage}; } pub use reexport_pub::*; @@ -29,7 +29,7 @@ struct InitUserData { #[cfg(since_api = "4.5")] unsafe extern "C" fn startup_func() { - E::on_main_loop_startup(); + E::on_stage_init(InitStage::MainLoop); } #[cfg(since_api = "4.5")] @@ -39,7 +39,7 @@ unsafe extern "C" fn frame_func() { #[cfg(since_api = "4.5")] unsafe extern "C" fn shutdown_func() { - E::on_main_loop_shutdown(); + E::on_stage_deinit(InitStage::MainLoop); } #[doc(hidden)] @@ -146,7 +146,7 @@ unsafe extern "C" fn ffi_initialize_layer( // SAFETY: Godot will call this from the main thread, after `__gdext_load_library` where the library is initialized, // and only once per level. unsafe { gdext_on_level_init(level, userdata) }; - E::on_level_init(level); + E::on_stage_init(level.to_stage()); } // Swallow panics. TODO consider crashing if gdext init fails. @@ -172,7 +172,7 @@ unsafe extern "C" fn ffi_deinitialize_layer( drop(Box::from_raw(userdata.cast::())); } - E::on_level_deinit(level); + E::on_stage_deinit(level.to_stage()); gdext_on_level_deinit(level); }); } @@ -327,43 +327,94 @@ pub unsafe trait ExtensionLibrary { InitLevel::Scene } - /// Custom logic when a certain init-level of Godot is loaded. + /// Custom logic when a certain initialization stage is loaded. /// - /// This will only be invoked for levels >= [`Self::min_level()`], in ascending order. Use `if` or `match` to hook to specific levels. + /// This will be invoked for stages >= [`Self::min_level()`], in ascending order. Use `if` or `match` to hook to specific stages. /// + /// The stages are loaded in order: `Core` → `Servers` → `Scene` → `Editor` (if in editor) → `MainLoop` (4.5+). \ + /// The `MainLoop` stage represents the fully initialized state of Godot, after all initialization levels and classes have been loaded. + /// + /// See also [`on_main_loop_frame()`][Self::on_main_loop_frame] for per-frame processing. + /// + /// # Panics /// If the overridden method panics, an error will be printed, but GDExtension loading is **not** aborted. #[allow(unused_variables)] - fn on_level_init(level: InitLevel) { - // Nothing by default. + fn on_stage_init(stage: InitStage) { + #[expect(deprecated)] // Fall back to older API. + stage + .try_to_level() + .inspect(|&level| Self::on_level_init(level)); } - /// Custom logic when a certain init-level of Godot is unloaded. + /// Custom logic when a certain initialization stage is unloaded. /// - /// This will only be invoked for levels >= [`Self::min_level()`], in descending order. Use `if` or `match` to hook to specific levels. + /// This will be invoked for stages >= [`Self::min_level()`], in descending order. Use `if` or `match` to hook to specific stages. /// + /// The stages are unloaded in reverse order: `MainLoop` (4.5+) → `Editor` (if in editor) → `Scene` → `Servers` → `Core`. \ + /// At the time `MainLoop` is deinitialized, all classes are still available. + /// + /// # Panics /// If the overridden method panics, an error will be printed, but GDExtension unloading is **not** aborted. #[allow(unused_variables)] - fn on_level_deinit(level: InitLevel) { - // Nothing by default. + fn on_stage_deinit(stage: InitStage) { + #[expect(deprecated)] // Fall back to older API. + stage + .try_to_level() + .inspect(|&level| Self::on_level_deinit(level)); } - /// Callback that is called after all initialization levels when Godot is fully initialized. - #[cfg(since_api = "4.5")] - fn on_main_loop_startup() { + /// Old callback before [`on_stage_init()`][Self::on_stage_deinit] was added. Does not support `MainLoop` stage. + #[deprecated = "Use `on_stage_init()` instead, which also includes the MainLoop stage."] + #[allow(unused_variables)] + fn on_level_init(level: InitLevel) { // Nothing by default. } - /// Callback that is called for every process frame. - /// - /// This will run after all `_process()` methods on Node, and before `ScriptServer::frame()`. - #[cfg(since_api = "4.5")] - fn on_main_loop_frame() { + /// Old callback before [`on_stage_deinit()`][Self::on_stage_deinit] was added. Does not support `MainLoop` stage. + #[deprecated = "Use `on_stage_deinit()` instead, which also includes the MainLoop stage."] + #[allow(unused_variables)] + fn on_level_deinit(level: InitLevel) { // Nothing by default. } - /// Callback that is called before Godot is shutdown when it is still fully initialized. + /// Callback invoked for every process frame. + /// + /// This is called during the main loop, after Godot is fully initialized. It runs after all + /// [`process()`][crate::classes::INode::process] methods on Node, and before the Godot-internal `ScriptServer::frame()`. + /// This is intended to be the equivalent of [`IScriptLanguageExtension::frame()`][`crate::classes::IScriptLanguageExtension::frame()`] + /// for GDExtension language bindings that don't use the script API. + /// + /// # Example + /// To hook into startup/shutdown of the main loop, use [`on_stage_init()`][Self::on_stage_init] and + /// [`on_stage_deinit()`][Self::on_stage_deinit] and watch for [`InitStage::MainLoop`]. + /// + /// ```no_run + /// # use godot::init::*; + /// # struct MyExtension; + /// #[gdextension] + /// unsafe impl ExtensionLibrary for MyExtension { + /// fn on_stage_init(stage: InitStage) { + /// if stage == InitStage::MainLoop { + /// // Startup code after fully initialized. + /// } + /// } + /// + /// fn on_main_loop_frame() { + /// // Per-frame logic. + /// } + /// + /// fn on_stage_deinit(stage: InitStage) { + /// if stage == InitStage::MainLoop { + /// // Cleanup code before shutdown. + /// } + /// } + /// } + /// ``` + /// + /// # Panics + /// If the overridden method panics, an error will be printed, but execution continues. #[cfg(since_api = "4.5")] - fn on_main_loop_shutdown() { + fn on_main_loop_frame() { // Nothing by default. } @@ -444,8 +495,10 @@ pub enum EditorRunBehavior { /// a different amount of engine functionality is available. Deinitialization happens in reverse order. /// /// See also: -/// - [`ExtensionLibrary::on_level_init()`] -/// - [`ExtensionLibrary::on_level_deinit()`] +/// - [`InitStage`] - Extended initialization stage that includes the main loop. +/// - [`ExtensionLibrary::on_stage_init()`] +/// - [`ExtensionLibrary::on_stage_deinit()`] +/// - [`ExtensionLibrary::on_main_loop_frame()`] pub type InitLevel = sys::InitLevel; // ---------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index 253e40942..168d67e5f 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -151,8 +151,44 @@ impl InitLevel { Self::Editor => crate::GDEXTENSION_INITIALIZATION_EDITOR, } } + + /// Convert this initialization level to an initialization stage. + pub fn to_stage(self) -> InitStage { + match self { + Self::Core => InitStage::Core, + Self::Servers => InitStage::Servers, + Self::Scene => InitStage::Scene, + Self::Editor => InitStage::Editor, + } + } } +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +pub enum InitStage { + Core, + Servers, + Scene, + Editor, + #[cfg(since_api = "4.5")] + MainLoop, +} + +impl InitStage { + pub fn try_to_level(self) -> Option { + match self { + Self::Core => Some(InitLevel::Core), + Self::Servers => Some(InitLevel::Servers), + Self::Scene => Some(InitLevel::Scene), + Self::Editor => Some(InitLevel::Editor), + #[cfg(since_api = "4.5")] + Self::MainLoop => None, + } + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + pub struct GdextRuntimeMetadata { godot_version: GDExtensionGodotVersion, } diff --git a/godot/src/prelude.rs b/godot/src/prelude.rs index 9d479b075..1188b155b 100644 --- a/godot/src/prelude.rs +++ b/godot/src/prelude.rs @@ -13,7 +13,7 @@ pub use super::classes::{ pub use super::global::{ godot_error, godot_print, godot_print_rich, godot_script_error, godot_warn, }; -pub use super::init::{gdextension, ExtensionLibrary, InitLevel}; +pub use super::init::{gdextension, ExtensionLibrary, InitLevel, InitStage}; pub use super::meta::error::{ConvertError, IoError}; pub use super::meta::{FromGodot, GodotConvert, ToGodot}; pub use super::obj::{ diff --git a/itest/hot-reload/rust/src/lib.rs b/itest/hot-reload/rust/src/lib.rs index 4b74b7bfc..69d3c9016 100644 --- a/itest/hot-reload/rust/src/lib.rs +++ b/itest/hot-reload/rust/src/lib.rs @@ -11,12 +11,12 @@ struct HotReload; #[gdextension] unsafe impl ExtensionLibrary for HotReload { - fn on_level_init(level: InitLevel) { - println!("[Rust] Init level {level:?}"); + fn on_stage_init(stage: InitStage) { + println!("[Rust] Init stage {stage:?}"); } - fn on_level_deinit(level: InitLevel) { - println!("[Rust] Deinit level {level:?}"); + fn on_stage_deinit(stage: InitStage) { + println!("[Rust] Deinit stage {stage:?}"); } } diff --git a/itest/rust/src/lib.rs b/itest/rust/src/lib.rs index 201977657..20aab16af 100644 --- a/itest/rust/src/lib.rs +++ b/itest/rust/src/lib.rs @@ -5,7 +5,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use godot::init::{gdextension, ExtensionLibrary, InitLevel}; +use godot::init::{gdextension, ExtensionLibrary, InitLevel, InitStage}; mod benchmarks; mod builtin_tests; @@ -24,22 +24,16 @@ unsafe impl ExtensionLibrary for framework::IntegrationTests { InitLevel::Core } - fn on_level_init(level: InitLevel) { - object_tests::on_level_init(level); + fn on_stage_init(stage: InitStage) { + object_tests::on_stage_init(stage); } - #[cfg(since_api = "4.5")] - fn on_main_loop_startup() { - object_tests::on_main_loop_startup(); + fn on_stage_deinit(stage: InitStage) { + object_tests::on_stage_deinit(stage); } #[cfg(since_api = "4.5")] fn on_main_loop_frame() { object_tests::on_main_loop_frame(); } - - #[cfg(since_api = "4.5")] - fn on_main_loop_shutdown() { - object_tests::on_main_loop_shutdown(); - } } diff --git a/itest/rust/src/object_tests/init_level_test.rs b/itest/rust/src/object_tests/init_stage_test.rs similarity index 65% rename from itest/rust/src/object_tests/init_level_test.rs rename to itest/rust/src/object_tests/init_stage_test.rs index ad4fe397c..f919a74f4 100644 --- a/itest/rust/src/object_tests/init_level_test.rs +++ b/itest/rust/src/object_tests/init_stage_test.rs @@ -5,19 +5,16 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use std::sync::atomic::{AtomicBool, Ordering}; - -use godot::builtin::Rid; +use godot::builtin::{Rid, Variant}; use godot::classes::{Engine, IObject, RenderingServer}; -use godot::init::InitLevel; +use godot::init::InitStage; use godot::obj::{Base, GodotClass, NewAlloc, Singleton}; use godot::register::{godot_api, GodotClass}; use godot::sys::Global; use crate::framework::{expect_panic, itest, runs_release, suppress_godot_print}; -static OBJECT_CALL_HAS_RUN: AtomicBool = AtomicBool::new(false); -static LEVELS_SEEN: Global> = Global::default(); +static STAGES_SEEN: Global> = Global::default(); #[derive(GodotClass)] #[class(base = Object, init)] @@ -26,57 +23,53 @@ struct SomeObject {} #[godot_api] impl SomeObject { #[func] - pub fn set_has_run_true(&self) { - OBJECT_CALL_HAS_RUN.store(true, Ordering::Release); + pub fn method(&self) -> i32 { + 356 } pub fn test() { - assert!(!OBJECT_CALL_HAS_RUN.load(Ordering::Acquire)); let mut some_object = SomeObject::new_alloc(); // Need to go through Godot here as otherwise we bypass the failure. - some_object.call("set_has_run_true", &[]); + let result = some_object.call("method", &[]); + assert_eq!(result, Variant::from(356)); + some_object.free(); } } -// Ensure that the above function has actually run and succeeded. -#[itest] -fn init_level_all_initialized() { - assert!( - OBJECT_CALL_HAS_RUN.load(Ordering::Relaxed), - "Object call function did not run during Core init level" - ); -} - // Ensure that we saw all the init levels expected. #[itest] fn init_level_observed_all() { - let levels_seen = LEVELS_SEEN.lock().clone(); + let actual_stages = STAGES_SEEN.lock().clone(); - assert_eq!(levels_seen[0], InitLevel::Core); - assert_eq!(levels_seen[1], InitLevel::Servers); - assert_eq!(levels_seen[2], InitLevel::Scene); + let mut expected_stages = vec![InitStage::Core, InitStage::Servers, InitStage::Scene]; // In Debug/Editor builds, Editor level is loaded; otherwise not. - let level_3 = levels_seen.get(3); - if runs_release() { - assert_eq!(level_3, None); - } else { - assert_eq!(level_3, Some(&InitLevel::Editor)); + if !runs_release() { + expected_stages.push(InitStage::Editor); } -} -// ---------------------------------------------------------------------------------------------------------------------------------------------- -// Level-specific callbacks + // From Godot 4.5, MainLoop level is added. + #[cfg(since_api = "4.5")] + expected_stages.push(InitStage::MainLoop); -pub fn on_level_init(level: InitLevel) { - LEVELS_SEEN.lock().push(level); + assert_eq!(actual_stages, expected_stages); +} - match level { - InitLevel::Core => on_init_core(), - InitLevel::Servers => on_init_servers(), - InitLevel::Scene => on_init_scene(), - InitLevel::Editor => on_init_editor(), +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Stage-specific callbacks + +pub fn on_stage_init(stage: InitStage) { + STAGES_SEEN.lock().push(stage); + + match stage { + InitStage::Core => on_init_core(), + InitStage::Servers => on_init_servers(), + InitStage::Scene => on_init_scene(), + InitStage::Editor => on_init_editor(), + #[cfg(since_api = "4.5")] + InitStage::MainLoop => on_init_main_loop(), + _ => { /* Needed due to #[non_exhaustive] */ } } } @@ -138,8 +131,9 @@ impl IObject for MainLoopCallbackSingleton { } } -pub fn on_main_loop_startup() { - // RenderingServer should be accessible in MainLoop startup and shutdown. +#[cfg(since_api = "4.5")] +fn on_init_main_loop() { + // RenderingServer should be accessible in MainLoop init and deinit. let singleton = MainLoopCallbackSingleton::new_alloc(); assert!(singleton.bind().tex.is_valid()); Engine::singleton().register_singleton( @@ -148,11 +142,23 @@ pub fn on_main_loop_startup() { ); } -pub fn on_main_loop_frame() { - // Nothing yet. +#[cfg(not(since_api = "4.5"))] +fn on_init_main_loop() { + // Nothing on older API versions. } -pub fn on_main_loop_shutdown() { +pub fn on_stage_deinit(stage: InitStage) { + match stage { + #[cfg(since_api = "4.5")] + InitStage::MainLoop => on_deinit_main_loop(), + _ => { + // Nothing for other stages yet. + } + } +} + +#[cfg(since_api = "4.5")] +fn on_deinit_main_loop() { let singleton = Engine::singleton() .get_singleton(&MainLoopCallbackSingleton::class_id().to_string_name()) .unwrap() @@ -164,3 +170,18 @@ pub fn on_main_loop_shutdown() { RenderingServer::singleton().free_rid(tex); singleton.free(); } + +#[cfg(not(since_api = "4.5"))] +fn on_deinit_main_loop() { + // Nothing on older API versions. +} + +#[cfg(since_api = "4.5")] +pub fn on_main_loop_frame() { + // Nothing yet. +} + +#[cfg(not(since_api = "4.5"))] +pub fn on_main_loop_frame() { + // Nothing on older API versions. +} diff --git a/itest/rust/src/object_tests/mod.rs b/itest/rust/src/object_tests/mod.rs index 01270a86c..319d8a8dd 100644 --- a/itest/rust/src/object_tests/mod.rs +++ b/itest/rust/src/object_tests/mod.rs @@ -15,7 +15,7 @@ mod enum_test; // `get_property_list` is only supported in Godot 4.3+ #[cfg(since_api = "4.3")] mod get_property_list_test; -mod init_level_test; +mod init_stage_test; mod object_arg_test; mod object_swap_test; mod object_test; @@ -33,4 +33,4 @@ mod virtual_methods_niche_test; mod virtual_methods_test; // Need to test this in the init level method. -pub use init_level_test::*; +pub use init_stage_test::*; From c5ff2d1d679ba0d89de27fe9d1837926ae7e703b Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Thu, 23 Oct 2025 18:39:56 +0200 Subject: [PATCH 2/4] Fix documentation of `InitLevel` We previously used a type alias because `use` statements don't allow RustDoc in this case. However, this caused many broken links and shows up in a "Type Aliases" section, rather than in "Enums" next to `InitStage`. The new setup requires HTML links because the godot-ffi crate doesn't know the symbols, but it ultimately works, and we can also avoid duplicating docs. --- godot-core/src/init/mod.rs | 14 ++------------ godot-ffi/src/lib.rs | 8 ++++++++ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/godot-core/src/init/mod.rs b/godot-core/src/init/mod.rs index 3377031fa..9d9d958ee 100644 --- a/godot-core/src/init/mod.rs +++ b/godot-core/src/init/mod.rs @@ -445,7 +445,7 @@ pub unsafe trait ExtensionLibrary { /// #[cfg(feature = "nothreads")] /// return None; /// - /// // Tell gdext we add a custom suffix to the binary with thread support. + /// // Tell godot-rust we add a custom suffix to the binary with thread support. /// // Please note that this is not needed if "mycrate.threads.wasm" is used. /// // (You could return `None` as well in that particular case.) /// #[cfg(not(feature = "nothreads"))] @@ -489,17 +489,7 @@ pub enum EditorRunBehavior { // ---------------------------------------------------------------------------------------------------------------------------------------------- -/// Stage of the Godot initialization process. -/// -/// Godot's initialization and deinitialization processes are split into multiple stages, like a stack. At each level, -/// a different amount of engine functionality is available. Deinitialization happens in reverse order. -/// -/// See also: -/// - [`InitStage`] - Extended initialization stage that includes the main loop. -/// - [`ExtensionLibrary::on_stage_init()`] -/// - [`ExtensionLibrary::on_stage_deinit()`] -/// - [`ExtensionLibrary::on_main_loop_frame()`] -pub type InitLevel = sys::InitLevel; +pub use sys::InitLevel; // ---------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index 168d67e5f..f26519e97 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -113,6 +113,12 @@ static MAIN_THREAD_ID: ManualInitCell = ManualInitCell::n /// /// Godot's initialization and deinitialization processes are split into multiple stages, like a stack. At each level, /// a different amount of engine functionality is available. Deinitialization happens in reverse order. +/// +/// See also: +// Explicit HTML links because this is re-exported in godot::init, and we can't document a `use` statement. +/// - [`InitStage`](enum.InitStage.html): all levels + main loop. +/// - [`ExtensionLibrary::on_stage_init()`](trait.ExtensionLibrary.html#method.on_stage_init) +/// - [`ExtensionLibrary::on_stage_deinit()`](trait.ExtensionLibrary.html#method.on_stage_deinit) #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] pub enum InitLevel { /// First level loaded by Godot. Builtin types are available, classes are not. @@ -165,6 +171,8 @@ impl InitLevel { // ---------------------------------------------------------------------------------------------------------------------------------------------- +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +#[non_exhaustive] pub enum InitStage { Core, Servers, From 025b1fb7478c2a9103d2a81707faf52a70f924de Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Thu, 23 Oct 2025 19:35:11 +0200 Subject: [PATCH 3/4] Extract InitLevel + InitStage to separate file --- godot-ffi/src/init_level.rs | 120 ++++++++++++++++++++++++++++++++++++ godot-ffi/src/lib.rs | 88 +------------------------- 2 files changed, 122 insertions(+), 86 deletions(-) create mode 100644 godot-ffi/src/init_level.rs diff --git a/godot-ffi/src/init_level.rs b/godot-ffi/src/init_level.rs new file mode 100644 index 000000000..47d515f93 --- /dev/null +++ b/godot-ffi/src/init_level.rs @@ -0,0 +1,120 @@ +/* + * Copyright (c) godot-rust; Bromeon and contributors. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +/// Stage of the Godot initialization process. +/// +/// Godot's initialization and deinitialization processes are split into multiple stages, like a stack. At each level, +/// a different amount of engine functionality is available. Deinitialization happens in reverse order. +/// +/// See also: +// Explicit HTML links because this is re-exported in godot::init, and we can't document a `use` statement. +/// - [`InitStage`](enum.InitStage.html): all levels + main loop. +/// - [`ExtensionLibrary::on_stage_init()`](trait.ExtensionLibrary.html#method.on_stage_init) +/// - [`ExtensionLibrary::on_stage_deinit()`](trait.ExtensionLibrary.html#method.on_stage_deinit) +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub enum InitLevel { + /// First level loaded by Godot. Builtin types are available, classes are not. + Core, + + /// Second level loaded by Godot. Only server classes and builtins are available. + Servers, + + /// Third level loaded by Godot. Most classes are available. + Scene, + + /// Fourth level loaded by Godot, only in the editor. All classes are available. + Editor, +} + +impl InitLevel { + #[doc(hidden)] + pub fn from_sys(level: crate::GDExtensionInitializationLevel) -> Self { + match level { + crate::GDEXTENSION_INITIALIZATION_CORE => Self::Core, + crate::GDEXTENSION_INITIALIZATION_SERVERS => Self::Servers, + crate::GDEXTENSION_INITIALIZATION_SCENE => Self::Scene, + crate::GDEXTENSION_INITIALIZATION_EDITOR => Self::Editor, + _ => { + eprintln!("WARNING: unknown initialization level {level}"); + Self::Scene + } + } + } + + #[doc(hidden)] + pub fn to_sys(self) -> crate::GDExtensionInitializationLevel { + match self { + Self::Core => crate::GDEXTENSION_INITIALIZATION_CORE, + Self::Servers => crate::GDEXTENSION_INITIALIZATION_SERVERS, + Self::Scene => crate::GDEXTENSION_INITIALIZATION_SCENE, + Self::Editor => crate::GDEXTENSION_INITIALIZATION_EDITOR, + } + } + + /// Convert this initialization level to an initialization stage. + pub fn to_stage(self) -> InitStage { + match self { + Self::Core => InitStage::Core, + Self::Servers => InitStage::Servers, + Self::Scene => InitStage::Scene, + Self::Editor => InitStage::Editor, + } + } +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +/// Extended initialization stage that includes both initialization levels and the main loop. +/// +/// This enum extends [`InitLevel`] with a `MainLoop` variant, representing the fully initialized state of Godot +/// after all initialization levels have been loaded and before any deinitialization begins. +/// +/// During initialization, stages are loaded in order: `Core` → `Servers` → `Scene` → `Editor` (if in editor) → `MainLoop`. \ +/// During deinitialization, stages are unloaded in reverse order. +/// +/// See also: +/// - [`InitLevel`](enum.InitLevel.html): only levels, without `MainLoop`. +/// - [`ExtensionLibrary::on_stage_init()`](trait.ExtensionLibrary.html#method.on_stage_init) +/// - [`ExtensionLibrary::on_stage_deinit()`](trait.ExtensionLibrary.html#method.on_stage_deinit) +/// - [`ExtensionLibrary::on_main_loop_frame()`](trait.ExtensionLibrary.html#method.on_main_loop_frame) +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +#[non_exhaustive] +pub enum InitStage { + /// First level loaded by Godot. Builtin types are available, classes are not. + Core, + + /// Second level loaded by Godot. Only server classes and builtins are available. + Servers, + + /// Third level loaded by Godot. Most classes are available. + Scene, + + /// Fourth level loaded by Godot, only in the editor. All classes are available. + Editor, + + /// The main loop stage, representing the fully initialized state of Godot. + /// + /// This variant is only available in Godot 4.5+. In earlier versions, it will never be passed to callbacks. + #[cfg(since_api = "4.5")] + MainLoop, +} + +impl InitStage { + /// Try to convert this initialization stage to an initialization level. + /// + /// Returns `None` for [`InitStage::MainLoop`], as it doesn't correspond to a Godot initialization level. + pub fn try_to_level(self) -> Option { + match self { + Self::Core => Some(InitLevel::Core), + Self::Servers => Some(InitLevel::Servers), + Self::Scene => Some(InitLevel::Scene), + Self::Editor => Some(InitLevel::Editor), + #[cfg(since_api = "4.5")] + Self::MainLoop => None, + } + } +} diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index f26519e97..ad7e6655e 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -87,6 +87,7 @@ pub use gen::table_scene_classes::*; pub use gen::table_servers_classes::*; pub use gen::table_utilities::*; pub use global::*; +pub use init_level::*; pub use string_cache::StringCache; pub use toolbox::*; @@ -98,6 +99,7 @@ pub use crate::godot_ffi::{ // API to access Godot via FFI mod binding; +mod init_level; pub use binding::*; use binding::{ @@ -109,92 +111,6 @@ use binding::{ #[cfg(not(wasm_nothreads))] static MAIN_THREAD_ID: ManualInitCell = ManualInitCell::new(); -/// Stage of the Godot initialization process. -/// -/// Godot's initialization and deinitialization processes are split into multiple stages, like a stack. At each level, -/// a different amount of engine functionality is available. Deinitialization happens in reverse order. -/// -/// See also: -// Explicit HTML links because this is re-exported in godot::init, and we can't document a `use` statement. -/// - [`InitStage`](enum.InitStage.html): all levels + main loop. -/// - [`ExtensionLibrary::on_stage_init()`](trait.ExtensionLibrary.html#method.on_stage_init) -/// - [`ExtensionLibrary::on_stage_deinit()`](trait.ExtensionLibrary.html#method.on_stage_deinit) -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] -pub enum InitLevel { - /// First level loaded by Godot. Builtin types are available, classes are not. - Core, - - /// Second level loaded by Godot. Only server classes and builtins are available. - Servers, - - /// Third level loaded by Godot. Most classes are available. - Scene, - - /// Fourth level loaded by Godot, only in the editor. All classes are available. - Editor, -} - -impl InitLevel { - #[doc(hidden)] - pub fn from_sys(level: crate::GDExtensionInitializationLevel) -> Self { - match level { - crate::GDEXTENSION_INITIALIZATION_CORE => Self::Core, - crate::GDEXTENSION_INITIALIZATION_SERVERS => Self::Servers, - crate::GDEXTENSION_INITIALIZATION_SCENE => Self::Scene, - crate::GDEXTENSION_INITIALIZATION_EDITOR => Self::Editor, - _ => { - eprintln!("WARNING: unknown initialization level {level}"); - Self::Scene - } - } - } - #[doc(hidden)] - pub fn to_sys(self) -> crate::GDExtensionInitializationLevel { - match self { - Self::Core => crate::GDEXTENSION_INITIALIZATION_CORE, - Self::Servers => crate::GDEXTENSION_INITIALIZATION_SERVERS, - Self::Scene => crate::GDEXTENSION_INITIALIZATION_SCENE, - Self::Editor => crate::GDEXTENSION_INITIALIZATION_EDITOR, - } - } - - /// Convert this initialization level to an initialization stage. - pub fn to_stage(self) -> InitStage { - match self { - Self::Core => InitStage::Core, - Self::Servers => InitStage::Servers, - Self::Scene => InitStage::Scene, - Self::Editor => InitStage::Editor, - } - } -} - -// ---------------------------------------------------------------------------------------------------------------------------------------------- - -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] -#[non_exhaustive] -pub enum InitStage { - Core, - Servers, - Scene, - Editor, - #[cfg(since_api = "4.5")] - MainLoop, -} - -impl InitStage { - pub fn try_to_level(self) -> Option { - match self { - Self::Core => Some(InitLevel::Core), - Self::Servers => Some(InitLevel::Servers), - Self::Scene => Some(InitLevel::Scene), - Self::Editor => Some(InitLevel::Editor), - #[cfg(since_api = "4.5")] - Self::MainLoop => None, - } - } -} - // ---------------------------------------------------------------------------------------------------------------------------------------------- pub struct GdextRuntimeMetadata { From 3d44fb683a37269d37d97629069da2c1b526394f Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Thu, 23 Oct 2025 22:48:46 +0200 Subject: [PATCH 4/4] Compat layer for old main-loop API --- godot-core/src/init/mod.rs | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/godot-core/src/init/mod.rs b/godot-core/src/init/mod.rs index 9d9d958ee..842d6870b 100644 --- a/godot-core/src/init/mod.rs +++ b/godot-core/src/init/mod.rs @@ -339,11 +339,16 @@ pub unsafe trait ExtensionLibrary { /// # Panics /// If the overridden method panics, an error will be printed, but GDExtension loading is **not** aborted. #[allow(unused_variables)] + #[expect(deprecated)] // Fall back to older API. fn on_stage_init(stage: InitStage) { - #[expect(deprecated)] // Fall back to older API. stage .try_to_level() .inspect(|&level| Self::on_level_init(level)); + + #[cfg(since_api = "4.5")] // Compat layer. + if stage == InitStage::MainLoop { + Self::on_main_loop_startup(); + } } /// Custom logic when a certain initialization stage is unloaded. @@ -356,8 +361,13 @@ pub unsafe trait ExtensionLibrary { /// # Panics /// If the overridden method panics, an error will be printed, but GDExtension unloading is **not** aborted. #[allow(unused_variables)] + #[expect(deprecated)] // Fall back to older API. fn on_stage_deinit(stage: InitStage) { - #[expect(deprecated)] // Fall back to older API. + #[cfg(since_api = "4.5")] // Compat layer. + if stage == InitStage::MainLoop { + Self::on_main_loop_shutdown(); + } + stage .try_to_level() .inspect(|&level| Self::on_level_deinit(level)); @@ -377,6 +387,20 @@ pub unsafe trait ExtensionLibrary { // Nothing by default. } + #[cfg(since_api = "4.5")] + #[deprecated = "Use `on_stage_init(InitStage::MainLoop)` instead."] + #[doc(hidden)] // Added by mistake -- works but don't advertise. + fn on_main_loop_startup() { + // Nothing by default. + } + + #[cfg(since_api = "4.5")] + #[deprecated = "Use `on_stage_deinit(InitStage::MainLoop)` instead."] + #[doc(hidden)] // Added by mistake -- works but don't advertise. + fn on_main_loop_shutdown() { + // Nothing by default. + } + /// Callback invoked for every process frame. /// /// This is called during the main loop, after Godot is fully initialized. It runs after all