Skip to content

Commit 66e5afc

Browse files
committed
Add main loop callbacks to ExtensionLibrary
1 parent b0ba40e commit 66e5afc

File tree

3 files changed

+117
-11
lines changed

3 files changed

+117
-11
lines changed

godot-core/src/init/mod.rs

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

23+
#[repr(C)]
24+
struct InitUserData {
25+
library: sys::GDExtensionClassLibraryPtr,
26+
main_loop_callbacks: sys::GDExtensionMainLoopCallbacks,
27+
}
28+
29+
unsafe extern "C" fn startup_func<E: ExtensionLibrary>() {
30+
E::on_main_loop_startup();
31+
}
32+
33+
unsafe extern "C" fn frame_func<E: ExtensionLibrary>() {
34+
E::on_main_loop_frame();
35+
}
36+
37+
unsafe extern "C" fn shutdown_func<E: ExtensionLibrary>() {
38+
E::on_main_loop_shutdown();
39+
}
40+
2341
#[doc(hidden)]
2442
#[deny(unsafe_op_in_unsafe_fn)]
2543
pub unsafe fn __gdext_load_library<E: ExtensionLibrary>(
@@ -60,10 +78,19 @@ pub unsafe fn __gdext_load_library<E: ExtensionLibrary>(
6078
// Currently no way to express failure; could be exposed to E if necessary.
6179
// No early exit, unclear if Godot still requires output parameters to be set.
6280
let success = true;
81+
// Userdata will be dropped in core level deinitialization.
82+
let userdata = Box::into_raw(Box::new(InitUserData {
83+
library,
84+
main_loop_callbacks: sys::GDExtensionMainLoopCallbacks {
85+
startup_func: Some(startup_func::<E>),
86+
frame_func: Some(frame_func::<E>),
87+
shutdown_func: Some(shutdown_func::<E>),
88+
},
89+
}));
6390

6491
let godot_init_params = sys::GDExtensionInitialization {
6592
minimum_initialization_level: E::min_level().to_sys(),
66-
userdata: std::ptr::null_mut(),
93+
userdata: userdata as *mut std::ffi::c_void,
6794
initialize: Some(ffi_initialize_layer::<E>),
6895
deinitialize: Some(ffi_deinitialize_layer::<E>),
6996
};
@@ -88,22 +115,23 @@ pub unsafe fn __gdext_load_library<E: ExtensionLibrary>(
88115
static LEVEL_SERVERS_CORE_LOADED: AtomicBool = AtomicBool::new(false);
89116

90117
unsafe extern "C" fn ffi_initialize_layer<E: ExtensionLibrary>(
91-
_userdata: *mut std::ffi::c_void,
118+
userdata: *mut std::ffi::c_void,
92119
init_level: sys::GDExtensionInitializationLevel,
93120
) {
121+
let userdata = (userdata as *mut InitUserData).as_ref().unwrap();
94122
let level = InitLevel::from_sys(init_level);
95123
let ctx = || format!("failed to initialize GDExtension level `{level:?}`");
96124

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

102130
// TODO: Remove this workaround once after the upstream issue is resolved.
103131
if level == InitLevel::Scene {
104132
if !LEVEL_SERVERS_CORE_LOADED.load(Ordering::Relaxed) {
105-
try_load::<E>(InitLevel::Core);
106-
try_load::<E>(InitLevel::Servers);
133+
try_load::<E>(InitLevel::Core, userdata);
134+
try_load::<E>(InitLevel::Servers, userdata);
107135
}
108136
} else if level == InitLevel::Core {
109137
// When it's normal initialization, the `Servers` level is normally initialized.
@@ -112,18 +140,18 @@ unsafe extern "C" fn ffi_initialize_layer<E: ExtensionLibrary>(
112140

113141
// SAFETY: Godot will call this from the main thread, after `__gdext_load_library` where the library is initialized,
114142
// and only once per level.
115-
unsafe { gdext_on_level_init(level) };
143+
unsafe { gdext_on_level_init(level, userdata) };
116144
E::on_level_init(level);
117145
}
118146

119147
// Swallow panics. TODO consider crashing if gdext init fails.
120148
let _ = crate::private::handle_panic(ctx, || {
121-
try_load::<E>(level);
149+
try_load::<E>(level, userdata);
122150
});
123151
}
124152

125153
unsafe extern "C" fn ffi_deinitialize_layer<E: ExtensionLibrary>(
126-
_userdata: *mut std::ffi::c_void,
154+
userdata: *mut std::ffi::c_void,
127155
init_level: sys::GDExtensionInitializationLevel,
128156
) {
129157
let level = InitLevel::from_sys(init_level);
@@ -134,6 +162,9 @@ unsafe extern "C" fn ffi_deinitialize_layer<E: ExtensionLibrary>(
134162
if level == InitLevel::Core {
135163
// Once the CORE api is unloaded, reset the flag to initial state.
136164
LEVEL_SERVERS_CORE_LOADED.store(false, Ordering::Relaxed);
165+
166+
// Drop the userdata.
167+
drop(Box::from_raw(userdata as *mut InitUserData));
137168
}
138169

139170
E::on_level_deinit(level);
@@ -149,7 +180,7 @@ unsafe extern "C" fn ffi_deinitialize_layer<E: ExtensionLibrary>(
149180
/// - The interface must have been initialized.
150181
/// - Must only be called once per level.
151182
#[deny(unsafe_op_in_unsafe_fn)]
152-
unsafe fn gdext_on_level_init(level: InitLevel) {
183+
unsafe fn gdext_on_level_init(level: InitLevel, userdata: &InitUserData) {
153184
// 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
154185
// (e.g. class registration). This would break the assumption that the load_class_method_table() calls are exclusive.
155186
// 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 +189,14 @@ unsafe fn gdext_on_level_init(level: InitLevel) {
158189
unsafe { sys::load_class_method_table(level) };
159190

160191
match level {
192+
InitLevel::Core => {
193+
unsafe {
194+
sys::interface_fn!(register_main_loop_callbacks)(
195+
userdata.library,
196+
&raw const userdata.main_loop_callbacks,
197+
)
198+
};
199+
}
161200
InitLevel::Servers => {
162201
// SAFETY: called from the main thread, sys::initialized has already been called.
163202
unsafe { sys::discover_main_thread() };
@@ -173,7 +212,6 @@ unsafe fn gdext_on_level_init(level: InitLevel) {
173212
crate::docs::register();
174213
}
175214
}
176-
_ => (),
177215
}
178216

179217
crate::registry::class::auto_register_classes(level);
@@ -303,6 +341,23 @@ pub unsafe trait ExtensionLibrary {
303341
// Nothing by default.
304342
}
305343

344+
/// Callback that is called after all initialization levels when Godot is fully initialized.
345+
fn on_main_loop_startup() {
346+
// Nothing by default.
347+
}
348+
349+
/// Callback that is called for every process frame.
350+
///
351+
/// This will run after all `_process()` methods on Node, and before `ScriptServer::frame()`.
352+
fn on_main_loop_frame() {
353+
// Nothing by default.
354+
}
355+
356+
/// Callback that is called before Godot is shutdown when it is still fully initialized.
357+
fn on_main_loop_shutdown() {
358+
// Nothing by default.
359+
}
360+
306361
/// Whether to override the Wasm binary filename used by your GDExtension which the library should expect at runtime. Return `None`
307362
/// to use the default where gdext expects either `{YourCrate}.wasm` (default binary name emitted by Rust) or
308363
/// `{YourCrate}.threads.wasm` (for builds producing separate single-threaded and multi-threaded binaries).

itest/rust/src/lib.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,16 @@ unsafe impl ExtensionLibrary for framework::IntegrationTests {
2727
fn on_level_init(level: InitLevel) {
2828
object_tests::on_level_init(level);
2929
}
30+
31+
fn on_main_loop_startup() {
32+
object_tests::on_main_loop_startup();
33+
}
34+
35+
fn on_main_loop_frame() {
36+
object_tests::on_main_loop_frame();
37+
}
38+
39+
fn on_main_loop_shutdown() {
40+
object_tests::on_main_loop_shutdown();
41+
}
3042
}

itest/rust/src/object_tests/init_level_test.rs

Lines changed: 40 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, RenderingServer};
1012
use godot::init::InitLevel;
11-
use godot::obj::NewAlloc;
13+
use godot::obj::{Gd, GodotClass, NewAlloc};
1214
use godot::register::{godot_api, GodotClass};
1315
use godot::sys::Global;
1416

@@ -120,3 +122,40 @@ fn on_init_scene() {
120122
pub fn on_init_editor() {
121123
// Nothing yet.
122124
}
125+
126+
#[derive(GodotClass)]
127+
#[class(base=Object, init)]
128+
struct MainLoopCallbackSingleton {
129+
#[init(val=RenderingServer::singleton())]
130+
rs: Gd<RenderingServer>,
131+
#[init(val=RenderingServer::singleton().texture_2d_placeholder_create())]
132+
tex: Rid,
133+
}
134+
135+
pub fn on_main_loop_startup() {
136+
let singleton = MainLoopCallbackSingleton::new_alloc();
137+
assert!(singleton.bind().rs.is_instance_valid());
138+
assert!(singleton.bind().tex.is_valid());
139+
Engine::singleton().register_singleton(
140+
&MainLoopCallbackSingleton::class_name().to_string_name(),
141+
&singleton,
142+
);
143+
}
144+
145+
pub fn on_main_loop_frame() {
146+
// Nothing yet.
147+
}
148+
149+
pub fn on_main_loop_shutdown() {
150+
let mut singleton = Engine::singleton()
151+
.get_singleton(&MainLoopCallbackSingleton::class_name().to_string_name())
152+
.unwrap()
153+
.cast::<MainLoopCallbackSingleton>();
154+
Engine::singleton()
155+
.unregister_singleton(&MainLoopCallbackSingleton::class_name().to_string_name());
156+
let tex = singleton.bind().tex;
157+
assert!(singleton.bind().rs.is_instance_valid());
158+
assert!(tex.is_valid());
159+
singleton.bind_mut().rs.free_rid(tex);
160+
singleton.free();
161+
}

0 commit comments

Comments
 (0)