@bornengine/engine 0.4.16 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +24 -14
  2. package/native/android/Cargo.lock +577 -28
  3. package/native/android/Cargo.toml +2 -0
  4. package/native/android/src/lib.rs +39 -3
  5. package/native/ios/Cargo.lock +573 -24
  6. package/native/ios/Cargo.toml +2 -0
  7. package/native/ios/src/lib.rs +72 -9
  8. package/native/ios/src/text_input.rs +48 -0
  9. package/native/linux/Cargo.lock +623 -35
  10. package/native/linux/Cargo.toml +3 -0
  11. package/native/macos/Cargo.lock +630 -35
  12. package/native/macos/Cargo.toml +3 -0
  13. package/native/shared/Cargo.lock +430 -27
  14. package/native/shared/Cargo.toml +5 -0
  15. package/native/shared/build.rs +41 -1
  16. package/native/shared/src/engine.rs +54 -2
  17. package/native/shared/src/ffi.rs +54 -1
  18. package/native/shared/src/ffi_core/mod.rs +2 -0
  19. package/native/shared/src/ffi_core/ui.rs +182 -0
  20. package/native/shared/src/input.rs +317 -12
  21. package/native/shared/src/lib.rs +1 -0
  22. package/native/shared/src/renderer/mod.rs +47 -201
  23. package/native/shared/src/renderer/texture_store.rs +5 -0
  24. package/native/shared/src/renderer/ui_pass.rs +742 -0
  25. package/native/shared/src/textures.rs +2 -0
  26. package/native/shared/src/ui/commands.rs +213 -0
  27. package/native/shared/src/ui/egui.rs +1282 -0
  28. package/native/shared/src/ui/imgui.rs +1056 -0
  29. package/native/shared/src/ui/input.rs +173 -0
  30. package/native/shared/src/ui/mod.rs +252 -0
  31. package/native/shared/src/ui/responses.rs +61 -0
  32. package/native/tvos/Cargo.lock +576 -27
  33. package/native/tvos/Cargo.toml +2 -0
  34. package/native/visionos/Cargo.lock +571 -22
  35. package/native/visionos/Cargo.toml +2 -0
  36. package/native/watchos/src/ffi_stubs.rs +27 -0
  37. package/native/web/Cargo.lock +346 -11
  38. package/native/web/Cargo.toml +6 -0
  39. package/native/web/bloom_glue.js +84 -11
  40. package/native/web/build.sh +150 -34
  41. package/native/web/src/lib.rs +2 -0
  42. package/native/web/src/ui_ffi.rs +148 -0
  43. package/native/windows/Cargo.lock +638 -38
  44. package/native/windows/Cargo.toml +3 -0
  45. package/native/windows/src/lib.rs +50 -1
  46. package/native/windows/src/windows_text.rs +69 -0
  47. package/package.json +91 -2
  48. package/src/debug-ui/index.ts +4 -0
  49. package/src/debug-ui/types.ts +1 -0
  50. package/src/game/adapters/audio-source-component.ts +67 -0
  51. package/src/game/adapters/rigid-body-component.ts +69 -0
  52. package/src/game/adapters/scene-node-component.ts +49 -0
  53. package/src/game/game-component.ts +89 -0
  54. package/src/game/game-object.ts +338 -0
  55. package/src/game/game-scene.ts +303 -0
  56. package/src/game/index.ts +7 -0
  57. package/src/game/transform.ts +315 -0
  58. package/src/index.ts +27 -0
  59. package/src/ui/api.ts +216 -0
  60. package/src/ui/index.ts +5 -0
  61. package/src/ui/opcodes.ts +66 -0
  62. package/src/ui/types.ts +82 -0
@@ -25,12 +25,17 @@ models3d = ["dep:gltf", "dep:image_dds"]
25
25
  # beyond PNG" in the same sense as jpeg/bmp/tga (decode-only here; the
26
26
  # encoder lives in tools/bloom-cook).
27
27
  image-extras = ["image/jpeg", "image/bmp", "image/tga", "image/hdr", "dep:image_dds"]
28
+ debug-ui = ["dep:imgui", "dep:imgui-wgpu"]
28
29
 
29
30
  [build-dependencies]
30
31
  cmake = "0.1"
31
32
 
32
33
  [dependencies]
33
34
  wgpu = "29"
35
+ egui = "0.35"
36
+ egui-wgpu = "0.35"
37
+ imgui = { version = "0.12", optional = true, features = ["tables-api"] }
38
+ imgui-wgpu = { version = "0.28", optional = true }
34
39
  raw-window-handle = "0.6"
