@bornengine/engine 0.4.16

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 (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +231 -0
  3. package/native/android/Cargo.lock +1848 -0
  4. package/native/android/Cargo.toml +24 -0
  5. package/native/android/src/lib.rs +702 -0
  6. package/native/ios/Cargo.lock +1690 -0
  7. package/native/ios/Cargo.toml +32 -0
  8. package/native/ios/src/lib.rs +1267 -0
  9. package/native/linux/Cargo.lock +3279 -0
  10. package/native/linux/Cargo.toml +29 -0
  11. package/native/linux/src/lib.rs +1331 -0
  12. package/native/macos/Cargo.lock +3310 -0
  13. package/native/macos/Cargo.toml +46 -0
  14. package/native/macos/src/lib.rs +1302 -0
  15. package/native/shared/Cargo.lock +1899 -0
  16. package/native/shared/Cargo.toml +62 -0
  17. package/native/shared/assets/default_font.ttf +0 -0
  18. package/native/shared/build.rs +270 -0
  19. package/native/shared/shaders/common/clouds.wgsl +122 -0
  20. package/native/shared/shaders/common/fog.wgsl +16 -0
  21. package/native/shared/shaders/common/foliage_wind.wgsl +98 -0
  22. package/native/shared/shaders/common/imposter.wgsl +112 -0
  23. package/native/shared/shaders/common/pbr.wgsl +186 -0
  24. package/native/shared/shaders/common/shadows.wgsl +186 -0
  25. package/native/shared/shaders/common/sky.wgsl +8 -0
  26. package/native/shared/shaders/common/tonemap.wgsl +25 -0
  27. package/native/shared/shaders/impulse_field.wgsl +57 -0
  28. package/native/shared/shaders/material_abi.wgsl +383 -0
  29. package/native/shared/shaders/materials/test_minimal.wgsl +42 -0
  30. package/native/shared/src/anim_mixer.rs +61 -0
  31. package/native/shared/src/attach.rs +263 -0
  32. package/native/shared/src/audio/decode.rs +123 -0
  33. package/native/shared/src/audio/mod.rs +863 -0
  34. package/native/shared/src/audio/render.rs +892 -0
  35. package/native/shared/src/audio/spsc.rs +156 -0
  36. package/native/shared/src/audio/stream.rs +226 -0
  37. package/native/shared/src/custom_shaders.rs +104 -0
  38. package/native/shared/src/decals.rs +245 -0
  39. package/native/shared/src/drs.rs +211 -0
  40. package/native/shared/src/engine.rs +261 -0
  41. package/native/shared/src/ffi.rs +116 -0
  42. package/native/shared/src/ffi_core/assets.rs +388 -0
  43. package/native/shared/src/ffi_core/audio_ffi.rs +184 -0
  44. package/native/shared/src/ffi_core/draw.rs +334 -0
  45. package/native/shared/src/ffi_core/game_loop.rs +577 -0
  46. package/native/shared/src/ffi_core/input.rs +234 -0
  47. package/native/shared/src/ffi_core/mod.rs +127 -0
  48. package/native/shared/src/ffi_core/models.rs +1154 -0
  49. package/native/shared/src/ffi_core/ragdoll_ffi.rs +261 -0
  50. package/native/shared/src/ffi_core/scene.rs +626 -0
  51. package/native/shared/src/ffi_core/vfx.rs +212 -0
  52. package/native/shared/src/ffi_core/visual.rs +691 -0
  53. package/native/shared/src/frame_callbacks.rs +122 -0
  54. package/native/shared/src/geometry.rs +236 -0
  55. package/native/shared/src/handles.rs +182 -0
  56. package/native/shared/src/input.rs +448 -0
  57. package/native/shared/src/jolt_sys.rs +822 -0
  58. package/native/shared/src/lib.rs +55 -0
  59. package/native/shared/src/models.rs +1093 -0
  60. package/native/shared/src/models_gltf.rs +1280 -0
  61. package/native/shared/src/particles.rs +391 -0
  62. package/native/shared/src/physics_jolt.rs +1908 -0
  63. package/native/shared/src/picking.rs +298 -0
  64. package/native/shared/src/postfx.rs +345 -0
  65. package/native/shared/src/profiler.rs +492 -0
  66. package/native/shared/src/ragdoll.rs +474 -0
  67. package/native/shared/src/renderer/atmosphere_lut.rs +573 -0
  68. package/native/shared/src/renderer/brdf_lut.rs +154 -0
  69. package/native/shared/src/renderer/draw2d.rs +143 -0
  70. package/native/shared/src/renderer/formats.rs +822 -0
  71. package/native/shared/src/renderer/froxel.rs +421 -0
  72. package/native/shared/src/renderer/gi_bake.rs +653 -0
  73. package/native/shared/src/renderer/graph.rs +462 -0
  74. package/native/shared/src/renderer/hiz.rs +269 -0
  75. package/native/shared/src/renderer/hot_reload.rs +390 -0
  76. package/native/shared/src/renderer/impulse_field.rs +456 -0
  77. package/native/shared/src/renderer/lighting.rs +154 -0
  78. package/native/shared/src/renderer/material_instancing.rs +171 -0
  79. package/native/shared/src/renderer/material_pipeline.rs +700 -0
  80. package/native/shared/src/renderer/material_system.rs +1996 -0
  81. package/native/shared/src/renderer/material_system_tests.rs +601 -0
  82. package/native/shared/src/renderer/material_system_wasm.rs +41 -0
  83. package/native/shared/src/renderer/mod.rs +12556 -0
  84. package/native/shared/src/renderer/model_draw.rs +641 -0
  85. package/native/shared/src/renderer/occlusion.rs +429 -0
  86. package/native/shared/src/renderer/planar_pass.rs +593 -0
  87. package/native/shared/src/renderer/planar_reflection.rs +499 -0
  88. package/native/shared/src/renderer/post_pass.rs +249 -0
  89. package/native/shared/src/renderer/postfx_chain.rs +728 -0
  90. package/native/shared/src/renderer/pt_pass.rs +577 -0
  91. package/native/shared/src/renderer/scene_pass.rs +607 -0
  92. package/native/shared/src/renderer/shader_include.rs +205 -0
  93. package/native/shared/src/renderer/shader_library.rs +135 -0
  94. package/native/shared/src/renderer/shaders/ao.rs +570 -0
  95. package/native/shared/src/renderer/shaders/core.rs +1243 -0
  96. package/native/shared/src/renderer/shaders/env.rs +907 -0
  97. package/native/shared/src/renderer/shaders/gi.rs +810 -0
  98. package/native/shared/src/renderer/shaders/mod.rs +19 -0
  99. package/native/shared/src/renderer/shaders/post.rs +1558 -0
  100. package/native/shared/src/renderer/shaders/pt.rs +1859 -0
  101. package/native/shared/src/renderer/shaders/ssgi.rs +1586 -0
  102. package/native/shared/src/renderer/shadow_pass.rs +731 -0
  103. package/native/shared/src/renderer/ssgi_pass.rs +392 -0
  104. package/native/shared/src/renderer/ssr_pass.rs +188 -0
  105. package/native/shared/src/renderer/texture_store.rs +473 -0
  106. package/native/shared/src/renderer/transient.rs +591 -0
  107. package/native/shared/src/renderer/types.rs +941 -0
  108. package/native/shared/src/renderer/util.rs +152 -0
  109. package/native/shared/src/scene.rs +1362 -0
  110. package/native/shared/src/sdf_cache.rs +274 -0
  111. package/native/shared/src/shadows.rs +1036 -0
  112. package/native/shared/src/staging.rs +102 -0
  113. package/native/shared/src/string_header.rs +266 -0
  114. package/native/shared/src/text_renderer.rs +502 -0
  115. package/native/shared/src/textures.rs +197 -0
  116. package/native/tvos/Cargo.lock +1693 -0
  117. package/native/tvos/Cargo.toml +36 -0
  118. package/native/tvos/metal-patched/Cargo.toml +178 -0
  119. package/native/tvos/metal-patched/LICENSE-APACHE +201 -0
  120. package/native/tvos/metal-patched/LICENSE-MIT +25 -0
  121. package/native/tvos/metal-patched/src/acceleration_structure.rs +667 -0
  122. package/native/tvos/metal-patched/src/acceleration_structure_pass.rs +108 -0
  123. package/native/tvos/metal-patched/src/argument.rs +366 -0
  124. package/native/tvos/metal-patched/src/blitpass.rs +102 -0
  125. package/native/tvos/metal-patched/src/buffer.rs +71 -0
  126. package/native/tvos/metal-patched/src/capturedescriptor.rs +76 -0
  127. package/native/tvos/metal-patched/src/capturemanager.rs +113 -0
  128. package/native/tvos/metal-patched/src/commandbuffer.rs +192 -0
  129. package/native/tvos/metal-patched/src/commandqueue.rs +44 -0
  130. package/native/tvos/metal-patched/src/computepass.rs +107 -0
  131. package/native/tvos/metal-patched/src/constants.rs +152 -0
  132. package/native/tvos/metal-patched/src/counters.rs +119 -0
  133. package/native/tvos/metal-patched/src/depthstencil.rs +190 -0
  134. package/native/tvos/metal-patched/src/device.rs +2134 -0
  135. package/native/tvos/metal-patched/src/drawable.rs +39 -0
  136. package/native/tvos/metal-patched/src/encoder.rs +2041 -0
  137. package/native/tvos/metal-patched/src/heap.rs +281 -0
  138. package/native/tvos/metal-patched/src/indirect_encoder.rs +344 -0
  139. package/native/tvos/metal-patched/src/lib.rs +657 -0
  140. package/native/tvos/metal-patched/src/library.rs +902 -0
  141. package/native/tvos/metal-patched/src/mps.rs +575 -0
  142. package/native/tvos/metal-patched/src/pipeline/compute.rs +475 -0
  143. package/native/tvos/metal-patched/src/pipeline/mod.rs +71 -0
  144. package/native/tvos/metal-patched/src/pipeline/render.rs +762 -0
  145. package/native/tvos/metal-patched/src/renderpass.rs +443 -0
  146. package/native/tvos/metal-patched/src/resource.rs +182 -0
  147. package/native/tvos/metal-patched/src/sampler.rs +165 -0
  148. package/native/tvos/metal-patched/src/sync.rs +178 -0
  149. package/native/tvos/metal-patched/src/texture.rs +352 -0
  150. package/native/tvos/metal-patched/src/types.rs +90 -0
  151. package/native/tvos/metal-patched/src/vertexdescriptor.rs +250 -0
  152. package/native/tvos/src/audio_backend.rs +197 -0
  153. package/native/tvos/src/lib.rs +1891 -0
  154. package/native/visionos/Cargo.lock +1693 -0
  155. package/native/visionos/Cargo.toml +40 -0
  156. package/native/visionos/src/audio_backend.rs +197 -0
  157. package/native/visionos/src/lib.rs +1887 -0
  158. package/native/watchos/Cargo.lock +16 -0
  159. package/native/watchos/Cargo.toml +19 -0
  160. package/native/watchos/shaders/bloom_postfx.metal +99 -0
  161. package/native/watchos/src/BloomWatchApp.swift +1267 -0
  162. package/native/watchos/src/BloomWatchAudio.swift +179 -0
  163. package/native/watchos/src/audio.rs +55 -0
  164. package/native/watchos/src/draw_list.rs +229 -0
  165. package/native/watchos/src/ffi_stubs.rs +915 -0
  166. package/native/watchos/src/ffi_stubs_manual.rs +35 -0
  167. package/native/watchos/src/lib.rs +1124 -0
  168. package/native/watchos/src/models.rs +746 -0
  169. package/native/watchos/src/postfx.rs +95 -0
  170. package/native/watchos/src/scene.rs +534 -0
  171. package/native/watchos/src/textures.rs +184 -0
  172. package/native/web/Cargo.lock +1657 -0
  173. package/native/web/Cargo.toml +43 -0
  174. package/native/web/bloom_glue.js +695 -0
  175. package/native/web/build.sh +131 -0
  176. package/native/web/index.html +35 -0
  177. package/native/web/jolt_bridge.js +1519 -0
  178. package/native/web/src/input_ffi.rs +286 -0
  179. package/native/web/src/lib.rs +1796 -0
  180. package/native/web/src/material_ffi.rs +710 -0
  181. package/native/web/src/parity_ffi.rs +343 -0
  182. package/native/web/src/physics_ffi.rs +643 -0
  183. package/native/web/src/ragdoll_ffi.rs +250 -0
  184. package/native/web/src/render_settings.rs +98 -0
  185. package/native/windows/Cargo.lock +1815 -0
  186. package/native/windows/Cargo.toml +68 -0
  187. package/native/windows/src/lib.rs +1486 -0
  188. package/package.json +4279 -0
  189. package/src/audio/index.ts +315 -0
  190. package/src/core/colors.ts +63 -0
  191. package/src/core/index.ts +1206 -0
  192. package/src/core/keys.ts +63 -0
  193. package/src/core/types.ts +104 -0
  194. package/src/index.ts +171 -0
  195. package/src/math/index.ts +516 -0
  196. package/src/mobile/index.ts +294 -0
  197. package/src/models/index.ts +1258 -0
  198. package/src/physics/index.ts +1134 -0
  199. package/src/scene/index.ts +698 -0
  200. package/src/shapes/index.ts +120 -0
  201. package/src/text/index.ts +48 -0
  202. package/src/textures/index.ts +187 -0
  203. package/src/vfx/index.ts +191 -0
  204. package/src/world/index.ts +24 -0
  205. package/src/world/loader.ts +423 -0
  206. package/src/world/prefab.ts +217 -0
  207. package/src/world/render.ts +172 -0
  208. package/src/world/saver.ts +108 -0
  209. package/src/world/serialize.ts +301 -0
  210. package/src/world/terrain.ts +355 -0
  211. package/src/world/types.ts +160 -0
  212. package/src/world/validate.ts +319 -0
  213. package/src/world/version.ts +114 -0
@@ -0,0 +1,702 @@
1
+ use bloom_shared::engine::EngineState;
2
+ use bloom_shared::renderer::Renderer;
3
+ use bloom_shared::string_header::{str_from_header, alloc_perry_string};
4
+ use bloom_shared::audio::{parse_wav, parse_ogg, parse_mp3};
5
+
6
+ use std::sync::OnceLock;
7
+ use std::sync::atomic::{AtomicBool, Ordering};
8
+
9
+ static mut ENGINE: OnceLock<EngineState> = OnceLock::new();
10
+ static mut NATIVE_WINDOW: *mut libc::c_void = std::ptr::null_mut();
11
+ static AUDIO_RUNNING: AtomicBool = AtomicBool::new(false);
12
+ static mut ASSET_BASE_PATH: Option<String> = None;
13
+
14
+ fn engine() -> &'static mut EngineState {
15
+ unsafe { ENGINE.get_mut().expect("Engine not initialized") }
16
+ }
17
+ /// Asset-path hook for define_core_ffi! — routes through this platform's
18
+ /// resolve_path (relative paths don't resolve from the app working dir here).
19
+ fn bloom_resolve_asset_path(path: &str) -> std::borrow::Cow<'_, str> {
20
+ std::borrow::Cow::Owned(resolve_path(path))
21
+ }
22
+
23
+ // The full shared (non-physics) FFI surface. See bloom_shared::ffi_core
24
+ // docs for the contract; tools/validate-ffi.js checks parity in CI.
25
+ bloom_shared::define_core_ffi!();
26
+
27
+
28
+ /// Resolve relative asset paths to the app's base asset directory.
29
+ /// On Android, relative paths like "assets/models/tree.glb" won't resolve
30
+ /// because the working directory isn't the app's data directory.
31
+ fn resolve_path(path: &str) -> String {
32
+ if path.starts_with('/') {
33
+ return path.to_string();
34
+ }
35
+ unsafe {
36
+ if let Some(ref base) = ASSET_BASE_PATH {
37
+ format!("{}/{}", base, path)
38
+ } else {
39
+ path.to_string()
40
+ }
41
+ }
42
+ }
43
+
44
+ /// Called by the Android Activity to set the base path for asset resolution.
45
+ /// Should be set to the app's files directory where assets are extracted.
46
+ #[no_mangle]
47
+ pub extern "C" fn bloom_android_set_asset_path(path_ptr: *const u8) {
48
+ let path = str_from_header(path_ptr);
49
+ unsafe {
50
+ ASSET_BASE_PATH = Some(path.to_string());
51
+ }
52
+ }
53
+
54
+ // ============================================================
55
+ // ANativeWindow FFI
56
+ // ============================================================
57
+
58
+ extern "C" {
59
+ fn ANativeWindow_getWidth(window: *mut libc::c_void) -> i32;
60
+ fn ANativeWindow_getHeight(window: *mut libc::c_void) -> i32;
61
+ fn ANativeWindow_acquire(window: *mut libc::c_void);
62
+ fn ANativeWindow_release(window: *mut libc::c_void);
63
+ }
64
+
65
+ /// Called by Perry's Android runtime to set the ANativeWindow pointer
66
+ /// before bloom_init_window is invoked.
67
+ #[no_mangle]
68
+ pub extern "C" fn bloom_android_set_native_window(window: *mut libc::c_void) {
69
+ unsafe {
70
+ if !window.is_null() {
71
+ ANativeWindow_acquire(window);
72
+ }
73
+ NATIVE_WINDOW = window;
74
+ }
75
+ }
76
+
77
+ fn pollster_block_on<F: std::future::Future>(future: F) -> F::Output {
78
+ use std::task::{Context, Poll, Wake, Waker};
79
+ use std::pin::Pin;
80
+ use std::sync::Arc;
81
+ struct NoopWaker;
82
+ impl Wake for NoopWaker { fn wake(self: Arc<Self>) {} }
83
+ let waker = Waker::from(Arc::new(NoopWaker));
84
+ let mut cx = Context::from_waker(&waker);
85
+ let mut future = unsafe { Pin::new_unchecked(Box::new(future)) };
86
+ loop {
87
+ match future.as_mut().poll(&mut cx) {
88
+ Poll::Ready(result) => return result,
89
+ Poll::Pending => std::thread::yield_now(),
90
+ }
91
+ }
92
+ }
93
+
94
+ // ============================================================
95
+ // Window + Renderer init
96
+ // ============================================================
97
+
98
+ #[no_mangle]
99
+ pub extern "C" fn bloom_init_window(width: f64, height: f64, title_ptr: *const u8, _fullscreen: f64) {
100
+ let _title = str_from_header(title_ptr);
101
+
102
+ unsafe {
103
+ __android_log_print(3, b"BloomEngine\0".as_ptr(), b"bloom_init_window: starting\0".as_ptr());
104
+ let window = NATIVE_WINDOW;
105
+ // If no native window was set, use requested dimensions with a headless surface
106
+ let (pixel_w, pixel_h) = if !window.is_null() {
107
+ (ANativeWindow_getWidth(window) as u32, ANativeWindow_getHeight(window) as u32)
108
+ } else {
109
+ (width as u32, height as u32)
110
+ };
111
+ // Logical size is half of physical: the game UI (fonts, layout constants)
112
+ // was sized for ~1170×540 landscape (iPhone-sized); on Android's 2340×1080
113
+ // panel we'd otherwise render at native pixel size and everything looks
114
+ // half-scale. wgpu still renders to the full physical surface; only the
115
+ // `screen_width`/`screen_height` the game sees are halved.
116
+ let logical_w = (pixel_w / 2).max(1);
117
+ let logical_h = (pixel_h / 2).max(1);
118
+
119
+ let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
120
+ backends: wgpu::Backends::VULKAN | wgpu::Backends::GL,
121
+ flags: wgpu::InstanceFlags::default(),
122
+ ..wgpu::InstanceDescriptor::new_without_display_handle()
123
+ });
124
+
125
+ // Create surface from ANativeWindow
126
+ if window.is_null() {
127
+ // Fallback: can't render without a window, but don't panic
128
+ // Create a minimal engine state (game logic will work, rendering will no-op)
129
+ return;
130
+ }
131
+
132
+ let handle = raw_window_handle::AndroidNdkWindowHandle::new(
133
+ std::ptr::NonNull::new(window).unwrap()
134
+ );
135
+ let raw = raw_window_handle::RawWindowHandle::AndroidNdk(handle);
136
+ let surface = instance.create_surface_unsafe(wgpu::SurfaceTargetUnsafe::RawHandle {
137
+ raw_display_handle: Some(raw_window_handle::RawDisplayHandle::Android(
138
+ raw_window_handle::AndroidDisplayHandle::new()
139
+ )),
140
+ raw_window_handle: raw,
141
+ }).expect("Failed to create surface");
142
+ __android_log_print(3, b"BloomEngine\0".as_ptr(), b"bloom_init_window: surface created\0".as_ptr());
143
+
144
+ let adapter = pollster_block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
145
+ compatible_surface: Some(&surface),
146
+ power_preference: wgpu::PowerPreference::HighPerformance,
147
+ ..Default::default()
148
+ }));
149
+ let adapter = match adapter {
150
+ Ok(a) => a,
151
+ Err(_) => {
152
+ // Try again without surface compatibility requirement
153
+ match pollster_block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
154
+ power_preference: wgpu::PowerPreference::HighPerformance,
155
+ ..Default::default()
156
+ })) {
157
+ Ok(a) => a,
158
+ Err(_) => panic!("No GPU adapter found"),
159
+ }
160
+ }
161
+ };
162
+ __android_log_print(3, b"BloomEngine\0".as_ptr(), b"bloom_init_window: adapter found\0".as_ptr());
163
+ {
164
+ let info = adapter.get_info();
165
+ let msg = std::ffi::CString::new(format!(
166
+ "adapter: name='{}' backend={:?} device_type={:?} driver='{}' driver_info='{}'",
167
+ info.name, info.backend, info.device_type, info.driver, info.driver_info
168
+ )).unwrap();
169
+ __android_log_print(3, b"BloomEngine\0".as_ptr(), b"%s\0".as_ptr(), msg.as_ptr());
170
+ }
171
+
172
+ // Ticket 007b: most Android GPUs lack RT, but recent Adreno /
173
+ // Mali-Immortalis devices do — request the feature if advertised.
174
+ // Limits merge: start from downlevel (required for older Android
175
+ // adapters), then layer acceleration-structure minimums on top
176
+ // when RT was granted.
177
+ let supported = adapter.features();
178
+ let force_sw_gi = std::env::var("BLOOM_FORCE_SW_GI")
179
+ .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
180
+ .unwrap_or(false);
181
+ let rt_mask = wgpu::Features::EXPERIMENTAL_RAY_QUERY;
182
+ let mut required_features = wgpu::Features::empty();
183
+ // Ticket 011: request TIMESTAMP_QUERY when supported so the profiler
184
+ // can record GPU timings. Optional — profiler falls back to CPU-only
185
+ // when the adapter doesn't grant it (many Android GPUs won't).
186
+ if supported.contains(wgpu::Features::TIMESTAMP_QUERY) {
187
+ required_features |= wgpu::Features::TIMESTAMP_QUERY;
188
+ }
189
+ // Cooked BC7 textures (bloom-cook) upload compressed when the
190
+ // adapter has BC support; without it they CPU-decode at load.
191
+ if supported.contains(wgpu::Features::TEXTURE_COMPRESSION_BC) {
192
+ required_features |= wgpu::Features::TEXTURE_COMPRESSION_BC;
193
+ }
194
+ if !force_sw_gi && supported.contains(rt_mask) {
195
+ required_features |= rt_mask;
196
+ }
197
+ let experimental_features = if required_features.intersects(rt_mask) {
198
+ unsafe { wgpu::ExperimentalFeatures::enabled() }
199
+ } else {
200
+ wgpu::ExperimentalFeatures::disabled()
201
+ };
202
+ let adapter_limits = adapter.limits();
203
+ let mut required_limits = wgpu::Limits::downlevel_defaults()
204
+ .using_resolution(adapter_limits.clone());
205
+ // The renderer's `joint_bg` binds a 64KB uniform buffer, but
206
+ // downlevel_defaults caps uniform-buffer bindings at 16KB, so
207
+ // create_bind_group panics on mobile GPUs (e.g. Adreno) with
208
+ // "range 65536 exceeds max_*_buffer_binding_size limit 16384". Raise the
209
+ // buffer-binding sizes (and bind-group count) to the adapter's maximum;
210
+ // these are guaranteed-supported and match the desktop limits.
211
+ required_limits.max_uniform_buffer_binding_size =
212
+ adapter_limits.max_uniform_buffer_binding_size;
213
+ required_limits.max_storage_buffer_binding_size =
214
+ adapter_limits.max_storage_buffer_binding_size;
215
+ required_limits.max_bind_groups =
216
+ required_limits.max_bind_groups.max(5).min(adapter_limits.max_bind_groups);
217
+ if required_features.intersects(rt_mask) {
218
+ required_limits = required_limits
219
+ .using_minimum_supported_acceleration_structure_values();
220
+ }
221
+ let (device, queue) = pollster_block_on(adapter.request_device(
222
+ &wgpu::DeviceDescriptor {
223
+ label: Some("bloom_device"),
224
+ required_features,
225
+ required_limits,
226
+ experimental_features,
227
+ ..Default::default()
228
+ },
229
+ )).expect("Failed to create device");
230
+ __android_log_print(3, b"BloomEngine\0".as_ptr(), b"bloom_init_window: device created\0".as_ptr());
231
+
232
+ let surface_caps = surface.get_capabilities(&adapter);
233
+ if surface_caps.formats.is_empty() {
234
+ panic!("Surface reports no supported formats (emulator Vulkan limitation)");
235
+ }
236
+ let format = surface_caps.formats.iter()
237
+ .find(|f| f.is_srgb()).copied()
238
+ .unwrap_or(surface_caps.formats[0]);
239
+
240
+ let alpha_mode = if surface_caps.alpha_modes.is_empty() {
241
+ wgpu::CompositeAlphaMode::Auto
242
+ } else {
243
+ surface_caps.alpha_modes[0]
244
+ };
245
+
246
+ let surface_config = wgpu::SurfaceConfiguration {
247
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
248
+ format,
249
+ width: pixel_w,
250
+ height: pixel_h,
251
+ present_mode: wgpu::PresentMode::Fifo,
252
+ alpha_mode,
253
+ view_formats: vec![],
254
+ desired_maximum_frame_latency: 2,
255
+ };
256
+ surface.configure(&device, &surface_config);
257
+
258
+ __android_log_print(3, b"BloomEngine\0".as_ptr(), b"bloom_init_window: surface configured\0".as_ptr());
259
+ let renderer = Renderer::new(device, queue, surface, surface_config, logical_w, logical_h);
260
+ let _ = ENGINE.set(EngineState::new(renderer));
261
+ __android_log_print(3, b"BloomEngine\0".as_ptr(), b"bloom_init_window: engine initialized\0".as_ptr());
262
+ }
263
+ }
264
+
265
+ /// Attach the engine to a host-owned `ANativeWindow*` instead of pulling
266
+ /// it from the global set by `bloom_android_set_native_window`
267
+ /// (PerryTS/perry#5519). `handle` is the `ANativeWindow*` the host
268
+ /// (Perry UI's `BloomView`, backed by a `SurfaceView`/`TextureView`)
269
+ /// owns; `width`/`height` are the surface size in physical pixels.
270
+ /// Returns 1.0 on success, 0.0 on a null/invalid handle or surface
271
+ /// bring-up failure. Idempotent once attached.
272
+ #[no_mangle]
273
+ pub extern "C" fn bloom_attach_native(handle: i64, width: f64, height: f64) -> f64 {
274
+ if handle == 0 {
275
+ return 0.0;
276
+ }
277
+ if unsafe { ENGINE.get() }.is_some() {
278
+ return 1.0;
279
+ }
280
+ let window = handle as *mut libc::c_void;
281
+ let Some(win_nn) = std::ptr::NonNull::new(window) else {
282
+ return 0.0;
283
+ };
284
+ // Hold a reference for as long as the engine renders into it.
285
+ unsafe {
286
+ ANativeWindow_acquire(window);
287
+ NATIVE_WINDOW = window;
288
+ }
289
+ let target = {
290
+ let h = raw_window_handle::AndroidNdkWindowHandle::new(win_nn);
291
+ wgpu::SurfaceTargetUnsafe::RawHandle {
292
+ raw_display_handle: Some(raw_window_handle::RawDisplayHandle::Android(
293
+ raw_window_handle::AndroidDisplayHandle::new(),
294
+ )),
295
+ raw_window_handle: raw_window_handle::RawWindowHandle::AndroidNdk(h),
296
+ }
297
+ };
298
+ match unsafe {
299
+ bloom_shared::attach::attach_engine(
300
+ target,
301
+ bloom_shared::attach::AttachParams {
302
+ backends: wgpu::Backends::VULKAN | wgpu::Backends::GL,
303
+ logical_w: (width as u32).max(1),
304
+ logical_h: (height as u32).max(1),
305
+ physical_w: (width as u32).max(1),
306
+ physical_h: (height as u32).max(1),
307
+ format: bloom_shared::attach::FormatPreference::Srgb,
308
+ },
309
+ )
310
+ } {
311
+ Ok(es) => {
312
+ unsafe {
313
+ let _ = ENGINE.set(es);
314
+ }
315
+ 1.0
316
+ }
317
+ Err(_) => 0.0,
318
+ }
319
+ }
320
+
321
+ /// Resize the engine's surface (#70 parity; used by host-driven
322
+ /// BloomViews on layout changes). `phys_*` physical px, `log_*` logical.
323
+ #[no_mangle]
324
+ pub extern "C" fn bloom_resize(phys_w: f64, phys_h: f64, log_w: f64, log_h: f64) {
325
+ if let Some(eng) = unsafe { ENGINE.get_mut() } {
326
+ eng.renderer.resize(phys_w as u32, phys_h as u32, log_w as u32, log_h as u32);
327
+ }
328
+ }
329
+
330
+ /// HWND host-embed (#70) — Windows only; a no-op here for FFI-manifest
331
+ /// parity. Non-Windows hosts attach via `bloom_attach_native`.
332
+ #[no_mangle]
333
+ pub extern "C" fn bloom_attach_hwnd(_hwnd_bits: f64, _width: f64, _height: f64) {}
334
+
335
+ #[no_mangle]
336
+ pub extern "C" fn bloom_close_window() {
337
+ unsafe {
338
+ if !NATIVE_WINDOW.is_null() {
339
+ ANativeWindow_release(NATIVE_WINDOW);
340
+ NATIVE_WINDOW = std::ptr::null_mut();
341
+ }
342
+ }
343
+ }
344
+
345
+ #[no_mangle]
346
+ pub extern "C" fn bloom_window_should_close() -> f64 {
347
+ if engine().should_close { 1.0 } else { 0.0 }
348
+ }
349
+
350
+ // ============================================================
351
+ // Touch input polling (called from Perry's Android event pump)
352
+ // ============================================================
353
+
354
+ /// Called by Perry's Android layer when a touch event occurs.
355
+ #[no_mangle]
356
+ pub extern "C" fn bloom_android_on_touch(action: i32, x: f64, y: f64, pointer_index: i32) {
357
+ unsafe {
358
+ if let Some(eng) = ENGINE.get_mut() {
359
+ let sw = eng.screen_width();
360
+ let sh = eng.screen_height();
361
+ let lx = x * 0.5;
362
+ let ly = y * 0.5;
363
+ let msg = std::ffi::CString::new(format!("touch a={} raw=({},{}) scaled=({},{}) sw={} sh={}", action, x, y, lx, ly, sw, sh)).unwrap();
364
+ __android_log_print(3, b"BloomTouch\0".as_ptr(), b"%s\0".as_ptr(), msg.as_ptr());
365
+ eng.input.set_mouse_position(lx, ly);
366
+ if action == 1 || action == 3 {
367
+ eng.input.release_touch(pointer_index as usize, lx, ly); // UP / CANCEL
368
+ } else {
369
+ eng.input.set_touch(pointer_index as usize, lx, ly, true); // DOWN / MOVE
370
+ }
371
+ match action {
372
+ 0 => eng.input.set_mouse_button_down(0), // ACTION_DOWN
373
+ 1 => eng.input.set_mouse_button_up(0), // ACTION_UP
374
+ 2 => {} // ACTION_MOVE
375
+ _ => {}
376
+ }
377
+ }
378
+ }
379
+ }
380
+
381
+ #[no_mangle]
382
+ pub extern "C" fn bloom_begin_drawing() {
383
+ engine().begin_frame();
384
+ }
385
+
386
+ #[no_mangle]
387
+ pub extern "C" fn bloom_end_drawing() {
388
+ let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
389
+ engine().end_frame();
390
+ }));
391
+ if let Err(e) = result {
392
+ let msg = if let Some(s) = e.downcast_ref::<&str>() {
393
+ s.to_string()
394
+ } else if let Some(s) = e.downcast_ref::<String>() {
395
+ s.clone()
396
+ } else {
397
+ format!("unknown panic: {:?}", e.type_id())
398
+ };
399
+ // Write to file since eprintln doesn't reach logcat on Android
400
+ static mut LOGGED: bool = false;
401
+ unsafe {
402
+ if !LOGGED {
403
+ LOGGED = true;
404
+ let path = resolve_path("bloom_panic.txt");
405
+ let _ = std::fs::write(&path, format!("end_drawing panic: {}\n", msg));
406
+ }
407
+ }
408
+ }
409
+ }
410
+
411
+ // ============================================================
412
+ // Audio (Oboe / AAudio)
413
+ // ============================================================
414
+
415
+ #[no_mangle]
416
+ pub extern "C" fn bloom_init_audio() {
417
+ AUDIO_RUNNING.store(true, Ordering::SeqCst);
418
+ // Move the render half into the audio thread; the engine keeps only
419
+ // the command-producing control half.
420
+ let renderer = engine().audio.take_renderer();
421
+ std::thread::spawn(move || {
422
+ android_audio_thread(renderer);
423
+ });
424
+ }
425
+
426
+ #[no_mangle]
427
+ pub extern "C" fn bloom_close_audio() {
428
+ AUDIO_RUNNING.store(false, Ordering::SeqCst);
429
+ std::thread::sleep(std::time::Duration::from_millis(50));
430
+ }
431
+
432
+ fn android_audio_thread(renderer: Option<bloom_shared::audio::AudioRenderer>) {
433
+ // Use oboe (AAudio/OpenSL ES wrapper) for audio output
434
+ use oboe::*;
435
+
436
+ // The render half is owned by the oboe callback — AAudio invokes
437
+ // on_audio_ready from its own realtime thread, and the callback never
438
+ // touches shared engine state (see audio/mod.rs contract).
439
+ struct BloomAudioCallback {
440
+ renderer: Option<bloom_shared::audio::AudioRenderer>,
441
+ }
442
+
443
+ impl AudioOutputCallback for BloomAudioCallback {
444
+ type FrameType = (f32, Stereo);
445
+
446
+ fn on_audio_ready(&mut self, _stream: &mut dyn AudioOutputStreamSafe, frames: &mut [(f32, f32)]) -> DataCallbackResult {
447
+ // Convert frame tuples to interleaved slice
448
+ let len = frames.len() * 2;
449
+ let ptr = frames.as_mut_ptr() as *mut f32;
450
+ let interleaved = unsafe { std::slice::from_raw_parts_mut(ptr, len) };
451
+ for s in interleaved.iter_mut() { *s = 0.0; }
452
+ if let Some(r) = self.renderer.as_mut() {
453
+ r.mix(interleaved);
454
+ }
455
+ if AUDIO_RUNNING.load(Ordering::SeqCst) {
456
+ DataCallbackResult::Continue
457
+ } else {
458
+ DataCallbackResult::Stop
459
+ }
460
+ }
461
+ }
462
+
463
+ let mut stream = AudioStreamBuilder::default()
464
+ .set_performance_mode(PerformanceMode::LowLatency)
465
+ .set_sharing_mode(SharingMode::Shared)
466
+ .set_format::<f32>()
467
+ .set_channel_count::<Stereo>()
468
+ .set_sample_rate(44100)
469
+ .set_callback(BloomAudioCallback { renderer })
470
+ .open_stream();
471
+
472
+ match stream {
473
+ Ok(ref mut s) => {
474
+ let _ = s.start();
475
+ // Keep thread alive while audio is running
476
+ while AUDIO_RUNNING.load(Ordering::SeqCst) {
477
+ std::thread::sleep(std::time::Duration::from_millis(100));
478
+ }
479
+ let _ = s.stop();
480
+ }
481
+ Err(_) => {}
482
+ }
483
+ }
484
+
485
+ // --- Texture FFI ---
486
+
487
+ // --- Camera FFI ---
488
+
489
+ // --- 3D Drawing FFI ---
490
+
491
+ // --- Model FFI ---
492
+
493
+ // ============================================================
494
+ // Phase 1c — material system FFI
495
+ // ============================================================
496
+
497
+ // --- Music FFI ---
498
+
499
+ // --- Gamepad FFI ---
500
+
501
+ // --- Skeletal Animation Debug ---
502
+
503
+ // --- Lighting ---
504
+
505
+ // --- Utility FFI ---
506
+
507
+ #[no_mangle]
508
+ pub extern "C" fn bloom_toggle_fullscreen() {}
509
+ #[no_mangle]
510
+ pub extern "C" fn bloom_set_window_title(title_ptr: *const u8) { let _ = str_from_header(title_ptr); }
511
+ #[no_mangle]
512
+ pub extern "C" fn bloom_set_window_icon(path_ptr: *const u8) { let _ = str_from_header(path_ptr); }
513
+
514
+ #[no_mangle]
515
+ pub extern "C" fn bloom_disable_cursor() {
516
+ engine().input.cursor_disabled = true;
517
+ }
518
+
519
+ #[no_mangle]
520
+ pub extern "C" fn bloom_enable_cursor() {
521
+ engine().input.cursor_disabled = false;
522
+ }
523
+
524
+ // E4: Clipboard (stub on this platform)
525
+ #[no_mangle]
526
+ pub extern "C" fn bloom_set_clipboard_text(_text_ptr: *const u8) {}
527
+ #[no_mangle]
528
+ pub extern "C" fn bloom_get_clipboard_text() -> *const u8 { std::ptr::null() }
529
+
530
+ // E5b: File dialogs (stub on this platform)
531
+ #[no_mangle]
532
+ pub extern "C" fn bloom_open_file_dialog(_filter_ptr: *const u8, _title_ptr: *const u8) -> *const u8 { std::ptr::null() }
533
+ #[no_mangle]
534
+ pub extern "C" fn bloom_save_file_dialog(_default_name_ptr: *const u8, _title_ptr: *const u8) -> *const u8 { std::ptr::null() }
535
+ #[no_mangle]
536
+ pub extern "C" fn bloom_get_platform() -> f64 { 5.0 }
537
+
538
+ /// Preferred OS language packed as `c0*256+c1` (ISO-639 primary subtag), read
539
+ /// from the device locale system property via the NDK (no JNI). Tries the
540
+ /// user-set locale first, then the factory defaults. Falls back to "en".
541
+ #[no_mangle]
542
+ pub extern "C" fn bloom_get_language() -> f64 {
543
+ fn parse(buf: &[u8], n: i32) -> Option<f64> {
544
+ if n < 2 { return None; }
545
+ let lc = |b: u8| if b.is_ascii_uppercase() { b + 32 } else { b };
546
+ let (c0, c1) = (lc(buf[0]), lc(buf[1]));
547
+ if c0.is_ascii_alphabetic() && c1.is_ascii_alphabetic() {
548
+ Some((c0 as f64) * 256.0 + (c1 as f64))
549
+ } else {
550
+ None
551
+ }
552
+ }
553
+ let props: [&[u8]; 3] = [
554
+ b"persist.sys.locale\0",
555
+ b"ro.product.locale\0",
556
+ b"ro.product.locale.language\0",
557
+ ];
558
+ for prop in props {
559
+ let mut buf = [0u8; 92]; // PROP_VALUE_MAX
560
+ let n = unsafe {
561
+ libc::__system_property_get(
562
+ prop.as_ptr() as *const libc::c_char,
563
+ buf.as_mut_ptr() as *mut libc::c_char,
564
+ )
565
+ };
566
+ if let Some(v) = parse(&buf, n) { return v; }
567
+ }
568
+ 25966.0
569
+ }
570
+
571
+ // ============================================================
572
+ // JNI Bridge for Bloom game applications
573
+ // ============================================================
574
+ //
575
+ // These functions bridge the Android Java/Kotlin layer to the
576
+ // Bloom engine. Any Bloom game on Android should use the
577
+ // com.bloomengine.game.BloomGameBridge Kotlin class.
578
+
579
+ extern "C" {
580
+ fn ANativeWindow_fromSurface(env: *mut libc::c_void, surface: *mut libc::c_void) -> *mut libc::c_void;
581
+ fn mallopt(param: i32, value: i32) -> i32;
582
+ fn __android_log_print(prio: i32, tag: *const u8, fmt: *const u8, ...) -> i32;
583
+ fn main() -> i32;
584
+ }
585
+
586
+ /// JNI_OnLoad: called when System.loadLibrary() loads this .so.
587
+ /// Disables MTE heap tagging (required for Perry NaN-boxing) and
588
+ /// reads the asset base path from BLOOM_ASSET_PATH env var.
589
+ #[no_mangle]
590
+ pub extern "C" fn JNI_OnLoad(_vm: *mut libc::c_void, _reserved: *mut libc::c_void) -> i32 {
591
+ unsafe {
592
+ // Disable MTE heap tagging for Perry NaN-boxing compatibility.
593
+ // Perry uses 48-bit pointers; Android's scudo allocator may tag
594
+ // the top byte, corrupting NaN-boxed pointer values.
595
+ mallopt(-204, 0);
596
+
597
+ __android_log_print(
598
+ 3, b"BloomEngine\0".as_ptr(),
599
+ b"JNI_OnLoad: MTE disabled\0".as_ptr(),
600
+ );
601
+ }
602
+
603
+ // Read asset base path from environment (set by Activity before loadLibrary)
604
+ if let Ok(path) = std::env::var("BLOOM_ASSET_PATH") {
605
+ unsafe {
606
+ __android_log_print(
607
+ 3, b"BloomEngine\0".as_ptr(),
608
+ b"JNI_OnLoad: asset path set\0".as_ptr(),
609
+ );
610
+ ASSET_BASE_PATH = Some(path);
611
+ }
612
+ }
613
+
614
+ 0x00010006 // JNI_VERSION_1_6
615
+ }
616
+
617
+ /// Pass the Android Surface to the engine so it can create a wgpu rendering surface.
618
+ /// Called from BloomGameBridge.nativeSetSurface(surface).
619
+ #[no_mangle]
620
+ pub unsafe extern "C" fn Java_com_bloomengine_game_BloomGameBridge_nativeSetSurface(
621
+ env: *mut libc::c_void,
622
+ _class: *mut libc::c_void,
623
+ surface: *mut libc::c_void,
624
+ ) {
625
+ let window = ANativeWindow_fromSurface(env, surface);
626
+ __android_log_print(
627
+ 3, b"BloomEngine\0".as_ptr(),
628
+ b"nativeSetSurface: ANativeWindow acquired\0".as_ptr(),
629
+ );
630
+ bloom_android_set_native_window(window);
631
+ }
632
+
633
+ /// Run the compiled game's main() function on the game thread.
634
+ /// Called from BloomGameBridge.nativeMain().
635
+ #[no_mangle]
636
+ pub unsafe extern "C" fn Java_com_bloomengine_game_BloomGameBridge_nativeMain(
637
+ _env: *mut libc::c_void,
638
+ _class: *mut libc::c_void,
639
+ ) {
640
+ __android_log_print(
641
+ 3, b"BloomEngine\0".as_ptr(),
642
+ b"nativeMain: calling main()\0".as_ptr(),
643
+ );
644
+ main();
645
+ __android_log_print(
646
+ 3, b"BloomEngine\0".as_ptr(),
647
+ b"nativeMain: main() returned\0".as_ptr(),
648
+ );
649
+ }
650
+
651
+ /// Forward touch events from the Android UI thread to the engine's input system.
652
+ /// Called from BloomGameBridge.nativeOnTouch(action, x, y, pointerIndex).
653
+ #[no_mangle]
654
+ pub extern "C" fn Java_com_bloomengine_game_BloomGameBridge_nativeOnTouch(
655
+ _env: *mut libc::c_void,
656
+ _class: *mut libc::c_void,
657
+ action: i32,
658
+ x: f64,
659
+ y: f64,
660
+ pointer_index: i32,
661
+ ) {
662
+ bloom_android_on_touch(action, x, y, pointer_index);
663
+ }
664
+
665
+ /// Signal the engine to close when the Activity is destroyed.
666
+ /// Called from BloomGameBridge.nativeOnDestroy().
667
+ #[no_mangle]
668
+ pub extern "C" fn Java_com_bloomengine_game_BloomGameBridge_nativeOnDestroy(
669
+ _env: *mut libc::c_void,
670
+ _class: *mut libc::c_void,
671
+ ) {
672
+ unsafe {
673
+ if let Some(eng) = ENGINE.get_mut() {
674
+ eng.should_close = true;
675
+ }
676
+ }
677
+ }
678
+
679
+ // ============================================================
680
+ // Thread-safe staging (for async asset loading via Perry threads)
681
+ // ============================================================
682
+
683
+
684
+
685
+ // Q6: Multi-hit picking
686
+ // ============================================================
687
+
688
+ // ============================================================
689
+ // Physics (Jolt 5.x) — FFI surface generated from shared macro
690
+ // ============================================================
691
+
692
+ #[cfg(feature = "jolt")]
693
+ #[inline]
694
+ fn bloom_jolt_ffi_physics() -> &'static mut bloom_shared::physics_jolt::JoltPhysics {
695
+ &mut engine().jolt
696
+ }
697
+
698
+ #[cfg(feature = "jolt")]
699
+ bloom_shared::define_physics_ffi!();
700
+
701
+ // === Android FFI parity: ported from native/linux/src/lib.rs (shared renderer/scene) ===
702
+ // Backing statics for the ported pick/project FFI (mirror native/linux).