@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,261 @@
1
+ use crate::audio::AudioMixer;
2
+ use crate::input::InputState;
3
+ use crate::renderer::Renderer;
4
+ use crate::text_renderer::TextRenderer;
5
+ use crate::textures::TextureManager;
6
+ #[cfg(feature = "models3d")]
7
+ use crate::models::ModelManager;
8
+ use crate::scene::SceneGraph;
9
+ use crate::frame_callbacks::FrameCallbackSystem;
10
+ use crate::postfx::PostFxPipeline;
11
+ use crate::profiler::Profiler;
12
+ use crate::drs::DrsController;
13
+ #[cfg(all(feature = "jolt", not(target_arch = "wasm32")))]
14
+ use crate::physics_jolt::JoltPhysics;
15
+
16
+ #[cfg(feature = "web")]
17
+ use web_time::Instant;
18
+ #[cfg(not(feature = "web"))]
19
+ use std::time::Instant;
20
+
21
+ pub struct EngineState {
22
+ pub renderer: Renderer,
23
+ pub text: TextRenderer,
24
+ pub input: InputState,
25
+ pub audio: AudioMixer,
26
+ pub textures: TextureManager,
27
+ #[cfg(feature = "models3d")]
28
+ pub models: ModelManager,
29
+ pub scene: SceneGraph,
30
+ pub frame_callbacks: FrameCallbackSystem,
31
+ pub postfx: Option<PostFxPipeline>,
32
+ pub profiler: Profiler,
33
+ pub screenshot_pending: bool,
34
+ pub drs: DrsController,
35
+ /// EN-026 / EN-027 — particle pools and the decal ring. Both are pure CPU
36
+ /// state that feeds dynamic instance buffers; they own no GPU resources of
37
+ /// their own beyond the buffer handles they were given at creation.
38
+ pub particles: crate::particles::ParticleManager,
39
+ pub decals: crate::decals::DecalManager,
40
+ /// EN-025 — ragdoll slots. Bodies live in the Jolt world; this owns the
41
+ /// bone->body mapping that turns them back into a skinned pose.
42
+ #[cfg(all(feature = "models3d", feature = "jolt"))]
43
+ pub ragdolls: crate::ragdoll::RagdollManager,
44
+
45
+ // Timing
46
+ pub target_fps: f64,
47
+ pub delta_time: f64,
48
+ last_frame_time: Instant,
49
+ frame_count: u64,
50
+ fps_timer: Instant,
51
+ fps_frame_count: u64,
52
+ current_fps: f64,
53
+ start_time: Instant,
54
+
55
+ pub should_close: bool,
56
+
57
+ // Read-back state for the split FFI getters: bloom_scene_pick /
58
+ // bloom_scene_pick_all / bloom_project_to_screen store here, and the
59
+ // bloom_pick_hit_* / bloom_pick_all_* / bloom_project_screen_y getters
60
+ // read the components back one f64 at a time (Perry FFI returns are
61
+ // scalar). Lived in per-crate `static mut`s before the FFI
62
+ // unification.
63
+ pub last_pick: Option<crate::picking::PickResult>,
64
+ pub last_pick_all: Vec<crate::picking::PickResult>,
65
+ pub last_project: (f64, f64),
66
+
67
+ // When true, end_frame() takes a direct-to-swapchain path that skips
68
+ // scene graph prep, shadow maps, HDR/post-FX, SDF/WSRC bakes, etc.
69
+ // Intended for pure-2D games that only need the batched 2D pipeline.
70
+ // Off by default to preserve existing behaviour.
71
+ pub direct_2d_mode: bool,
72
+
73
+ #[cfg(all(feature = "jolt", not(target_arch = "wasm32")))]
74
+ pub jolt: JoltPhysics,
75
+ }
76
+
77
+ impl EngineState {
78
+ pub fn new(renderer: Renderer) -> Self {
79
+ let now = Instant::now();
80
+ let mut profiler = Profiler::new();
81
+ profiler.init_gpu(&renderer.device, &renderer.queue);
82
+ // Ticket 007b: tell the scene graph whether the device was
83
+ // created with HW ray-query so geometry upload pays the BLAS /
84
+ // BLAS_INPUT cost only when it can be used.
85
+ let mut scene = SceneGraph::new();
86
+ scene.hw_rt_enabled = renderer.hw_rt_enabled;
87
+ Self {
88
+ renderer,
89
+ text: TextRenderer::new(),
90
+ input: InputState::new(),
91
+ audio: AudioMixer::new(),
92
+ textures: TextureManager::new(),
93
+ #[cfg(feature = "models3d")]
94
+ models: ModelManager::new(),
95
+ scene,
96
+ frame_callbacks: FrameCallbackSystem::new(),
97
+ postfx: None,
98
+ profiler,
99
+ screenshot_pending: false,
100
+ drs: DrsController::new(),
101
+ particles: crate::particles::ParticleManager::new(),
102
+ decals: crate::decals::DecalManager::new(),
103
+ #[cfg(all(feature = "models3d", feature = "jolt"))]
104
+ ragdolls: crate::ragdoll::RagdollManager::new(),
105
+ target_fps: 60.0,
106
+ delta_time: 0.0,
107
+ last_frame_time: now,
108
+ frame_count: 0,
109
+ fps_timer: now,
110
+ fps_frame_count: 0,
111
+ current_fps: 0.0,
112
+ start_time: now,
113
+ should_close: false,
114
+ last_pick: None,
115
+ last_pick_all: Vec::new(),
116
+ last_project: (0.0, 0.0),
117
+ direct_2d_mode: false,
118
+ #[cfg(all(feature = "jolt", not(target_arch = "wasm32")))]
119
+ jolt: JoltPhysics::new(),
120
+ }
121
+ }
122
+
123
+ /// Hard ceiling on the delta time games observe. A debugger pause, OS
124
+ /// hitch, or backgrounded tab produces one slowed-down frame instead
125
+ /// of a quarter-hour dt that explodes physics, animation, and any
126
+ /// game-side `pos += vel * dt`. Matches Unity's maximumDeltaTime
127
+ /// default order of magnitude.
128
+ pub const MAX_DELTA_TIME: f64 = 0.25;
129
+
130
+ pub fn begin_frame(&mut self) {
131
+ self.begin_frame_without_callbacks();
132
+
133
+ // Run frame callbacks after begin_frame (matching R3F's useFrame timing)
134
+ self.profiler.begin("frame_callbacks");
135
+ self.frame_callbacks.run_all(self.delta_time);
136
+ self.profiler.end("frame_callbacks");
137
+ }
138
+
139
+ /// Advance the engine to the callback boundary without invoking foreign
140
+ /// callbacks. Linux uses this split because its engine state is protected
141
+ /// by a mutex and callbacks may call back into the FFI.
142
+ pub fn begin_frame_without_callbacks(&mut self) {
143
+ let now = Instant::now();
144
+ self.delta_time = now
145
+ .duration_since(self.last_frame_time)
146
+ .as_secs_f64()
147
+ .min(Self::MAX_DELTA_TIME);
148
+ self.last_frame_time = now;
149
+
150
+ // DRS samples wall-clock frame time and may step render_scale
151
+ // up or down before this frame's renderer work begins. No-op
152
+ // when disabled (default) — and the controller's own cooldown
153
+ // + hysteresis bound how often a step actually happens.
154
+ self.drs.tick(self.delta_time, &mut self.renderer);
155
+
156
+ self.fps_frame_count += 1;
157
+ let fps_elapsed = now.duration_since(self.fps_timer).as_secs_f64();
158
+ if fps_elapsed >= 1.0 {
159
+ self.current_fps = self.fps_frame_count as f64 / fps_elapsed;
160
+ self.fps_frame_count = 0;
161
+ self.fps_timer = now;
162
+ }
163
+
164
+ self.input.begin_frame();
165
+ self.renderer.begin_frame();
166
+ self.frame_count += 1;
167
+ }
168
+
169
+ /// Start profiling and snapshot the callbacks for the current frame. The
170
+ /// returned function pointers are owned by the caller, so the engine lock
171
+ /// can be released before they are invoked.
172
+ pub fn begin_frame_callbacks(&mut self) -> (f64, Vec<extern "C" fn(f64)>) {
173
+ self.profiler.begin("frame_callbacks");
174
+ let delta_time = self.delta_time;
175
+ let callbacks = self.frame_callbacks.callbacks_for_frame();
176
+ (delta_time, callbacks)
177
+ }
178
+
179
+ /// Finish the profiling span opened by [`Self::begin_frame_callbacks`].
180
+ pub fn finish_frame_callbacks(&mut self) {
181
+ self.profiler.end("frame_callbacks");
182
+ }
183
+
184
+ pub fn end_frame(&mut self) {
185
+ if self.direct_2d_mode {
186
+ // Fast path for pure-2D games: render direct to the swapchain,
187
+ // skipping scene prep, shadow maps, HDR/tonemap, SSAO, bloom,
188
+ // SDF/WSRC bakes and mesh-card capture. On mobile GPUs the
189
+ // full deferred pipeline easily costs tens of ms/frame even
190
+ // when the scene graph is empty; this path typically hits
191
+ // 60 fps on the same device.
192
+ self.profiler.begin("render_total");
193
+ self.renderer.end_frame();
194
+ self.profiler.end("render_total");
195
+ } else {
196
+ self.profiler.begin("scene_prepare");
197
+ // Collect last frame's occlusion-grid readback (if it
198
+ // completed) before culling against it.
199
+ self.renderer.occlusion.poll(&self.renderer.device);
200
+ // EN-057 — skip the occlusion reduce + readback entirely when no
201
+ // rasterized node could consume a verdict (gi_only proxies never
202
+ // draw; the shooter's 267 of them paid this every frame for
203
+ // nothing). The Hi-Z pyramid itself stays: SSAO consumes it.
204
+ self.renderer.occlusion.set_has_consumers(
205
+ self.scene.nodes.iter().any(|(_, n)| n.visible && !n.gi_only));
206
+ self.scene.prepare(
207
+ &self.renderer.device,
208
+ &self.renderer.queue,
209
+ &self.renderer.vp_matrix(),
210
+ // EN-022 fix: the velocity reference (prev unjittered VP
211
+ // + current jitter) — NOT the raw jittered prev VP — so
212
+ // static scene nodes get true zero velocity instead of
213
+ // jitter-delta noise that flickers TAA history.
214
+ &self.renderer.velocity_ref_vp,
215
+ self.renderer.uniform_3d_layout(),
216
+ Some(&self.renderer.occlusion),
217
+ );
218
+ self.scene.prepare_materials(&self.renderer);
219
+ self.profiler.end("scene_prepare");
220
+
221
+ // Phase 6 — drain hot-reload events and rebuild any
222
+ // material whose .wgsl file changed on disk. Cheap when
223
+ // nothing has changed (just a try_recv on an empty queue).
224
+ self.renderer.poll_material_hot_reload();
225
+
226
+ // Sync material-system PerFrame + PerView UBOs with the
227
+ // current clock + camera before dispatching any queued
228
+ // material draws. Without this, group 1 (view/proj/camera/
229
+ // lights/shadow) is zero and shaders that read e.g.
230
+ // `view.view_proj` produce offscreen geometry.
231
+ let t = self.get_time() as f32;
232
+ let dt = self.delta_time as f32;
233
+ self.renderer.material_system_begin_frame(t, dt);
234
+
235
+ self.profiler.begin("render_total");
236
+ self.renderer.end_frame_with_scene(&mut self.scene, &mut self.profiler);
237
+ self.profiler.end("render_total");
238
+ }
239
+
240
+ self.profiler.frame_end(&self.renderer.device);
241
+ self.input.end_frame();
242
+
243
+ // Vsync (PresentMode::Fifo, the wgpu default) already caps frame rate.
244
+ // Only apply CPU sleep-based cap when vsync is not active.
245
+ // On WASM, frame pacing is handled by requestAnimationFrame.
246
+ #[cfg(not(target_arch = "wasm32"))]
247
+ if self.target_fps > 0.0 && !self.renderer.vsync_active() {
248
+ let target_frame_time = 1.0 / self.target_fps;
249
+ let elapsed = self.last_frame_time.elapsed().as_secs_f64();
250
+ if elapsed < target_frame_time {
251
+ let sleep_time = target_frame_time - elapsed;
252
+ std::thread::sleep(std::time::Duration::from_secs_f64(sleep_time));
253
+ }
254
+ }
255
+ }
256
+
257
+ pub fn get_fps(&self) -> f64 { self.current_fps }
258
+ pub fn get_time(&self) -> f64 { self.start_time.elapsed().as_secs_f64() }
259
+ pub fn screen_width(&self) -> f64 { self.renderer.width() as f64 }
260
+ pub fn screen_height(&self) -> f64 { self.renderer.height() as f64 }
261
+ }
@@ -0,0 +1,116 @@
1
+ //! FFI boundary infrastructure shared by every platform crate.
2
+ //!
3
+ //! All `bloom_*` extern "C" functions generated by [`define_core_ffi!`]
4
+ //! (and, over time, the hand-written platform-specific ones) route their
5
+ //! bodies through [`guard`], which:
6
+ //!
7
+ //! 1. Prevents a Rust panic from unwinding across the `extern "C"`
8
+ //! boundary into Perry-compiled code. Release builds use
9
+ //! `panic = "abort"` where this is moot, but dev/test profiles unwind,
10
+ //! and rustc only aborts at the boundary since 1.81 — logging the
11
+ //! function name first is strictly more debuggable than a bare abort.
12
+ //! 2. Converts a panic into a logged, once-per-symbol error plus a safe
13
+ //! default return value, so a single bad call (stale handle, NaN
14
+ //! argument, asset race) degrades that call instead of killing the
15
+ //! game process.
16
+ //!
17
+ //! Logging goes through [`log_error`], which writes to logcat on Android
18
+ //! (stderr is discarded there) and stderr everywhere else.
19
+
20
+ /// Default return value for an FFI function whose body panicked.
21
+ pub trait FfiDefault {
22
+ fn ffi_default() -> Self;
23
+ }
24
+
25
+ impl FfiDefault for () {
26
+ fn ffi_default() -> Self {}
27
+ }
28
+
29
+ impl FfiDefault for f64 {
30
+ fn ffi_default() -> Self {
31
+ 0.0
32
+ }
33
+ }
34
+
35
+ impl FfiDefault for *const u8 {
36
+ fn ffi_default() -> Self {
37
+ std::ptr::null()
38
+ }
39
+ }
40
+
41
+ /// Run an FFI function body, catching panics at the boundary.
42
+ ///
43
+ /// On panic: logs `name` + the panic payload (once per symbol, so a
44
+ /// panicking per-frame call doesn't flood the log) and returns
45
+ /// `T::ffi_default()`.
46
+ pub fn guard<T: FfiDefault>(name: &'static str, f: impl FnOnce() -> T) -> T {
47
+ match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
48
+ Ok(v) => v,
49
+ Err(payload) => {
50
+ warn_once(name, || {
51
+ let msg = if let Some(s) = payload.downcast_ref::<&str>() {
52
+ (*s).to_string()
53
+ } else if let Some(s) = payload.downcast_ref::<String>() {
54
+ s.clone()
55
+ } else {
56
+ "non-string panic payload".to_string()
57
+ };
58
+ format!("bloom: panic in {name}(): {msg} — returning default; further panics in this function are suppressed")
59
+ });
60
+ T::ffi_default()
61
+ }
62
+ }
63
+ }
64
+
65
+ /// Log `make_msg()` the first time `key` is seen, silently no-op after.
66
+ fn warn_once(key: &'static str, make_msg: impl FnOnce() -> String) {
67
+ use std::sync::Mutex;
68
+ static SEEN: Mutex<Vec<&'static str>> = Mutex::new(Vec::new());
69
+ let mut seen = match SEEN.lock() {
70
+ Ok(g) => g,
71
+ // Poisoned only if a previous caller panicked while holding the
72
+ // lock; logging is best-effort, keep going with the inner data.
73
+ Err(p) => p.into_inner(),
74
+ };
75
+ if !seen.contains(&key) {
76
+ seen.push(key);
77
+ log_error(&make_msg());
78
+ }
79
+ }
80
+
81
+ /// One-time "this function is compiled out" notice for feature-gated FFI
82
+ /// stubs (models3d, image-extras). A silent stub here cost weeks of
83
+ /// confusion historically (Windows shipped silent no-ops for the whole
84
+ /// scene-graph surface) — never strip the warning.
85
+ pub fn feature_off_warn_once(name: &'static str, feature: &'static str) {
86
+ warn_once(name, || {
87
+ format!("bloom: {name}() ignored — engine built without the `{feature}` cargo feature")
88
+ });
89
+ }
90
+
91
+ /// Platform-appropriate error log: logcat on Android, stderr elsewhere.
92
+ pub fn log_error(msg: &str) {
93
+ #[cfg(target_os = "android")]
94
+ {
95
+ // liblog is linked by every Android app process.
96
+ extern "C" {
97
+ fn __android_log_print(prio: i32, tag: *const u8, fmt: *const u8, ...) -> i32;
98
+ }
99
+ if let Ok(c) = std::ffi::CString::new(msg) {
100
+ // 6 = ANDROID_LOG_ERROR
101
+ unsafe {
102
+ __android_log_print(6, b"BloomEngine\0".as_ptr(), b"%s\0".as_ptr(), c.as_ptr());
103
+ }
104
+ }
105
+ return;
106
+ }
107
+ // EN-063 — a browser has no stderr, so eprintln! drops the message
108
+ // entirely. Every engine diagnostic was invisible on web until this.
109
+ #[cfg(target_arch = "wasm32")]
110
+ {
111
+ web_sys::console::error_1(&msg.into());
112
+ return;
113
+ }
114
+ #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
115
+ eprintln!("{msg}");
116
+ }