Skip to content

Commit a012d2f

Browse files
committed
Add main loop callbacks to ExtensionLibrary
1 parent d8864f1 commit a012d2f

File tree

3 files changed

+134
-11
lines changed

3 files changed

+134
-11
lines changed

godot-core/src/init/mod.rs

Lines changed: 74 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,28 @@ mod reexport_pub {
2020
}
2121
pub use reexport_pub::*;
2222

23+
#[repr(C)]
24+
struct InitUserData {
25+
library: sys::GDExtensionClassLibraryPtr,
26+
#[cfg(since_api = "4.5")]
27+
main_loop_callbacks: sys::GDExtensionMainLoopCallbacks,
28+
}
29+
30+
#[cfg(since_api = "4.5")]
31+
unsafe extern "C" fn startup_func<E: ExtensionLibrary>() {
32+
E::on_main_loop_startup();
33+
}
34+
35+
#[cfg(since_api = "4.5")]
36+
unsafe extern "C" fn frame_func<E: ExtensionLibrary>() {
37+
E::on_main_loop_frame();
38+
}
39+
40+
#[cfg(since_api = "4.5")]
41+
unsafe extern "C" fn shutdown_func<E: ExtensionLibrary>() {
42+
E::on_main_loop_shutdown();
43+
}
44+
2345
#[doc(hidden)]
2446
#[deny(unsafe_op_in_unsafe_fn)]
2547
pub unsafe fn __gdext_load_library<E: ExtensionLibrary>(
@@ -60,10 +82,20 @@ pub unsafe fn __gdext_load_library<E: ExtensionLibrary>(
6082
// Currently no way to express failure; could be exposed to E if necessary.
6183
// No early exit, unclear if Godot still requires output parameters to be set.
6284
let success = true;
85+
// Leak the userdata. It will be dropped in core level deinitialization.
86+
let userdata = Box::into_raw(Box::new(InitUserData {
87+
library,
88+
#[cfg(since_api = "4.5")]
89+
main_loop_callbacks: sys::GDExtensionMainLoopCallbacks {
90+
startup_func: Some(startup_func::<E>),
91+
frame_func: Some(frame_func::<E>),
92+
shutdown_func: Some(shutdown_func::<E>),
93+
},
94+
}));
6395

6496
let godot_init_params = sys::GDExtensionInitialization {
6597
minimum_initialization_level: E::min_level().to_sys(),
66-
userdata: std::ptr::null_mut(),
98+
userdata: userdata.cast(),
6799
initialize: Some(ffi_initialize_layer::<E>),
68100
deinitialize: Some(ffi_deinitialize_layer::<E>),
69101
};
@@ -88,22 +120,23 @@ pub unsafe fn __gdext_load_library<E: ExtensionLibrary>(
88120
static LEVEL_SERVERS_CORE_LOADED: AtomicBool = AtomicBool::new(false);
89121

90122
unsafe extern "C" fn ffi_initialize_layer<E: ExtensionLibrary>(
91-
_userdata: *mut std::ffi::c_void,
123+
userdata: *mut std::ffi::c_void,
92124
init_level: sys::GDExtensionInitializationLevel,
93125
) {
126+
let userdata = userdata.cast::<InitUserData>().as_ref().unwrap();
94127
let level = InitLevel::from_sys(init_level);
95128
let ctx = || format!("failed to initialize GDExtension level `{level:?}`");
96129

97-
fn try_load<E: ExtensionLibrary>(level: InitLevel) {
130+
fn try_load<E: ExtensionLibrary>(level: InitLevel, userdata: &InitUserData) {
98131
// Workaround for https://github.com/godot-rust/gdext/issues/629:
99132
// When using editor plugins, Godot may unload all levels but only reload from Scene upward.
100133
// Manually run initialization of lower levels.
101134

102135
// TODO: Remove this workaround once after the upstream issue is resolved.
103136
if level == InitLevel::Scene {
104137
if !LEVEL_SERVERS_CORE_LOADED.load(Ordering::Relaxed) {
105-
try_load::<E>(InitLevel::Core);
106-
try_load::<E>(InitLevel::Servers);
138+
try_load::<E>(InitLevel::Core, userdata);
139+
try_load::<E>(InitLevel::Servers, userdata);
107140
}
108141
} else if level == InitLevel::Core {
109142
// When it's normal initialization, the `Servers` level is normally initialized.
@@ -112,18 +145,18 @@ unsafe extern "C" fn ffi_initialize_layer<E: ExtensionLibrary>(
112145

113146
// SAFETY: Godot will call this from the main thread, after `__gdext_load_library` where the library is initialized,
114147
// and only once per level.
115-
unsafe { gdext_on_level_init(level) };
148+
unsafe { gdext_on_level_init(level, userdata) };
116149
E::on_level_init(level);
117150
}
118151

119152
// Swallow panics. TODO consider crashing if gdext init fails.
120153
let _ = crate::private::handle_panic(ctx, || {
121-
try_load::<E>(level);
154+
try_load::<E>(level, userdata);
122155
});
123156
}
124157

125158
unsafe extern "C" fn ffi_deinitialize_layer<E: ExtensionLibrary>(
126-
_userdata: *mut std::ffi::c_void,
159+
userdata: *mut std::ffi::c_void,
127160
init_level: sys::GDExtensionInitializationLevel,
128161
) {
129162
let level = InitLevel::from_sys(init_level);
@@ -134,6 +167,9 @@ unsafe extern "C" fn ffi_deinitialize_layer<E: ExtensionLibrary>(
134167
if level == InitLevel::Core {
135168
// Once the CORE api is unloaded, reset the flag to initial state.
136169
LEVEL_SERVERS_CORE_LOADED.store(false, Ordering::Relaxed);
170+
171+
// Drop the userdata.
172+
drop(Box::from_raw(userdata.cast::<InitUserData>()));
137173
}
138174

139175
E::on_level_deinit(level);
@@ -149,7 +185,7 @@ unsafe extern "C" fn ffi_deinitialize_layer<E: ExtensionLibrary>(
149185
/// - The interface must have been initialized.
150186
/// - Must only be called once per level.
151187
#[deny(unsafe_op_in_unsafe_fn)]
152-
unsafe fn gdext_on_level_init(level: InitLevel) {
188+
unsafe fn gdext_on_level_init(level: InitLevel, userdata: &InitUserData) {
153189
// TODO: in theory, a user could start a thread in one of the early levels, and run concurrent code that messes with the global state
154190
// (e.g. class registration). This would break the assumption that the load_class_method_table() calls are exclusive.
155191
// We could maybe protect globals with a mutex until initialization is complete, and then move it to a directly-accessible, read-only static.
@@ -158,6 +194,15 @@ unsafe fn gdext_on_level_init(level: InitLevel) {
158194
unsafe { sys::load_class_method_table(level) };
159195

160196
match level {
197+
InitLevel::Core => {
198+
#[cfg(since_api = "4.5")]
199+
unsafe {
200+
sys::interface_fn!(register_main_loop_callbacks)(
201+
userdata.library,
202+
&raw const userdata.main_loop_callbacks,
203+
)
204+
};
205+
}
161206
InitLevel::Servers => {
162207
// SAFETY: called from the main thread, sys::initialized has already been called.
163208
unsafe { sys::discover_main_thread() };
@@ -173,7 +218,6 @@ unsafe fn gdext_on_level_init(level: InitLevel) {
173218
crate::docs::register();
174219
}
175220
}
176-
_ => (),
177221
}
178222

179223
crate::registry::class::auto_register_classes(level);
@@ -303,6 +347,26 @@ pub unsafe trait ExtensionLibrary {
303347
// Nothing by default.
304348
}
305349

350+
/// Callback that is called after all initialization levels when Godot is fully initialized.
351+
#[cfg(since_api = "4.5")]
352+
fn on_main_loop_startup() {
353+
// Nothing by default.
354+
}
355+
356+
/// Callback that is called for every process frame.
357+
///
358+
/// This will run after all `_process()` methods on Node, and before `ScriptServer::frame()`.
359+
#[cfg(since_api = "4.5")]
360+
fn on_main_loop_frame() {
361+
// Nothing by default.
362+
}
363+
364+
/// Callback that is called before Godot is shutdown when it is still fully initialized.
365+
#[cfg(since_api = "4.5")]
366+
fn on_main_loop_shutdown() {
367+
// Nothing by default.
368+
}
369+
306370
/// Whether to override the Wasm binary filename used by your GDExtension which the library should expect at runtime. Return `None`
307371
/// to use the default where gdext expects either `{YourCrate}.wasm` (default binary name emitted by Rust) or
308372
/// `{YourCrate}.threads.wasm` (for builds producing separate single-threaded and multi-threaded binaries).

itest/rust/src/lib.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,19 @@ unsafe impl ExtensionLibrary for framework::IntegrationTests {
2727
fn on_level_init(level: InitLevel) {
2828
object_tests::on_level_init(level);
2929
}
30+
31+
#[cfg(since_api = "4.5")]
32+
fn on_main_loop_startup() {
33+
object_tests::on_main_loop_startup();
34+
}
35+
36+
#[cfg(since_api = "4.5")]
37+
fn on_main_loop_frame() {
38+
object_tests::on_main_loop_frame();
39+
}
40+
41+
#[cfg(since_api = "4.5")]
42+
fn on_main_loop_shutdown() {
43+
object_tests::on_main_loop_shutdown();
44+
}
3045
}

itest/rust/src/object_tests/init_level_test.rs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77

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

10+
use godot::builtin::Rid;
11+
use godot::classes::{Engine, IObject, RenderingServer};
1012
use godot::init::InitLevel;
11-
use godot::obj::{NewAlloc, Singleton};
13+
use godot::obj::{Base, GodotClass, NewAlloc, Singleton};
1214
use godot::register::{godot_api, GodotClass};
1315
use godot::sys::Global;
1416

@@ -120,3 +122,45 @@ fn on_init_scene() {
120122
pub fn on_init_editor() {
121123
// Nothing yet.
122124
}
125+
126+
#[derive(GodotClass)]
127+
#[class(base=Object)]
128+
struct MainLoopCallbackSingleton {
129+
tex: Rid,
130+
}
131+
132+
#[godot_api]
133+
impl IObject for MainLoopCallbackSingleton {
134+
fn init(_: Base<Self::Base>) -> Self {
135+
Self {
136+
tex: RenderingServer::singleton().texture_2d_placeholder_create(),
137+
}
138+
}
139+
}
140+
141+
pub fn on_main_loop_startup() {
142+
// RenderingServer should be accessible in MainLoop startup and shutdown.
143+
let singleton = MainLoopCallbackSingleton::new_alloc();
144+
assert!(singleton.bind().tex.is_valid());
145+
Engine::singleton().register_singleton(
146+
&MainLoopCallbackSingleton::class_id().to_string_name(),
147+
&singleton,
148+
);
149+
}
150+
151+
pub fn on_main_loop_frame() {
152+
// Nothing yet.
153+
}
154+
155+
pub fn on_main_loop_shutdown() {
156+
let singleton = Engine::singleton()
157+
.get_singleton(&MainLoopCallbackSingleton::class_id().to_string_name())
158+
.unwrap()
159+
.cast::<MainLoopCallbackSingleton>();
160+
Engine::singleton()
161+
.unregister_singleton(&MainLoopCallbackSingleton::class_id().to_string_name());
162+
let tex = singleton.bind().tex;
163+
assert!(tex.is_valid());
164+
RenderingServer::singleton().free_rid(tex);
165+
singleton.free();
166+
}

0 commit comments

Comments
 (0)