@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,1124 @@
1
+ //! bloom-watchos: Canvas + (future) SceneKit backend for watchOS.
2
+ //!
3
+ //! The Swift shell (`BloomWatchApp.swift`, compiled by Perry via
4
+ //! `--features watchos-swift-app`) owns `@main`. During init it spawns the
5
+ //! game thread calling `_perry_user_main`. The root view is a SwiftUI
6
+ //! `Canvas` that drains this crate's draw list each frame. Digital Crown
7
+ //! rotation, taps, and layout dimensions come back in through the
8
+ //! `bloom_watchos_*` inbound functions.
9
+
10
+ #![allow(non_upper_case_globals)]
11
+
12
+ mod ffi_stubs;
13
+ mod ffi_stubs_manual;
14
+ mod draw_list;
15
+ mod textures;
16
+ mod audio;
17
+ mod scene;
18
+ mod models;
19
+ mod postfx;
20
+
21
+ /// Perry StringHeader layout — mirrors bloom-shared's copy. Inlined here
22
+ /// because we don't depend on bloom-shared (keeps the watchos crate
23
+ /// wgpu-free).
24
+ #[repr(C)]
25
+ struct StringHeader {
26
+ utf16_len: u32,
27
+ byte_len: u32,
28
+ capacity: u32,
29
+ refcount: u32,
30
+ // Perry 0.5.x's canonical StringHeader is 5×u32 = 20 bytes (data at +20).
31
+ // Omitting `flags` made this 16 bytes, so `perry_str` read every incoming
32
+ // string 4 bytes early — text rendered with a 4-null prefix + truncated
33
+ // tail ("BLOOM JUMP" → "BLOOM"), and file paths came back corrupted so
34
+ // levels never loaded.
35
+ flags: u32,
36
+ }
37
+
38
+ /// Decode a Perry-side string pointer (i64 on this ABI) into a borrowed &str.
39
+ /// Returns "" for null / too-small pointers to keep the FFI boundary robust.
40
+ fn perry_str<'a>(ptr: i64) -> &'a str {
41
+ if ptr == 0 { return ""; }
42
+ let p = ptr as *const u8;
43
+ if (p as usize) < 0x1000 { return ""; }
44
+ unsafe {
45
+ let header = p as *const StringHeader;
46
+ let len = (*header).byte_len as usize;
47
+ let data = p.add(std::mem::size_of::<StringHeader>());
48
+ std::str::from_utf8_unchecked(std::slice::from_raw_parts(data, len))
49
+ }
50
+ }
51
+
52
+ /// Allocate a Perry-side heap string (StringHeader + UTF-8 payload) and return
53
+ /// its pointer as the i64 the FFI boundary expects. Mirrors `perry_str`'s
54
+ /// layout so a string we hand back is read identically to one Perry passed us.
55
+ /// Returning this (never 0) for a `returns:"string"` function is mandatory:
56
+ /// Perry's inline `.length` dereferences the pointer, so a null segfaults the
57
+ /// caller.
58
+ fn alloc_perry_string(s: &str) -> i64 {
59
+ let bytes = s.as_bytes();
60
+ let byte_len = bytes.len();
61
+ let utf16_len = if bytes.iter().all(|&b| b < 0x80) {
62
+ byte_len
63
+ } else {
64
+ s.encode_utf16().count()
65
+ };
66
+ // A value RETURNED across the FFI boundary is consumed by Perry's internal
67
+ // string machinery, whose canonical StringHeader is 5×u32 = 20 bytes
68
+ // (utf16_len, byte_len, capacity, refcount, flags) with data at +20.
69
+ // (The local 16-byte `StringHeader`/`perry_str` above describe the *incoming*
70
+ // arg representation, which omits `flags`; don't conflate the two.)
71
+ const HEADER_SIZE: usize = 20;
72
+ let total = HEADER_SIZE + byte_len;
73
+ unsafe {
74
+ let layout = std::alloc::Layout::from_size_align(total, 4).unwrap();
75
+ let ptr = std::alloc::alloc(layout);
76
+ if ptr.is_null() {
77
+ return 0;
78
+ }
79
+ *(ptr.add(0) as *mut u32) = utf16_len as u32;
80
+ *(ptr.add(4) as *mut u32) = byte_len as u32;
81
+ *(ptr.add(8) as *mut u32) = byte_len as u32; // capacity
82
+ *(ptr.add(12) as *mut u32) = 1; // refcount = unique
83
+ *(ptr.add(16) as *mut u32) = 0; // flags
84
+ std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.add(HEADER_SIZE), byte_len);
85
+ ptr as i64
86
+ }
87
+ }
88
+
89
+ use std::ffi::{c_char, c_void};
90
+ use std::sync::atomic::{AtomicI64, AtomicU64, AtomicUsize, Ordering};
91
+ use std::sync::OnceLock;
92
+ use std::time::Instant;
93
+
94
+ use draw_list::{kind, DrawCmd};
95
+
96
+ // ============================================================
97
+ // Minimal engine state (input + timing). Rendering state lives
98
+ // in the draw_list and textures modules.
99
+ // ============================================================
100
+
101
+ const MAX_TOUCH: usize = 4;
102
+
103
+ struct WatchState {
104
+ crown_bits: AtomicU64, // f64 bits
105
+ language_code: AtomicU64, // packed 2-letter code (c0*256+c1), set by Swift at launch
106
+ touch_x: [AtomicU64; MAX_TOUCH],
107
+ touch_y: [AtomicU64; MAX_TOUCH],
108
+ touch_active: [AtomicU64; MAX_TOUCH],
109
+ screen_w: AtomicU64, // f64 bits
110
+ screen_h: AtomicU64, // f64 bits
111
+ target_fps: AtomicI64,
112
+ start: OnceLock<Instant>,
113
+ last_frame_ns: AtomicU64,
114
+ frame_count: AtomicUsize,
115
+ }
116
+
117
+ fn state() -> &'static WatchState {
118
+ static S: OnceLock<WatchState> = OnceLock::new();
119
+ S.get_or_init(|| WatchState {
120
+ crown_bits: AtomicU64::new(0),
121
+ language_code: AtomicU64::new(25966), // "en" until Swift reports the locale
122
+ touch_x: std::array::from_fn(|_| AtomicU64::new(0)),
123
+ touch_y: std::array::from_fn(|_| AtomicU64::new(0)),
124
+ touch_active: std::array::from_fn(|_| AtomicU64::new(0)),
125
+ screen_w: AtomicU64::new(198f64.to_bits()),
126
+ screen_h: AtomicU64::new(242f64.to_bits()),
127
+ target_fps: AtomicI64::new(30),
128
+ start: OnceLock::new(),
129
+ last_frame_ns: AtomicU64::new(0),
130
+ frame_count: AtomicUsize::new(0),
131
+ })
132
+ }
133
+
134
+ fn now_nanos() -> u64 {
135
+ let s = state();
136
+ let start = s.start.get_or_init(Instant::now);
137
+ start.elapsed().as_nanos() as u64
138
+ }
139
+
140
+ fn add_crown(delta: f64) {
141
+ let s = state();
142
+ loop {
143
+ let cur = s.crown_bits.load(Ordering::Acquire);
144
+ let new = (f64::from_bits(cur) + delta).to_bits();
145
+ if s.crown_bits
146
+ .compare_exchange_weak(cur, new, Ordering::AcqRel, Ordering::Acquire)
147
+ .is_ok()
148
+ {
149
+ break;
150
+ }
151
+ }
152
+ }
153
+
154
+ fn consume_crown() -> f64 {
155
+ let s = state();
156
+ let bits = s.crown_bits.swap(0, Ordering::AcqRel);
157
+ f64::from_bits(bits)
158
+ }
159
+
160
+ // ============================================================
161
+ // Swift → Rust inbound hooks. Called from BloomWatchApp.swift.
162
+ // ============================================================
163
+
164
+ #[no_mangle]
165
+ pub extern "C" fn bloom_watchos_crown_delta(delta: f64) { add_crown(delta); }
166
+
167
+ /// Swift reports the user's preferred language at launch as a packed
168
+ /// 2-letter ISO-639 primary subtag (c0*256 + c1, lowercased), matching
169
+ /// the bloom_get_language contract on the other platforms.
170
+ #[no_mangle]
171
+ pub extern "C" fn bloom_watchos_set_language(code: f64) {
172
+ let c = code as u64;
173
+ if c > 0 {
174
+ state().language_code.store(c, Ordering::Release);
175
+ }
176
+ }
177
+
178
+ #[no_mangle]
179
+ pub extern "C" fn bloom_watchos_touch(index: i64, x: f64, y: f64, active: i64) {
180
+ let s = state();
181
+ let i = index as usize;
182
+ if i < MAX_TOUCH {
183
+ s.touch_x[i].store(x.to_bits(), Ordering::Release);
184
+ s.touch_y[i].store(y.to_bits(), Ordering::Release);
185
+ s.touch_active[i].store(active as u64, Ordering::Release);
186
+ }
187
+ }
188
+
189
+ #[no_mangle]
190
+ pub extern "C" fn bloom_watchos_set_screen(w: f64, h: f64) {
191
+ let s = state();
192
+ s.screen_w.store(w.to_bits(), Ordering::Release);
193
+ s.screen_h.store(h.to_bits(), Ordering::Release);
194
+ }
195
+
196
+ #[no_mangle]
197
+ pub extern "C" fn bloom_watchos_set_bundle_path(path: *const c_char) {
198
+ if path.is_null() { return; }
199
+ let s = unsafe { std::ffi::CStr::from_ptr(path) };
200
+ if let Ok(str) = s.to_str() {
201
+ textures::set_bundle_path(str);
202
+ }
203
+ }
204
+
205
+ // Draw-list snapshot accessors for Swift Canvas.
206
+
207
+ #[no_mangle]
208
+ pub extern "C" fn bloom_watchos_frame_count() -> u64 { draw_list::frame() }
209
+
210
+ #[no_mangle]
211
+ pub extern "C" fn bloom_watchos_copy_draw_list(dst: *mut DrawCmd, max: i64) -> i64 {
212
+ draw_list::copy_into(dst, max)
213
+ }
214
+
215
+ /// Returns [r,g,b,a] of the current clear color, 0-255 per channel, into a
216
+ /// Swift-supplied 4-element f64 buffer.
217
+ #[no_mangle]
218
+ pub extern "C" fn bloom_watchos_clear_color(out: *mut f64) {
219
+ if out.is_null() { return; }
220
+ let c = draw_list::clear_rgba();
221
+ unsafe { std::ptr::copy_nonoverlapping(c.as_ptr(), out, 4); }
222
+ }
223
+
224
+ #[no_mangle]
225
+ pub extern "C" fn bloom_watchos_texture_path(handle: u32) -> *const c_char {
226
+ textures::path_ptr(handle)
227
+ }
228
+
229
+ /// Copy camera state [px,py,pz, tx,ty,tz, ux,uy,uz, fovy, proj] into a
230
+ /// Swift-supplied 11-element f64 buffer. Called by the Swift SceneView.
231
+ #[no_mangle]
232
+ pub extern "C" fn bloom_watchos_camera_state(out: *mut f64) {
233
+ draw_list::camera_snapshot(out);
234
+ }
235
+
236
+ /// 1 if the game has ever opened a 3D mode section — hints to the Swift
237
+ /// shell that it should host a SceneView layer underneath the Canvas.
238
+ #[no_mangle]
239
+ pub extern "C" fn bloom_watchos_has_3d() -> f64 {
240
+ if draw_list::has_3d() { 1.0 } else { 0.0 }
241
+ }
242
+
243
+ // ============================================================
244
+ // Perry game-loop handshake.
245
+ // ============================================================
246
+ //
247
+ // With --features watchos-swift-app, Perry doesn't generate its own main() —
248
+ // BloomWatchApp.swift is @main. Perry's watchos runtime still provides the
249
+ // `perry_main_init` C symbol which does global TS init; the Swift App calls
250
+ // it before spawning the game thread. We don't need to register any ObjC
251
+ // classes from this crate.
252
+
253
+ #[no_mangle]
254
+ pub extern "C" fn perry_register_native_classes() {}
255
+
256
+ #[no_mangle]
257
+ pub extern "C" fn perry_scene_will_connect(_scene: *const c_void) {}
258
+
259
+ // ============================================================
260
+ // Platform / timing / input overrides
261
+ // ============================================================
262
+
263
+ #[no_mangle]
264
+ pub extern "C" fn bloom_get_platform() -> f64 { 8.0 }
265
+
266
+ /// Preferred OS language packed as `c0*256+c1`. TODO: real per-OS detection; returns "en" for now.
267
+ #[no_mangle]
268
+ pub extern "C" fn bloom_get_language() -> f64 {
269
+ state().language_code.load(Ordering::Acquire) as f64
270
+ }
271
+
272
+ #[no_mangle]
273
+ pub extern "C" fn bloom_get_crown_rotation() -> f64 { consume_crown() }
274
+
275
+ #[no_mangle]
276
+ pub extern "C" fn bloom_get_screen_width() -> f64 {
277
+ f64::from_bits(state().screen_w.load(Ordering::Acquire))
278
+ }
279
+
280
+ #[no_mangle]
281
+ pub extern "C" fn bloom_get_screen_height() -> f64 {
282
+ f64::from_bits(state().screen_h.load(Ordering::Acquire))
283
+ }
284
+
285
+ #[no_mangle]
286
+ pub extern "C" fn bloom_get_touch_x(index: f64) -> f64 {
287
+ let s = state();
288
+ let i = index as usize;
289
+ if i < MAX_TOUCH { f64::from_bits(s.touch_x[i].load(Ordering::Acquire)) } else { 0.0 }
290
+ }
291
+
292
+ #[no_mangle]
293
+ pub extern "C" fn bloom_get_touch_y(index: f64) -> f64 {
294
+ let s = state();
295
+ let i = index as usize;
296
+ if i < MAX_TOUCH { f64::from_bits(s.touch_y[i].load(Ordering::Acquire)) } else { 0.0 }
297
+ }
298
+
299
+ #[no_mangle]
300
+ pub extern "C" fn bloom_get_touch_count() -> f64 {
301
+ let s = state();
302
+ let mut n = 0.0;
303
+ for i in 0..MAX_TOUCH {
304
+ if s.touch_active[i].load(Ordering::Acquire) != 0 { n += 1.0; }
305
+ }
306
+ n
307
+ }
308
+
309
+ #[no_mangle]
310
+ pub extern "C" fn bloom_is_touch_active(index: f64) -> f64 {
311
+ let s = state();
312
+ let i = index as usize;
313
+ if i < MAX_TOUCH && s.touch_active[i].load(Ordering::Acquire) != 0 { 1.0 } else { 0.0 }
314
+ }
315
+
316
+ #[no_mangle]
317
+ pub extern "C" fn bloom_get_max_touch_points() -> f64 {
318
+ MAX_TOUCH as f64
319
+ }
320
+
321
+ #[no_mangle]
322
+ pub extern "C" fn bloom_get_delta_time() -> f64 {
323
+ let s = state();
324
+ let now = now_nanos();
325
+ let last = s.last_frame_ns.swap(now, Ordering::AcqRel);
326
+ if last == 0 { 1.0 / 30.0 } else { (now - last) as f64 / 1_000_000_000.0 }
327
+ }
328
+
329
+ #[no_mangle]
330
+ pub extern "C" fn bloom_get_time() -> f64 {
331
+ now_nanos() as f64 / 1_000_000_000.0
332
+ }
333
+
334
+ #[no_mangle]
335
+ pub extern "C" fn bloom_get_fps() -> f64 {
336
+ state().target_fps.load(Ordering::Acquire) as f64
337
+ }
338
+
339
+ #[no_mangle]
340
+ pub extern "C" fn bloom_set_target_fps(fps: f64) {
341
+ state().target_fps.store(fps as i64, Ordering::Release);
342
+ }
343
+
344
+ // No-op on watchOS (no deferred 3D pipeline); kept to satisfy the FFI
345
+ // surface so cross-platform game code can call setDirect2DMode() freely.
346
+ #[no_mangle]
347
+ pub extern "C" fn bloom_set_direct_2d_mode(_on: f64) {}
348
+
349
+ #[no_mangle]
350
+ pub extern "C" fn bloom_init_window(_w: f64, _h: f64, _title: i64, _fullscreen: f64) {}
351
+
352
+ #[no_mangle]
353
+ pub extern "C" fn bloom_close_window() {}
354
+
355
+ #[no_mangle]
356
+ pub extern "C" fn bloom_window_should_close() -> f64 { 0.0 }
357
+
358
+ // ============================================================
359
+ // Drawing lifecycle
360
+ // ============================================================
361
+
362
+ #[no_mangle]
363
+ pub extern "C" fn bloom_begin_drawing() {
364
+ // Pace the game thread to target fps. Canvas refresh is driven by the
365
+ // frame counter change on the Swift side, so this sleep also controls
366
+ // apparent frame rate in the view.
367
+ let fps = state().target_fps.load(Ordering::Acquire);
368
+ if fps > 0 {
369
+ let frame_ns = 1_000_000_000u64 / fps as u64;
370
+ std::thread::sleep(std::time::Duration::from_nanos(frame_ns));
371
+ }
372
+ draw_list::begin();
373
+ }
374
+
375
+ #[no_mangle]
376
+ pub extern "C" fn bloom_end_drawing() {
377
+ draw_list::end();
378
+ state().frame_count.fetch_add(1, Ordering::Relaxed);
379
+ }
380
+
381
+ #[no_mangle]
382
+ pub extern "C" fn bloom_clear_background(r: f64, g: f64, b: f64, a: f64) {
383
+ draw_list::set_clear(r, g, b, a);
384
+ }
385
+
386
+ #[no_mangle]
387
+ pub extern "C" fn bloom_run_game(_callback: f64) {}
388
+
389
+ #[no_mangle]
390
+ pub extern "C" fn bloom_is_any_input_pressed() -> f64 {
391
+ let s = state();
392
+ for i in 0..MAX_TOUCH {
393
+ if s.touch_active[i].load(Ordering::Acquire) != 0 { return 1.0; }
394
+ }
395
+ if f64::from_bits(s.crown_bits.load(Ordering::Acquire)).abs() > 0.0 { return 1.0; }
396
+ 0.0
397
+ }
398
+
399
+ #[no_mangle] pub extern "C" fn bloom_is_key_pressed(_k: f64) -> f64 { 0.0 }
400
+ #[no_mangle] pub extern "C" fn bloom_is_key_down(_k: f64) -> f64 { 0.0 }
401
+ #[no_mangle] pub extern "C" fn bloom_is_key_released(_k: f64) -> f64 { 0.0 }
402
+
403
+ #[no_mangle] pub extern "C" fn bloom_inject_key_down(_k: f64) {}
404
+ #[no_mangle] pub extern "C" fn bloom_inject_key_up(_k: f64) {}
405
+ #[no_mangle] pub extern "C" fn bloom_inject_gamepad_axis(_a: f64, _v: f64) {}
406
+ #[no_mangle] pub extern "C" fn bloom_inject_gamepad_button_down(_b: f64) {}
407
+ #[no_mangle] pub extern "C" fn bloom_inject_gamepad_button_up(_b: f64) {}
408
+
409
+ #[no_mangle] pub extern "C" fn bloom_is_gamepad_available() -> f64 { 0.0 }
410
+ #[no_mangle] pub extern "C" fn bloom_get_gamepad_axis(_a: f64) -> f64 { 0.0 }
411
+ #[no_mangle] pub extern "C" fn bloom_is_gamepad_button_pressed(_b: f64) -> f64 { 0.0 }
412
+ #[no_mangle] pub extern "C" fn bloom_is_gamepad_button_down(_b: f64) -> f64 { 0.0 }
413
+ #[no_mangle] pub extern "C" fn bloom_is_gamepad_button_released(_b: f64) -> f64 { 0.0 }
414
+ #[no_mangle] pub extern "C" fn bloom_get_gamepad_axis_count() -> f64 { 0.0 }
415
+
416
+ // ============================================================
417
+ // 2D shape commands → draw list
418
+ // ============================================================
419
+
420
+ #[no_mangle]
421
+ pub extern "C" fn bloom_draw_rect(x: f64, y: f64, w: f64, h: f64, r: f64, g: f64, b: f64, a: f64) {
422
+ let mut c = DrawCmd::zero();
423
+ c.kind = kind::RECT;
424
+ c.x = x; c.y = y; c.w = w; c.h = h;
425
+ c.r = r; c.g = g; c.b = b; c.a = a;
426
+ draw_list::push(c);
427
+ }
428
+
429
+ #[no_mangle]
430
+ pub extern "C" fn bloom_draw_rect_lines(x: f64, y: f64, w: f64, h: f64, thickness: f64, r: f64, g: f64, b: f64, a: f64) {
431
+ let mut c = DrawCmd::zero();
432
+ c.kind = kind::RECT_LINES;
433
+ c.x = x; c.y = y; c.w = w; c.h = h;
434
+ c.thickness = thickness;
435
+ c.r = r; c.g = g; c.b = b; c.a = a;
436
+ draw_list::push(c);
437
+ }
438
+
439
+ #[no_mangle]
440
+ pub extern "C" fn bloom_draw_circle(cx: f64, cy: f64, rad: f64, r: f64, g: f64, b: f64, a: f64) {
441
+ let mut c = DrawCmd::zero();
442
+ c.kind = kind::CIRCLE;
443
+ c.x = cx; c.y = cy; c.w = rad; c.h = rad;
444
+ c.r = r; c.g = g; c.b = b; c.a = a;
445
+ draw_list::push(c);
446
+ }
447
+
448
+ // 2D camera: emit marker commands carrying the camera so the Swift Canvas can
449
+ // apply the matching affine transform to every world-space draw until end.
450
+ // world→screen is (world - target) * zoom + offset. Without this, gameplay
451
+ // tiles/sprites (drawn between begin/end) render at raw world coords off-screen.
452
+ #[no_mangle]
453
+ pub extern "C" fn bloom_begin_mode_2d(ox: f64, oy: f64, tx: f64, ty: f64, _rot: f64, zoom: f64) {
454
+ let mut c = DrawCmd::zero();
455
+ c.kind = kind::BEGIN_2D;
456
+ c.x = ox; c.y = oy; // screen offset
457
+ c.w = tx; c.h = ty; // world target
458
+ c.size = zoom; // zoom
459
+ draw_list::push(c);
460
+ }
461
+
462
+ #[no_mangle]
463
+ pub extern "C" fn bloom_end_mode_2d() {
464
+ let mut c = DrawCmd::zero();
465
+ c.kind = kind::END_2D;
466
+ draw_list::push(c);
467
+ }
468
+
469
+ #[no_mangle]
470
+ pub extern "C" fn bloom_draw_circle_lines(cx: f64, cy: f64, rad: f64, r: f64, g: f64, b: f64, a: f64) {
471
+ // ABI is (cx, cy, radius, r, g, b, a) — no thickness param (matches
472
+ // package.json + src/shapes). A phantom 4th `thickness` param here
473
+ // used to shift every color one slot right on watchOS.
474
+ let mut c = DrawCmd::zero();
475
+ c.kind = kind::CIRCLE_LINES;
476
+ c.x = cx; c.y = cy; c.w = rad; c.h = rad;
477
+ c.thickness = 1.0;
478
+ c.r = r; c.g = g; c.b = b; c.a = a;
479
+ draw_list::push(c);
480
+ }
481
+
482
+ #[no_mangle]
483
+ pub extern "C" fn bloom_draw_line(x1: f64, y1: f64, x2: f64, y2: f64, thickness: f64, r: f64, g: f64, b: f64, a: f64) {
484
+ let mut c = DrawCmd::zero();
485
+ c.kind = kind::LINE;
486
+ c.x = x1; c.y = y1;
487
+ c.src_x = x2; c.src_y = y2;
488
+ c.thickness = thickness;
489
+ c.r = r; c.g = g; c.b = b; c.a = a;
490
+ draw_list::push(c);
491
+ }
492
+
493
+ #[no_mangle]
494
+ pub extern "C" fn bloom_draw_triangle(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, r: f64, g: f64, b: f64, a: f64) {
495
+ let mut c = DrawCmd::zero();
496
+ c.kind = kind::TRIANGLE;
497
+ c.x = x1; c.y = y1;
498
+ c.src_x = x2; c.src_y = y2;
499
+ c.src_w = x3; c.src_h = y3;
500
+ c.r = r; c.g = g; c.b = b; c.a = a;
501
+ draw_list::push(c);
502
+ }
503
+
504
+ #[no_mangle]
505
+ pub extern "C" fn bloom_draw_poly(_cx: f64, _cy: f64, _sides: f64, _radius: f64, _rot: f64, _r: f64, _g: f64, _b: f64, _a: f64) {
506
+ // Polygon fill — defer; Canvas handles via context.fill(Path.addArc).
507
+ // Will land with the rest of the Canvas geometry work.
508
+ }
509
+
510
+ // ============================================================
511
+ // Texture commands
512
+ // ============================================================
513
+
514
+ #[no_mangle]
515
+ pub extern "C" fn bloom_load_texture(path: i64) -> f64 {
516
+ textures::load(perry_str(path)) as f64
517
+ }
518
+
519
+ #[no_mangle]
520
+ pub extern "C" fn bloom_unload_texture(_handle: f64) {
521
+ // Retain by handle — unload is a no-op on watch (reloading isn't common
522
+ // in Jump-class games; we keep cached CGImages on the Swift side for
523
+ // the life of the process).
524
+ }
525
+
526
+ #[no_mangle]
527
+ pub extern "C" fn bloom_get_texture_width(handle: f64) -> f64 {
528
+ textures::width(handle as u32) as f64
529
+ }
530
+
531
+ #[no_mangle]
532
+ pub extern "C" fn bloom_get_texture_height(handle: f64) -> f64 {
533
+ textures::height(handle as u32) as f64
534
+ }
535
+
536
+ #[no_mangle]
537
+ pub extern "C" fn bloom_draw_texture(handle: f64, x: f64, y: f64, r: f64, g: f64, b: f64, a: f64) {
538
+ let h = handle as u32;
539
+ if h == 0 { return; }
540
+ let mut c = DrawCmd::zero();
541
+ c.kind = kind::TEXTURE;
542
+ c.tex = h;
543
+ c.x = x; c.y = y;
544
+ c.w = textures::width(h) as f64;
545
+ c.h = textures::height(h) as f64;
546
+ c.r = r; c.g = g; c.b = b; c.a = a;
547
+ draw_list::push(c);
548
+ }
549
+
550
+ #[no_mangle]
551
+ pub extern "C" fn bloom_draw_texture_rec(handle: f64,
552
+ src_x: f64, src_y: f64, src_w: f64, src_h: f64,
553
+ dst_x: f64, dst_y: f64,
554
+ r: f64, g: f64, b: f64, a: f64,
555
+ ) {
556
+ let h = handle as u32;
557
+ if h == 0 { return; }
558
+ let mut c = DrawCmd::zero();
559
+ c.kind = kind::TEXTURE_REC;
560
+ c.tex = h;
561
+ c.x = dst_x; c.y = dst_y;
562
+ c.w = src_w; c.h = src_h;
563
+ c.src_x = src_x; c.src_y = src_y; c.src_w = src_w; c.src_h = src_h;
564
+ c.r = r; c.g = g; c.b = b; c.a = a;
565
+ draw_list::push(c);
566
+ }
567
+
568
+ #[no_mangle]
569
+ pub extern "C" fn bloom_draw_texture_pro(handle: f64,
570
+ src_x: f64, src_y: f64, src_w: f64, src_h: f64,
571
+ dst_x: f64, dst_y: f64, dst_w: f64, dst_h: f64,
572
+ ox: f64, oy: f64, rot: f64,
573
+ r: f64, g: f64, b: f64, a: f64,
574
+ ) {
575
+ let h = handle as u32;
576
+ if h == 0 { return; }
577
+ let mut c = DrawCmd::zero();
578
+ c.kind = kind::TEXTURE_PRO;
579
+ c.tex = h;
580
+ c.x = dst_x; c.y = dst_y; c.w = dst_w; c.h = dst_h;
581
+ c.src_x = src_x; c.src_y = src_y; c.src_w = src_w; c.src_h = src_h;
582
+ c.ox = ox; c.oy = oy; c.rot = rot;
583
+ c.r = r; c.g = g; c.b = b; c.a = a;
584
+ draw_list::push(c);
585
+ }
586
+
587
+ #[no_mangle]
588
+ pub extern "C" fn bloom_set_texture_filter(_handle: f64, _mode: f64) {}
589
+
590
+ // ============================================================
591
+ // Text
592
+ // ============================================================
593
+
594
+ #[no_mangle]
595
+ pub extern "C" fn bloom_draw_text(text: i64, x: f64, y: f64, size: f64,
596
+ r: f64, g: f64, b: f64, a: f64,
597
+ ) {
598
+ push_text(perry_str(text), x, y, size, r, g, b, a);
599
+ }
600
+
601
+ #[no_mangle]
602
+ pub extern "C" fn bloom_draw_text_ex(_font: f64, text: i64,
603
+ x: f64, y: f64, size: f64, _spacing: f64,
604
+ r: f64, g: f64, b: f64, a: f64,
605
+ ) {
606
+ push_text(perry_str(text), x, y, size, r, g, b, a);
607
+ }
608
+
609
+ fn push_text(s: &str, x: f64, y: f64, size: f64,
610
+ r: f64, g: f64, b: f64, a: f64,
611
+ ) {
612
+ if s.is_empty() { return; }
613
+ let mut c = DrawCmd::zero();
614
+ c.kind = kind::TEXT;
615
+ c.x = x; c.y = y;
616
+ c.size = size;
617
+ c.r = r; c.g = g; c.b = b; c.a = a;
618
+ c.set_text(s);
619
+ draw_list::push(c);
620
+ }
621
+
622
+ /// Crude text width estimate — fontSize * 0.55 * char_count. Good enough for
623
+ /// HUD layout; real glyph metrics would require calling back into Swift
624
+ /// (CoreText) or bundling a font parser.
625
+ #[no_mangle]
626
+ pub extern "C" fn bloom_measure_text(text: i64, size: f64) -> f64 {
627
+ perry_str(text).chars().count() as f64 * size * 0.55
628
+ }
629
+
630
+ #[no_mangle]
631
+ pub extern "C" fn bloom_measure_text_ex(_font: f64, text: i64, size: f64, _spacing: f64) -> f64 {
632
+ bloom_measure_text(text, size)
633
+ }
634
+
635
+ #[no_mangle]
636
+ pub extern "C" fn bloom_load_font(_path: i64, _size: f64) -> f64 {
637
+ 1.0 // All text uses the system font on watch; single sentinel handle.
638
+ }
639
+
640
+ #[no_mangle]
641
+ pub extern "C" fn bloom_unload_font(_handle: f64) {}
642
+
643
+ // ============================================================
644
+ // File I/O — minimal std::fs-backed implementations
645
+ // ============================================================
646
+
647
+ #[no_mangle]
648
+ pub extern "C" fn bloom_file_exists(path: i64) -> f64 {
649
+ let p = perry_str(path);
650
+ if p.is_empty() { return 0.0; }
651
+ // Resolve bundle-relative paths the same way textures does.
652
+ let full = if p.starts_with('/') { p.to_string() } else { textures::resolve_bundle_path(p) };
653
+ if std::path::Path::new(&full).exists() { 1.0 } else { 0.0 }
654
+ }
655
+
656
+ #[no_mangle]
657
+ pub extern "C" fn bloom_read_file(path: i64) -> i64 {
658
+ let p = perry_str(path);
659
+ if p.is_empty() {
660
+ return alloc_perry_string("");
661
+ }
662
+ // Resolve bundle-relative paths the same way bloom_file_exists / textures do.
663
+ let full = if p.starts_with('/') {
664
+ p.to_string()
665
+ } else {
666
+ textures::resolve_bundle_path(p)
667
+ };
668
+ // Always hand back a valid StringHeader — empty on a miss, never null.
669
+ match std::fs::read_to_string(&full) {
670
+ Ok(contents) => alloc_perry_string(&contents),
671
+ Err(_) => alloc_perry_string(""),
672
+ }
673
+ }
674
+
675
+ #[no_mangle]
676
+ pub extern "C" fn bloom_write_file(_path: i64, _data: i64) -> f64 {
677
+ 0.0 // watchOS app sandbox — defer real write support.
678
+ }
679
+
680
+ // ============================================================
681
+ // 3D immediate-mode primitives
682
+ // ============================================================
683
+
684
+ #[no_mangle]
685
+ pub extern "C" fn bloom_begin_mode_3d(
686
+ px: f64, py: f64, pz: f64,
687
+ tx: f64, ty: f64, tz: f64,
688
+ ux: f64, uy: f64, uz: f64,
689
+ fovy: f64, proj: f64,
690
+ ) {
691
+ draw_list::set_camera(px, py, pz, tx, ty, tz, ux, uy, uz, fovy, proj);
692
+ }
693
+
694
+ #[no_mangle]
695
+ pub extern "C" fn bloom_end_mode_3d() {
696
+ // No-op: 3D commands are already in the list; the camera stays set until
697
+ // the next begin_mode_3d. Swift's SceneView draws 3D cmds under the
698
+ // Canvas layer each frame.
699
+ }
700
+
701
+ #[no_mangle]
702
+ pub extern "C" fn bloom_draw_cube(x: f64, y: f64, z: f64, w: f64, h: f64, d: f64,
703
+ r: f64, g: f64, b: f64, a: f64,
704
+ ) {
705
+ let mut c = DrawCmd::zero();
706
+ c.kind = kind::CUBE;
707
+ c.x = x; c.y = y; c.src_x = z; // src_x doubles as z on 3D cmds
708
+ c.w = w; c.h = h; c.size = d;
709
+ c.r = r; c.g = g; c.b = b; c.a = a;
710
+ draw_list::push(c);
711
+ }
712
+
713
+ #[no_mangle]
714
+ pub extern "C" fn bloom_draw_cube_wires(x: f64, y: f64, z: f64, w: f64, h: f64, d: f64,
715
+ r: f64, g: f64, b: f64, a: f64,
716
+ ) {
717
+ let mut c = DrawCmd::zero();
718
+ c.kind = kind::CUBE_WIRES;
719
+ c.x = x; c.y = y; c.src_x = z; // src_x doubles as z on 3D cmds
720
+ c.w = w; c.h = h; c.size = d;
721
+ c.r = r; c.g = g; c.b = b; c.a = a;
722
+ draw_list::push(c);
723
+ }
724
+
725
+ #[no_mangle]
726
+ pub extern "C" fn bloom_draw_sphere(x: f64, y: f64, z: f64, radius: f64,
727
+ r: f64, g: f64, b: f64, a: f64,
728
+ ) {
729
+ let mut c = DrawCmd::zero();
730
+ c.kind = kind::SPHERE;
731
+ c.x = x; c.y = y; c.src_x = z; // src_x doubles as z on 3D cmds
732
+ c.w = radius;
733
+ c.r = r; c.g = g; c.b = b; c.a = a;
734
+ draw_list::push(c);
735
+ }
736
+
737
+ #[no_mangle]
738
+ pub extern "C" fn bloom_draw_sphere_wires(x: f64, y: f64, z: f64, radius: f64,
739
+ r: f64, g: f64, b: f64, a: f64,
740
+ ) {
741
+ let mut c = DrawCmd::zero();
742
+ c.kind = kind::SPHERE_WIRES;
743
+ c.x = x; c.y = y; c.src_x = z; // src_x doubles as z on 3D cmds
744
+ c.w = radius;
745
+ c.r = r; c.g = g; c.b = b; c.a = a;
746
+ draw_list::push(c);
747
+ }
748
+
749
+ #[no_mangle]
750
+ pub extern "C" fn bloom_draw_cylinder(x: f64, y: f64, z: f64,
751
+ radius_top: f64, _radius_bottom: f64, height: f64,
752
+ r: f64, g: f64, b: f64, a: f64,
753
+ ) {
754
+ // ABI is 10 params (no slices) — that's bloom_draw_cylinder_ex. The
755
+ // phantom `slices` param shifted every color one slot right.
756
+ let mut c = DrawCmd::zero();
757
+ c.kind = kind::CYLINDER;
758
+ c.x = x; c.y = y; c.src_x = z; // src_x doubles as z on 3D cmds
759
+ c.w = radius_top; c.h = height;
760
+ c.r = r; c.g = g; c.b = b; c.a = a;
761
+ draw_list::push(c);
762
+ }
763
+
764
+ #[no_mangle]
765
+ pub extern "C" fn bloom_draw_plane(x: f64, y: f64, z: f64, w: f64, h: f64,
766
+ r: f64, g: f64, b: f64, a: f64,
767
+ ) {
768
+ let mut c = DrawCmd::zero();
769
+ c.kind = kind::PLANE;
770
+ c.x = x; c.y = y; c.src_x = z; // src_x doubles as z on 3D cmds
771
+ c.w = w; c.h = h;
772
+ c.r = r; c.g = g; c.b = b; c.a = a;
773
+ draw_list::push(c);
774
+ }
775
+
776
+ // ============================================================
777
+ // Audio (forwards to Swift BloomAudioManager)
778
+ // ============================================================
779
+
780
+ #[no_mangle] pub extern "C" fn bloom_init_audio() { audio::init_audio(); }
781
+ #[no_mangle] pub extern "C" fn bloom_close_audio() { audio::close_audio(); }
782
+
783
+ #[no_mangle]
784
+ pub extern "C" fn bloom_load_sound(path: i64) -> f64 {
785
+ audio::load_sound(perry_str(path)) as f64
786
+ }
787
+ #[no_mangle] pub extern "C" fn bloom_play_sound(handle: f64) { audio::play_sound(handle as u32); }
788
+ #[no_mangle] pub extern "C" fn bloom_stop_sound(handle: f64) { audio::stop_sound(handle as u32); }
789
+ #[no_mangle] pub extern "C" fn bloom_set_sound_volume(handle: f64, v: f64) {
790
+ audio::set_sound_volume(handle as u32, v as f32);
791
+ }
792
+ #[no_mangle] pub extern "C" fn bloom_set_master_volume(v: f64) {
793
+ audio::set_master_volume(v as f32);
794
+ }
795
+ #[no_mangle] pub extern "C" fn bloom_play_sound_3d(handle: f64, _x: f64, _y: f64, _z: f64) {
796
+ // No spatial audio on watch — play at full 2D volume.
797
+ audio::play_sound(handle as u32);
798
+ }
799
+
800
+ #[no_mangle]
801
+ pub extern "C" fn bloom_load_music(path: i64) -> f64 {
802
+ audio::load_music(perry_str(path)) as f64
803
+ }
804
+ #[no_mangle] pub extern "C" fn bloom_play_music(handle: f64) { audio::play_music(handle as u32); }
805
+ #[no_mangle] pub extern "C" fn bloom_stop_music(handle: f64) { audio::stop_music(handle as u32); }
806
+ #[no_mangle] pub extern "C" fn bloom_set_music_volume(handle: f64, v: f64) {
807
+ audio::set_music_volume(handle as u32, v as f32);
808
+ }
809
+ #[no_mangle] pub extern "C" fn bloom_is_music_playing(handle: f64) -> f64 {
810
+ if audio::is_music_playing(handle as u32) { 1.0 } else { 0.0 }
811
+ }
812
+ #[no_mangle] pub extern "C" fn bloom_update_music_stream(_handle: f64) {
813
+ // AVAudioPlayer owns stream pumping internally — no per-frame poke needed.
814
+ }
815
+
816
+ // ============================================================
817
+ // Retained scene graph (bloom_scene_*) — synced to SCNNodes in Swift
818
+ // ============================================================
819
+
820
+ #[no_mangle] pub extern "C" fn bloom_scene_create_node() -> f64 { scene::create() as f64 }
821
+ #[no_mangle] pub extern "C" fn bloom_scene_destroy_node(h: f64) { scene::destroy(h as u32); }
822
+ #[no_mangle] pub extern "C" fn bloom_scene_set_visible(h: f64, v: f64) {
823
+ scene::set_visible(h as u32, v > 0.5);
824
+ }
825
+ #[no_mangle] pub extern "C" fn bloom_scene_set_cast_shadow(_h: f64, _v: f64) {
826
+ // SceneKit manages shadow casting via per-node + light config; deferred.
827
+ }
828
+ #[no_mangle] pub extern "C" fn bloom_scene_set_receive_shadow(_h: f64, _v: f64) {}
829
+ #[no_mangle] pub extern "C" fn bloom_scene_set_parent(h: f64, parent: f64) {
830
+ scene::set_parent(h as u32, parent as u32);
831
+ }
832
+
833
+ /// Transform is a raw pointer to 16 column-major f64s (128 bytes). No
834
+ /// StringHeader wrapping — bloom's TS side passes a `number[]` which Perry
835
+ /// forwards as a pointer to the Float64 data.
836
+ #[no_mangle]
837
+ pub extern "C" fn bloom_scene_set_transform(handle: f64, matrix_ptr: i64) {
838
+ if matrix_ptr == 0 { return; }
839
+ let src = unsafe { std::slice::from_raw_parts(matrix_ptr as *const f64, 16) };
840
+ let mut arr = [0.0f32; 16];
841
+ for i in 0..16 { arr[i] = src[i] as f32; }
842
+ scene::set_transform(handle as u32, arr);
843
+ }
844
+
845
+ /// Geometry: vert_ptr points to `vertex_count * 12` f64s (12 floats per
846
+ /// vertex: xyz, nx ny nz, rgba, uv). idx_ptr points to `index_count` f64s
847
+ /// (each integer index boxed as f64 — matches bloom's TS `number[]` calls).
848
+ #[no_mangle]
849
+ pub extern "C" fn bloom_scene_update_geometry(
850
+ handle: f64, verts_ptr: i64, vertex_count: f64,
851
+ idx_ptr: i64, index_count: f64,
852
+ ) {
853
+ if verts_ptr == 0 || idx_ptr == 0 { return; }
854
+ let vcount = vertex_count as usize;
855
+ let icount = index_count as usize;
856
+ let verts = unsafe { std::slice::from_raw_parts(verts_ptr as *const f64, vcount * 12) };
857
+ let idx_f64 = unsafe { std::slice::from_raw_parts(idx_ptr as *const f64, icount) };
858
+ scene::update_geometry_f64(handle as u32, verts, idx_f64);
859
+ }
860
+
861
+ #[no_mangle]
862
+ pub extern "C" fn bloom_scene_set_material_color(h: f64, r: f64, g: f64, b: f64, a: f64) {
863
+ scene::set_color(h as u32, [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a as f32 / 255.0]);
864
+ }
865
+ #[no_mangle]
866
+ pub extern "C" fn bloom_scene_set_material_pbr(h: f64, roughness: f64, metalness: f64) {
867
+ scene::set_pbr(h as u32, roughness as f32, metalness as f32);
868
+ }
869
+ #[no_mangle]
870
+ pub extern "C" fn bloom_scene_set_material_texture(h: f64, tex: f64) {
871
+ scene::set_texture(h as u32, tex as u32);
872
+ }
873
+ #[no_mangle] pub extern "C" fn bloom_scene_node_count() -> f64 { scene::node_count() as f64 }
874
+
875
+ #[no_mangle]
876
+ pub extern "C" fn bloom_scene_get_transform(handle: f64, out: f64) -> f64 {
877
+ // `out` is a pointer to 16 f32s the caller has allocated. Write them.
878
+ if out == 0.0 { return 0.0; }
879
+ let t = scene::get_transform(handle as u32);
880
+ unsafe {
881
+ std::ptr::copy_nonoverlapping(t.as_ptr(), out as usize as *mut f32, 16);
882
+ }
883
+ 1.0
884
+ }
885
+
886
+ // ============================================================
887
+ // Models — glTF (.glb) loading + scene attachment
888
+ // ============================================================
889
+
890
+ #[no_mangle]
891
+ pub extern "C" fn bloom_load_model(path: i64) -> f64 {
892
+ let p = perry_str(path);
893
+ if p.is_empty() { return 0.0; }
894
+ let full = if p.starts_with('/') { p.to_string() } else { textures::resolve_bundle_path(p) };
895
+ models::load(&full) as f64
896
+ }
897
+
898
+ #[no_mangle]
899
+ pub extern "C" fn bloom_gen_mesh_cube(w: f64, h: f64, d: f64) -> f64 {
900
+ models::gen_cube_mesh(w as f32, h as f32, d as f32) as f64
901
+ }
902
+
903
+ /// Attach a loaded model to a scene node. If the model has a glTF scene
904
+ /// hierarchy, the full subtree is spawned as bloom scene-node descendants of
905
+ /// `node_handle` with each glTF node's TRS transform baked in. For
906
+ /// single-node models this collapses to the pre-#15 behavior (the target
907
+ /// node gets the one mesh's first primitive, sibling primitives become its
908
+ /// children).
909
+ ///
910
+ /// The `mesh_idx` parameter is only honored when the model has no scene
911
+ /// hierarchy — it picks which mesh to instantiate into `node_handle`.
912
+ #[no_mangle]
913
+ pub extern "C" fn bloom_scene_attach_model(node_handle: f64, model_handle: f64, mesh_idx: f64) {
914
+ let Some(model) = models::get(model_handle as u32) else { return; };
915
+ let root = node_handle as u32;
916
+
917
+ // If the model has a scene graph with non-trivial hierarchy (>1 node or a
918
+ // transform on the root), walk it. Otherwise fall back to single-mesh
919
+ // mode so mesh_idx still picks which mesh to attach.
920
+ let single_mesh = model.nodes.len() <= 1
921
+ && model.scene_roots.len() <= 1
922
+ && model.nodes.first().map(|n| n.matrix.is_none()
923
+ && n.translation == [0.0; 3]
924
+ && n.rotation == [0.0, 0.0, 0.0, 1.0]
925
+ && n.scale == [1.0; 3]).unwrap_or(true);
926
+
927
+ if single_mesh {
928
+ let mesh_i = mesh_idx as usize;
929
+ let Some(mesh) = model.meshes.get(mesh_i) else { return; };
930
+ attach_mesh(root, mesh);
931
+ return;
932
+ }
933
+
934
+ // Scene-hierarchy path: one bloom scene node per glTF node. The attach
935
+ // target `root` itself doesn't gain geometry — it's the hierarchy's
936
+ // anchor. Children materialize via scene::create + scene::set_parent.
937
+ // We build a gltf_node_idx → bloom_handle map during the walk, then
938
+ // resolve skin joint references once everyone's in place.
939
+ let mut gltf_to_bloom: std::collections::HashMap<usize, u32> = std::collections::HashMap::new();
940
+ let mut skinned_nodes: Vec<(u32, usize)> = Vec::new(); // (bloom_handle, gltf_node_idx)
941
+ for scene_root in &model.scene_roots {
942
+ spawn_node_subtree(&model, *scene_root, root, &mut gltf_to_bloom, &mut skinned_nodes);
943
+ }
944
+ // Second pass: attach skin data to each skinned mesh-node. Joints are
945
+ // resolved to the bloom handles we just created.
946
+ for (bloom_handle, gltf_idx) in skinned_nodes {
947
+ let gltf_node = &model.nodes[gltf_idx];
948
+ let Some(skin_idx) = gltf_node.skin else { continue };
949
+ let Some(skin) = model.skins.get(skin_idx) else { continue };
950
+ let Some(mesh_idx) = gltf_node.mesh else { continue };
951
+ let Some(mesh) = model.meshes.get(mesh_idx) else { continue };
952
+ let Some(prim) = mesh.primitives.first() else { continue };
953
+
954
+ let joint_handles: Vec<u32> = skin.joints.iter()
955
+ .map(|&gi| *gltf_to_bloom.get(&gi).unwrap_or(&0))
956
+ .collect();
957
+ scene::set_skin(bloom_handle,
958
+ joint_handles,
959
+ skin.inverse_bind_matrices.clone(),
960
+ prim.joint_indices.clone(),
961
+ prim.weights.clone(),
962
+ );
963
+
964
+ // Animation tracks — resolve channel targets to bloom handles.
965
+ // Auto-select animation 0 for now (typically the idle/survey clip).
966
+ // Follow-up: bloom_update_model_animation to switch between them.
967
+ if let Some(anim) = model.animations.first() {
968
+ let mut tracks: Vec<scene::AnimTrack> = Vec::new();
969
+ for ch in &anim.channels {
970
+ let Some(&bone_h) = gltf_to_bloom.get(&ch.target_node) else { continue };
971
+ if bone_h == 0 { continue; }
972
+ tracks.push(scene::AnimTrack {
973
+ bone_handle: bone_h,
974
+ path: ch.path as u32,
975
+ times: ch.times.clone(),
976
+ values: ch.values.clone(),
977
+ });
978
+ }
979
+ if !tracks.is_empty() {
980
+ scene::set_anim_tracks(bloom_handle, tracks);
981
+ }
982
+ }
983
+ }
984
+ }
985
+
986
+ fn attach_mesh(handle: u32, mesh: &models::Mesh) {
987
+ if mesh.primitives.is_empty() { return; }
988
+ apply_primitive(handle, &mesh.primitives[0]);
989
+ for prim in mesh.primitives.iter().skip(1) {
990
+ let child = scene::create();
991
+ scene::set_parent(child, handle);
992
+ apply_primitive(child, prim);
993
+ }
994
+ }
995
+
996
+ fn spawn_node_subtree(
997
+ model: &models::Model, node_idx: usize, parent: u32,
998
+ gltf_to_bloom: &mut std::collections::HashMap<usize, u32>,
999
+ skinned_nodes: &mut Vec<(u32, usize)>,
1000
+ ) {
1001
+ let Some(gltf_node) = model.nodes.get(node_idx) else { return; };
1002
+ let bloom_node = scene::create();
1003
+ gltf_to_bloom.insert(node_idx, bloom_node);
1004
+ scene::set_parent(bloom_node, parent);
1005
+ scene::set_transform(bloom_node, gltf_node.local_transform());
1006
+ if let Some(mi) = gltf_node.mesh {
1007
+ if let Some(mesh) = model.meshes.get(mi) {
1008
+ attach_mesh(bloom_node, mesh);
1009
+ if gltf_node.skin.is_some() {
1010
+ skinned_nodes.push((bloom_node, node_idx));
1011
+ }
1012
+ }
1013
+ }
1014
+ for &child_idx in &gltf_node.children {
1015
+ spawn_node_subtree(model, child_idx, bloom_node, gltf_to_bloom, skinned_nodes);
1016
+ }
1017
+ }
1018
+
1019
+ fn apply_primitive(handle: u32, prim: &models::Primitive) {
1020
+ scene::set_geometry(handle,
1021
+ prim.positions.clone(), prim.normals.clone(),
1022
+ prim.uvs.clone(), prim.indices.clone());
1023
+ scene::set_color(handle, prim.color);
1024
+ scene::set_pbr(handle, prim.roughness, prim.metallic);
1025
+ scene::set_pbr_textures(handle,
1026
+ prim.tex_base_color, prim.tex_normal,
1027
+ prim.tex_metallic_roughness, prim.tex_emissive, prim.tex_occlusion);
1028
+ }
1029
+
1030
+ // ============================================================
1031
+ // Post-processing — stored for Swift to translate into SwiftUI modifiers
1032
+ // ============================================================
1033
+
1034
+ #[no_mangle] pub extern "C" fn bloom_enable_postfx() { postfx::set_enabled(true); }
1035
+ #[no_mangle] pub extern "C" fn bloom_disable_postfx() { postfx::set_enabled(false); }
1036
+ #[no_mangle] pub extern "C" fn bloom_set_vignette(strength: f64, softness: f64) {
1037
+ postfx::set_vignette(strength, softness);
1038
+ }
1039
+ #[no_mangle] pub extern "C" fn bloom_set_chromatic_aberration(strength: f64) {
1040
+ postfx::set_chromatic_aberration(strength);
1041
+ }
1042
+ #[no_mangle] pub extern "C" fn bloom_set_film_grain(strength: f64) {
1043
+ postfx::set_film_grain(strength);
1044
+ }
1045
+ #[no_mangle] pub extern "C" fn bloom_set_manual_exposure(v: f64) { postfx::set_exposure(v); }
1046
+ #[no_mangle] pub extern "C" fn bloom_set_auto_exposure(on: f64) {
1047
+ postfx::set_auto_exposure(on > 0.5);
1048
+ }
1049
+ #[no_mangle] pub extern "C" fn bloom_set_sun_shafts(
1050
+ strength: f64, decay: f64, r: f64, g: f64, b: f64,
1051
+ ) {
1052
+ postfx::set_sun_shafts(strength, decay, r, g, b);
1053
+ }
1054
+
1055
+ #[no_mangle]
1056
+ pub extern "C" fn bloom_watchos_postfx_state(out: *mut postfx::PostFxState) {
1057
+ postfx::snapshot(out);
1058
+ }
1059
+
1060
+ #[no_mangle] pub extern "C" fn bloom_add_directional_light(
1061
+ dx: f64, dy: f64, dz: f64, r: f64, g: f64, b: f64, intensity: f64,
1062
+ ) {
1063
+ scene::add_directional_light(dx as f32, dy as f32, dz as f32,
1064
+ r as f32, g as f32, b as f32, intensity as f32);
1065
+ }
1066
+
1067
+ #[no_mangle] pub extern "C" fn bloom_add_point_light(
1068
+ x: f64, y: f64, z: f64, range: f64, r: f64, g: f64, b: f64, intensity: f64,
1069
+ ) {
1070
+ scene::add_point_light(x as f32, y as f32, z as f32, range as f32,
1071
+ r as f32, g as f32, b as f32, intensity as f32);
1072
+ }
1073
+
1074
+ // ============================================================
1075
+ // Scene snapshot accessors for Swift
1076
+ // ============================================================
1077
+
1078
+ #[no_mangle]
1079
+ pub extern "C" fn bloom_watchos_scene_drain_dirty(dst: *mut scene::SceneNodeInfo, max: i64) -> i64 {
1080
+ scene::drain_dirty(dst, max)
1081
+ }
1082
+
1083
+ #[no_mangle]
1084
+ pub extern "C" fn bloom_watchos_scene_drain_destroyed(dst: *mut u32, max: i64) -> i64 {
1085
+ scene::drain_destroyed(dst, max)
1086
+ }
1087
+
1088
+ #[no_mangle]
1089
+ pub extern "C" fn bloom_watchos_scene_copy_lights(dst: *mut scene::Light, max: i64) -> i64 {
1090
+ scene::copy_lights(dst, max)
1091
+ }
1092
+
1093
+ #[no_mangle]
1094
+ pub extern "C" fn bloom_watchos_scene_geometry(handle: u32, out: *mut scene::GeometryPtrs) {
1095
+ if out.is_null() { return; }
1096
+ unsafe { *out = scene::geometry_ptrs(handle); }
1097
+ }
1098
+
1099
+ #[no_mangle]
1100
+ pub extern "C" fn bloom_watchos_scene_skin(handle: u32, out: *mut scene::SkinPtrs) {
1101
+ if out.is_null() { return; }
1102
+ unsafe { *out = scene::skin_ptrs(handle); }
1103
+ }
1104
+
1105
+ #[no_mangle]
1106
+ pub extern "C" fn bloom_watchos_scene_anim_track_count(handle: u32) -> i64 {
1107
+ scene::anim_track_count(handle)
1108
+ }
1109
+
1110
+ #[no_mangle]
1111
+ pub extern "C" fn bloom_watchos_scene_anim_track_info(handle: u32, idx: i64, out: *mut scene::AnimTrackInfo) {
1112
+ scene::anim_track_info(handle, idx, out);
1113
+ }
1114
+
1115
+ #[no_mangle]
1116
+ pub extern "C" fn bloom_draw_grid(slices: f64, spacing: f64) {
1117
+ let mut c = DrawCmd::zero();
1118
+ c.kind = kind::GRID;
1119
+ c.w = slices;
1120
+ c.h = spacing;
1121
+ // Grid color — light gray, matches bloom's default.
1122
+ c.r = 200.0; c.g = 200.0; c.b = 200.0; c.a = 255.0;
1123
+ draw_list::push(c);
1124
+ }