35
40
  fontdue = "0.9"
36
41
  libc = "0.2"
@@ -53,6 +53,9 @@ fn build_jolt() {
53
53
  let from_source = std::env::var_os("BLOOM_JOLT_FROM_SOURCE").is_some();
54
54
  if !from_source {
55
55
  if let Some(prebuilt_dir) = find_prebuilt_dir(&manifest_dir, &target_os, &target_arch) {
56
+ if target_os == "windows" {
57
+ stage_windows_prebuilt_for_perry(&prebuilt_dir, &shim_dir, &target_arch);
58
+ }
56
59
  link_prebuilt(&prebuilt_dir, &target_os);
57
60
  emit_cxx_runtime_link(&target_os);
58
61
  return;
@@ -171,7 +174,12 @@ fn find_prebuilt_dir(
171
174
  } else {
172
175
  ""
173
176
  };
174
- let target_token = format!("{}-{}{}", target_os, arch_token, sim_suffix);
177
+ let os_token = if target_os == "windows" {
178
+ "win32"
179
+ } else {
180
+ target_os
181
+ };
182
+ let target_token = format!("{}-{}{}", os_token, arch_token, sim_suffix);
175
183
 
176
184
  let candidates = std::iter::empty::<std::path::PathBuf>()
177
185
  .chain(
@@ -224,6 +232,38 @@ fn walk_up_for_node_modules(
224
232
  })
225
233
  }
226
234
 
235
+ #[cfg(feature = "jolt")]
236
+ fn stage_windows_prebuilt_for_perry(
237
+ prebuilt_dir: &std::path::Path,
238
+ shim_dir: &std::path::Path,
239
+ target_arch: &str,
240
+ ) {
241
+ let lib_dir = shim_dir
242
+ .join("build")
243
+ .join(format!("windows-{target_arch}"))
244
+ .join("lib");
245
+ std::fs::create_dir_all(&lib_dir)
246
+ .unwrap_or_else(|error| panic!("could not create {}: {error}", lib_dir.display()));
247
+ for name in ["bloom_jolt.lib", "Jolt.lib"] {
248
+ let source = prebuilt_dir.join(name);
249
+ let destination = lib_dir.join(name);
250
+ let same_file = source
251
+ .canonicalize()
252
+ .ok()
253
+ .zip(destination.canonicalize().ok())
254
+ .is_some_and(|(source, destination)| source == destination);
255
+ if !same_file {
256
+ std::fs::copy(&source, &destination).unwrap_or_else(|error| {
257
+ panic!(
258
+ "could not stage {} to {}: {error}",
259
+ source.display(),
260
+ destination.display()
261
+ )
262
+ });
263
+ }
264
+ }
265
+ }
266
+
227
267
  #[cfg(feature = "jolt")]
228
268
  fn link_prebuilt(dir: &std::path::Path, target_os: &str) {
229
269
  // Rerun if any archive in the prebuilt dir changes — covers the
@@ -22,6 +22,7 @@ pub struct EngineState {
22
22
  pub renderer: Renderer,
23
23
  pub text: TextRenderer,
24
24
  pub input: InputState,
25
+ pub ui: crate::ui::UiSystem,
25
26
  pub audio: AudioMixer,
26
27
  pub textures: TextureManager,
27
28
  #[cfg(feature = "models3d")]
@@ -84,10 +85,18 @@ impl EngineState {
84
85
  // BLAS_INPUT cost only when it can be used.
85
86
  let mut scene = SceneGraph::new();
86
87
  scene.hw_rt_enabled = renderer.hw_rt_enabled;
88
+ let ui = crate::ui::UiSystem::default();
89
+ #[cfg(feature = "debug-ui")]
90
+ let renderer = {
91
+ let mut renderer = renderer;
92
+ renderer.init_dear_imgui();
93
+ renderer
94
+ };
87
95
  Self {
88
96
  renderer,
89
97
  text: TextRenderer::new(),
90
98
  input: InputState::new(),
99
+ ui,
91
100
  audio: AudioMixer::new(),
92
101
  textures: TextureManager::new(),
93
102
  #[cfg(feature = "models3d")]
@@ -162,6 +171,8 @@ impl EngineState {
162
171
  }
163
172
 
164
173
  self.input.begin_frame();
174
+ self.ui.begin_frame();
175
+ self.ui.set_input_snapshot(self.input.ui_snapshot());
165
176
  self.renderer.begin_frame();
166
177
  self.frame_count += 1;
167
178
  }
@@ -182,6 +193,37 @@ impl EngineState {
182
193
  }
183
194
 
184
195
  pub fn end_frame(&mut self) {
196
+ let logical_width = self.renderer.width().max(1) as f32;
197
+ let logical_height = self.renderer.height().max(1) as f32;
198
+ let pixels_per_point = self.renderer.physical_width() as f32 / logical_width;
199
+ let ui_frame = self.ui.evaluate_egui(
200
+ [0.0, 0.0, logical_width, logical_height],
201
+ pixels_per_point,
202
+ self.delta_time,
203
+ );
204
+ let native_texture_indices = ui_frame
205
+ .registered_textures
206
+ .values()
207
+ .filter_map(|&handle| {
208
+ self.textures
209
+ .get(handle as f64)
210
+ .map(|texture| (handle, texture.bind_group_idx))
211
+ })
212
+ .collect();
213
+ self.renderer
214
+ .set_egui_frame(ui_frame, native_texture_indices);
215
+
216
+ let debug_native_texture_indices = self
217
+ .ui
218
+ .registered_texture_handles(crate::ui::UiBackend::DearImGui)
219
+ .into_iter()
220
+ .filter_map(|handle| {
221
+ self.textures
222
+ .get(handle as f64)
223
+ .map(|texture| (handle, texture.bind_group_idx))
224
+ })
225
+ .collect();
226
+
185
227
  if self.direct_2d_mode {
186
228
  // Fast path for pure-2D games: render direct to the swapchain,
187
229
  // skipping scene prep, shadow maps, HDR/tonemap, SSAO, bloom,
@@ -190,7 +232,11 @@ impl EngineState {
190
232
  // when the scene graph is empty; this path typically hits
191
233
  // 60 fps on the same device.
192
234
  self.profiler.begin("render_total");
193
- self.renderer.end_frame();
235
+ self.renderer.end_frame_with_ui(
236
+ &mut self.ui,
237
+ self.delta_time,
238
+ debug_native_texture_indices,
239
+ );
194
240
  self.profiler.end("render_total");
195
241
  } else {
196
242
  self.profiler.begin("scene_prepare");
@@ -233,7 +279,13 @@ impl EngineState {
233
279
  self.renderer.material_system_begin_frame(t, dt);
234
280
 
235
281
  self.profiler.begin("render_total");
236
- self.renderer.end_frame_with_scene(&mut self.scene, &mut self.profiler);
282
+ self.renderer.end_frame_with_scene_ui(
283
+ &mut self.scene,
284
+ &mut self.profiler,
285
+ &mut self.ui,
286
+ self.delta_time,
287
+ debug_native_texture_indices,
288
+ );
237
289
  self.profiler.end("render_total");
238
290
  }
239
291
 
@@ -17,6 +17,9 @@
17
17
  //! Logging goes through [`log_error`], which writes to logcat on Android
18
18
  //! (stderr is discarded there) and stderr everywhere else.
19
19
 
20
+ #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
21
+ use std::borrow::Cow;
22
+
20
23
  /// Default return value for an FFI function whose body panicked.
21
24
  pub trait FfiDefault {
22
25
  fn ffi_default() -> Self;
@@ -112,5 +115,55 @@ pub fn log_error(msg: &str) {
112
115
  return;
113
116
  }
114
117
  #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
115
- eprintln!("{msg}");
118
+ {
119
+ use std::io::IsTerminal;
120
+
121
+ let color_enabled = should_color_terminal_output(
122
+ std::io::stderr().is_terminal(),
123
+ std::env::var_os("NO_COLOR").is_some(),
124
+ );
125
+ eprintln!("{}", format_terminal_error(msg, color_enabled));
126
+ }
127
+ }
128
+
129
+ #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
130
+ fn should_color_terminal_output(stderr_is_terminal: bool, no_color_is_set: bool) -> bool {
131
+ stderr_is_terminal && !no_color_is_set
132
+ }
133
+
134
+ #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
135
+ fn format_terminal_error(msg: &str, color_enabled: bool) -> Cow<'_, str> {
136
+ if color_enabled {
137
+ Cow::Owned(format!("\x1b[31m{msg}\x1b[0m"))
138
+ } else {
139
+ Cow::Borrowed(msg)
140
+ }
141
+ }
142
+
143
+ #[cfg(all(test, not(target_os = "android"), not(target_arch = "wasm32")))]
144
+ mod tests {
145
+ use super::{format_terminal_error, should_color_terminal_output};
146
+
147
+ #[test]
148
+ fn terminal_errors_are_red_and_reset_terminal_style() {
149
+ assert_eq!(
150
+ format_terminal_error("bloom: failure", true).as_ref(),
151
+ "\x1b[31mbloom: failure\x1b[0m"
152
+ );
153
+ }
154
+
155
+ #[test]
156
+ fn errors_remain_plain_when_color_is_disabled() {
157
+ assert_eq!(
158
+ format_terminal_error("bloom: failure", false).as_ref(),
159
+ "bloom: failure"
160
+ );
161
+ }
162
+
163
+ #[test]
164
+ fn color_is_only_enabled_for_tty_without_no_color() {
165
+ assert!(should_color_terminal_output(true, false));
166
+ assert!(!should_color_terminal_output(false, false));
167
+ assert!(!should_color_terminal_output(true, true));
168
+ }
116
169
  }
@@ -69,6 +69,7 @@ mod scene;
69
69
  mod visual;
70
70
  mod vfx;
71
71
  mod ragdoll_ffi;
72
+ mod ui;
72
73
 
73
74
  /// Expand the full shared (non-physics) FFI surface. Composed from the
74
75
  /// per-subsystem section macros in this directory; platform crates invoke
@@ -88,6 +89,7 @@ macro_rules! define_core_ffi {
88
89
  $crate::__bloom_ffi_visual!();
89
90
  $crate::__bloom_ffi_vfx!();
90
91
  $crate::__bloom_ffi_ragdoll!();
92
+ $crate::__bloom_ffi_ui!();
91
93
  };
92
94
  }
93
95
 
@@ -0,0 +1,182 @@
1
+ //! Typed UI command, input, response, and availability FFI.
2
+ //!
3
+ //! Section of [`define_core_ffi!`](crate::define_core_ffi) — see
4
+ //! `ffi_core/mod.rs` for the architecture and the invoking-crate contract.
5
+
6
+ #[doc(hidden)]
7
+ #[macro_export]
8
+ macro_rules! __bloom_ffi_ui {
9
+ () => {
10
+ #[no_mangle]
11
+ pub extern "C" fn bloom_ui_command(
12
+ backend: f64,
13
+ opcode: f64,
14
+ id: f64,
15
+ a: f64,
16
+ b: f64,
17
+ c: f64,
18
+ d: f64,
19
+ text_ptr: *const u8,
20
+ ) -> f64 {
21
+ $crate::ffi::guard("bloom_ui_command", move || {
22
+ let Some(backend) = $crate::ui::UiBackend::from_abi(backend) else {
23
+ return 0.0;
24
+ };
25
+ let Some(opcode) = $crate::ui::UiOpcode::from_abi(opcode) else {
26
+ return 0.0;
27
+ };
28
+ let Some(id) = $crate::ui::id_from_abi(id) else {
29
+ return 0.0;
30
+ };
31
+ let text = $crate::string_header::str_from_header(text_ptr).to_owned();
32
+ let command = $crate::ui::UiCommand::new(backend, opcode, id, [a, b, c, d], text);
33
+ if engine().ui.queue_command(command) {
34
+ 1.0
35
+ } else {
36
+ 0.0
37
+ }
38
+ })
39
+ }
40
+
41
+ #[no_mangle]
42
+ pub extern "C" fn bloom_ui_scratch_reset(backend: f64) {
43
+ $crate::ffi::guard("bloom_ui_scratch_reset", move || {
44
+ let Some(backend) = $crate::ui::UiBackend::from_abi(backend) else {
45
+ return;
46
+ };
47
+ engine().ui.reset_scratch(backend);
48
+ })
49
+ }
50
+
51
+ #[no_mangle]
52
+ pub extern "C" fn bloom_ui_scratch_push_f64(backend: f64, value: f64) {
53
+ $crate::ffi::guard("bloom_ui_scratch_push_f64", move || {
54
+ let Some(backend) = $crate::ui::UiBackend::from_abi(backend) else {
55
+ return;
56
+ };
57
+ engine().ui.push_scratch(backend, value);
58
+ })
59
+ }
60
+
61
+ #[no_mangle]
62
+ pub extern "C" fn bloom_ui_scratch_command(
63
+ backend: f64,
64
+ opcode: f64,
65
+ id: f64,
66
+ count: f64,
67
+ text_ptr: *const u8,
68
+ ) -> f64 {
69
+ $crate::ffi::guard("bloom_ui_scratch_command", move || {
70
+ let Some(backend) = $crate::ui::UiBackend::from_abi(backend) else {
71
+ return 0.0;
72
+ };
73
+ let Some(opcode) = $crate::ui::UiOpcode::from_abi(opcode) else {
74
+ return 0.0;
75
+ };
76
+ let Some(id) = $crate::ui::id_from_abi(id) else {
77
+ return 0.0;
78
+ };
79
+ let Some(count) = $crate::ui::id_from_abi(count) else {
80
+ return 0.0;
81
+ };
82
+ let text = $crate::string_header::str_from_header(text_ptr).to_owned();
83
+ if engine()
84
+ .ui
85
+ .queue_scratch_command(backend, opcode, id, count as usize, text)
86
+ {
87
+ 1.0
88
+ } else {
89
+ 0.0
90
+ }
91
+ })
92
+ }
93
+
94
+ #[no_mangle]
95
+ pub extern "C" fn bloom_ui_inject_text(text_ptr: *const u8) {
96
+ $crate::ffi::guard("bloom_ui_inject_text", move || {
97
+ let text = $crate::string_header::str_from_header(text_ptr).to_owned();
98
+ engine().ui.inject_text(text);
99
+ })
100
+ }
101
+
102
+ #[no_mangle]
103
+ pub extern "C" fn bloom_ui_response(backend: f64, id: f64, field: f64) -> f64 {
104
+ $crate::ffi::guard("bloom_ui_response", move || {
105
+ let Some(backend) = $crate::ui::UiBackend::from_abi(backend) else {
106
+ return 0.0;
107
+ };
108
+ let Some(id) = $crate::ui::id_from_abi(id) else {
109
+ return 0.0;
110
+ };
111
+ let Some(field) = $crate::ui::id_from_abi(field) else {
112
+ return 0.0;
113
+ };
114
+ let response = engine().ui.response(backend, id);
115
+ match field {
116
+ 0 => response.clicked as u8 as f64,
117
+ 1 => response.changed as u8 as f64,
118
+ 2 => response.hovered as u8 as f64,
119
+ 3 => response.focused as u8 as f64,
120
+ 4 => response.dragged as u8 as f64,
121
+ 5 => response.value,
122
+ 6 => engine().ui.has_response(backend, id) as u8 as f64,
123
+ _ => 0.0,
124
+ }
125
+ })
126
+ }
127
+
128
+ #[no_mangle]
129
+ pub extern "C" fn bloom_ui_response_text(backend: f64, id: f64) -> *const u8 {
130
+ $crate::ffi::guard("bloom_ui_response_text", move || {
131
+ let Some(backend) = $crate::ui::UiBackend::from_abi(backend) else {
132
+ return $crate::string_header::alloc_perry_string("");
133
+ };
134
+ let Some(id) = $crate::ui::id_from_abi(id) else {
135
+ return $crate::string_header::alloc_perry_string("");
136
+ };
137
+ let text = engine().ui.response_text(backend, id);
138
+ $crate::string_header::alloc_perry_string(&text)
139
+ })
140
+ }
141
+
142
+ #[no_mangle]
143
+ pub extern "C" fn bloom_ui_is_available(backend: f64) -> f64 {
144
+ $crate::ffi::guard("bloom_ui_is_available", move || {
145
+ let Some(backend) = $crate::ui::UiBackend::from_abi(backend) else {
146
+ return 0.0;
147
+ };
148
+ engine().ui.is_available(backend) as u8 as f64
149
+ })
150
+ }
151
+
152
+ #[no_mangle]
153
+ pub extern "C" fn bloom_ui_wants_input(backend: f64, kind: f64) -> f64 {
154
+ $crate::ffi::guard("bloom_ui_wants_input", move || {
155
+ let Some(backend) = $crate::ui::UiBackend::from_abi(backend) else {
156
+ return 0.0;
157
+ };
158
+ let Some(kind) = $crate::ui::id_from_abi(kind) else {
159
+ return 0.0;
160
+ };
161
+ engine().ui.wants_input(backend, kind) as u8 as f64
162
+ })
163
+ }
164
+
165
+ #[no_mangle]
166
+ pub extern "C" fn bloom_ui_take_keyboard_request(backend: f64) -> f64 {
167
+ $crate::ffi::guard("bloom_ui_take_keyboard_request", move || {
168
+ let Some(backend) = $crate::ui::UiBackend::from_abi(backend) else {
169
+ return -1.0;
170
+ };
171
+ if backend != $crate::ui::UiBackend::Egui {
172
+ return -1.0;
173
+ }
174
+ match engine().ui.take_keyboard_request() {
175
+ Some(true) => 1.0,
176
+ Some(false) => 0.0,
177
+ None => -1.0,
178
+ }
179
+ })
180
+ }
181
+ };
182
+ }