godot_testability_runtime/
runtime.rs1use crate::error::{TestError, TestResult};
8use once_cell::sync::Lazy;
9use parking_lot::Mutex;
10use std::sync::Arc;
11use tracing::{error, info};
12
13type GDExtensionInterfaceGetProcAddress = *const std::ffi::c_void;
15type GDExtensionClassLibraryPtr = *mut std::ffi::c_void;
16type SceneTreePtr = *mut std::ffi::c_void;
17
18static USER_CALLBACKS: Mutex<Option<UserCallbacks>> = Mutex::new(None);
20
21#[allow(clippy::type_complexity)]
23static SCENE_CALLBACK: Mutex<Option<Box<dyn FnOnce(SceneTreePtr) -> TestResult<()> + Send>>> =
24 Mutex::new(None);
25
26pub struct UserCallbacks {
28 pub initialize_ffi:
30 fn(GDExtensionInterfaceGetProcAddress, GDExtensionClassLibraryPtr) -> Result<(), String>,
31 pub load_class_method_table: fn(u32),
33}
34
35static RUNTIME_STATE: Lazy<Arc<Mutex<RuntimeState>>> =
37 Lazy::new(|| Arc::new(Mutex::new(RuntimeState::new())));
38
39#[derive(Debug)]
41struct RuntimeState {
42 initialized: bool,
43 running: bool,
44}
45
46impl RuntimeState {
47 fn new() -> Self {
48 Self {
49 initialized: false,
50 running: false,
51 }
52 }
53}
54
55#[derive(Debug, Clone)]
57pub struct RuntimeConfig {
58 pub headless: bool,
60 pub verbose: bool,
62 pub custom_args: Vec<String>,
64}
65
66impl Default for RuntimeConfig {
67 fn default() -> Self {
68 Self {
69 headless: true,
70 verbose: false,
71 custom_args: Vec::new(),
72 }
73 }
74}
75
76pub struct GodotRuntime;
82
83#[cfg(feature = "embedded_runtime")]
84impl GodotRuntime {
85 pub fn is_initialized() -> bool {
87 RUNTIME_STATE.lock().initialized
88 }
89
90 pub fn is_running() -> bool {
92 RUNTIME_STATE.lock().running
93 }
94
95 pub fn shutdown() -> TestResult<()> {
99 let mut state = RUNTIME_STATE.lock();
100 if !state.initialized {
101 return Ok(());
102 }
103
104 info!("Shutting down Godot runtime");
105 state.running = false;
106 state.initialized = false;
107 Ok(())
108 }
109
110 pub fn run_godot<F>(
115 _config: RuntimeConfig,
116 callbacks: UserCallbacks,
117 load_scene: F,
118 ) -> TestResult<i32>
119 where
120 F: FnOnce(SceneTreePtr) -> TestResult<()> + Send + 'static,
121 {
122 use crate::ffi::{
123 libgodot_gdextension_bind, GDExtensionClassLibraryPtr, GDExtensionInitialization,
124 GDExtensionInitializationLevel, GDExtensionInterfaceGetProcAddress,
125 };
126 use std::ffi::c_void;
127
128 info!("Starting Godot runtime with SwiftGodot-style initialization");
129
130 {
132 USER_CALLBACKS.lock().replace(callbacks);
133 SCENE_CALLBACK.lock().replace(Box::new(load_scene));
134 }
135
136 extern "C" fn initialization_callback(
137 get_proc_addr: Option<GDExtensionInterfaceGetProcAddress>,
138 library: GDExtensionClassLibraryPtr,
139 r_initialization: *mut GDExtensionInitialization,
140 ) -> i32 {
141 if get_proc_addr.is_none() || library.is_null() {
142 return 0;
143 }
144
145 unsafe {
146 if !r_initialization.is_null() {
147 (*r_initialization).minimum_initialization_level =
148 GDExtensionInitializationLevel::Core;
149 (*r_initialization).userdata = library;
150 (*r_initialization).initialize = Some(godot_rust_bridge_initialize);
151 (*r_initialization).deinitialize = Some(godot_rust_bridge_deinitialize);
152 }
153
154 if let Some(callbacks) = USER_CALLBACKS.lock().as_ref() {
156 if let Some(get_proc_address_fn) = get_proc_addr {
157 if let Err(e) = (callbacks.initialize_ffi)(
158 get_proc_address_fn as *const c_void,
159 library,
160 ) {
161 error!("Failed to initialize godot-rust FFI: {}", e);
162 return 0;
163 }
164 }
165 }
166 }
167 1
168 }
169
170 extern "C" fn scene_callback(scene_tree_ptr: *mut c_void) {
171 info!("Scene tree ready - Godot engine available");
172 if !scene_tree_ptr.is_null() {
173 if let Some(callback) = SCENE_CALLBACK.lock().take() {
174 info!("Executing test callback with SceneTree pointer");
175 match callback(scene_tree_ptr) {
176 Ok(()) => {
177 info!("Test callback executed successfully");
178 }
179 Err(e) => {
180 error!("Test callback failed: {:?}", e);
181 }
182 }
183 } else {
184 info!("No test callback to execute");
185 }
186 } else {
187 error!("Scene tree pointer is null!");
188 }
189 }
190
191 unsafe {
192 libgodot_gdextension_bind(initialization_callback, Some(scene_callback));
193 }
194
195 std::env::set_var("__CFBundleIdentifier", "GodotBevyKit");
196
197 let args = vec![
198 "GodotBevyKit".to_string(),
199 "--headless".to_string(),
200 "--verbose".to_string(),
201 "--quit".to_string(),
202 ];
203
204 let mut runtime = crate::ffi::LibgodotRuntime::new();
205 runtime
206 .initialize()
207 .map_err(TestError::RuntimeInitialization)?;
208
209 info!("Starting godot_main");
210 let result = runtime
211 .run_main(&args)
212 .map_err(TestError::RuntimeInitialization)?;
213
214 info!("Godot main loop finished with exit code: {}", result);
215 Ok(result)
216 }
217}
218
219extern "C" fn godot_rust_bridge_initialize(
220 _userdata: *mut std::ffi::c_void,
221 level: crate::ffi::GDExtensionInitializationLevel,
222) {
223 info!("Godot-Rust bridge initialize (level: {:?})", level);
224
225 let init_level = match level {
227 crate::ffi::GDExtensionInitializationLevel::Core => 0,
228 crate::ffi::GDExtensionInitializationLevel::Servers => 1,
229 crate::ffi::GDExtensionInitializationLevel::Scene => 2,
230 crate::ffi::GDExtensionInitializationLevel::Editor => 3,
231 _ => return,
232 };
233
234 if let Some(callbacks) = USER_CALLBACKS.lock().as_ref() {
236 (callbacks.load_class_method_table)(init_level);
237 }
238}
239
240extern "C" fn godot_rust_bridge_deinitialize(
241 _userdata: *mut std::ffi::c_void,
242 level: crate::ffi::GDExtensionInitializationLevel,
243) {
244 info!("Godot-Rust bridge deinitialize (level: {:?})", level);
245}