@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,1154 @@
1
+ //! 3D model + mesh surface (gated on the `models3d` cargo feature).
2
+ //!
3
+ //! Section of [`define_core_ffi!`](crate::define_core_ffi) — see
4
+ //! `ffi_core/mod.rs` for the architecture and the invoking-crate contract.
5
+ //! Internal: platform crates must invoke `define_core_ffi!()`, never the
6
+ //! section macros directly.
7
+
8
+ #[doc(hidden)]
9
+ #[macro_export]
10
+ macro_rules! __bloom_ffi_models {
11
+ () => {
12
+
13
+ // bloom_load_model [source: curated; gated: models3d]
14
+ #[cfg(feature = "models3d")]
15
+ #[no_mangle]
16
+ pub extern "C" fn bloom_load_model(path_ptr: *const u8) -> f64 {
17
+ $crate::ffi::guard("bloom_load_model", move || {
18
+ let path = $crate::string_header::str_from_header(path_ptr);
19
+ let path = bloom_resolve_asset_path(path);
20
+ match std::fs::read(path.as_ref()) {
21
+ Ok(data) => {
22
+ let mut eng = engine();
23
+ let $crate::engine::EngineState { models, renderer, .. } = &mut *eng;
24
+ models.load_model_with_textures(&data, renderer)
25
+ }
26
+ Err(_) => 0.0,
27
+ }
28
+ })
29
+ }
30
+ #[cfg(not(feature = "models3d"))]
31
+ #[no_mangle]
32
+ pub extern "C" fn bloom_load_model(_path_ptr: *const u8) -> f64 {
33
+ $crate::ffi::feature_off_warn_once("bloom_load_model", "models3d");
34
+ 0.0
35
+ }
36
+
37
+ // bloom_draw_model [source: linux; gated: models3d]
38
+ #[cfg(feature = "models3d")]
39
+ #[no_mangle]
40
+ pub extern "C" fn bloom_draw_model(handle: f64, x: f64, y: f64, z: f64, scale: f64, r: f64, g: f64, b: f64, a: f64) {
41
+ $crate::ffi::guard("bloom_draw_model", move || {
42
+ let mut eng = engine();
43
+ let $crate::engine::EngineState { models, renderer, .. } = &mut *eng;
44
+ if let Some(model) = models.get(handle) {
45
+ let tint = [(r / 255.0) as f32, (g / 255.0) as f32, (b / 255.0) as f32, (a / 255.0) as f32];
46
+ let position = [x as f32, y as f32, z as f32];
47
+ let handle_bits = handle.to_bits();
48
+ if renderer.cache_model_if_static(handle_bits, &model.meshes) {
49
+ // Skinned models cache too now (bind-pose VB with raw
50
+ // joint indices, skinned in the scene VS) — routed to
51
+ // the skinned cached draw, which pops the staged pose
52
+ // ONCE for the whole model and shares the offset
53
+ // across every primitive (per-primitive pops starved
54
+ // multi-primitive models onto joint offset 0 —
55
+ // another model's matrices).
56
+ if renderer.is_model_skinned(handle_bits) {
57
+ renderer.draw_model_cached_skinned(handle_bits, position, scale as f32, tint);
58
+ } else {
59
+ renderer.draw_model_cached(handle_bits, position, scale as f32, tint);
60
+ }
61
+ }
62
+ }
63
+ })
64
+ }
65
+ #[cfg(not(feature = "models3d"))]
66
+ #[no_mangle]
67
+ pub extern "C" fn bloom_draw_model(_handle: f64, _x: f64, _y: f64, _z: f64, _scale: f64, _r: f64, _g: f64, _b: f64, _a: f64) {
68
+ $crate::ffi::feature_off_warn_once("bloom_draw_model", "models3d");
69
+ }
70
+
71
+ // bloom_draw_model_rotated [source: linux; gated: models3d]
72
+ #[cfg(feature = "models3d")]
73
+ #[no_mangle]
74
+ pub extern "C" fn bloom_draw_model_rotated(
75
+ handle: f64, x: f64, y: f64, z: f64,
76
+ scale: f64, rot_y: f64,
77
+ color_packed_argb: f64,
78
+ ) {
79
+ $crate::ffi::guard("bloom_draw_model_rotated", move || {
80
+ let bits = color_packed_argb as u32;
81
+ let a = ((bits >> 24) & 0xff) as f32 / 255.0;
82
+ let r = ((bits >> 16) & 0xff) as f32 / 255.0;
83
+ let g = ((bits >> 8) & 0xff) as f32 / 255.0;
84
+ let b = ( bits & 0xff) as f32 / 255.0;
85
+ let mut eng = engine();
86
+ let $crate::engine::EngineState { models, renderer, .. } = &mut *eng;
87
+ if let Some(model) = models.get(handle) {
88
+ let position = [x as f32, y as f32, z as f32];
89
+ let scale = scale as f32;
90
+ let tint = [r, g, b, a];
91
+ let handle_bits = handle.to_bits();
92
+ // Static models go through the cached scene pipeline
93
+ // (alpha cutout, normal/MR maps, foliage wind +
94
+ // transmission, cutout shadows, planar reflections) —
95
+ // the old immediate path had none of that and used
96
+ // to render cutout foliage as opaque cards. Skinned
97
+ // models take the skinned cached draw and IGNORE the
98
+ // rotation — their joint matrices bake orientation,
99
+ // exactly as the old immediate fallback behaved.
100
+ if renderer.cache_model_if_static(handle_bits, &model.meshes) {
101
+ if renderer.is_model_skinned(handle_bits) {
102
+ renderer.draw_model_cached_skinned(
103
+ handle_bits, position, scale, tint,
104
+ );
105
+ } else {
106
+ renderer.draw_model_cached_rotated(
107
+ handle_bits, position, scale, rot_y as f32, tint,
108
+ );
109
+ }
110
+ }
111
+ }
112
+ })
113
+ }
114
+ #[cfg(not(feature = "models3d"))]
115
+ #[no_mangle]
116
+ pub extern "C" fn bloom_draw_model_rotated(_handle: f64, _x: f64, _y: f64, _z: f64, _scale: f64, _rot_y: f64, _color_packed_argb: f64) {
117
+ $crate::ffi::feature_off_warn_once("bloom_draw_model_rotated", "models3d");
118
+ }
119
+
120
+ // bloom_draw_model_transform16 — EN-039. [gated: models3d]
121
+ //
122
+ // The immediate-mode twin of bloom_scene_set_transform16: a full
123
+ // column-major 4x4, so an immediate draw can finally pitch and roll.
124
+ // bloom_draw_model_rotated only takes a Y rotation, which is why the
125
+ // shooter's gun cannot tilt with the aim.
126
+ //
127
+ // The ticket proposed routing this through the mesh scratch on the
128
+ // grounds that "Perry can't pass 16 f64 args". It can:
129
+ // bloom_scene_set_transform16 already passes 17 and works. Spelling the
130
+ // matrix out keeps this STATELESS — no scratch to reset, no ordering
131
+ // hazard between a reset and a draw — which is the same reasoning
132
+ // recorded on the scene twin.
133
+ //
134
+ // SKINNED models are rejected rather than silently mis-drawn: their
135
+ // joint matrices already bake world orientation, so a caller-supplied
136
+ // model matrix would double-transform them. bloom_draw_model_rotated
137
+ // has the same carve-out (it ignores rot_y for skinned).
138
+ #[cfg(feature = "models3d")]
139
+ #[no_mangle]
140
+ #[allow(clippy::too_many_arguments)]
141
+ pub extern "C" fn bloom_draw_model_transform16(
142
+ handle: f64,
143
+ m0: f64, m1: f64, m2: f64, m3: f64,
144
+ m4: f64, m5: f64, m6: f64, m7: f64,
145
+ m8: f64, m9: f64, m10: f64, m11: f64,
146
+ m12: f64, m13: f64, m14: f64, m15: f64,
147
+ color_packed_argb: f64,
148
+ ) {
149
+ $crate::ffi::guard("bloom_draw_model_transform16", move || {
150
+ let bits = color_packed_argb as u32;
151
+ let a = ((bits >> 24) & 0xff) as f32 / 255.0;
152
+ let r = ((bits >> 16) & 0xff) as f32 / 255.0;
153
+ let g = ((bits >> 8) & 0xff) as f32 / 255.0;
154
+ let b = ( bits & 0xff) as f32 / 255.0;
155
+ let s = [
156
+ m0, m1, m2, m3,
157
+ m4, m5, m6, m7,
158
+ m8, m9, m10, m11,
159
+ m12, m13, m14, m15,
160
+ ];
161
+ let mut mat = [[0.0f32; 4]; 4];
162
+ for col in 0..4 {
163
+ for row in 0..4 {
164
+ mat[col][row] = s[col * 4 + row] as f32;
165
+ }
166
+ }
167
+ let mut eng = engine();
168
+ let $crate::engine::EngineState { models, renderer, .. } = &mut *eng;
169
+ if let Some(model) = models.get(handle) {
170
+ let handle_bits = handle.to_bits();
171
+ if renderer.cache_model_if_static(handle_bits, &model.meshes) {
172
+ if renderer.is_model_skinned(handle_bits) {
173
+ return; // see the note above
174
+ }
175
+ renderer.draw_model_cached_transform(
176
+ handle_bits, mat, [r, g, b, a],
177
+ );
178
+ }
179
+ }
180
+ })
181
+ }
182
+ #[cfg(not(feature = "models3d"))]
183
+ #[no_mangle]
184
+ #[allow(clippy::too_many_arguments)]
185
+ pub extern "C" fn bloom_draw_model_transform16(
186
+ _handle: f64,
187
+ _m0: f64, _m1: f64, _m2: f64, _m3: f64,
188
+ _m4: f64, _m5: f64, _m6: f64, _m7: f64,
189
+ _m8: f64, _m9: f64, _m10: f64, _m11: f64,
190
+ _m12: f64, _m13: f64, _m14: f64, _m15: f64,
191
+ _color_packed_argb: f64,
192
+ ) {
193
+ $crate::ffi::feature_off_warn_once("bloom_draw_model_transform16", "models3d");
194
+ }
195
+
196
+ // bloom_unload_model [source: linux; gated: models3d]
197
+ #[cfg(feature = "models3d")]
198
+ #[no_mangle]
199
+ pub extern "C" fn bloom_unload_model(handle: f64) {
200
+ $crate::ffi::guard("bloom_unload_model", move || {
201
+ let mut eng = engine();
202
+ // Evict cached GPU meshes (keyed by handle bits) before the
203
+ // handle dies — without this the buffers leak and a slot-
204
+ // reusing future model would render the stale geometry.
205
+ eng.renderer.evict_model_cache(handle.to_bits());
206
+ eng.models.unload_model(handle);
207
+ })
208
+ }
209
+ #[cfg(not(feature = "models3d"))]
210
+ #[no_mangle]
211
+ pub extern "C" fn bloom_unload_model(_handle: f64) {
212
+ $crate::ffi::feature_off_warn_once("bloom_unload_model", "models3d");
213
+ }
214
+
215
+ // bloom_gen_mesh_cube [source: linux; gated: models3d]
216
+ #[cfg(feature = "models3d")]
217
+ #[no_mangle]
218
+ pub extern "C" fn bloom_gen_mesh_cube(w: f64, h: f64, d: f64) -> f64 {
219
+ $crate::ffi::guard("bloom_gen_mesh_cube", move || {
220
+ engine().models.gen_mesh_cube(w as f32, h as f32, d as f32)
221
+ })
222
+ }
223
+ #[cfg(not(feature = "models3d"))]
224
+ #[no_mangle]
225
+ pub extern "C" fn bloom_gen_mesh_cube(_w: f64, _h: f64, _d: f64) -> f64 {
226
+ $crate::ffi::feature_off_warn_once("bloom_gen_mesh_cube", "models3d");
227
+ 0.0
228
+ }
229
+
230
+ // bloom_gen_mesh_heightmap [source: linux; gated: models3d]
231
+ #[cfg(feature = "models3d")]
232
+ #[no_mangle]
233
+ pub extern "C" fn bloom_gen_mesh_heightmap(image_handle: f64, size_x: f64, size_y: f64, size_z: f64) -> f64 {
234
+ $crate::ffi::guard("bloom_gen_mesh_heightmap", move || {
235
+ let mut eng = engine();
236
+ if let Some(img) = eng.textures.images.get(image_handle) {
237
+ let data = img.data.clone();
238
+ let w = img.width;
239
+ let h = img.height;
240
+ eng.models.gen_mesh_heightmap(&data, w, h, size_x as f32, size_y as f32, size_z as f32)
241
+ } else {
242
+ 0.0
243
+ }
244
+ })
245
+ }
246
+ #[cfg(not(feature = "models3d"))]
247
+ #[no_mangle]
248
+ pub extern "C" fn bloom_gen_mesh_heightmap(_image_handle: f64, _size_x: f64, _size_y: f64, _size_z: f64) -> f64 {
249
+ $crate::ffi::feature_off_warn_once("bloom_gen_mesh_heightmap", "models3d");
250
+ 0.0
251
+ }
252
+
253
+ // bloom_compile_material [source: macos]
254
+ #[no_mangle]
255
+ pub extern "C" fn bloom_compile_material(source_ptr: *const u8) -> f64 {
256
+ $crate::ffi::guard("bloom_compile_material", move || {
257
+ let source = $crate::string_header::str_from_header(source_ptr);
258
+ match engine().renderer.compile_material(source) {
259
+ Ok(handle) => handle as f64,
260
+ Err(e) => {
261
+ eprintln!("[material] compile failed: {:?}", e);
262
+ 0.0
263
+ }
264
+ }
265
+ })
266
+ }
267
+
268
+ // bloom_compile_material_refractive [source: macos]
269
+ #[no_mangle]
270
+ pub extern "C" fn bloom_compile_material_refractive(source_ptr: *const u8) -> f64 {
271
+ $crate::ffi::guard("bloom_compile_material_refractive", move || {
272
+ use $crate::renderer::material_pipeline::{FragmentProfile, Bucket};
273
+ let source = $crate::string_header::str_from_header(source_ptr);
274
+ match engine().renderer.compile_material_with_options(
275
+ source, FragmentProfile::Translucent, Bucket::Refractive, true, false,
276
+ ) {
277
+ Ok(handle) => handle as f64,
278
+ Err(e) => { eprintln!("[refractive] compile failed: {:?}", e); 0.0 }
279
+ }
280
+ })
281
+ }
282
+
283
+ // bloom_compile_material_transparent [source: macos]
284
+ #[no_mangle]
285
+ pub extern "C" fn bloom_compile_material_transparent(source_ptr: *const u8) -> f64 {
286
+ $crate::ffi::guard("bloom_compile_material_transparent", move || {
287
+ use $crate::renderer::material_pipeline::{FragmentProfile, Bucket};
288
+ let source = $crate::string_header::str_from_header(source_ptr);
289
+ match engine().renderer.compile_material_with_options(
290
+ source, FragmentProfile::Translucent, Bucket::Transparent, false, false,
291
+ ) {
292
+ Ok(handle) => handle as f64,
293
+ Err(e) => { eprintln!("[material] compile failed: {:?}", e); 0.0 }
294
+ }
295
+ })
296
+ }
297
+
298
+ // bloom_compile_material_additive [source: macos]
299
+ #[no_mangle]
300
+ pub extern "C" fn bloom_compile_material_additive(source_ptr: *const u8) -> f64 {
301
+ $crate::ffi::guard("bloom_compile_material_additive", move || {
302
+ use $crate::renderer::material_pipeline::{FragmentProfile, Bucket};
303
+ let source = $crate::string_header::str_from_header(source_ptr);
304
+ match engine().renderer.compile_material_with_options(
305
+ source, FragmentProfile::Translucent, Bucket::Additive, false, false,
306
+ ) {
307
+ Ok(handle) => handle as f64,
308
+ Err(e) => { eprintln!("[material] compile failed: {:?}", e); 0.0 }
309
+ }
310
+ })
311
+ }
312
+
313
+ // bloom_compile_material_cutout [source: macos]
314
+ #[no_mangle]
315
+ pub extern "C" fn bloom_compile_material_cutout(source_ptr: *const u8) -> f64 {
316
+ $crate::ffi::guard("bloom_compile_material_cutout", move || {
317
+ use $crate::renderer::material_pipeline::{FragmentProfile, Bucket};
318
+ let source = $crate::string_header::str_from_header(source_ptr);
319
+ match engine().renderer.compile_material_with_options(
320
+ source, FragmentProfile::Opaque, Bucket::Cutout, false, false,
321
+ ) {
322
+ Ok(handle) => handle as f64,
323
+ Err(e) => { eprintln!("[material] compile failed: {:?}", e); 0.0 }
324
+ }
325
+ })
326
+ }
327
+
328
+ // bloom_compile_material_instanced [source: macos]
329
+ #[no_mangle]
330
+ pub extern "C" fn bloom_compile_material_instanced(source_ptr: *const u8) -> f64 {
331
+ $crate::ffi::guard("bloom_compile_material_instanced", move || {
332
+ let source = $crate::string_header::str_from_header(source_ptr);
333
+ match engine().renderer.compile_material_instanced(source) {
334
+ Ok(handle) => handle as f64,
335
+ Err(e) => { eprintln!("[material] instanced compile failed: {:?}", e); 0.0 }
336
+ }
337
+ })
338
+ }
339
+
340
+ // bloom_compile_material_instanced_bucket [EN-026/027]
341
+ // bucket: 0 = opaque, 1 = cutout, 2 = additive, 3 = transparent.
342
+ // reads_scene: bind the scene colour/depth snapshot (soft particles).
343
+ // The plain instanced compile above is hardcoded to opaque, which is
344
+ // right for grass and wrong for the two things that most want
345
+ // instancing: particles (additive) and decals (cutout).
346
+ #[no_mangle]
347
+ pub extern "C" fn bloom_compile_material_instanced_bucket(
348
+ source_ptr: *const u8, bucket: f64, reads_scene: f64,
349
+ ) -> f64 {
350
+ $crate::ffi::guard("bloom_compile_material_instanced_bucket", move || {
351
+ let source = $crate::string_header::str_from_header(source_ptr);
352
+ match engine().renderer.compile_material_instanced_bucket(
353
+ source, bucket as u32, reads_scene != 0.0)
354
+ {
355
+ Ok(handle) => handle as f64,
356
+ Err(e) => { eprintln!("[material] instanced compile failed: {:?}", e); 0.0 }
357
+ }
358
+ })
359
+ }
360
+
361
+ // bloom_submit_material_draw_instanced [source: linux; gated: models3d]
362
+ #[cfg(feature = "models3d")]
363
+ #[no_mangle]
364
+ pub extern "C" fn bloom_submit_material_draw_instanced(
365
+ material: f64, mesh_handle: f64, mesh_idx: f64,
366
+ instance_buffer: f64, instance_count: f64,
367
+ ) {
368
+ $crate::ffi::guard("bloom_submit_material_draw_instanced", move || {
369
+ let mut eng = engine();
370
+ let $crate::engine::EngineState { models, renderer, .. } = &mut *eng;
371
+ let handle_bits = mesh_handle.to_bits();
372
+ if let Some(model) = models.get(mesh_handle) {
373
+ renderer.cache_model_if_static(handle_bits, &model.meshes);
374
+ }
375
+ renderer.submit_material_draw_instanced(
376
+ material as u32,
377
+ handle_bits,
378
+ mesh_idx as usize,
379
+ instance_buffer as u32,
380
+ instance_count as u32,
381
+ );
382
+ })
383
+ }
384
+ #[cfg(not(feature = "models3d"))]
385
+ #[no_mangle]
386
+ pub extern "C" fn bloom_submit_material_draw_instanced(_material: f64, _mesh_handle: f64, _mesh_idx: f64, _instance_buffer: f64, _instance_count: f64) {
387
+ $crate::ffi::feature_off_warn_once("bloom_submit_material_draw_instanced", "models3d");
388
+ }
389
+
390
+ // bloom_draw_material [source: linux; gated: models3d]
391
+ #[cfg(feature = "models3d")]
392
+ #[no_mangle]
393
+ pub extern "C" fn bloom_draw_material(
394
+ material: f64,
395
+ mesh_handle: f64,
396
+ mesh_idx: f64,
397
+ x: f64, y: f64, z: f64, scale: f64,
398
+ r: f64, g: f64, b: f64, a: f64,
399
+ ) {
400
+ $crate::ffi::guard("bloom_draw_material", move || {
401
+ let mut eng = engine();
402
+ let $crate::engine::EngineState { models, renderer, .. } = &mut *eng;
403
+ let handle_bits = mesh_handle.to_bits();
404
+ if let Some(model) = models.get(mesh_handle) {
405
+ renderer.cache_model_if_static(handle_bits, &model.meshes);
406
+ }
407
+ renderer.submit_material_draw(
408
+ material as u32,
409
+ handle_bits,
410
+ mesh_idx as usize,
411
+ [x as f32, y as f32, z as f32],
412
+ scale as f32,
413
+ [(r / 255.0) as f32, (g / 255.0) as f32, (b / 255.0) as f32, (a / 255.0) as f32],
414
+ );
415
+ })
416
+ }
417
+ #[cfg(not(feature = "models3d"))]
418
+ #[no_mangle]
419
+ pub extern "C" fn bloom_draw_material(_material: f64, _mesh_handle: f64, _mesh_idx: f64, _x: f64, _y: f64, _z: f64, _scale: f64, _r: f64, _g: f64, _b: f64, _a: f64) {
420
+ $crate::ffi::feature_off_warn_once("bloom_draw_material", "models3d");
421
+ }
422
+
423
+ // bloom_load_model_animation [source: linux; gated: models3d]
424
+ #[cfg(feature = "models3d")]
425
+ #[no_mangle]
426
+ pub extern "C" fn bloom_load_model_animation(path_ptr: *const u8) -> f64 {
427
+ $crate::ffi::guard("bloom_load_model_animation", move || {
428
+ let path = $crate::string_header::str_from_header(path_ptr);
429
+ let path = bloom_resolve_asset_path(path);
430
+ match std::fs::read(path.as_ref()) {
431
+ Ok(data) => engine().models.load_model_animation(&data),
432
+ Err(_) => 0.0,
433
+ }
434
+ })
435
+ }
436
+ #[cfg(not(feature = "models3d"))]
437
+ #[no_mangle]
438
+ pub extern "C" fn bloom_load_model_animation(_path_ptr: *const u8) -> f64 {
439
+ $crate::ffi::feature_off_warn_once("bloom_load_model_animation", "models3d");
440
+ 0.0
441
+ }
442
+
443
+ // bloom_instantiate_animation [source: EN-055; gated: models3d]
444
+ // A fresh animation INSTANCE (own mixer/joint state) over an
445
+ // already-loaded clip set — clip data is Arc-shared, so N characters
446
+ // no longer cost N GLB re-parses at load.
447
+ #[cfg(feature = "models3d")]
448
+ #[no_mangle]
449
+ pub extern "C" fn bloom_instantiate_animation(src: f64) -> f64 {
450
+ $crate::ffi::guard("bloom_instantiate_animation", move || {
451
+ engine().models.instantiate_animation(src)
452
+ })
453
+ }
454
+ #[cfg(not(feature = "models3d"))]
455
+ #[no_mangle]
456
+ pub extern "C" fn bloom_instantiate_animation(_src: f64) -> f64 {
457
+ $crate::ffi::feature_off_warn_once("bloom_instantiate_animation", "models3d");
458
+ 0.0
459
+ }
460
+
461
+ // bloom_update_model_animation [source: linux; gated: models3d]
462
+ #[cfg(feature = "models3d")]
463
+ #[no_mangle]
464
+ pub extern "C" fn bloom_update_model_animation(handle: f64, anim_index: f64, time: f64, scale: f64, px: f64, py: f64, pz: f64, rot_y: f64) {
465
+ $crate::ffi::guard("bloom_update_model_animation", move || {
466
+ // Take a single Y-axis angle (radians) instead of sin/cos, so the
467
+ // engine reconstructs both with full precision + correct signs.
468
+ // Older callers that passed (rot_sin, rot_cos) hit a Perry-ARM64
469
+ // 9th-arg garbling bug AND a sqrt(1-sin²) workaround that lost
470
+ // the sign of cos — model rotation was correct only on half the
471
+ // circle. 8-arg signature dodges both issues. Matches macOS.
472
+ let rot_y_f = rot_y as f32;
473
+ let rot_sin = rot_y_f.sin();
474
+ let rot_cos = rot_y_f.cos();
475
+ let mut eng = engine();
476
+ let $crate::engine::EngineState { models, renderer, .. } = &mut *eng;
477
+ models.update_model_animation(handle, anim_index as usize, time as f32);
478
+ if let Some(anim) = models.get_animation(handle) {
479
+ if !anim.joint_matrices.is_empty() {
480
+ // PT-7: the anim handle keys the prev-palette
481
+ // pairing for skinned motion vectors.
482
+ renderer.set_joint_matrices_scaled(handle.to_bits(), &anim.joint_matrices, scale as f32, [px as f32, py as f32, pz as f32], rot_sin, rot_cos);
483
+ }
484
+ }
485
+ })
486
+ }
487
+ #[cfg(not(feature = "models3d"))]
488
+ #[no_mangle]
489
+ pub extern "C" fn bloom_update_model_animation(_handle: f64, _anim_index: f64, _time: f64, _scale: f64, _px: f64, _py: f64, _pz: f64, _rot_y: f64) {
490
+ $crate::ffi::feature_off_warn_once("bloom_update_model_animation", "models3d");
491
+ }
492
+
493
+ // bloom_create_mesh [source: curated; gated: models3d]
494
+ #[cfg(feature = "models3d")]
495
+ #[no_mangle]
496
+ pub extern "C" fn bloom_create_mesh(vertex_ptr: *const f64, vertex_count: f64, index_ptr: *const f64, index_count: f64) -> f64 {
497
+ $crate::ffi::guard("bloom_create_mesh", move || {
498
+ // Perry arrays are pointers to inline f64 data (8-byte ArrayHeader
499
+ // skipped by the compiler before the call) — see perry-codegen
500
+ // lower_call/extern_func.rs. Reading f32/u32 here was a latent
501
+ // misread on windows/android/ios/tvos before the FFI unification.
502
+ if vertex_ptr.is_null() || index_ptr.is_null() { return 0.0; }
503
+ let vcount = vertex_count as usize;
504
+ let icount = index_count as usize;
505
+ let vertex_f64 = unsafe { std::slice::from_raw_parts(vertex_ptr, vcount * 12) };
506
+ let index_f64 = unsafe { std::slice::from_raw_parts(index_ptr, icount) };
507
+ let vertices: Vec<f32> = vertex_f64.iter().map(|&v| v as f32).collect();
508
+ let indices: Vec<u32> = index_f64.iter().map(|&v| v as u32).collect();
509
+ engine().models.create_mesh(&vertices, &indices)
510
+ })
511
+ }
512
+ #[cfg(not(feature = "models3d"))]
513
+ #[no_mangle]
514
+ pub extern "C" fn bloom_create_mesh(_vertex_ptr: *const f64, _vertex_count: f64, _index_ptr: *const f64, _index_count: f64) -> f64 {
515
+ $crate::ffi::feature_off_warn_once("bloom_create_mesh", "models3d");
516
+ 0.0
517
+ }
518
+
519
+ // Array-free mesh upload (Perry 0.5.1171 rejects number[] -> i64 pointer
520
+ // params; see ModelManager::scratch_f32). Push the 12-float vertex
521
+ // records + u32 indices one scalar at a time (all-f64 ABI), then build.
522
+ #[cfg(feature = "models3d")]
523
+ #[no_mangle]
524
+ pub extern "C" fn bloom_mesh_scratch_reset() {
525
+ $crate::ffi::guard("bloom_mesh_scratch_reset", move || {
526
+ engine().models.mesh_scratch_reset();
527
+ })
528
+ }
529
+ #[cfg(not(feature = "models3d"))]
530
+ #[no_mangle]
531
+ pub extern "C" fn bloom_mesh_scratch_reset() {
532
+ $crate::ffi::feature_off_warn_once("bloom_mesh_scratch_reset", "models3d");
533
+ }
534
+
535
+ #[cfg(feature = "models3d")]
536
+ #[no_mangle]
537
+ pub extern "C" fn bloom_mesh_scratch_push_f32(v: f64) {
538
+ $crate::ffi::guard("bloom_mesh_scratch_push_f32", move || {
539
+ engine().models.mesh_scratch_push_f32(v as f32);
540
+ })
541
+ }
542
+ #[cfg(not(feature = "models3d"))]
543
+ #[no_mangle]
544
+ pub extern "C" fn bloom_mesh_scratch_push_f32(_v: f64) {
545
+ $crate::ffi::feature_off_warn_once("bloom_mesh_scratch_push_f32", "models3d");
546
+ }
547
+
548
+ #[cfg(feature = "models3d")]
549
+ #[no_mangle]
550
+ pub extern "C" fn bloom_mesh_scratch_push_u32(v: f64) {
551
+ $crate::ffi::guard("bloom_mesh_scratch_push_u32", move || {
552
+ engine().models.mesh_scratch_push_u32(v as u32);
553
+ })
554
+ }
555
+ #[cfg(not(feature = "models3d"))]
556
+ #[no_mangle]
557
+ pub extern "C" fn bloom_mesh_scratch_push_u32(_v: f64) {
558
+ $crate::ffi::feature_off_warn_once("bloom_mesh_scratch_push_u32", "models3d");
559
+ }
560
+
561
+ #[cfg(feature = "models3d")]
562
+ #[no_mangle]
563
+ pub extern "C" fn bloom_create_mesh_scratch(vertex_count: f64, index_count: f64) -> f64 {
564
+ $crate::ffi::guard("bloom_create_mesh_scratch", move || {
565
+ engine().models.create_mesh_from_scratch(vertex_count as u32, index_count as u32)
566
+ })
567
+ }
568
+ #[cfg(not(feature = "models3d"))]
569
+ #[no_mangle]
570
+ pub extern "C" fn bloom_create_mesh_scratch(_vertex_count: f64, _index_count: f64) -> f64 {
571
+ $crate::ffi::feature_off_warn_once("bloom_create_mesh_scratch", "models3d");
572
+ 0.0
573
+ }
574
+
575
+ // Array-free scene-geometry upload. Same motivation as the mesh scratch
576
+ // above: `bloom_scene_update_geometry` takes `i64` pointers, which
577
+ // Perry cannot produce from a `number[]`. Push the vertex floats and
578
+ // u32 indices through the mesh scratch, then re-upload them into an
579
+ // existing scene node (the editor's terrain re-meshes every brush
580
+ // stroke this way).
581
+ #[cfg(feature = "models3d")]
582
+ #[no_mangle]
583
+ pub extern "C" fn bloom_scene_update_geometry_scratch(
584
+ handle: f64,
585
+ vertex_count: f64,
586
+ index_count: f64,
587
+ ) {
588
+ $crate::ffi::guard("bloom_scene_update_geometry_scratch", move || {
589
+ let taken = engine()
590
+ .models
591
+ .take_scratch_geometry(vertex_count as u32, index_count as u32);
592
+ let (vert_floats, indices) = match taken {
593
+ Some(v) => v,
594
+ None => return,
595
+ };
596
+ let nv = vertex_count as usize;
597
+ let mut vertices = Vec::with_capacity(nv);
598
+ for i in 0..nv {
599
+ let base = i * 12;
600
+ vertices.push($crate::renderer::Vertex3D {
601
+ position: [vert_floats[base], vert_floats[base + 1], vert_floats[base + 2]],
602
+ normal: [vert_floats[base + 3], vert_floats[base + 4], vert_floats[base + 5]],
603
+ color: [
604
+ vert_floats[base + 6],
605
+ vert_floats[base + 7],
606
+ vert_floats[base + 8],
607
+ vert_floats[base + 9],
608
+ ],
609
+ uv: [vert_floats[base + 10], vert_floats[base + 11]],
610
+ joints: [0.0; 4],
611
+ weights: [0.0; 4],
612
+ tangent: [0.0; 4],
613
+ });
614
+ }
615
+ engine().scene.update_geometry(handle, vertices, indices);
616
+ })
617
+ }
618
+ #[cfg(not(feature = "models3d"))]
619
+ #[no_mangle]
620
+ pub extern "C" fn bloom_scene_update_geometry_scratch(
621
+ _handle: f64,
622
+ _vertex_count: f64,
623
+ _index_count: f64,
624
+ ) {
625
+ $crate::ffi::feature_off_warn_once("bloom_scene_update_geometry_scratch", "models3d");
626
+ }
627
+
628
+ // bloom_get_model_mesh_count [source: linux; gated: models3d]
629
+ #[cfg(feature = "models3d")]
630
+ #[no_mangle]
631
+ pub extern "C" fn bloom_get_model_mesh_count(handle: f64) -> f64 {
632
+ $crate::ffi::guard("bloom_get_model_mesh_count", move || {
633
+ match engine().models.get(handle) {
634
+ Some(model) => model.meshes.len() as f64,
635
+ None => 0.0,
636
+ }
637
+ })
638
+ }
639
+ #[cfg(not(feature = "models3d"))]
640
+ #[no_mangle]
641
+ pub extern "C" fn bloom_get_model_mesh_count(_handle: f64) -> f64 {
642
+ $crate::ffi::feature_off_warn_once("bloom_get_model_mesh_count", "models3d");
643
+ 0.0
644
+ }
645
+
646
+ // bloom_get_model_material_count [source: linux; gated: models3d]
647
+ #[cfg(feature = "models3d")]
648
+ #[no_mangle]
649
+ pub extern "C" fn bloom_get_model_material_count(handle: f64) -> f64 {
650
+ $crate::ffi::guard("bloom_get_model_material_count", move || {
651
+ match engine().models.get(handle) {
652
+ Some(model) => model.meshes.len() as f64,
653
+ None => 0.0,
654
+ }
655
+ })
656
+ }
657
+ #[cfg(not(feature = "models3d"))]
658
+ #[no_mangle]
659
+ pub extern "C" fn bloom_get_model_material_count(_handle: f64) -> f64 {
660
+ $crate::ffi::feature_off_warn_once("bloom_get_model_material_count", "models3d");
661
+ 0.0
662
+ }
663
+
664
+ // bloom_get_model_bounds_min_x [source: linux; gated: models3d]
665
+ #[cfg(feature = "models3d")]
666
+ #[no_mangle]
667
+ pub extern "C" fn bloom_get_model_bounds_min_x(model_handle: f64) -> f64 {
668
+ $crate::ffi::guard("bloom_get_model_bounds_min_x", move || {
669
+ engine().models.get_bounds(model_handle).0[0] as f64
670
+ })
671
+ }
672
+ #[cfg(not(feature = "models3d"))]
673
+ #[no_mangle]
674
+ pub extern "C" fn bloom_get_model_bounds_min_x(_model_handle: f64) -> f64 {
675
+ $crate::ffi::feature_off_warn_once("bloom_get_model_bounds_min_x", "models3d");
676
+ 0.0
677
+ }
678
+
679
+ // bloom_get_model_bounds_min_y [source: linux; gated: models3d]
680
+ #[cfg(feature = "models3d")]
681
+ #[no_mangle]
682
+ pub extern "C" fn bloom_get_model_bounds_min_y(model_handle: f64) -> f64 {
683
+ $crate::ffi::guard("bloom_get_model_bounds_min_y", move || {
684
+ engine().models.get_bounds(model_handle).0[1] as f64
685
+ })
686
+ }
687
+ #[cfg(not(feature = "models3d"))]
688
+ #[no_mangle]
689
+ pub extern "C" fn bloom_get_model_bounds_min_y(_model_handle: f64) -> f64 {
690
+ $crate::ffi::feature_off_warn_once("bloom_get_model_bounds_min_y", "models3d");
691
+ 0.0
692
+ }
693
+
694
+ // bloom_get_model_bounds_min_z [source: linux; gated: models3d]
695
+ #[cfg(feature = "models3d")]
696
+ #[no_mangle]
697
+ pub extern "C" fn bloom_get_model_bounds_min_z(model_handle: f64) -> f64 {
698
+ $crate::ffi::guard("bloom_get_model_bounds_min_z", move || {
699
+ engine().models.get_bounds(model_handle).0[2] as f64
700
+ })
701
+ }
702
+ #[cfg(not(feature = "models3d"))]
703
+ #[no_mangle]
704
+ pub extern "C" fn bloom_get_model_bounds_min_z(_model_handle: f64) -> f64 {
705
+ $crate::ffi::feature_off_warn_once("bloom_get_model_bounds_min_z", "models3d");
706
+ 0.0
707
+ }
708
+
709
+ // bloom_get_model_bounds_max_x [source: linux; gated: models3d]
710
+ #[cfg(feature = "models3d")]
711
+ #[no_mangle]
712
+ pub extern "C" fn bloom_get_model_bounds_max_x(model_handle: f64) -> f64 {
713
+ $crate::ffi::guard("bloom_get_model_bounds_max_x", move || {
714
+ engine().models.get_bounds(model_handle).1[0] as f64
715
+ })
716
+ }
717
+ #[cfg(not(feature = "models3d"))]
718
+ #[no_mangle]
719
+ pub extern "C" fn bloom_get_model_bounds_max_x(_model_handle: f64) -> f64 {
720
+ $crate::ffi::feature_off_warn_once("bloom_get_model_bounds_max_x", "models3d");
721
+ 0.0
722
+ }
723
+
724
+ // bloom_get_model_bounds_max_y [source: linux; gated: models3d]
725
+ #[cfg(feature = "models3d")]
726
+ #[no_mangle]
727
+ pub extern "C" fn bloom_get_model_bounds_max_y(model_handle: f64) -> f64 {
728
+ $crate::ffi::guard("bloom_get_model_bounds_max_y", move || {
729
+ engine().models.get_bounds(model_handle).1[1] as f64
730
+ })
731
+ }
732
+ #[cfg(not(feature = "models3d"))]
733
+ #[no_mangle]
734
+ pub extern "C" fn bloom_get_model_bounds_max_y(_model_handle: f64) -> f64 {
735
+ $crate::ffi::feature_off_warn_once("bloom_get_model_bounds_max_y", "models3d");
736
+ 0.0
737
+ }
738
+
739
+ // bloom_get_model_bounds_max_z [source: linux; gated: models3d]
740
+ #[cfg(feature = "models3d")]
741
+ #[no_mangle]
742
+ pub extern "C" fn bloom_get_model_bounds_max_z(model_handle: f64) -> f64 {
743
+ $crate::ffi::guard("bloom_get_model_bounds_max_z", move || {
744
+ engine().models.get_bounds(model_handle).1[2] as f64
745
+ })
746
+ }
747
+ #[cfg(not(feature = "models3d"))]
748
+ #[no_mangle]
749
+ pub extern "C" fn bloom_get_model_bounds_max_z(_model_handle: f64) -> f64 {
750
+ $crate::ffi::feature_off_warn_once("bloom_get_model_bounds_max_z", "models3d");
751
+ 0.0
752
+ }
753
+
754
+ // bloom_gen_mesh_spline_ribbon [source: curated; gated: models3d]
755
+ #[cfg(feature = "models3d")]
756
+ #[no_mangle]
757
+ pub extern "C" fn bloom_gen_mesh_spline_ribbon(points_ptr: *const f64, point_count: f64, widths_ptr: *const f64, width_count: f64) -> f64 {
758
+ $crate::ffi::guard("bloom_gen_mesh_spline_ribbon", move || {
759
+ // Perry arrays are f64 buffers (see bloom_create_mesh note).
760
+ if points_ptr.is_null() || widths_ptr.is_null() { return 0.0; }
761
+ let n = point_count as usize;
762
+ let wn = width_count as usize;
763
+ let points_f64 = unsafe { std::slice::from_raw_parts(points_ptr, n * 3) };
764
+ let widths_f64 = unsafe { std::slice::from_raw_parts(widths_ptr, wn) };
765
+ let points: Vec<f32> = points_f64.iter().map(|&v| v as f32).collect();
766
+ let widths: Vec<f32> = widths_f64.iter().map(|&v| v as f32).collect();
767
+ engine().models.gen_mesh_spline_ribbon(&points, &widths)
768
+ })
769
+ }
770
+ #[cfg(not(feature = "models3d"))]
771
+ #[no_mangle]
772
+ pub extern "C" fn bloom_gen_mesh_spline_ribbon(_points_ptr: *const f64, _point_count: f64, _widths_ptr: *const f64, _width_count: f64) -> f64 {
773
+ $crate::ffi::feature_off_warn_once("bloom_gen_mesh_spline_ribbon", "models3d");
774
+ 0.0
775
+ }
776
+
777
+ // Array-free spline ribbon. Same reason as the mesh scratch above: the
778
+ // pointer form is unreachable from TypeScript (Perry won't pass a
779
+ // `number[]` into an i64 param). Push `point_count * 3` position floats
780
+ // followed by `width_count` width floats into the mesh scratch, then
781
+ // call this.
782
+ #[cfg(feature = "models3d")]
783
+ #[no_mangle]
784
+ pub extern "C" fn bloom_gen_mesh_spline_ribbon_scratch(point_count: f64, width_count: f64) -> f64 {
785
+ $crate::ffi::guard("bloom_gen_mesh_spline_ribbon_scratch", move || {
786
+ let n = point_count as usize;
787
+ let wn = width_count as usize;
788
+ let (points, widths) = {
789
+ let eng = engine();
790
+ let scratch = eng.models.scratch_floats();
791
+ if n < 2 || wn == 0 || scratch.len() < n * 3 + wn {
792
+ return 0.0;
793
+ }
794
+ (
795
+ scratch[..n * 3].to_vec(),
796
+ scratch[n * 3..n * 3 + wn].to_vec(),
797
+ )
798
+ };
799
+ engine().models.gen_mesh_spline_ribbon(&points, &widths)
800
+ })
801
+ }
802
+ #[cfg(not(feature = "models3d"))]
803
+ #[no_mangle]
804
+ pub extern "C" fn bloom_gen_mesh_spline_ribbon_scratch(_point_count: f64, _width_count: f64) -> f64 {
805
+ $crate::ffi::feature_off_warn_once("bloom_gen_mesh_spline_ribbon_scratch", "models3d");
806
+ 0.0
807
+ }
808
+
809
+ // bloom_stage_model [source: linux; gated: models3d]
810
+ #[cfg(feature = "models3d")]
811
+ #[no_mangle]
812
+ pub extern "C" fn bloom_stage_model(path_ptr: *const u8) -> f64 {
813
+ $crate::ffi::guard("bloom_stage_model", move || {
814
+ let path = $crate::string_header::str_from_header(path_ptr);
815
+ let path = bloom_resolve_asset_path(path);
816
+ let data = match std::fs::read(path.as_ref()) {
817
+ Ok(d) => d,
818
+ Err(_) => return 0.0,
819
+ };
820
+ match $crate::models::load_gltf_staged(&data) {
821
+ Some(staged) => $crate::staging::stage_model(staged),
822
+ None => 0.0,
823
+ }
824
+ })
825
+ }
826
+ #[cfg(not(feature = "models3d"))]
827
+ #[no_mangle]
828
+ pub extern "C" fn bloom_stage_model(_path_ptr: *const u8) -> f64 {
829
+ $crate::ffi::feature_off_warn_once("bloom_stage_model", "models3d");
830
+ 0.0
831
+ }
832
+
833
+ // bloom_commit_model [source: linux; gated: models3d]
834
+ #[cfg(feature = "models3d")]
835
+ #[no_mangle]
836
+ pub extern "C" fn bloom_commit_model(staging_handle: f64) -> f64 {
837
+ $crate::ffi::guard("bloom_commit_model", move || {
838
+ let staged = match $crate::staging::take_model(staging_handle) {
839
+ Some(s) => s,
840
+ None => return 0.0,
841
+ };
842
+ let mut eng = engine();
843
+ let $crate::engine::EngineState { models, renderer, .. } = &mut *eng;
844
+ // Normal maps must go through the kind-aware registration
845
+ // (linear space + LEADR mips) or the committed model shades
846
+ // visibly flatter than the same GLB through loadModel.
847
+ let mut tex_map: Vec<u32> = Vec::with_capacity(staged.textures.len());
848
+ for tex in &staged.textures {
849
+ tex_map.push(renderer.register_texture_kind(
850
+ tex.width, tex.height, &tex.data, tex.is_normal));
851
+ }
852
+ let mut model = staged.model;
853
+ // Remap EVERY texture slot, not just the base colour — this
854
+ // used to drop normal/MR/emissive/occlusion references on the
855
+ // floor (the round-5 "stale aux-texture remap" bug), which
856
+ // silently stripped the SH-046 character maps on any model
857
+ // loaded through the staged path.
858
+ let remap = |idx: &mut Option<u32>| {
859
+ if let Some(i) = *idx {
860
+ let s = i as usize;
861
+ *idx = if s > 0 && s <= tex_map.len() {
862
+ Some(tex_map[s - 1])
863
+ } else {
864
+ None
865
+ };
866
+ }
867
+ };
868
+ for mesh in &mut model.meshes {
869
+ remap(&mut mesh.texture_idx);
870
+ remap(&mut mesh.normal_texture_idx);
871
+ remap(&mut mesh.metallic_roughness_texture_idx);
872
+ remap(&mut mesh.emissive_texture_idx);
873
+ remap(&mut mesh.occlusion_texture_idx);
874
+ }
875
+ models.models.alloc(model)
876
+ })
877
+ }
878
+ #[cfg(not(feature = "models3d"))]
879
+ #[no_mangle]
880
+ pub extern "C" fn bloom_commit_model(_staging_handle: f64) -> f64 {
881
+ $crate::ffi::feature_off_warn_once("bloom_commit_model", "models3d");
882
+ 0.0
883
+ }
884
+
885
+ // bloom_compile_material_from_file [source: macos]
886
+ #[no_mangle]
887
+ pub extern "C" fn bloom_compile_material_from_file(
888
+ path_ptr: *const u8,
889
+ bucket_kind: f64,
890
+ ) -> f64 {
891
+ $crate::ffi::guard("bloom_compile_material_from_file", move || {
892
+ use $crate::renderer::material_pipeline::{FragmentProfile, Bucket};
893
+ let path = $crate::string_header::str_from_header(path_ptr);
894
+ // Every other asset loader routes through the platform's
895
+ // resolve hook; this one read the raw relative path straight
896
+ // off the working directory, which is only the asset root on
897
+ // the desktop hosts. On iOS the CWD is not the app bundle, so
898
+ // every from-file material failed to canonicalize and the whole
899
+ // scene lost its shaders.
900
+ let path = bloom_resolve_asset_path(path);
901
+ let (profile, bucket, reads_scene) = match bucket_kind as u32 {
902
+ 0 => (FragmentProfile::Opaque, Bucket::Opaque, false),
903
+ 1 => (FragmentProfile::Translucent, Bucket::Transparent, false),
904
+ 2 => (FragmentProfile::Translucent, Bucket::Refractive, true),
905
+ 3 => (FragmentProfile::Translucent, Bucket::Additive, false),
906
+ 4 => (FragmentProfile::Opaque, Bucket::Cutout, false),
907
+ _ => {
908
+ eprintln!("[material] from_file: unknown bucket_kind {bucket_kind}");
909
+ return 0.0;
910
+ }
911
+ };
912
+ match engine().renderer.compile_material_from_file(
913
+ std::path::Path::new(path.as_ref()), profile, bucket, reads_scene,
914
+ ) {
915
+ Ok(handle) => handle as f64,
916
+ Err(e) => { eprintln!("[material] from_file failed: {e}"); 0.0 }
917
+ }
918
+ })
919
+ }
920
+
921
+ // bloom_set_material_params_scratch — params arrive via the mesh
922
+ // scratch (bloom_mesh_scratch_reset + push_f32 × N) instead of the
923
+ // pointer param below, which Perry 0.5.x rejects for JS arrays.
924
+ // Same idiom as bloom_create_instance_buffer_scratch. ≤ 64 floats.
925
+ #[cfg(feature = "models3d")]
926
+ #[no_mangle]
927
+ pub extern "C" fn bloom_set_material_params_scratch(handle: f64, param_count: f64) {
928
+ $crate::ffi::guard("bloom_set_material_params_scratch", move || {
929
+ let mut eng = engine();
930
+ let count = param_count as usize;
931
+ if count > 64 {
932
+ eprintln!("[material] set_material_params_scratch: param_count {} > 64 (256-byte UBO cap)", count);
933
+ return;
934
+ }
935
+ let $crate::engine::EngineState { models, renderer, .. } = &mut *eng;
936
+ if models.scratch_f32.len() < count { return; }
937
+ let mut bytes = vec![0u8; count * 4];
938
+ for i in 0..count {
939
+ bytes[i*4..i*4+4].copy_from_slice(&models.scratch_f32[i].to_le_bytes());
940
+ }
941
+ models.mesh_scratch_reset();
942
+ if let Err(e) = renderer.material_system.set_user_params(
943
+ &renderer.device, &renderer.queue,
944
+ handle as u32, &bytes,
945
+ ) {
946
+ eprintln!("[material] set_material_params_scratch failed: {}", e);
947
+ }
948
+ })
949
+ }
950
+ #[cfg(not(feature = "models3d"))]
951
+ #[no_mangle]
952
+ pub extern "C" fn bloom_set_material_params_scratch(_handle: f64, _param_count: f64) {
953
+ $crate::ffi::feature_off_warn_once("bloom_set_material_params_scratch", "models3d");
954
+ }
955
+
956
+ // bloom_set_material_params [source: linux; gated: models3d]
957
+ #[cfg(feature = "models3d")]
958
+ #[no_mangle]
959
+ pub extern "C" fn bloom_set_material_params(
960
+ handle: f64,
961
+ params_ptr: *const f64,
962
+ param_count: f64,
963
+ ) {
964
+ $crate::ffi::guard("bloom_set_material_params", move || {
965
+ let count = param_count as usize;
966
+ if count > 64 {
967
+ eprintln!("[material] set_material_params: param_count {} > 64 (256-byte UBO cap)", count);
968
+ return;
969
+ }
970
+ let mut bytes = vec![0u8; count * 4];
971
+ if !params_ptr.is_null() && count > 0 {
972
+ let slots = unsafe { std::slice::from_raw_parts(params_ptr, count) };
973
+ for (i, &v) in slots.iter().enumerate() {
974
+ let f = v as f32;
975
+ bytes[i*4..i*4+4].copy_from_slice(&f.to_le_bytes());
976
+ }
977
+ }
978
+ let mut eng = engine();
979
+ let $crate::engine::EngineState { renderer, .. } = &mut *eng;
980
+ if let Err(e) = renderer.material_system.set_user_params(
981
+ &renderer.device, &renderer.queue,
982
+ handle as u32, &bytes,
983
+ ) {
984
+ eprintln!("[material] set_material_params failed: {}", e);
985
+ }
986
+ })
987
+ }
988
+ #[cfg(not(feature = "models3d"))]
989
+ #[no_mangle]
990
+ pub extern "C" fn bloom_set_material_params(_handle: f64, _params_ptr: *const f64, _param_count: f64) {
991
+ $crate::ffi::feature_off_warn_once("bloom_set_material_params", "models3d");
992
+ }
993
+
994
+ // ==================================================================
995
+ // EN-028 — animation mixer (crossfade + masked layer + root motion)
996
+ // EN-033 — bone sockets
997
+ //
998
+ // All numeric ABI, <= 8 f64 args per call (the 9th arg garbles on
999
+ // Perry/ARM64 — see bloom_update_model_animation's note).
1000
+ // ==================================================================
1001
+
1002
+ // bloom_anim_play [gated: models3d]
1003
+ #[cfg(feature = "models3d")]
1004
+ #[no_mangle]
1005
+ pub extern "C" fn bloom_anim_play(handle: f64, clip: f64, fade: f64, speed: f64, looping: f64) {
1006
+ $crate::ffi::guard("bloom_anim_play", move || {
1007
+ engine().models.anim_play(
1008
+ handle, clip as usize, fade as f32, speed as f32, looping != 0.0);
1009
+ })
1010
+ }
1011
+ #[cfg(not(feature = "models3d"))]
1012
+ #[no_mangle]
1013
+ pub extern "C" fn bloom_anim_play(_h: f64, _c: f64, _f: f64, _s: f64, _l: f64) {
1014
+ $crate::ffi::feature_off_warn_once("bloom_anim_play", "models3d");
1015
+ }
1016
+
1017
+ // bloom_anim_set_layer [gated: models3d]
1018
+ #[cfg(feature = "models3d")]
1019
+ #[no_mangle]
1020
+ pub extern "C" fn bloom_anim_set_layer(handle: f64, clip: f64, weight: f64, mask_root: f64, speed: f64, looping: f64) {
1021
+ $crate::ffi::guard("bloom_anim_set_layer", move || {
1022
+ engine().models.anim_set_layer(
1023
+ handle, clip as i32, weight as f32, mask_root as i32,
1024
+ speed as f32, looping != 0.0);
1025
+ })
1026
+ }
1027
+ #[cfg(not(feature = "models3d"))]
1028
+ #[no_mangle]
1029
+ pub extern "C" fn bloom_anim_set_layer(_h: f64, _c: f64, _w: f64, _m: f64, _s: f64, _l: f64) {
1030
+ $crate::ffi::feature_off_warn_once("bloom_anim_set_layer", "models3d");
1031
+ }
1032
+
1033
+ // bloom_anim_set_root_motion [gated: models3d]
1034
+ #[cfg(feature = "models3d")]
1035
+ #[no_mangle]
1036
+ pub extern "C" fn bloom_anim_set_root_motion(handle: f64, on: f64) {
1037
+ $crate::ffi::guard("bloom_anim_set_root_motion", move || {
1038
+ engine().models.anim_set_root_motion(handle, on != 0.0);
1039
+ })
1040
+ }
1041
+ #[cfg(not(feature = "models3d"))]
1042
+ #[no_mangle]
1043
+ pub extern "C" fn bloom_anim_set_root_motion(_h: f64, _o: f64) {
1044
+ $crate::ffi::feature_off_warn_once("bloom_anim_set_root_motion", "models3d");
1045
+ }
1046
+
1047
+ // bloom_anim_update [gated: models3d]
1048
+ // Advances every clock on the model by dt, rebuilds the blended pose,
1049
+ // and uploads the joint matrices — the mixer-driven replacement for
1050
+ // bloom_update_model_animation (which stays for single-clip callers).
1051
+ #[cfg(feature = "models3d")]
1052
+ #[no_mangle]
1053
+ pub extern "C" fn bloom_anim_update(handle: f64, dt: f64, scale: f64, px: f64, py: f64, pz: f64, rot_y: f64) {
1054
+ $crate::ffi::guard("bloom_anim_update", move || {
1055
+ let rot_y_f = rot_y as f32;
1056
+ let rot_sin = rot_y_f.sin();
1057
+ let rot_cos = rot_y_f.cos();
1058
+ let mut eng = engine();
1059
+ let $crate::engine::EngineState { models, renderer, .. } = &mut *eng;
1060
+ models.advance_and_update(handle, dt as f32);
1061
+ if let Some(anim) = models.get_animation(handle) {
1062
+ if !anim.joint_matrices.is_empty() {
1063
+ // PT-7: anim handle = prev-palette pairing key.
1064
+ renderer.set_joint_matrices_scaled(
1065
+ handle.to_bits(), &anim.joint_matrices, scale as f32,
1066
+ [px as f32, py as f32, pz as f32], rot_sin, rot_cos);
1067
+ }
1068
+ }
1069
+ })
1070
+ }
1071
+ #[cfg(not(feature = "models3d"))]
1072
+ #[no_mangle]
1073
+ pub extern "C" fn bloom_anim_update(_h: f64, _d: f64, _s: f64, _x: f64, _y: f64, _z: f64, _r: f64) {
1074
+ $crate::ffi::feature_off_warn_once("bloom_anim_update", "models3d");
1075
+ }
1076
+
1077
+ // bloom_anim_finished [gated: models3d]
1078
+ #[cfg(feature = "models3d")]
1079
+ #[no_mangle]
1080
+ pub extern "C" fn bloom_anim_finished(handle: f64) -> f64 {
1081
+ $crate::ffi::guard("bloom_anim_finished", move || {
1082
+ if engine().models.anim_finished(handle) { 1.0 } else { 0.0 }
1083
+ })
1084
+ }
1085
+ #[cfg(not(feature = "models3d"))]
1086
+ #[no_mangle]
1087
+ pub extern "C" fn bloom_anim_finished(_h: f64) -> f64 { 1.0 }
1088
+
1089
+ // bloom_anim_clip_duration [gated: models3d]
1090
+ #[cfg(feature = "models3d")]
1091
+ #[no_mangle]
1092
+ pub extern "C" fn bloom_anim_clip_duration(handle: f64, clip: f64) -> f64 {
1093
+ $crate::ffi::guard("bloom_anim_clip_duration", move || {
1094
+ engine().models.anim_clip_duration(handle, clip as usize) as f64
1095
+ })
1096
+ }
1097
+ #[cfg(not(feature = "models3d"))]
1098
+ #[no_mangle]
1099
+ pub extern "C" fn bloom_anim_clip_duration(_h: f64, _c: f64) -> f64 { 0.0 }
1100
+
1101
+ // bloom_anim_root_delta [gated: models3d] axis: 0=x 1=y 2=z
1102
+ #[cfg(feature = "models3d")]
1103
+ #[no_mangle]
1104
+ pub extern "C" fn bloom_anim_root_delta(handle: f64, axis: f64) -> f64 {
1105
+ $crate::ffi::guard("bloom_anim_root_delta", move || {
1106
+ let d = engine().models.anim_root_delta(handle);
1107
+ let i = axis as usize;
1108
+ if i < 3 { d[i] as f64 } else { 0.0 }
1109
+ })
1110
+ }
1111
+ #[cfg(not(feature = "models3d"))]
1112
+ #[no_mangle]
1113
+ pub extern "C" fn bloom_anim_root_delta(_h: f64, _a: f64) -> f64 { 0.0 }
1114
+
1115
+ // bloom_model_find_joint [gated: models3d]
1116
+ // Load-time only (it parses a string) — cache the index, per
1117
+ // perry-quirks #5.
1118
+ #[cfg(feature = "models3d")]
1119
+ #[no_mangle]
1120
+ pub extern "C" fn bloom_model_find_joint(handle: f64, name_ptr: *const u8) -> f64 {
1121
+ $crate::ffi::guard("bloom_model_find_joint", move || {
1122
+ let name = $crate::string_header::str_from_header(name_ptr);
1123
+ engine().models.find_joint(handle, &name) as f64
1124
+ })
1125
+ }
1126
+ #[cfg(not(feature = "models3d"))]
1127
+ #[no_mangle]
1128
+ pub extern "C" fn bloom_model_find_joint(_h: f64, _n: *const u8) -> f64 { -1.0 }
1129
+
1130
+ // bloom_model_joint_world [gated: models3d]
1131
+ // Model-space 4x4 of a joint, column-major, component 0..15.
1132
+ // Translation is components 12/13/14. The caller applies the same
1133
+ // (scale, position, yaw) it passed to bloom_anim_update to lift this
1134
+ // into world space — the engine deliberately does not, so a socket
1135
+ // costs no extra state.
1136
+ #[cfg(feature = "models3d")]
1137
+ #[no_mangle]
1138
+ pub extern "C" fn bloom_model_joint_world(handle: f64, joint: f64, comp: f64) -> f64 {
1139
+ $crate::ffi::guard("bloom_model_joint_world", move || {
1140
+ let j = joint as i64;
1141
+ let c = comp as usize;
1142
+ if j < 0 || c > 15 { return 0.0; }
1143
+ match engine().models.joint_world(handle, j as usize) {
1144
+ Some(m) => m[c / 4][c % 4] as f64,
1145
+ None => 0.0,
1146
+ }
1147
+ })
1148
+ }
1149
+ #[cfg(not(feature = "models3d"))]
1150
+ #[no_mangle]
1151
+ pub extern "C" fn bloom_model_joint_world(_h: f64, _j: f64, _c: f64) -> f64 { 0.0 }
1152
+
1153
+ };
1154
+ }