@bloomengine/engine 0.3.1

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 (562) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +169 -0
  3. package/native/android/Cargo.lock +1848 -0
  4. package/native/android/Cargo.toml +20 -0
  5. package/native/android/src/lib.rs +1747 -0
  6. package/native/ios/Cargo.lock +1688 -0
  7. package/native/ios/Cargo.toml +19 -0
  8. package/native/ios/src/lib.rs +2258 -0
  9. package/native/linux/Cargo.lock +1719 -0
  10. package/native/linux/Cargo.toml +22 -0
  11. package/native/linux/src/lib.rs +2236 -0
  12. package/native/macos/Cargo.lock +3310 -0
  13. package/native/macos/Cargo.toml +29 -0
  14. package/native/macos/src/lib.rs +3444 -0
  15. package/native/shared/Cargo.lock +1898 -0
  16. package/native/shared/Cargo.toml +42 -0
  17. package/native/shared/assets/default_font.ttf +0 -0
  18. package/native/shared/build.rs +77 -0
  19. package/native/shared/shaders/common/fog.wgsl +16 -0
  20. package/native/shared/shaders/common/imposter.wgsl +112 -0
  21. package/native/shared/shaders/common/pbr.wgsl +186 -0
  22. package/native/shared/shaders/common/shadows.wgsl +77 -0
  23. package/native/shared/shaders/common/sky.wgsl +8 -0
  24. package/native/shared/shaders/common/tonemap.wgsl +25 -0
  25. package/native/shared/shaders/impulse_field.wgsl +53 -0
  26. package/native/shared/shaders/material_abi.wgsl +360 -0
  27. package/native/shared/shaders/materials/test_minimal.wgsl +42 -0
  28. package/native/shared/src/audio.rs +363 -0
  29. package/native/shared/src/custom_shaders.rs +104 -0
  30. package/native/shared/src/drs.rs +211 -0
  31. package/native/shared/src/engine.rs +186 -0
  32. package/native/shared/src/frame_callbacks.rs +88 -0
  33. package/native/shared/src/geometry.rs +236 -0
  34. package/native/shared/src/handles.rs +76 -0
  35. package/native/shared/src/input.rs +273 -0
  36. package/native/shared/src/jolt_sys.rs +822 -0
  37. package/native/shared/src/lib.rs +43 -0
  38. package/native/shared/src/models.rs +1941 -0
  39. package/native/shared/src/physics_jolt.rs +1528 -0
  40. package/native/shared/src/picking.rs +298 -0
  41. package/native/shared/src/postfx.rs +339 -0
  42. package/native/shared/src/profiler.rs +416 -0
  43. package/native/shared/src/renderer/atmosphere_lut.rs +573 -0
  44. package/native/shared/src/renderer/brdf_lut.rs +154 -0
  45. package/native/shared/src/renderer/formats.rs +778 -0
  46. package/native/shared/src/renderer/graph.rs +465 -0
  47. package/native/shared/src/renderer/hot_reload.rs +390 -0
  48. package/native/shared/src/renderer/impulse_field.rs +455 -0
  49. package/native/shared/src/renderer/material_pipeline.rs +604 -0
  50. package/native/shared/src/renderer/material_system.rs +2106 -0
  51. package/native/shared/src/renderer/mod.rs +13923 -0
  52. package/native/shared/src/renderer/planar_reflection.rs +458 -0
  53. package/native/shared/src/renderer/post_pass.rs +249 -0
  54. package/native/shared/src/renderer/shader_include.rs +205 -0
  55. package/native/shared/src/renderer/shader_library.rs +134 -0
  56. package/native/shared/src/renderer/shaders.rs +5855 -0
  57. package/native/shared/src/renderer/transient.rs +576 -0
  58. package/native/shared/src/renderer/types.rs +259 -0
  59. package/native/shared/src/renderer/util.rs +151 -0
  60. package/native/shared/src/scene.rs +1066 -0
  61. package/native/shared/src/sdf_cache.rs +274 -0
  62. package/native/shared/src/shadows.rs +551 -0
  63. package/native/shared/src/staging.rs +90 -0
  64. package/native/shared/src/string_header.rs +35 -0
  65. package/native/shared/src/text_renderer.rs +456 -0
  66. package/native/shared/src/textures.rs +154 -0
  67. package/native/third_party/JoltPhysics/Jolt/AABBTree/AABBTreeBuilder.cpp +242 -0
  68. package/native/third_party/JoltPhysics/Jolt/AABBTree/AABBTreeBuilder.h +121 -0
  69. package/native/third_party/JoltPhysics/Jolt/AABBTree/AABBTreeToBuffer.h +296 -0
  70. package/native/third_party/JoltPhysics/Jolt/AABBTree/NodeCodec/NodeCodecQuadTreeHalfFloat.h +323 -0
  71. package/native/third_party/JoltPhysics/Jolt/AABBTree/TriangleCodec/TriangleCodecIndexed8BitPackSOA4Flags.h +555 -0
  72. package/native/third_party/JoltPhysics/Jolt/ConfigurationString.h +112 -0
  73. package/native/third_party/JoltPhysics/Jolt/Core/ARMNeon.h +94 -0
  74. package/native/third_party/JoltPhysics/Jolt/Core/Array.h +713 -0
  75. package/native/third_party/JoltPhysics/Jolt/Core/Atomics.h +44 -0
  76. package/native/third_party/JoltPhysics/Jolt/Core/BinaryHeap.h +96 -0
  77. package/native/third_party/JoltPhysics/Jolt/Core/ByteBuffer.h +74 -0
  78. package/native/third_party/JoltPhysics/Jolt/Core/Color.cpp +38 -0
  79. package/native/third_party/JoltPhysics/Jolt/Core/Color.h +98 -0
  80. package/native/third_party/JoltPhysics/Jolt/Core/Core.h +652 -0
  81. package/native/third_party/JoltPhysics/Jolt/Core/FPControlWord.h +143 -0
  82. package/native/third_party/JoltPhysics/Jolt/Core/FPException.h +96 -0
  83. package/native/third_party/JoltPhysics/Jolt/Core/FPFlushDenormals.h +43 -0
  84. package/native/third_party/JoltPhysics/Jolt/Core/Factory.cpp +92 -0
  85. package/native/third_party/JoltPhysics/Jolt/Core/Factory.h +54 -0
  86. package/native/third_party/JoltPhysics/Jolt/Core/FixedSizeFreeList.h +122 -0
  87. package/native/third_party/JoltPhysics/Jolt/Core/FixedSizeFreeList.inl +215 -0
  88. package/native/third_party/JoltPhysics/Jolt/Core/HashCombine.h +234 -0
  89. package/native/third_party/JoltPhysics/Jolt/Core/HashTable.h +876 -0
  90. package/native/third_party/JoltPhysics/Jolt/Core/InsertionSort.h +58 -0
  91. package/native/third_party/JoltPhysics/Jolt/Core/IssueReporting.cpp +27 -0
  92. package/native/third_party/JoltPhysics/Jolt/Core/IssueReporting.h +38 -0
  93. package/native/third_party/JoltPhysics/Jolt/Core/JobSystem.h +311 -0
  94. package/native/third_party/JoltPhysics/Jolt/Core/JobSystem.inl +56 -0
  95. package/native/third_party/JoltPhysics/Jolt/Core/JobSystemSingleThreaded.cpp +65 -0
  96. package/native/third_party/JoltPhysics/Jolt/Core/JobSystemSingleThreaded.h +62 -0
  97. package/native/third_party/JoltPhysics/Jolt/Core/JobSystemThreadPool.cpp +364 -0
  98. package/native/third_party/JoltPhysics/Jolt/Core/JobSystemThreadPool.h +101 -0
  99. package/native/third_party/JoltPhysics/Jolt/Core/JobSystemWithBarrier.cpp +230 -0
  100. package/native/third_party/JoltPhysics/Jolt/Core/JobSystemWithBarrier.h +85 -0
  101. package/native/third_party/JoltPhysics/Jolt/Core/LinearCurve.cpp +51 -0
  102. package/native/third_party/JoltPhysics/Jolt/Core/LinearCurve.h +67 -0
  103. package/native/third_party/JoltPhysics/Jolt/Core/LockFreeHashMap.h +182 -0
  104. package/native/third_party/JoltPhysics/Jolt/Core/LockFreeHashMap.inl +351 -0
  105. package/native/third_party/JoltPhysics/Jolt/Core/Memory.cpp +85 -0
  106. package/native/third_party/JoltPhysics/Jolt/Core/Memory.h +85 -0
  107. package/native/third_party/JoltPhysics/Jolt/Core/Mutex.h +223 -0
  108. package/native/third_party/JoltPhysics/Jolt/Core/MutexArray.h +98 -0
  109. package/native/third_party/JoltPhysics/Jolt/Core/NonCopyable.h +18 -0
  110. package/native/third_party/JoltPhysics/Jolt/Core/Profiler.cpp +677 -0
  111. package/native/third_party/JoltPhysics/Jolt/Core/Profiler.h +301 -0
  112. package/native/third_party/JoltPhysics/Jolt/Core/Profiler.inl +90 -0
  113. package/native/third_party/JoltPhysics/Jolt/Core/QuickSort.h +137 -0
  114. package/native/third_party/JoltPhysics/Jolt/Core/RTTI.cpp +149 -0
  115. package/native/third_party/JoltPhysics/Jolt/Core/RTTI.h +436 -0
  116. package/native/third_party/JoltPhysics/Jolt/Core/Reference.h +244 -0
  117. package/native/third_party/JoltPhysics/Jolt/Core/Result.h +174 -0
  118. package/native/third_party/JoltPhysics/Jolt/Core/STLAlignedAllocator.h +72 -0
  119. package/native/third_party/JoltPhysics/Jolt/Core/STLAllocator.h +127 -0
  120. package/native/third_party/JoltPhysics/Jolt/Core/STLLocalAllocator.h +170 -0
  121. package/native/third_party/JoltPhysics/Jolt/Core/STLTempAllocator.h +80 -0
  122. package/native/third_party/JoltPhysics/Jolt/Core/ScopeExit.h +49 -0
  123. package/native/third_party/JoltPhysics/Jolt/Core/Semaphore.cpp +135 -0
  124. package/native/third_party/JoltPhysics/Jolt/Core/Semaphore.h +68 -0
  125. package/native/third_party/JoltPhysics/Jolt/Core/StaticArray.h +329 -0
  126. package/native/third_party/JoltPhysics/Jolt/Core/StreamIn.h +120 -0
  127. package/native/third_party/JoltPhysics/Jolt/Core/StreamOut.h +97 -0
  128. package/native/third_party/JoltPhysics/Jolt/Core/StreamUtils.h +168 -0
  129. package/native/third_party/JoltPhysics/Jolt/Core/StreamWrapper.h +53 -0
  130. package/native/third_party/JoltPhysics/Jolt/Core/StridedPtr.h +63 -0
  131. package/native/third_party/JoltPhysics/Jolt/Core/StringTools.cpp +101 -0
  132. package/native/third_party/JoltPhysics/Jolt/Core/StringTools.h +38 -0
  133. package/native/third_party/JoltPhysics/Jolt/Core/TempAllocator.h +209 -0
  134. package/native/third_party/JoltPhysics/Jolt/Core/TickCounter.cpp +37 -0
  135. package/native/third_party/JoltPhysics/Jolt/Core/TickCounter.h +58 -0
  136. package/native/third_party/JoltPhysics/Jolt/Core/UnorderedMap.h +80 -0
  137. package/native/third_party/JoltPhysics/Jolt/Core/UnorderedSet.h +32 -0
  138. package/native/third_party/JoltPhysics/Jolt/Geometry/AABox.h +313 -0
  139. package/native/third_party/JoltPhysics/Jolt/Geometry/AABox4.h +224 -0
  140. package/native/third_party/JoltPhysics/Jolt/Geometry/ClipPoly.h +200 -0
  141. package/native/third_party/JoltPhysics/Jolt/Geometry/ClosestPoint.h +498 -0
  142. package/native/third_party/JoltPhysics/Jolt/Geometry/ConvexHullBuilder.cpp +1467 -0
  143. package/native/third_party/JoltPhysics/Jolt/Geometry/ConvexHullBuilder.h +276 -0
  144. package/native/third_party/JoltPhysics/Jolt/Geometry/ConvexHullBuilder2D.cpp +335 -0
  145. package/native/third_party/JoltPhysics/Jolt/Geometry/ConvexHullBuilder2D.h +105 -0
  146. package/native/third_party/JoltPhysics/Jolt/Geometry/ConvexSupport.h +188 -0
  147. package/native/third_party/JoltPhysics/Jolt/Geometry/EPAConvexHullBuilder.h +845 -0
  148. package/native/third_party/JoltPhysics/Jolt/Geometry/EPAPenetrationDepth.h +557 -0
  149. package/native/third_party/JoltPhysics/Jolt/Geometry/Ellipse.h +77 -0
  150. package/native/third_party/JoltPhysics/Jolt/Geometry/GJKClosestPoint.h +945 -0
  151. package/native/third_party/JoltPhysics/Jolt/Geometry/IndexedTriangle.h +130 -0
  152. package/native/third_party/JoltPhysics/Jolt/Geometry/Indexify.cpp +222 -0
  153. package/native/third_party/JoltPhysics/Jolt/Geometry/Indexify.h +19 -0
  154. package/native/third_party/JoltPhysics/Jolt/Geometry/MortonCode.h +40 -0
  155. package/native/third_party/JoltPhysics/Jolt/Geometry/OrientedBox.cpp +178 -0
  156. package/native/third_party/JoltPhysics/Jolt/Geometry/OrientedBox.h +39 -0
  157. package/native/third_party/JoltPhysics/Jolt/Geometry/Plane.h +104 -0
  158. package/native/third_party/JoltPhysics/Jolt/Geometry/RayAABox.h +241 -0
  159. package/native/third_party/JoltPhysics/Jolt/Geometry/RayCapsule.h +37 -0
  160. package/native/third_party/JoltPhysics/Jolt/Geometry/RayCylinder.h +101 -0
  161. package/native/third_party/JoltPhysics/Jolt/Geometry/RaySphere.h +96 -0
  162. package/native/third_party/JoltPhysics/Jolt/Geometry/RayTriangle.h +158 -0
  163. package/native/third_party/JoltPhysics/Jolt/Geometry/Sphere.h +72 -0
  164. package/native/third_party/JoltPhysics/Jolt/Geometry/Triangle.h +34 -0
  165. package/native/third_party/JoltPhysics/Jolt/Jolt.cmake +703 -0
  166. package/native/third_party/JoltPhysics/Jolt/Jolt.h +16 -0
  167. package/native/third_party/JoltPhysics/Jolt/Jolt.natvis +116 -0
  168. package/native/third_party/JoltPhysics/Jolt/Math/BVec16.h +99 -0
  169. package/native/third_party/JoltPhysics/Jolt/Math/BVec16.inl +177 -0
  170. package/native/third_party/JoltPhysics/Jolt/Math/DMat44.h +158 -0
  171. package/native/third_party/JoltPhysics/Jolt/Math/DMat44.inl +310 -0
  172. package/native/third_party/JoltPhysics/Jolt/Math/DVec3.h +291 -0
  173. package/native/third_party/JoltPhysics/Jolt/Math/DVec3.inl +941 -0
  174. package/native/third_party/JoltPhysics/Jolt/Math/Double3.h +48 -0
  175. package/native/third_party/JoltPhysics/Jolt/Math/DynMatrix.h +31 -0
  176. package/native/third_party/JoltPhysics/Jolt/Math/EigenValueSymmetric.h +177 -0
  177. package/native/third_party/JoltPhysics/Jolt/Math/FindRoot.h +42 -0
  178. package/native/third_party/JoltPhysics/Jolt/Math/Float2.h +36 -0
  179. package/native/third_party/JoltPhysics/Jolt/Math/Float3.h +50 -0
  180. package/native/third_party/JoltPhysics/Jolt/Math/Float4.h +44 -0
  181. package/native/third_party/JoltPhysics/Jolt/Math/GaussianElimination.h +102 -0
  182. package/native/third_party/JoltPhysics/Jolt/Math/HalfFloat.h +208 -0
  183. package/native/third_party/JoltPhysics/Jolt/Math/Mat44.h +243 -0
  184. package/native/third_party/JoltPhysics/Jolt/Math/Mat44.inl +952 -0
  185. package/native/third_party/JoltPhysics/Jolt/Math/Math.h +208 -0
  186. package/native/third_party/JoltPhysics/Jolt/Math/MathTypes.h +32 -0
  187. package/native/third_party/JoltPhysics/Jolt/Math/Matrix.h +259 -0
  188. package/native/third_party/JoltPhysics/Jolt/Math/Quat.h +268 -0
  189. package/native/third_party/JoltPhysics/Jolt/Math/Quat.inl +406 -0
  190. package/native/third_party/JoltPhysics/Jolt/Math/Real.h +44 -0
  191. package/native/third_party/JoltPhysics/Jolt/Math/Swizzle.h +19 -0
  192. package/native/third_party/JoltPhysics/Jolt/Math/Trigonometry.h +79 -0
  193. package/native/third_party/JoltPhysics/Jolt/Math/UVec4.h +232 -0
  194. package/native/third_party/JoltPhysics/Jolt/Math/UVec4.inl +636 -0
  195. package/native/third_party/JoltPhysics/Jolt/Math/Vec3.cpp +71 -0
  196. package/native/third_party/JoltPhysics/Jolt/Math/Vec3.h +308 -0
  197. package/native/third_party/JoltPhysics/Jolt/Math/Vec3.inl +942 -0
  198. package/native/third_party/JoltPhysics/Jolt/Math/Vec4.h +320 -0
  199. package/native/third_party/JoltPhysics/Jolt/Math/Vec4.inl +1152 -0
  200. package/native/third_party/JoltPhysics/Jolt/Math/Vector.h +211 -0
  201. package/native/third_party/JoltPhysics/Jolt/ObjectStream/GetPrimitiveTypeOfType.h +54 -0
  202. package/native/third_party/JoltPhysics/Jolt/ObjectStream/ObjectStream.cpp +38 -0
  203. package/native/third_party/JoltPhysics/Jolt/ObjectStream/ObjectStream.h +337 -0
  204. package/native/third_party/JoltPhysics/Jolt/ObjectStream/ObjectStreamBinaryIn.cpp +252 -0
  205. package/native/third_party/JoltPhysics/Jolt/ObjectStream/ObjectStreamBinaryIn.h +57 -0
  206. package/native/third_party/JoltPhysics/Jolt/ObjectStream/ObjectStreamBinaryOut.cpp +165 -0
  207. package/native/third_party/JoltPhysics/Jolt/ObjectStream/ObjectStreamBinaryOut.h +57 -0
  208. package/native/third_party/JoltPhysics/Jolt/ObjectStream/ObjectStreamIn.cpp +635 -0
  209. package/native/third_party/JoltPhysics/Jolt/ObjectStream/ObjectStreamIn.h +148 -0
  210. package/native/third_party/JoltPhysics/Jolt/ObjectStream/ObjectStreamOut.cpp +166 -0
  211. package/native/third_party/JoltPhysics/Jolt/ObjectStream/ObjectStreamOut.h +101 -0
  212. package/native/third_party/JoltPhysics/Jolt/ObjectStream/ObjectStreamTextIn.cpp +418 -0
  213. package/native/third_party/JoltPhysics/Jolt/ObjectStream/ObjectStreamTextIn.h +55 -0
  214. package/native/third_party/JoltPhysics/Jolt/ObjectStream/ObjectStreamTextOut.cpp +255 -0
  215. package/native/third_party/JoltPhysics/Jolt/ObjectStream/ObjectStreamTextOut.h +62 -0
  216. package/native/third_party/JoltPhysics/Jolt/ObjectStream/ObjectStreamTypes.h +26 -0
  217. package/native/third_party/JoltPhysics/Jolt/ObjectStream/SerializableAttribute.h +111 -0
  218. package/native/third_party/JoltPhysics/Jolt/ObjectStream/SerializableAttributeEnum.h +67 -0
  219. package/native/third_party/JoltPhysics/Jolt/ObjectStream/SerializableAttributeTyped.h +60 -0
  220. package/native/third_party/JoltPhysics/Jolt/ObjectStream/SerializableObject.cpp +15 -0
  221. package/native/third_party/JoltPhysics/Jolt/ObjectStream/SerializableObject.h +170 -0
  222. package/native/third_party/JoltPhysics/Jolt/ObjectStream/TypeDeclarations.cpp +70 -0
  223. package/native/third_party/JoltPhysics/Jolt/ObjectStream/TypeDeclarations.h +45 -0
  224. package/native/third_party/JoltPhysics/Jolt/Physics/Body/AllowedDOFs.h +68 -0
  225. package/native/third_party/JoltPhysics/Jolt/Physics/Body/Body.cpp +426 -0
  226. package/native/third_party/JoltPhysics/Jolt/Physics/Body/Body.h +452 -0
  227. package/native/third_party/JoltPhysics/Jolt/Physics/Body/Body.inl +197 -0
  228. package/native/third_party/JoltPhysics/Jolt/Physics/Body/BodyAccess.h +68 -0
  229. package/native/third_party/JoltPhysics/Jolt/Physics/Body/BodyActivationListener.h +28 -0
  230. package/native/third_party/JoltPhysics/Jolt/Physics/Body/BodyCreationSettings.cpp +234 -0
  231. package/native/third_party/JoltPhysics/Jolt/Physics/Body/BodyCreationSettings.h +124 -0
  232. package/native/third_party/JoltPhysics/Jolt/Physics/Body/BodyFilter.h +130 -0
  233. package/native/third_party/JoltPhysics/Jolt/Physics/Body/BodyID.h +101 -0
  234. package/native/third_party/JoltPhysics/Jolt/Physics/Body/BodyInterface.cpp +1099 -0
  235. package/native/third_party/JoltPhysics/Jolt/Physics/Body/BodyInterface.h +324 -0
  236. package/native/third_party/JoltPhysics/Jolt/Physics/Body/BodyLock.h +111 -0
  237. package/native/third_party/JoltPhysics/Jolt/Physics/Body/BodyLockInterface.h +134 -0
  238. package/native/third_party/JoltPhysics/Jolt/Physics/Body/BodyLockMulti.h +120 -0
  239. package/native/third_party/JoltPhysics/Jolt/Physics/Body/BodyManager.cpp +1220 -0
  240. package/native/third_party/JoltPhysics/Jolt/Physics/Body/BodyManager.h +403 -0
  241. package/native/third_party/JoltPhysics/Jolt/Physics/Body/BodyPair.h +36 -0
  242. package/native/third_party/JoltPhysics/Jolt/Physics/Body/BodyType.h +19 -0
  243. package/native/third_party/JoltPhysics/Jolt/Physics/Body/MassProperties.cpp +185 -0
  244. package/native/third_party/JoltPhysics/Jolt/Physics/Body/MassProperties.h +58 -0
  245. package/native/third_party/JoltPhysics/Jolt/Physics/Body/MotionProperties.cpp +92 -0
  246. package/native/third_party/JoltPhysics/Jolt/Physics/Body/MotionProperties.h +308 -0
  247. package/native/third_party/JoltPhysics/Jolt/Physics/Body/MotionProperties.inl +178 -0
  248. package/native/third_party/JoltPhysics/Jolt/Physics/Body/MotionQuality.h +31 -0
  249. package/native/third_party/JoltPhysics/Jolt/Physics/Body/MotionType.h +17 -0
  250. package/native/third_party/JoltPhysics/Jolt/Physics/Character/Character.cpp +354 -0
  251. package/native/third_party/JoltPhysics/Jolt/Physics/Character/Character.h +159 -0
  252. package/native/third_party/JoltPhysics/Jolt/Physics/Character/CharacterBase.cpp +59 -0
  253. package/native/third_party/JoltPhysics/Jolt/Physics/Character/CharacterBase.h +157 -0
  254. package/native/third_party/JoltPhysics/Jolt/Physics/Character/CharacterID.h +98 -0
  255. package/native/third_party/JoltPhysics/Jolt/Physics/Character/CharacterVirtual.cpp +1933 -0
  256. package/native/third_party/JoltPhysics/Jolt/Physics/Character/CharacterVirtual.h +752 -0
  257. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/AABoxCast.h +20 -0
  258. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/ActiveEdgeMode.h +17 -0
  259. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/ActiveEdges.h +114 -0
  260. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/BackFaceMode.h +16 -0
  261. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/BroadPhase/BroadPhase.cpp +16 -0
  262. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/BroadPhase/BroadPhase.h +109 -0
  263. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/BroadPhase/BroadPhaseBruteForce.cpp +313 -0
  264. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/BroadPhase/BroadPhaseBruteForce.h +38 -0
  265. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/BroadPhase/BroadPhaseLayer.h +148 -0
  266. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/BroadPhase/BroadPhaseLayerInterfaceMask.h +92 -0
  267. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/BroadPhase/BroadPhaseLayerInterfaceTable.h +64 -0
  268. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/BroadPhase/BroadPhaseQuadTree.cpp +629 -0
  269. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/BroadPhase/BroadPhaseQuadTree.h +108 -0
  270. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/BroadPhase/BroadPhaseQuery.h +56 -0
  271. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/BroadPhase/ObjectVsBroadPhaseLayerFilterMask.h +35 -0
  272. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/BroadPhase/ObjectVsBroadPhaseLayerFilterTable.h +66 -0
  273. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/BroadPhase/QuadTree.cpp +1768 -0
  274. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/BroadPhase/QuadTree.h +389 -0
  275. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/CastConvexVsTriangles.cpp +107 -0
  276. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/CastConvexVsTriangles.h +46 -0
  277. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/CastResult.h +37 -0
  278. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/CastSphereVsTriangles.cpp +223 -0
  279. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/CastSphereVsTriangles.h +49 -0
  280. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/CollectFacesMode.h +16 -0
  281. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/CollideConvexVsTriangles.cpp +155 -0
  282. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/CollideConvexVsTriangles.h +56 -0
  283. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/CollidePointResult.h +25 -0
  284. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/CollideShape.h +106 -0
  285. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/CollideShapeVsShapePerLeaf.h +94 -0
  286. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/CollideSoftBodyVertexIterator.h +110 -0
  287. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/CollideSoftBodyVerticesVsTriangles.h +102 -0
  288. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/CollideSphereVsTriangles.cpp +121 -0
  289. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/CollideSphereVsTriangles.h +50 -0
  290. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/CollisionCollector.h +109 -0
  291. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/CollisionCollectorImpl.h +219 -0
  292. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/CollisionDispatch.cpp +107 -0
  293. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/CollisionDispatch.h +97 -0
  294. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/CollisionGroup.cpp +35 -0
  295. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/CollisionGroup.h +97 -0
  296. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/ContactListener.h +143 -0
  297. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/EstimateCollisionResponse.cpp +213 -0
  298. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/EstimateCollisionResponse.h +48 -0
  299. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/GroupFilter.cpp +32 -0
  300. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/GroupFilter.h +46 -0
  301. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/GroupFilterTable.cpp +38 -0
  302. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/GroupFilterTable.h +130 -0
  303. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/InternalEdgeRemovingCollector.h +279 -0
  304. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/ManifoldBetweenTwoFaces.cpp +271 -0
  305. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/ManifoldBetweenTwoFaces.h +44 -0
  306. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/NarrowPhaseQuery.cpp +448 -0
  307. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/NarrowPhaseQuery.h +77 -0
  308. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/NarrowPhaseStats.cpp +62 -0
  309. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/NarrowPhaseStats.h +110 -0
  310. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/ObjectLayer.h +111 -0
  311. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/ObjectLayerPairFilterMask.h +52 -0
  312. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/ObjectLayerPairFilterTable.h +78 -0
  313. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/PhysicsMaterial.cpp +35 -0
  314. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/PhysicsMaterial.h +57 -0
  315. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/PhysicsMaterialSimple.cpp +38 -0
  316. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/PhysicsMaterialSimple.h +37 -0
  317. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/RayCast.h +87 -0
  318. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/BoxShape.cpp +318 -0
  319. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/BoxShape.h +115 -0
  320. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/CapsuleShape.cpp +438 -0
  321. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/CapsuleShape.h +129 -0
  322. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/CompoundShape.cpp +433 -0
  323. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/CompoundShape.h +354 -0
  324. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/CompoundShapeVisitors.h +461 -0
  325. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/ConvexHullShape.cpp +1311 -0
  326. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/ConvexHullShape.h +202 -0
  327. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/ConvexShape.cpp +566 -0
  328. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/ConvexShape.h +150 -0
  329. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/CylinderShape.cpp +418 -0
  330. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/CylinderShape.h +126 -0
  331. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/DecoratedShape.cpp +87 -0
  332. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/DecoratedShape.h +80 -0
  333. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/EmptyShape.cpp +64 -0
  334. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/EmptyShape.h +75 -0
  335. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/GetTrianglesContext.h +248 -0
  336. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/HeightFieldShape.cpp +2754 -0
  337. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/HeightFieldShape.h +380 -0
  338. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/MeshShape.cpp +1305 -0
  339. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/MeshShape.h +228 -0
  340. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/MutableCompoundShape.cpp +596 -0
  341. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/MutableCompoundShape.h +176 -0
  342. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/OffsetCenterOfMassShape.cpp +217 -0
  343. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/OffsetCenterOfMassShape.h +140 -0
  344. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/PlaneShape.cpp +541 -0
  345. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/PlaneShape.h +147 -0
  346. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/PolyhedronSubmergedVolumeCalculator.h +319 -0
  347. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/RotatedTranslatedShape.cpp +333 -0
  348. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/RotatedTranslatedShape.h +161 -0
  349. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/ScaleHelpers.h +83 -0
  350. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/ScaledShape.cpp +238 -0
  351. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/ScaledShape.h +145 -0
  352. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/Shape.cpp +325 -0
  353. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/Shape.h +466 -0
  354. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/SphereShape.cpp +347 -0
  355. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/SphereShape.h +125 -0
  356. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/StaticCompoundShape.cpp +674 -0
  357. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/StaticCompoundShape.h +139 -0
  358. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/SubShapeID.h +138 -0
  359. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/SubShapeIDPair.h +65 -0
  360. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/TaperedCapsuleShape.cpp +453 -0
  361. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/TaperedCapsuleShape.gliffy +1 -0
  362. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/TaperedCapsuleShape.h +135 -0
  363. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/TaperedCylinderShape.cpp +691 -0
  364. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/TaperedCylinderShape.h +132 -0
  365. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/TriangleShape.cpp +430 -0
  366. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/Shape/TriangleShape.h +143 -0
  367. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/ShapeCast.h +173 -0
  368. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/ShapeFilter.h +73 -0
  369. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/SimShapeFilter.h +40 -0
  370. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/SimShapeFilterWrapper.h +58 -0
  371. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/SortReverseAndStore.h +48 -0
  372. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/TransformedShape.cpp +180 -0
  373. package/native/third_party/JoltPhysics/Jolt/Physics/Collision/TransformedShape.h +194 -0
  374. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/CalculateSolverSteps.h +70 -0
  375. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/ConeConstraint.cpp +246 -0
  376. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/ConeConstraint.h +133 -0
  377. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/Constraint.cpp +73 -0
  378. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/Constraint.h +243 -0
  379. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/ConstraintManager.cpp +289 -0
  380. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/ConstraintManager.h +100 -0
  381. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/ConstraintPart/AngleConstraintPart.h +257 -0
  382. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/ConstraintPart/AxisConstraintPart.h +682 -0
  383. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/ConstraintPart/DualAxisConstraintPart.h +276 -0
  384. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/ConstraintPart/GearConstraintPart.h +195 -0
  385. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/ConstraintPart/HingeRotationConstraintPart.h +222 -0
  386. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/ConstraintPart/IndependentAxisConstraintPart.h +246 -0
  387. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/ConstraintPart/PointConstraintPart.h +239 -0
  388. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/ConstraintPart/RackAndPinionConstraintPart.h +196 -0
  389. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/ConstraintPart/RotationEulerConstraintPart.h +283 -0
  390. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/ConstraintPart/RotationQuatConstraintPart.h +246 -0
  391. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/ConstraintPart/SpringPart.h +169 -0
  392. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/ConstraintPart/SwingTwistConstraintPart.h +597 -0
  393. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/ContactConstraintManager.cpp +1804 -0
  394. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/ContactConstraintManager.h +524 -0
  395. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/DistanceConstraint.cpp +266 -0
  396. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/DistanceConstraint.h +120 -0
  397. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/FixedConstraint.cpp +215 -0
  398. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/FixedConstraint.h +96 -0
  399. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/GearConstraint.cpp +188 -0
  400. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/GearConstraint.h +116 -0
  401. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/HingeConstraint.cpp +443 -0
  402. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/HingeConstraint.h +205 -0
  403. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/MotorSettings.cpp +43 -0
  404. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/MotorSettings.h +66 -0
  405. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/PathConstraint.cpp +458 -0
  406. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/PathConstraint.h +191 -0
  407. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/PathConstraintPath.cpp +85 -0
  408. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/PathConstraintPath.h +76 -0
  409. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/PathConstraintPathHermite.cpp +308 -0
  410. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/PathConstraintPathHermite.h +54 -0
  411. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/PointConstraint.cpp +157 -0
  412. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/PointConstraint.h +94 -0
  413. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/PulleyConstraint.cpp +253 -0
  414. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/PulleyConstraint.h +137 -0
  415. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/RackAndPinionConstraint.cpp +189 -0
  416. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/RackAndPinionConstraint.h +118 -0
  417. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/SixDOFConstraint.cpp +900 -0
  418. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/SixDOFConstraint.h +289 -0
  419. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/SliderConstraint.cpp +501 -0
  420. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/SliderConstraint.h +198 -0
  421. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/SpringSettings.cpp +35 -0
  422. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/SpringSettings.h +70 -0
  423. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/SwingTwistConstraint.cpp +524 -0
  424. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/SwingTwistConstraint.h +197 -0
  425. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/TwoBodyConstraint.cpp +56 -0
  426. package/native/third_party/JoltPhysics/Jolt/Physics/Constraints/TwoBodyConstraint.h +65 -0
  427. package/native/third_party/JoltPhysics/Jolt/Physics/DeterminismLog.cpp +17 -0
  428. package/native/third_party/JoltPhysics/Jolt/Physics/DeterminismLog.h +159 -0
  429. package/native/third_party/JoltPhysics/Jolt/Physics/EActivation.h +16 -0
  430. package/native/third_party/JoltPhysics/Jolt/Physics/EPhysicsUpdateError.h +37 -0
  431. package/native/third_party/JoltPhysics/Jolt/Physics/IslandBuilder.cpp +492 -0
  432. package/native/third_party/JoltPhysics/Jolt/Physics/IslandBuilder.h +144 -0
  433. package/native/third_party/JoltPhysics/Jolt/Physics/LargeIslandSplitter.cpp +582 -0
  434. package/native/third_party/JoltPhysics/Jolt/Physics/LargeIslandSplitter.h +187 -0
  435. package/native/third_party/JoltPhysics/Jolt/Physics/PhysicsLock.h +169 -0
  436. package/native/third_party/JoltPhysics/Jolt/Physics/PhysicsScene.cpp +261 -0
  437. package/native/third_party/JoltPhysics/Jolt/Physics/PhysicsScene.h +104 -0
  438. package/native/third_party/JoltPhysics/Jolt/Physics/PhysicsSettings.h +125 -0
  439. package/native/third_party/JoltPhysics/Jolt/Physics/PhysicsStepListener.h +37 -0
  440. package/native/third_party/JoltPhysics/Jolt/Physics/PhysicsSystem.cpp +2915 -0
  441. package/native/third_party/JoltPhysics/Jolt/Physics/PhysicsSystem.h +391 -0
  442. package/native/third_party/JoltPhysics/Jolt/Physics/PhysicsUpdateContext.cpp +25 -0
  443. package/native/third_party/JoltPhysics/Jolt/Physics/PhysicsUpdateContext.h +176 -0
  444. package/native/third_party/JoltPhysics/Jolt/Physics/Ragdoll/Ragdoll.cpp +744 -0
  445. package/native/third_party/JoltPhysics/Jolt/Physics/Ragdoll/Ragdoll.h +245 -0
  446. package/native/third_party/JoltPhysics/Jolt/Physics/SoftBody/SoftBodyContactListener.h +55 -0
  447. package/native/third_party/JoltPhysics/Jolt/Physics/SoftBody/SoftBodyCreationSettings.cpp +128 -0
  448. package/native/third_party/JoltPhysics/Jolt/Physics/SoftBody/SoftBodyCreationSettings.h +75 -0
  449. package/native/third_party/JoltPhysics/Jolt/Physics/SoftBody/SoftBodyManifold.h +74 -0
  450. package/native/third_party/JoltPhysics/Jolt/Physics/SoftBody/SoftBodyMotionProperties.cpp +1501 -0
  451. package/native/third_party/JoltPhysics/Jolt/Physics/SoftBody/SoftBodyMotionProperties.h +333 -0
  452. package/native/third_party/JoltPhysics/Jolt/Physics/SoftBody/SoftBodyShape.cpp +354 -0
  453. package/native/third_party/JoltPhysics/Jolt/Physics/SoftBody/SoftBodyShape.h +73 -0
  454. package/native/third_party/JoltPhysics/Jolt/Physics/SoftBody/SoftBodySharedSettings.cpp +1487 -0
  455. package/native/third_party/JoltPhysics/Jolt/Physics/SoftBody/SoftBodySharedSettings.h +390 -0
  456. package/native/third_party/JoltPhysics/Jolt/Physics/SoftBody/SoftBodyUpdateContext.h +63 -0
  457. package/native/third_party/JoltPhysics/Jolt/Physics/SoftBody/SoftBodyVertex.h +36 -0
  458. package/native/third_party/JoltPhysics/Jolt/Physics/StateRecorder.h +136 -0
  459. package/native/third_party/JoltPhysics/Jolt/Physics/StateRecorderImpl.cpp +90 -0
  460. package/native/third_party/JoltPhysics/Jolt/Physics/StateRecorderImpl.h +50 -0
  461. package/native/third_party/JoltPhysics/Jolt/Physics/Vehicle/MotorcycleController.cpp +306 -0
  462. package/native/third_party/JoltPhysics/Jolt/Physics/Vehicle/MotorcycleController.h +119 -0
  463. package/native/third_party/JoltPhysics/Jolt/Physics/Vehicle/TrackedVehicleController.cpp +547 -0
  464. package/native/third_party/JoltPhysics/Jolt/Physics/Vehicle/TrackedVehicleController.h +169 -0
  465. package/native/third_party/JoltPhysics/Jolt/Physics/Vehicle/VehicleAntiRollBar.cpp +33 -0
  466. package/native/third_party/JoltPhysics/Jolt/Physics/Vehicle/VehicleAntiRollBar.h +33 -0
  467. package/native/third_party/JoltPhysics/Jolt/Physics/Vehicle/VehicleCollisionTester.cpp +376 -0
  468. package/native/third_party/JoltPhysics/Jolt/Physics/Vehicle/VehicleCollisionTester.h +146 -0
  469. package/native/third_party/JoltPhysics/Jolt/Physics/Vehicle/VehicleConstraint.cpp +703 -0
  470. package/native/third_party/JoltPhysics/Jolt/Physics/Vehicle/VehicleConstraint.h +252 -0
  471. package/native/third_party/JoltPhysics/Jolt/Physics/Vehicle/VehicleController.cpp +17 -0
  472. package/native/third_party/JoltPhysics/Jolt/Physics/Vehicle/VehicleController.h +87 -0
  473. package/native/third_party/JoltPhysics/Jolt/Physics/Vehicle/VehicleDifferential.cpp +81 -0
  474. package/native/third_party/JoltPhysics/Jolt/Physics/Vehicle/VehicleDifferential.h +39 -0
  475. package/native/third_party/JoltPhysics/Jolt/Physics/Vehicle/VehicleEngine.cpp +122 -0
  476. package/native/third_party/JoltPhysics/Jolt/Physics/Vehicle/VehicleEngine.h +93 -0
  477. package/native/third_party/JoltPhysics/Jolt/Physics/Vehicle/VehicleTrack.cpp +52 -0
  478. package/native/third_party/JoltPhysics/Jolt/Physics/Vehicle/VehicleTrack.h +56 -0
  479. package/native/third_party/JoltPhysics/Jolt/Physics/Vehicle/VehicleTransmission.cpp +159 -0
  480. package/native/third_party/JoltPhysics/Jolt/Physics/Vehicle/VehicleTransmission.h +87 -0
  481. package/native/third_party/JoltPhysics/Jolt/Physics/Vehicle/Wheel.cpp +93 -0
  482. package/native/third_party/JoltPhysics/Jolt/Physics/Vehicle/Wheel.h +148 -0
  483. package/native/third_party/JoltPhysics/Jolt/Physics/Vehicle/WheeledVehicleController.cpp +866 -0
  484. package/native/third_party/JoltPhysics/Jolt/Physics/Vehicle/WheeledVehicleController.h +205 -0
  485. package/native/third_party/JoltPhysics/Jolt/RegisterTypes.cpp +204 -0
  486. package/native/third_party/JoltPhysics/Jolt/RegisterTypes.h +29 -0
  487. package/native/third_party/JoltPhysics/Jolt/Renderer/DebugRenderer.cpp +1107 -0
  488. package/native/third_party/JoltPhysics/Jolt/Renderer/DebugRenderer.h +383 -0
  489. package/native/third_party/JoltPhysics/Jolt/Renderer/DebugRendererPlayback.cpp +168 -0
  490. package/native/third_party/JoltPhysics/Jolt/Renderer/DebugRendererPlayback.h +48 -0
  491. package/native/third_party/JoltPhysics/Jolt/Renderer/DebugRendererRecorder.cpp +158 -0
  492. package/native/third_party/JoltPhysics/Jolt/Renderer/DebugRendererRecorder.h +130 -0
  493. package/native/third_party/JoltPhysics/Jolt/Renderer/DebugRendererSimple.cpp +80 -0
  494. package/native/third_party/JoltPhysics/Jolt/Renderer/DebugRendererSimple.h +88 -0
  495. package/native/third_party/JoltPhysics/Jolt/Skeleton/SkeletalAnimation.cpp +165 -0
  496. package/native/third_party/JoltPhysics/Jolt/Skeleton/SkeletalAnimation.h +91 -0
  497. package/native/third_party/JoltPhysics/Jolt/Skeleton/Skeleton.cpp +82 -0
  498. package/native/third_party/JoltPhysics/Jolt/Skeleton/Skeleton.h +72 -0
  499. package/native/third_party/JoltPhysics/Jolt/Skeleton/SkeletonMapper.cpp +237 -0
  500. package/native/third_party/JoltPhysics/Jolt/Skeleton/SkeletonMapper.h +145 -0
  501. package/native/third_party/JoltPhysics/Jolt/Skeleton/SkeletonPose.cpp +87 -0
  502. package/native/third_party/JoltPhysics/Jolt/Skeleton/SkeletonPose.h +82 -0
  503. package/native/third_party/JoltPhysics/Jolt/TriangleSplitter/TriangleSplitter.cpp +73 -0
  504. package/native/third_party/JoltPhysics/Jolt/TriangleSplitter/TriangleSplitter.h +84 -0
  505. package/native/third_party/JoltPhysics/Jolt/TriangleSplitter/TriangleSplitterBinning.cpp +139 -0
  506. package/native/third_party/JoltPhysics/Jolt/TriangleSplitter/TriangleSplitterBinning.h +52 -0
  507. package/native/third_party/JoltPhysics/Jolt/TriangleSplitter/TriangleSplitterMean.cpp +43 -0
  508. package/native/third_party/JoltPhysics/Jolt/TriangleSplitter/TriangleSplitterMean.h +28 -0
  509. package/native/third_party/JoltPhysics/LICENSE +7 -0
  510. package/native/third_party/JoltPhysics/README.md +173 -0
  511. package/native/third_party/bloom_jolt/CMakeLists.txt +78 -0
  512. package/native/third_party/bloom_jolt/include/bloom_jolt.h +519 -0
  513. package/native/third_party/bloom_jolt/src/bloom_jolt.cpp +1780 -0
  514. package/native/tvos/Cargo.lock +1692 -0
  515. package/native/tvos/Cargo.toml +22 -0
  516. package/native/tvos/src/lib.rs +3179 -0
  517. package/native/watchos/Cargo.lock +16 -0
  518. package/native/watchos/Cargo.toml +19 -0
  519. package/native/watchos/shaders/bloom_postfx.metal +99 -0
  520. package/native/watchos/src/BloomWatchApp.swift +1236 -0
  521. package/native/watchos/src/BloomWatchAudio.swift +179 -0
  522. package/native/watchos/src/audio.rs +55 -0
  523. package/native/watchos/src/draw_list.rs +223 -0
  524. package/native/watchos/src/ffi_stubs.rs +454 -0
  525. package/native/watchos/src/lib.rs +1013 -0
  526. package/native/watchos/src/models.rs +746 -0
  527. package/native/watchos/src/postfx.rs +91 -0
  528. package/native/watchos/src/scene.rs +534 -0
  529. package/native/watchos/src/textures.rs +184 -0
  530. package/native/web/Cargo.lock +1656 -0
  531. package/native/web/Cargo.toml +38 -0
  532. package/native/web/bloom_glue.js +218 -0
  533. package/native/web/build.sh +101 -0
  534. package/native/web/index.html +390 -0
  535. package/native/web/jolt_bridge.js +1311 -0
  536. package/native/web/src/lib.rs +2739 -0
  537. package/native/windows/Cargo.lock +1813 -0
  538. package/native/windows/Cargo.toml +31 -0
  539. package/native/windows/src/lib.rs +1933 -0
  540. package/package.json +558 -0
  541. package/src/audio/index.ts +151 -0
  542. package/src/core/colors.ts +56 -0
  543. package/src/core/index.ts +903 -0
  544. package/src/core/keys.ts +63 -0
  545. package/src/core/types.ts +102 -0
  546. package/src/index.ts +158 -0
  547. package/src/math/index.ts +502 -0
  548. package/src/mobile/index.ts +294 -0
  549. package/src/models/index.ts +859 -0
  550. package/src/physics/index.ts +1072 -0
  551. package/src/scene/index.ts +570 -0
  552. package/src/shapes/index.ts +120 -0
  553. package/src/text/index.ts +48 -0
  554. package/src/textures/index.ts +173 -0
  555. package/src/world/index.ts +22 -0
  556. package/src/world/loader.ts +385 -0
  557. package/src/world/prefab.ts +205 -0
  558. package/src/world/saver.ts +61 -0
  559. package/src/world/terrain.ts +254 -0
  560. package/src/world/types.ts +136 -0
  561. package/src/world/validate.ts +202 -0
  562. package/src/world/version.ts +47 -0
@@ -0,0 +1,3444 @@
1
+ // `static mut` is intentional throughout this FFI surface — Perry calls
2
+ // us from a single OS thread (the macOS run-loop), so the engine
3
+ // singleton + scratch state never race. The 2024 lint flagging
4
+ // `&LAST_PICK`-style accesses is a real concern in multi-threaded
5
+ // code, but inapplicable here. Suppress at the crate root to avoid
6
+ // 16+ noise lines in every build.
7
+ #![allow(static_mut_refs)]
8
+
9
+ use bloom_shared::engine::EngineState;
10
+ use bloom_shared::renderer::Renderer;
11
+ use bloom_shared::string_header::str_from_header;
12
+ use bloom_shared::audio::{parse_wav, parse_ogg, parse_mp3};
13
+
14
+ use objc2::rc::Retained;
15
+ use objc2::{msg_send, MainThreadMarker, MainThreadOnly};
16
+ use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSEventMask, NSEventType, NSWindow, NSWindowStyleMask};
17
+ use objc2_foundation::{NSDate, NSDefaultRunLoopMode, NSPoint, NSRect, NSSize, NSString};
18
+
19
+ use raw_window_handle::{RawWindowHandle, AppKitWindowHandle};
20
+ use std::sync::OnceLock;
21
+
22
+ static mut ENGINE: OnceLock<EngineState> = OnceLock::new();
23
+ static mut WINDOW: Option<Retained<NSWindow>> = None;
24
+ // Set by bloom_init_window when BLOOM_HEADLESS=1. Skips the
25
+ // `!isVisible → should_close` shortcut (hidden windows aren't
26
+ // 'closed', just invisible) so headless --capture can run to
27
+ // completion.
28
+ static mut HEADLESS: bool = false;
29
+ static mut AUDIO_UNIT: Option<AudioUnitInstance> = None;
30
+
31
+ fn engine() -> &'static mut EngineState {
32
+ unsafe { ENGINE.get_mut().expect("Engine not initialized") }
33
+ }
34
+
35
+ /// Map macOS virtual key code to Bloom key code.
36
+ fn map_keycode(keycode: u16) -> usize {
37
+ match keycode {
38
+ 0 => 65, // A
39
+ 1 => 83, // S
40
+ 2 => 68, // D
41
+ 3 => 70, // F
42
+ 4 => 72, // H
43
+ 5 => 71, // G
44
+ 6 => 90, // Z
45
+ 7 => 88, // X
46
+ 8 => 67, // C
47
+ 9 => 86, // V
48
+ 11 => 66, // B
49
+ 12 => 81, // Q
50
+ 13 => 87, // W
51
+ 14 => 69, // E
52
+ 15 => 82, // R
53
+ 16 => 89, // Y
54
+ 17 => 84, // T
55
+ 18 => 49, // 1
56
+ 19 => 50, // 2
57
+ 20 => 51, // 3
58
+ 21 => 52, // 4
59
+ 22 => 54, // 6
60
+ 23 => 53, // 5
61
+ 24 => 61, // =
62
+ 25 => 57, // 9
63
+ 26 => 55, // 7
64
+ 27 => 45, // -
65
+ 28 => 56, // 8
66
+ 29 => 48, // 0
67
+ 30 => 93, // ]
68
+ 31 => 79, // O
69
+ 32 => 85, // U
70
+ 33 => 91, // [
71
+ 34 => 73, // I
72
+ 35 => 80, // P
73
+ 36 => 265, // Enter (mapped to Bloom ENTER = 265)
74
+ 37 => 76, // L
75
+ 38 => 74, // J
76
+ 39 => 39, // '
77
+ 40 => 75, // K
78
+ 41 => 59, // ;
79
+ 42 => 92, // backslash
80
+ 43 => 44, // ,
81
+ 44 => 47, // /
82
+ 45 => 78, // N
83
+ 46 => 77, // M
84
+ 47 => 46, // .
85
+ 48 => 9, // Tab
86
+ 49 => 32, // Space
87
+ 50 => 96, // `
88
+ 51 => 8, // Backspace
89
+ 53 => 27, // Escape
90
+ // Arrow keys
91
+ 123 => 258, // Left
92
+ 124 => 259, // Right
93
+ 125 => 257, // Down
94
+ 126 => 256, // Up
95
+ // Function keys
96
+ 122 => 112, // F1
97
+ 120 => 113, // F2
98
+ 99 => 114, // F3
99
+ 118 => 115, // F4
100
+ 96 => 116, // F5
101
+ 97 => 117, // F6
102
+ 98 => 118, // F7
103
+ 100 => 119, // F8
104
+ 101 => 120, // F9
105
+ 109 => 121, // F10
106
+ 103 => 122, // F11
107
+ 111 => 123, // F12
108
+ // Modifiers
109
+ 56 => 280, // Left Shift
110
+ 60 => 281, // Right Shift
111
+ 59 => 282, // Left Control
112
+ 62 => 283, // Right Control
113
+ 58 => 284, // Left Alt/Option
114
+ 61 => 285, // Right Alt/Option
115
+ 55 => 286, // Left Command
116
+ 54 => 287, // Right Command
117
+ _ => 0,
118
+ }
119
+ }
120
+
121
+ // ============================================================
122
+ // CoreAudio FFI types and setup
123
+ // ============================================================
124
+
125
+ type AudioUnit = *mut std::ffi::c_void;
126
+ type OSStatus = i32;
127
+ type AudioUnitPropertyID = u32;
128
+ type AudioUnitScope = u32;
129
+ type AudioUnitElement = u32;
130
+
131
+ #[repr(C)]
132
+ #[derive(Clone, Copy)]
133
+ struct AudioComponentDescription {
134
+ component_type: u32,
135
+ component_sub_type: u32,
136
+ component_manufacturer: u32,
137
+ component_flags: u32,
138
+ component_flags_mask: u32,
139
+ }
140
+
141
+ #[repr(C)]
142
+ #[derive(Clone, Copy)]
143
+ struct AudioStreamBasicDescription {
144
+ sample_rate: f64,
145
+ format_id: u32,
146
+ format_flags: u32,
147
+ bytes_per_packet: u32,
148
+ frames_per_packet: u32,
149
+ bytes_per_frame: u32,
150
+ channels_per_frame: u32,
151
+ bits_per_channel: u32,
152
+ reserved: u32,
153
+ }
154
+
155
+ #[repr(C)]
156
+ struct AudioBufferList {
157
+ number_buffers: u32,
158
+ buffers: [AudioBuffer; 1],
159
+ }
160
+
161
+ #[repr(C)]
162
+ struct AudioBuffer {
163
+ number_channels: u32,
164
+ data_byte_size: u32,
165
+ data: *mut std::ffi::c_void,
166
+ }
167
+
168
+ type AURenderCallback = unsafe extern "C" fn(
169
+ in_ref_con: *mut std::ffi::c_void,
170
+ io_action_flags: *mut u32,
171
+ in_time_stamp: *const std::ffi::c_void,
172
+ in_bus_number: u32,
173
+ in_number_frames: u32,
174
+ io_data: *mut AudioBufferList,
175
+ ) -> OSStatus;
176
+
177
+ #[repr(C)]
178
+ struct AURenderCallbackStruct {
179
+ input_proc: AURenderCallback,
180
+ input_proc_ref_con: *mut std::ffi::c_void,
181
+ }
182
+
183
+ type AudioComponent = *mut std::ffi::c_void;
184
+
185
+ #[link(name = "AudioToolbox", kind = "framework")]
186
+ extern "C" {
187
+ fn AudioComponentFindNext(component: AudioComponent, desc: *const AudioComponentDescription) -> AudioComponent;
188
+ fn AudioComponentInstanceNew(component: AudioComponent, out: *mut AudioUnit) -> OSStatus;
189
+ fn AudioUnitSetProperty(
190
+ unit: AudioUnit,
191
+ property_id: AudioUnitPropertyID,
192
+ scope: AudioUnitScope,
193
+ element: AudioUnitElement,
194
+ data: *const std::ffi::c_void,
195
+ data_size: u32,
196
+ ) -> OSStatus;
197
+ fn AudioUnitInitialize(unit: AudioUnit) -> OSStatus;
198
+ fn AudioOutputUnitStart(unit: AudioUnit) -> OSStatus;
199
+ fn AudioOutputUnitStop(unit: AudioUnit) -> OSStatus;
200
+ fn AudioComponentInstanceDispose(unit: AudioUnit) -> OSStatus;
201
+ }
202
+
203
+ const K_AUDIO_UNIT_TYPE_OUTPUT: u32 = u32::from_be_bytes(*b"auou");
204
+ const K_AUDIO_UNIT_SUB_TYPE_DEFAULT_OUTPUT: u32 = u32::from_be_bytes(*b"def ");
205
+ const K_AUDIO_UNIT_MANUFACTURER_APPLE: u32 = u32::from_be_bytes(*b"appl");
206
+
207
+ const K_AUDIO_UNIT_PROPERTY_STREAM_FORMAT: AudioUnitPropertyID = 8;
208
+ const K_AUDIO_UNIT_PROPERTY_SET_RENDER_CALLBACK: AudioUnitPropertyID = 23;
209
+ const K_AUDIO_UNIT_SCOPE_INPUT: AudioUnitScope = 1;
210
+
211
+ const K_AUDIO_FORMAT_LINEAR_PCM: u32 = u32::from_be_bytes(*b"lpcm");
212
+ const K_AUDIO_FORMAT_FLAG_IS_FLOAT: u32 = 1;
213
+ const K_AUDIO_FORMAT_FLAG_IS_PACKED: u32 = 8;
214
+
215
+ struct AudioUnitInstance {
216
+ unit: AudioUnit,
217
+ }
218
+
219
+ // Safety: AudioUnit is accessed only from audio thread callback + main thread init/deinit
220
+ unsafe impl Send for AudioUnitInstance {}
221
+ unsafe impl Sync for AudioUnitInstance {}
222
+
223
+ unsafe extern "C" fn audio_render_callback(
224
+ _in_ref_con: *mut std::ffi::c_void,
225
+ _io_action_flags: *mut u32,
226
+ _in_time_stamp: *const std::ffi::c_void,
227
+ _in_bus_number: u32,
228
+ in_number_frames: u32,
229
+ io_data: *mut AudioBufferList,
230
+ ) -> OSStatus {
231
+ let buffer_list = &mut *io_data;
232
+ let buffer = &mut buffer_list.buffers[0];
233
+ let num_samples = in_number_frames as usize * 2; // stereo
234
+ let output = std::slice::from_raw_parts_mut(
235
+ buffer.data as *mut f32,
236
+ num_samples,
237
+ );
238
+
239
+ ENGINE.get_mut().map(|eng| {
240
+ eng.audio.mix_output(output);
241
+ });
242
+
243
+ 0 // noErr
244
+ }
245
+
246
+ // ============================================================
247
+ // FFI entry points
248
+ // ============================================================
249
+
250
+ #[no_mangle]
251
+ pub extern "C" fn bloom_init_window(width: f64, height: f64, title_ptr: *const u8, fullscreen: f64) {
252
+ let title = str_from_header(title_ptr);
253
+ let mtm = MainThreadMarker::from(unsafe { MainThreadMarker::new_unchecked() });
254
+
255
+ // Headless mode: BLOOM_HEADLESS=1 keeps the NSWindow + CAMetalLayer
256
+ // alive (wgpu's Metal backend requires a CAMetalLayer-backed view)
257
+ // but hides the window and suppresses dock icon / focus. Needed
258
+ // so an agent can spin up the renderer in a batch loop without
259
+ // stealing the user's focus on every sample.
260
+ let headless = std::env::var("BLOOM_HEADLESS")
261
+ .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
262
+ .unwrap_or(false);
263
+ unsafe { HEADLESS = headless; }
264
+
265
+ let app = NSApplication::sharedApplication(mtm);
266
+ if headless {
267
+ // Prohibited = no dock icon, no menu bar, no activation.
268
+ app.setActivationPolicy(NSApplicationActivationPolicy::Prohibited);
269
+ } else {
270
+ app.setActivationPolicy(NSApplicationActivationPolicy::Regular);
271
+ }
272
+
273
+ // Far-off-screen origin keeps the window out of every display
274
+ // even if the OS insists on showing something.
275
+ let origin_x = if headless { -20000.0 } else { 100.0 };
276
+ let content_rect = NSRect::new(NSPoint::new(origin_x, 100.0), NSSize::new(width, height));
277
+ let style = NSWindowStyleMask::Titled
278
+ | NSWindowStyleMask::Closable
279
+ | NSWindowStyleMask::Miniaturizable
280
+ | NSWindowStyleMask::Resizable;
281
+
282
+ let window = unsafe {
283
+ NSWindow::initWithContentRect_styleMask_backing_defer(
284
+ NSWindow::alloc(mtm),
285
+ content_rect,
286
+ style,
287
+ objc2_app_kit::NSBackingStoreType(2), // NSBackingStoreBuffered
288
+ false,
289
+ )
290
+ };
291
+
292
+ let ns_title = NSString::from_str(title);
293
+ window.setTitle(&ns_title);
294
+
295
+ // Don't persist window size/fullscreen state across launches.
296
+ // NSWindow restoration was resurrecting a prior fullscreen toggle on
297
+ // the 4K display, which silently rendered benchmarks at 4× the
298
+ // requested pixel count.
299
+ unsafe { let _: () = msg_send![&window, setRestorable: false]; }
300
+
301
+ // BLOOM_NO_FULLSCREEN=1 hard-disables fullscreen capability: the
302
+ // window cannot be entered into fullscreen via the green button,
303
+ // cmd-ctrl-F, or inheriting a fullscreen Space from the launching
304
+ // terminal. Intended for benchmark harnesses where the 4K-display
305
+ // fullscreen path would otherwise silently quadruple render cost.
306
+ let no_fullscreen = std::env::var("BLOOM_NO_FULLSCREEN")
307
+ .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
308
+ .unwrap_or(false);
309
+ if no_fullscreen {
310
+ window.setCollectionBehavior(objc2_app_kit::NSWindowCollectionBehavior::FullScreenNone);
311
+ }
312
+
313
+ if headless {
314
+ // Alpha 0 + off-screen origin + prohibited-activation app
315
+ // policy = fully invisible window that still backs the
316
+ // CAMetalLayer wgpu renders into. Don't call
317
+ // `makeKeyAndOrderFront` — that brings it to front.
318
+ unsafe {
319
+ let _: () = msg_send![&window, setAlphaValue: 0.0_f64];
320
+ let _: () = msg_send![&window, setIgnoresMouseEvents: true];
321
+ let _: () = msg_send![&window, orderOut: std::ptr::null::<objc2::runtime::AnyObject>()];
322
+ }
323
+ } else {
324
+ window.center();
325
+ window.makeKeyAndOrderFront(None);
326
+ #[allow(deprecated)]
327
+ app.activateIgnoringOtherApps(true);
328
+ }
329
+
330
+ // Set up CAMetalLayer on the content view
331
+ let content_view = window.contentView().expect("No content view");
332
+ unsafe {
333
+ let _: () = msg_send![&content_view, setWantsLayer: true];
334
+ }
335
+
336
+ // Create wgpu surface and renderer
337
+ // wgpu expects the NSView pointer (not NSWindow) for AppKit
338
+ let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
339
+ backends: wgpu::Backends::METAL,
340
+ ..wgpu::InstanceDescriptor::new_without_display_handle()
341
+ });
342
+
343
+ let surface = unsafe {
344
+ let view_ptr = Retained::as_ptr(&content_view) as *mut std::ffi::c_void;
345
+ let handle = AppKitWindowHandle::new(
346
+ std::ptr::NonNull::new(view_ptr).unwrap()
347
+ );
348
+ let raw = RawWindowHandle::AppKit(handle);
349
+ instance.create_surface_unsafe(wgpu::SurfaceTargetUnsafe::RawHandle {
350
+ raw_display_handle: Some(raw_window_handle::RawDisplayHandle::AppKit(raw_window_handle::AppKitDisplayHandle::new())),
351
+ raw_window_handle: raw,
352
+ }).expect("Failed to create surface")
353
+ };
354
+
355
+ let adapter = pollster_block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
356
+ compatible_surface: Some(&surface),
357
+ power_preference: wgpu::PowerPreference::HighPerformance,
358
+ ..Default::default()
359
+ })).expect("No adapter found");
360
+
361
+ // Request TIMESTAMP_QUERY when the adapter supports it so the profiler
362
+ // can collect GPU timings. It's optional — profiler falls back to CPU
363
+ // only when the feature isn't available.
364
+ let supported = adapter.features();
365
+ let mut required_features = wgpu::Features::empty();
366
+ if supported.contains(wgpu::Features::TIMESTAMP_QUERY) {
367
+ required_features |= wgpu::Features::TIMESTAMP_QUERY;
368
+ }
369
+ // Ticket 007b: request ray-query + BLAS/TLAS where the adapter
370
+ // supports both (Apple Silicon Metal, DXR 1.1, VK_KHR_ray_query).
371
+ // `BLOOM_FORCE_SW_GI=1` forces the SW fallback for testing parity
372
+ // with non-RT adapters.
373
+ let force_sw_gi = std::env::var("BLOOM_FORCE_SW_GI")
374
+ .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
375
+ .unwrap_or(false);
376
+ // wgpu 29 gates BLAS/TLAS creation + ray-query WGSL on a single
377
+ // feature bit; there's no separate "acceleration structure" flag.
378
+ let rt_mask = wgpu::Features::EXPERIMENTAL_RAY_QUERY;
379
+ if !force_sw_gi && supported.contains(rt_mask) {
380
+ required_features |= rt_mask;
381
+ }
382
+ // wgpu 29 requires an explicit `ExperimentalFeatures::enabled()` token
383
+ // when requesting any `EXPERIMENTAL_*` feature (ray query in our case).
384
+ // The token is constructed through an `unsafe` API acknowledging that
385
+ // experimental paths may hit undefined behavior — Apple Silicon's Metal
386
+ // ray-query path has been stable in wgpu releases since v25 so we're
387
+ // willing to take that risk here.
388
+ let experimental_features = if required_features.intersects(rt_mask) {
389
+ unsafe { wgpu::ExperimentalFeatures::enabled() }
390
+ } else {
391
+ wgpu::ExperimentalFeatures::disabled()
392
+ };
393
+ // Acceleration-structure limits default to 0 when RT is disabled.
394
+ // `using_minimum_supported_acceleration_structure_values` bumps
395
+ // them to the spec minimums (2^24 BLAS geometries / TLAS instances,
396
+ // etc.) whenever ray query was granted.
397
+ let mut required_limits = wgpu::Limits::default();
398
+ // Phase 1c: the material ABI declares 5 bind groups (PerFrame,
399
+ // PerView, PerMaterial, PerDraw, SceneInputs). wgpu's default
400
+ // limit is 4. Metal / Vulkan / D3D12 support at least 7, so 5 is
401
+ // safely within every real backend's capabilities.
402
+ required_limits.max_bind_groups = 5;
403
+ if required_features.intersects(rt_mask) {
404
+ required_limits = required_limits
405
+ .using_minimum_supported_acceleration_structure_values();
406
+ }
407
+ let (device, queue) = pollster_block_on(adapter.request_device(
408
+ &wgpu::DeviceDescriptor {
409
+ label: Some("bloom_device"),
410
+ required_features,
411
+ required_limits,
412
+ experimental_features,
413
+ ..Default::default()
414
+ },
415
+ )).expect("Failed to create device");
416
+
417
+ let surface_caps = surface.get_capabilities(&adapter);
418
+ let format = surface_caps.formats.iter()
419
+ .find(|f| f.is_srgb())
420
+ .copied()
421
+ .unwrap_or(surface_caps.formats[0]);
422
+
423
+ // Retina/HiDPI: AppKit reports window dimensions in points, but
424
+ // CAMetalLayer's drawable needs to be sized in physical pixels or
425
+ // AppKit will bilinearly upscale a low-res image to the display.
426
+ // `backingScaleFactor` is typically 2.0 on Retina Macs, 1.0
427
+ // otherwise; on mixed-DPI setups it tracks whichever screen the
428
+ // window is on.
429
+ let scale: f64 = unsafe { msg_send![&*window, backingScaleFactor] };
430
+ let scale = if scale > 0.0 { scale } else { 1.0 };
431
+ let logical_w = width as u32;
432
+ let logical_h = height as u32;
433
+ let physical_w = ((width * scale) as u32).max(1);
434
+ let physical_h = ((height * scale) as u32).max(1);
435
+
436
+ let surface_config = wgpu::SurfaceConfiguration {
437
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
438
+ format,
439
+ width: physical_w,
440
+ height: physical_h,
441
+ present_mode: wgpu::PresentMode::Fifo,
442
+ alpha_mode: surface_caps.alpha_modes[0],
443
+ view_formats: vec![],
444
+ desired_maximum_frame_latency: 2,
445
+ };
446
+ surface.configure(&device, &surface_config);
447
+
448
+ let renderer = Renderer::new(device, queue, surface, surface_config, logical_w, logical_h);
449
+ let engine_state = EngineState::new(renderer);
450
+
451
+ unsafe {
452
+ let _ = ENGINE.set(engine_state);
453
+ WINDOW = Some(window);
454
+ }
455
+
456
+ // Register Bloom's GPU screenshot capture with perry-geisterhand (if linked)
457
+ bloom_register_geisterhand_screenshot();
458
+
459
+ if fullscreen != 0.0 {
460
+ bloom_toggle_fullscreen();
461
+ }
462
+ }
463
+
464
+ #[no_mangle]
465
+ pub extern "C" fn bloom_close_window() {
466
+ unsafe {
467
+ WINDOW = None;
468
+ }
469
+ }
470
+
471
+ #[no_mangle]
472
+ pub extern "C" fn bloom_window_should_close() -> f64 {
473
+ if engine().should_close { 1.0 } else { 0.0 }
474
+ }
475
+
476
+ #[no_mangle]
477
+ pub extern "C" fn bloom_begin_drawing() {
478
+ // Poll events
479
+ let mtm = unsafe { MainThreadMarker::new_unchecked() };
480
+ let app = NSApplication::sharedApplication(mtm);
481
+
482
+ loop {
483
+ let event = unsafe {
484
+ app.nextEventMatchingMask_untilDate_inMode_dequeue(
485
+ NSEventMask::Any,
486
+ Some(&NSDate::distantPast()),
487
+ NSDefaultRunLoopMode,
488
+ true,
489
+ )
490
+ };
491
+ match event {
492
+ Some(event) => {
493
+ let event_type = event.r#type();
494
+ match event_type {
495
+ NSEventType::KeyDown => {
496
+ let keycode = event.keyCode();
497
+ let bloom_key = map_keycode(keycode);
498
+ if bloom_key > 0 {
499
+ engine().input.set_key_down(bloom_key);
500
+ }
501
+ // Extract typed characters for text input (E3b).
502
+ let chars_obj = event.characters();
503
+ if let Some(chars) = chars_obj {
504
+ let s = chars.to_string();
505
+ for c in s.chars() {
506
+ let cp = c as u32;
507
+ // Filter out control characters (keep printable + backspace).
508
+ if cp >= 32 || cp == 8 || cp == 13 || cp == 9 {
509
+ engine().input.push_char(cp);
510
+ }
511
+ }
512
+ }
513
+ }
514
+ NSEventType::KeyUp => {
515
+ let keycode = event.keyCode();
516
+ let bloom_key = map_keycode(keycode);
517
+ if bloom_key > 0 {
518
+ engine().input.set_key_up(bloom_key);
519
+ }
520
+ }
521
+ NSEventType::MouseMoved | NSEventType::LeftMouseDragged | NSEventType::RightMouseDragged => {
522
+ if engine().input.cursor_disabled {
523
+ // In disabled-cursor mode, use raw deltas from NSEvent
524
+ let dx: f64 = unsafe { msg_send![&*event, deltaX] };
525
+ let dy: f64 = unsafe { msg_send![&*event, deltaY] };
526
+ engine().input.accumulate_mouse_delta(dx, dy);
527
+ } else if let Some(window) = unsafe { &WINDOW } {
528
+ let loc = event.locationInWindow();
529
+ let frame = window.contentView().map(|v| v.frame()).unwrap_or(NSRect::ZERO);
530
+ engine().input.set_mouse_position(loc.x, frame.size.height - loc.y);
531
+ }
532
+ }
533
+ NSEventType::LeftMouseDown => {
534
+ engine().input.set_mouse_button_down(0);
535
+ }
536
+ NSEventType::LeftMouseUp => {
537
+ engine().input.set_mouse_button_up(0);
538
+ }
539
+ NSEventType::RightMouseDown => {
540
+ engine().input.set_mouse_button_down(1);
541
+ }
542
+ NSEventType::RightMouseUp => {
543
+ engine().input.set_mouse_button_up(1);
544
+ }
545
+ NSEventType::ScrollWheel => {
546
+ // NSEvent's scrollingDeltaY is positive when scrolling up
547
+ // (away from the user). Normalize to "positive = zoom in"
548
+ // by flipping the sign — matches editor orbit convention.
549
+ let dy: f64 = unsafe { msg_send![&*event, scrollingDeltaY] };
550
+ engine().input.accumulate_mouse_wheel(dy);
551
+ }
552
+ _ => {}
553
+ }
554
+ app.sendEvent(&event);
555
+ }
556
+ None => break,
557
+ }
558
+ }
559
+
560
+ // Check if window was closed. In headless mode the window is
561
+ // intentionally hidden — skip the isVisible check so --capture
562
+ // can actually run to completion without instant-exit.
563
+ let is_headless = unsafe { HEADLESS };
564
+ if !is_headless && unsafe { WINDOW.as_ref().map(|w| !w.isVisible()).unwrap_or(true) } {
565
+ engine().should_close = true;
566
+ }
567
+
568
+ // Handle window resize — track physical (backing) size for the
569
+ // swapchain while keeping the logical (points) size for user code.
570
+ if let Some(window) = unsafe { &WINDOW } {
571
+ if let Some(content_view) = window.contentView() {
572
+ let frame = content_view.frame();
573
+ let logical_w = frame.size.width as u32;
574
+ let logical_h = frame.size.height as u32;
575
+ let scale: f64 = unsafe { msg_send![&*window, backingScaleFactor] };
576
+ let scale = if scale > 0.0 { scale } else { 1.0 };
577
+ let physical_w = ((frame.size.width * scale) as u32).max(1);
578
+ let physical_h = ((frame.size.height * scale) as u32).max(1);
579
+ let eng = engine();
580
+ if logical_w > 0 && logical_h > 0
581
+ && (physical_w != eng.renderer.physical_width()
582
+ || physical_h != eng.renderer.physical_height()
583
+ || logical_w != eng.renderer.width()
584
+ || logical_h != eng.renderer.height())
585
+ {
586
+ eng.renderer.resize(physical_w, physical_h, logical_w, logical_h);
587
+ }
588
+ }
589
+ }
590
+
591
+ // Apply cursor shape (Q2). resizeLeftRightCursor /
592
+ // resizeUpDownCursor are deprecated in newer AppKit but still
593
+ // function; the suggested replacements need a richer per-frame-
594
+ // axis API we don't have. Keep + suppress the deprecation
595
+ // warning until we land that.
596
+ #[allow(deprecated)]
597
+ match engine().input.cursor_shape {
598
+ 1 => objc2_app_kit::NSCursor::pointingHandCursor().set(),
599
+ 2 => objc2_app_kit::NSCursor::openHandCursor().set(),
600
+ 3 => objc2_app_kit::NSCursor::IBeamCursor().set(),
601
+ 4 => objc2_app_kit::NSCursor::resizeLeftRightCursor().set(),
602
+ 5 => objc2_app_kit::NSCursor::resizeUpDownCursor().set(),
603
+ 6 => objc2_app_kit::NSCursor::crosshairCursor().set(),
604
+ _ => {},
605
+ }
606
+
607
+ engine().begin_frame();
608
+ }
609
+
610
+ #[no_mangle]
611
+ pub extern "C" fn bloom_end_drawing() {
612
+ // Pump geisterhand BEFORE end_frame.
613
+ // Screenshot function re-renders inline with captured VP + vertices.
614
+ extern "C" { fn perry_geisterhand_pump(); }
615
+ unsafe { perry_geisterhand_pump(); }
616
+
617
+ engine().end_frame();
618
+ }
619
+
620
+ /// Request a PNG screenshot of the next rendered frame.
621
+ /// The capture happens during the next end_drawing(), so the caller
622
+ /// should call beginDrawing/endDrawing once after this for the file
623
+ /// to actually appear on disk. Used by bloom-diff and CI image
624
+ /// regression workflows.
625
+ #[no_mangle]
626
+ pub extern "C" fn bloom_take_screenshot(path_ptr: *const u8) {
627
+ let path = str_from_header(path_ptr).to_string();
628
+ let eng = engine();
629
+ eng.renderer.screenshot_requested = true;
630
+ eng.renderer.pending_screenshot_path = Some(path);
631
+ }
632
+
633
+ #[no_mangle]
634
+ pub extern "C" fn bloom_clear_background(r: f64, g: f64, b: f64, a: f64) {
635
+ engine().renderer.set_clear_color(r, g, b, a);
636
+ }
637
+
638
+ /// Load an HDR equirectangular environment map and upload it to the
639
+ /// GPU. Subsequent frames sample it per-background-pixel via a sky
640
+ /// pass, so the background matches a path-traced reference instead of
641
+ /// being a flat clear color. The file must be Radiance HDR (.hdr).
642
+ #[no_mangle]
643
+ pub extern "C" fn bloom_set_env_clear_from_hdr(path_ptr: *const u8) {
644
+ use image::ImageDecoder;
645
+ let path = str_from_header(path_ptr).to_string();
646
+ let file = match std::fs::File::open(&path) {
647
+ Ok(f) => f,
648
+ Err(_) => return,
649
+ };
650
+ let decoder = match image::codecs::hdr::HdrDecoder::new(std::io::BufReader::new(file)) {
651
+ Ok(d) => d,
652
+ Err(_) => return,
653
+ };
654
+ let (w, h) = decoder.dimensions();
655
+ let byte_len = (w as usize) * (h as usize) * 3 * 4;
656
+ let mut buf = vec![0u8; byte_len];
657
+ if decoder.read_image(&mut buf).is_err() {
658
+ return;
659
+ }
660
+ // Reinterpret the byte buffer as f32 RGB triples for the renderer.
661
+ let rgb_f32: Vec<f32> = buf
662
+ .chunks_exact(4)
663
+ .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
664
+ .collect();
665
+ engine().renderer.load_env_from_hdr(w, h, &rgb_f32);
666
+ }
667
+
668
+ #[no_mangle]
669
+ pub extern "C" fn bloom_set_target_fps(fps: f64) {
670
+ engine().target_fps = fps;
671
+ }
672
+
673
+ #[no_mangle]
674
+ pub extern "C" fn bloom_set_direct_2d_mode(on: f64) {
675
+ engine().direct_2d_mode = on > 0.5;
676
+ }
677
+
678
+ #[no_mangle]
679
+ pub extern "C" fn bloom_get_delta_time() -> f64 {
680
+ engine().delta_time
681
+ }
682
+
683
+ #[no_mangle]
684
+ pub extern "C" fn bloom_get_fps() -> f64 {
685
+ engine().get_fps()
686
+ }
687
+
688
+ #[no_mangle]
689
+ pub extern "C" fn bloom_get_screen_width() -> f64 {
690
+ engine().screen_width()
691
+ }
692
+
693
+ #[no_mangle]
694
+ pub extern "C" fn bloom_get_screen_height() -> f64 {
695
+ engine().screen_height()
696
+ }
697
+
698
+ #[no_mangle]
699
+ pub extern "C" fn bloom_get_time() -> f64 {
700
+ engine().get_time()
701
+ }
702
+
703
+ // ============================================================
704
+ // Input - Keyboard
705
+ // ============================================================
706
+
707
+ #[no_mangle]
708
+ pub extern "C" fn bloom_is_key_pressed(key: f64) -> f64 {
709
+ if engine().input.is_key_pressed(key as usize) { 1.0 } else { 0.0 }
710
+ }
711
+
712
+ #[no_mangle]
713
+ pub extern "C" fn bloom_is_key_down(key: f64) -> f64 {
714
+ if engine().input.is_key_down(key as usize) { 1.0 } else { 0.0 }
715
+ }
716
+
717
+ #[no_mangle]
718
+ pub extern "C" fn bloom_is_key_released(key: f64) -> f64 {
719
+ if engine().input.is_key_released(key as usize) { 1.0 } else { 0.0 }
720
+ }
721
+
722
+ // ============================================================
723
+ // Input - Mouse
724
+ // ============================================================
725
+
726
+ #[no_mangle]
727
+ pub extern "C" fn bloom_get_mouse_x() -> f64 {
728
+ engine().input.mouse_x
729
+ }
730
+
731
+ #[no_mangle]
732
+ pub extern "C" fn bloom_get_mouse_y() -> f64 {
733
+ engine().input.mouse_y
734
+ }
735
+
736
+ #[no_mangle]
737
+ pub extern "C" fn bloom_is_mouse_button_pressed(btn: f64) -> f64 {
738
+ if engine().input.is_mouse_button_pressed(btn as usize) { 1.0 } else { 0.0 }
739
+ }
740
+
741
+ #[no_mangle]
742
+ pub extern "C" fn bloom_is_mouse_button_down(btn: f64) -> f64 {
743
+ if engine().input.is_mouse_button_down(btn as usize) { 1.0 } else { 0.0 }
744
+ }
745
+
746
+ #[no_mangle]
747
+ pub extern "C" fn bloom_is_mouse_button_released(btn: f64) -> f64 {
748
+ if engine().input.is_mouse_button_released(btn as usize) { 1.0 } else { 0.0 }
749
+ }
750
+
751
+ // ============================================================
752
+ // Input - Gamepad
753
+ // ============================================================
754
+
755
+ #[no_mangle]
756
+ pub extern "C" fn bloom_is_gamepad_available(gamepad: f64) -> f64 {
757
+ let _ = gamepad;
758
+ if engine().input.is_gamepad_available() { 1.0 } else { 0.0 }
759
+ }
760
+
761
+ #[no_mangle]
762
+ pub extern "C" fn bloom_get_gamepad_axis(gamepad: f64, axis: f64) -> f64 {
763
+ let _ = gamepad;
764
+ engine().input.get_gamepad_axis(axis as usize) as f64
765
+ }
766
+
767
+ #[no_mangle]
768
+ pub extern "C" fn bloom_is_gamepad_button_pressed(gamepad: f64, button: f64) -> f64 {
769
+ let _ = gamepad;
770
+ if engine().input.is_gamepad_button_pressed(button as usize) { 1.0 } else { 0.0 }
771
+ }
772
+
773
+ #[no_mangle]
774
+ pub extern "C" fn bloom_is_gamepad_button_down(gamepad: f64, button: f64) -> f64 {
775
+ let _ = gamepad;
776
+ if engine().input.is_gamepad_button_down(button as usize) { 1.0 } else { 0.0 }
777
+ }
778
+
779
+ #[no_mangle]
780
+ pub extern "C" fn bloom_is_gamepad_button_released(gamepad: f64, button: f64) -> f64 {
781
+ let _ = gamepad;
782
+ if engine().input.is_gamepad_button_released(button as usize) { 1.0 } else { 0.0 }
783
+ }
784
+
785
+ #[no_mangle]
786
+ pub extern "C" fn bloom_get_gamepad_axis_count(gamepad: f64) -> f64 {
787
+ let _ = gamepad;
788
+ engine().input.get_gamepad_axis_count() as f64
789
+ }
790
+
791
+ // ============================================================
792
+ // Input - Touch
793
+ // ============================================================
794
+
795
+ #[no_mangle]
796
+ pub extern "C" fn bloom_get_touch_x() -> f64 {
797
+ engine().input.get_touch_x(0)
798
+ }
799
+
800
+ #[no_mangle]
801
+ pub extern "C" fn bloom_get_touch_y() -> f64 {
802
+ engine().input.get_touch_y(0)
803
+ }
804
+
805
+ #[no_mangle]
806
+ pub extern "C" fn bloom_get_touch_count() -> f64 {
807
+ engine().input.get_touch_count() as f64
808
+ }
809
+
810
+ // ============================================================
811
+ // Shapes
812
+ // ============================================================
813
+
814
+ #[no_mangle]
815
+ 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) {
816
+ engine().renderer.draw_line(x1, y1, x2, y2, thickness, r, g, b, a);
817
+ }
818
+
819
+ #[no_mangle]
820
+ pub extern "C" fn bloom_draw_rect(x: f64, y: f64, w: f64, h: f64, r: f64, g: f64, b: f64, a: f64) {
821
+ engine().renderer.draw_rect(x, y, w, h, r, g, b, a);
822
+ }
823
+
824
+ #[no_mangle]
825
+ 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) {
826
+ engine().renderer.draw_rect_lines(x, y, w, h, thickness, r, g, b, a);
827
+ }
828
+
829
+ #[no_mangle]
830
+ pub extern "C" fn bloom_draw_circle(cx: f64, cy: f64, radius: f64, r: f64, g: f64, b: f64, a: f64) {
831
+ engine().renderer.draw_circle(cx, cy, radius, r, g, b, a);
832
+ }
833
+
834
+ #[no_mangle]
835
+ pub extern "C" fn bloom_draw_circle_lines(cx: f64, cy: f64, radius: f64, r: f64, g: f64, b: f64, a: f64) {
836
+ engine().renderer.draw_circle_lines(cx, cy, radius, r, g, b, a);
837
+ }
838
+
839
+ #[no_mangle]
840
+ 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) {
841
+ engine().renderer.draw_triangle(x1, y1, x2, y2, x3, y3, r, g, b, a);
842
+ }
843
+
844
+ #[no_mangle]
845
+ pub extern "C" fn bloom_draw_poly(cx: f64, cy: f64, sides: f64, radius: f64, rotation: f64, r: f64, g: f64, b: f64, a: f64) {
846
+ engine().renderer.draw_poly(cx, cy, sides, radius, rotation, r, g, b, a);
847
+ }
848
+
849
+ // ============================================================
850
+ // Text
851
+ // ============================================================
852
+
853
+ #[no_mangle]
854
+ pub extern "C" fn bloom_draw_text(text_ptr: *const u8, x: f64, y: f64, size: f64, r: f64, g: f64, b: f64, a: f64) {
855
+ let text = str_from_header(text_ptr);
856
+ let eng = engine();
857
+ // Need to split borrow: take text out temporarily
858
+ let mut text_renderer = std::mem::replace(&mut eng.text, bloom_shared::text_renderer::TextRenderer::empty());
859
+ text_renderer.draw_text(&mut eng.renderer, text, x, y, size as u32, r, g, b, a);
860
+ eng.text = text_renderer;
861
+ }
862
+
863
+ #[no_mangle]
864
+ pub extern "C" fn bloom_measure_text(text_ptr: *const u8, size: f64) -> f64 {
865
+ let text = str_from_header(text_ptr);
866
+ engine().text.measure_text(text, size as u32)
867
+ }
868
+
869
+ #[no_mangle]
870
+ pub extern "C" fn bloom_load_font(path_ptr: *const u8, _size: f64) -> f64 {
871
+ let path = str_from_header(path_ptr);
872
+ match std::fs::read(path) {
873
+ Ok(data) => engine().text.load_font(&data) as f64,
874
+ Err(_) => 0.0,
875
+ }
876
+ }
877
+
878
+ #[no_mangle]
879
+ pub extern "C" fn bloom_unload_font(font_handle: f64) {
880
+ engine().text.unload_font(font_handle as usize);
881
+ }
882
+
883
+ #[no_mangle]
884
+ pub extern "C" fn bloom_draw_text_ex(font_handle: f64, text_ptr: *const u8, x: f64, y: f64, size: f64, spacing: f64, r: f64, g: f64, b: f64, a: f64) {
885
+ let text = str_from_header(text_ptr);
886
+ let eng = engine();
887
+ let mut text_renderer = std::mem::replace(&mut eng.text, bloom_shared::text_renderer::TextRenderer::empty());
888
+ text_renderer.draw_text_ex(&mut eng.renderer, font_handle as usize, text, x, y, size as u32, spacing as f32, r, g, b, a);
889
+ eng.text = text_renderer;
890
+ }
891
+
892
+ #[no_mangle]
893
+ pub extern "C" fn bloom_measure_text_ex(font_handle: f64, text_ptr: *const u8, size: f64, spacing: f64) -> f64 {
894
+ let text = str_from_header(text_ptr);
895
+ engine().text.measure_text_ex(font_handle as usize, text, size as u32, spacing as f32)
896
+ }
897
+
898
+ // ============================================================
899
+ // Textures
900
+ // ============================================================
901
+
902
+ #[no_mangle]
903
+ pub extern "C" fn bloom_load_texture(path_ptr: *const u8) -> f64 {
904
+ let path = str_from_header(path_ptr);
905
+ match std::fs::read(path) {
906
+ Ok(data) => {
907
+ let eng = engine();
908
+ let EngineState { ref mut textures, ref mut renderer, .. } = *eng;
909
+ textures.load_texture(renderer, &data)
910
+ }
911
+ Err(_) => 0.0,
912
+ }
913
+ }
914
+
915
+ #[no_mangle]
916
+ pub extern "C" fn bloom_unload_texture(handle: f64) {
917
+ let eng = engine();
918
+ let EngineState { ref mut textures, ref mut renderer, .. } = *eng;
919
+ textures.unload_texture(handle, renderer);
920
+ }
921
+
922
+ #[no_mangle]
923
+ pub extern "C" fn bloom_draw_texture(handle: f64, x: f64, y: f64, tint_r: f64, tint_g: f64, tint_b: f64, tint_a: f64) {
924
+ let eng = engine();
925
+ if let Some(tex) = eng.textures.get(handle) {
926
+ let bind_group_idx = tex.bind_group_idx;
927
+ eng.renderer.draw_texture(bind_group_idx, x, y, tint_r, tint_g, tint_b, tint_a);
928
+ }
929
+ }
930
+
931
+ #[no_mangle]
932
+ pub extern "C" fn bloom_draw_texture_pro(
933
+ handle: f64,
934
+ src_x: f64, src_y: f64, src_w: f64, src_h: f64,
935
+ dst_x: f64, dst_y: f64, dst_w: f64, dst_h: f64,
936
+ origin_x: f64, origin_y: f64, rotation: f64,
937
+ tint_r: f64, tint_g: f64, tint_b: f64, tint_a: f64,
938
+ ) {
939
+ let eng = engine();
940
+ if let Some(tex) = eng.textures.get(handle) {
941
+ let bind_group_idx = tex.bind_group_idx;
942
+ eng.renderer.draw_texture_pro(
943
+ bind_group_idx,
944
+ src_x, src_y, src_w, src_h,
945
+ dst_x, dst_y, dst_w, dst_h,
946
+ origin_x, origin_y, rotation,
947
+ tint_r, tint_g, tint_b, tint_a,
948
+ );
949
+ }
950
+ }
951
+
952
+ #[no_mangle]
953
+ pub extern "C" fn bloom_draw_texture_rec(
954
+ handle: f64,
955
+ src_x: f64, src_y: f64, src_w: f64, src_h: f64,
956
+ dst_x: f64, dst_y: f64,
957
+ tint_r: f64, tint_g: f64, tint_b: f64, tint_a: f64,
958
+ ) {
959
+ let eng = engine();
960
+ if let Some(tex) = eng.textures.get(handle) {
961
+ let bind_group_idx = tex.bind_group_idx;
962
+ eng.renderer.draw_texture_rec(
963
+ bind_group_idx,
964
+ src_x, src_y, src_w, src_h,
965
+ dst_x, dst_y,
966
+ tint_r, tint_g, tint_b, tint_a,
967
+ );
968
+ }
969
+ }
970
+
971
+ #[no_mangle]
972
+ pub extern "C" fn bloom_get_texture_width(handle: f64) -> f64 {
973
+ let eng = engine();
974
+ eng.textures.get(handle).map(|t| t.width as f64).unwrap_or(0.0)
975
+ }
976
+
977
+ #[no_mangle]
978
+ pub extern "C" fn bloom_get_texture_height(handle: f64) -> f64 {
979
+ let eng = engine();
980
+ eng.textures.get(handle).map(|t| t.height as f64).unwrap_or(0.0)
981
+ }
982
+
983
+ #[no_mangle]
984
+ pub extern "C" fn bloom_load_image(path_ptr: *const u8) -> f64 {
985
+ let path = str_from_header(path_ptr);
986
+ match std::fs::read(path) {
987
+ Ok(data) => engine().textures.load_image(&data),
988
+ Err(_) => 0.0,
989
+ }
990
+ }
991
+
992
+ #[no_mangle]
993
+ pub extern "C" fn bloom_image_resize(handle: f64, w: f64, h: f64) {
994
+ engine().textures.image_resize(handle, w as u32, h as u32);
995
+ }
996
+
997
+ #[no_mangle]
998
+ pub extern "C" fn bloom_image_crop(handle: f64, x: f64, y: f64, w: f64, h: f64) {
999
+ engine().textures.image_crop(handle, x as u32, y as u32, w as u32, h as u32);
1000
+ }
1001
+
1002
+ #[no_mangle]
1003
+ pub extern "C" fn bloom_image_flip_h(handle: f64) {
1004
+ engine().textures.image_flip_h(handle);
1005
+ }
1006
+
1007
+ #[no_mangle]
1008
+ pub extern "C" fn bloom_image_flip_v(handle: f64) {
1009
+ engine().textures.image_flip_v(handle);
1010
+ }
1011
+
1012
+ #[no_mangle]
1013
+ pub extern "C" fn bloom_load_texture_from_image(handle: f64) -> f64 {
1014
+ let eng = engine();
1015
+ let EngineState { ref mut textures, ref mut renderer, .. } = *eng;
1016
+ textures.load_texture_from_image(handle, renderer)
1017
+ }
1018
+
1019
+ #[no_mangle]
1020
+ pub extern "C" fn bloom_gen_texture_mipmaps(_handle: f64) {
1021
+ // Mipmap generation is handled by the GPU texture creation pipeline
1022
+ // This is a no-op for now as wgpu handles mipmaps internally
1023
+ }
1024
+
1025
+ #[no_mangle]
1026
+ pub extern "C" fn bloom_set_texture_filter(handle: f64, mode: f64) {
1027
+ let eng = engine();
1028
+ if let Some(tex) = eng.textures.get(handle) {
1029
+ let bind_group_idx = tex.bind_group_idx;
1030
+ eng.renderer.set_texture_filter(bind_group_idx, mode > 0.5);
1031
+ }
1032
+ }
1033
+
1034
+ // ============================================================
1035
+ // Camera 2D
1036
+ // ============================================================
1037
+
1038
+ #[no_mangle]
1039
+ pub extern "C" fn bloom_begin_mode_2d(offset_x: f64, offset_y: f64, target_x: f64, target_y: f64, rotation: f64, zoom: f64) {
1040
+ engine().renderer.begin_mode_2d(
1041
+ offset_x as f32, offset_y as f32,
1042
+ target_x as f32, target_y as f32,
1043
+ rotation as f32, zoom as f32,
1044
+ );
1045
+ }
1046
+
1047
+ #[no_mangle]
1048
+ pub extern "C" fn bloom_end_mode_2d() {
1049
+ engine().renderer.end_mode_2d();
1050
+ }
1051
+
1052
+ // ============================================================
1053
+ // Camera 3D and 3D drawing
1054
+ // ============================================================
1055
+
1056
+ #[no_mangle]
1057
+ pub extern "C" fn bloom_begin_mode_3d(
1058
+ pos_x: f64, pos_y: f64, pos_z: f64,
1059
+ target_x: f64, target_y: f64, target_z: f64,
1060
+ up_x: f64, up_y: f64, up_z: f64,
1061
+ fovy: f64, projection: f64,
1062
+ ) {
1063
+ engine().renderer.begin_mode_3d(
1064
+ pos_x as f32, pos_y as f32, pos_z as f32,
1065
+ target_x as f32, target_y as f32, target_z as f32,
1066
+ up_x as f32, up_y as f32, up_z as f32,
1067
+ fovy as f32, projection as f32,
1068
+ );
1069
+ }
1070
+
1071
+ #[no_mangle]
1072
+ pub extern "C" fn bloom_end_mode_3d() {
1073
+ engine().renderer.end_mode_3d();
1074
+ }
1075
+
1076
+ #[no_mangle]
1077
+ pub extern "C" fn bloom_draw_cube(x: f64, y: f64, z: f64, w: f64, h: f64, d: f64, r: f64, g: f64, b: f64, a: f64) {
1078
+ engine().renderer.draw_cube(x, y, z, w, h, d, r, g, b, a);
1079
+ }
1080
+
1081
+ #[no_mangle]
1082
+ pub extern "C" fn bloom_draw_cube_wires(x: f64, y: f64, z: f64, w: f64, h: f64, d: f64, r: f64, g: f64, b: f64, a: f64) {
1083
+ engine().renderer.draw_cube_wires(x, y, z, w, h, d, r, g, b, a);
1084
+ }
1085
+
1086
+ #[no_mangle]
1087
+ pub extern "C" fn bloom_draw_sphere(cx: f64, cy: f64, cz: f64, radius: f64, r: f64, g: f64, b: f64, a: f64) {
1088
+ engine().renderer.draw_sphere(cx, cy, cz, radius, r, g, b, a);
1089
+ }
1090
+
1091
+ #[no_mangle]
1092
+ pub extern "C" fn bloom_draw_sphere_wires(cx: f64, cy: f64, cz: f64, radius: f64, r: f64, g: f64, b: f64, a: f64) {
1093
+ engine().renderer.draw_sphere_wires(cx, cy, cz, radius, r, g, b, a);
1094
+ }
1095
+
1096
+ #[no_mangle]
1097
+ pub extern "C" fn bloom_draw_cylinder(x: f64, y: f64, z: f64, radius_top: f64, radius_bottom: f64, height: f64, r: f64, g: f64, b: f64, a: f64) {
1098
+ engine().renderer.draw_cylinder(x, y, z, radius_top, radius_bottom, height, r, g, b, a);
1099
+ }
1100
+
1101
+ #[no_mangle]
1102
+ pub extern "C" fn bloom_draw_plane(cx: f64, cy: f64, cz: f64, w: f64, d: f64, r: f64, g: f64, b: f64, a: f64) {
1103
+ engine().renderer.draw_plane(cx, cy, cz, w, d, r, g, b, a);
1104
+ }
1105
+
1106
+ #[no_mangle]
1107
+ pub extern "C" fn bloom_draw_grid(slices: f64, spacing: f64) {
1108
+ engine().renderer.draw_grid(slices as i32, spacing);
1109
+ }
1110
+
1111
+ #[no_mangle]
1112
+ pub extern "C" fn bloom_draw_ray(origin_x: f64, origin_y: f64, origin_z: f64, dir_x: f64, dir_y: f64, dir_z: f64, r: f64, g: f64, b: f64, a: f64) {
1113
+ engine().renderer.draw_ray(origin_x, origin_y, origin_z, dir_x, dir_y, dir_z, r, g, b, a);
1114
+ }
1115
+
1116
+ // ============================================================
1117
+ // Joint test
1118
+ // ============================================================
1119
+
1120
+ #[no_mangle]
1121
+ pub extern "C" fn bloom_set_joint_test(joint_index: f64, angle: f64) {
1122
+ engine().renderer.set_joint_test(joint_index as usize, angle as f32);
1123
+ }
1124
+
1125
+ // ============================================================
1126
+ // Lighting
1127
+ // ============================================================
1128
+
1129
+ #[no_mangle]
1130
+ pub extern "C" fn bloom_set_ambient_light(r: f64, g: f64, b: f64, intensity: f64) {
1131
+ engine().renderer.set_ambient_light(r, g, b, intensity);
1132
+ }
1133
+
1134
+ #[no_mangle]
1135
+ pub extern "C" fn bloom_set_directional_light(dx: f64, dy: f64, dz: f64, r: f64, g: f64, b: f64, intensity: f64) {
1136
+ engine().renderer.set_directional_light(dx, dy, dz, r, g, b, intensity);
1137
+ }
1138
+
1139
+ // --- EN-005: procedural sky ---
1140
+ // Toggle procedural-atmosphere rendering and steer the sun. The
1141
+ // renderer owns the on/off flag + LUT state; setting the sun marks
1142
+ // the sky-view LUT dirty so it re-bakes before the next frame.
1143
+
1144
+ #[no_mangle]
1145
+ pub extern "C" fn bloom_set_procedural_sky(enabled: f64, rayleigh_density: f64, mie_density: f64, ground_albedo: f64) {
1146
+ engine().renderer.set_procedural_sky(
1147
+ enabled != 0.0,
1148
+ rayleigh_density as f32,
1149
+ mie_density as f32,
1150
+ ground_albedo as f32,
1151
+ );
1152
+ }
1153
+
1154
+ #[no_mangle]
1155
+ pub extern "C" fn bloom_set_sun_direction(dx: f64, dy: f64, dz: f64, intensity: f64) {
1156
+ engine().renderer.set_sun_direction(dx as f32, dy as f32, dz as f32, intensity as f32);
1157
+ }
1158
+
1159
+ // --- Post-FX knobs (heuristic visual layer; default-off) ---
1160
+
1161
+ #[no_mangle]
1162
+ pub extern "C" fn bloom_set_fog(r: f64, g: f64, b: f64, density: f64, height_ref: f64, height_falloff: f64) {
1163
+ let r_ = engine();
1164
+ r_.renderer.set_fog_color(r as f32, g as f32, b as f32);
1165
+ r_.renderer.set_fog_density(density as f32);
1166
+ r_.renderer.set_fog_height_falloff(height_ref as f32, height_falloff as f32);
1167
+ }
1168
+
1169
+ #[no_mangle]
1170
+ pub extern "C" fn bloom_set_chromatic_aberration(strength: f64) {
1171
+ engine().renderer.set_chromatic_aberration(strength as f32);
1172
+ }
1173
+
1174
+ #[no_mangle]
1175
+ pub extern "C" fn bloom_set_vignette(strength: f64, softness: f64) {
1176
+ engine().renderer.set_vignette(strength as f32, softness as f32);
1177
+ }
1178
+
1179
+ #[no_mangle]
1180
+ pub extern "C" fn bloom_set_film_grain(strength: f64) {
1181
+ engine().renderer.set_film_grain(strength as f32);
1182
+ }
1183
+
1184
+ #[no_mangle]
1185
+ pub extern "C" fn bloom_set_sun_shafts(strength: f64, decay: f64, r: f64, g: f64, b: f64) {
1186
+ let eng = engine();
1187
+ eng.renderer.set_sun_shaft_strength(strength as f32);
1188
+ eng.renderer.set_sun_shaft_decay(decay as f32);
1189
+ eng.renderer.set_sun_shaft_color(r as f32, g as f32, b as f32);
1190
+ }
1191
+
1192
+ #[no_mangle]
1193
+ pub extern "C" fn bloom_set_auto_exposure(on: f64) {
1194
+ engine().renderer.set_auto_exposure(on != 0.0);
1195
+ }
1196
+
1197
+ #[no_mangle]
1198
+ pub extern "C" fn bloom_set_taa_enabled(on: f64) {
1199
+ engine().renderer.set_taa_enabled(on != 0.0);
1200
+ }
1201
+
1202
+ #[no_mangle]
1203
+ pub extern "C" fn bloom_set_render_scale(scale: f64) {
1204
+ engine().renderer.set_render_scale(scale as f32);
1205
+ }
1206
+
1207
+ #[no_mangle]
1208
+ pub extern "C" fn bloom_get_render_scale() -> f64 {
1209
+ engine().renderer.render_scale() as f64
1210
+ }
1211
+
1212
+ #[no_mangle]
1213
+ pub extern "C" fn bloom_set_upscale_mode(mode: f64) {
1214
+ engine().renderer.set_upscale_mode(mode as u32);
1215
+ }
1216
+
1217
+ #[no_mangle]
1218
+ pub extern "C" fn bloom_set_cas_strength(strength: f64) {
1219
+ engine().renderer.set_cas_strength(strength as f32);
1220
+ }
1221
+
1222
+ #[no_mangle]
1223
+ pub extern "C" fn bloom_get_physical_width() -> f64 {
1224
+ engine().renderer.physical_width() as f64
1225
+ }
1226
+
1227
+ #[no_mangle]
1228
+ pub extern "C" fn bloom_get_physical_height() -> f64 {
1229
+ engine().renderer.physical_height() as f64
1230
+ }
1231
+
1232
+ #[no_mangle]
1233
+ pub extern "C" fn bloom_set_auto_resolution(target_hz: f64, enabled: f64) {
1234
+ let eng = engine();
1235
+ if enabled != 0.0 {
1236
+ let current = eng.renderer.render_scale();
1237
+ eng.drs.enable(target_hz as f32, current);
1238
+ } else {
1239
+ eng.drs.disable();
1240
+ }
1241
+ }
1242
+
1243
+ #[no_mangle]
1244
+ pub extern "C" fn bloom_set_manual_exposure(value: f64) {
1245
+ engine().renderer.set_manual_exposure(value as f32);
1246
+ }
1247
+
1248
+ #[no_mangle]
1249
+ pub extern "C" fn bloom_set_env_intensity(intensity: f64) {
1250
+ engine().renderer.set_env_intensity(intensity as f32);
1251
+ }
1252
+
1253
+ #[no_mangle]
1254
+ pub extern "C" fn bloom_set_ssgi_enabled(enabled: f64) {
1255
+ engine().renderer.set_ssgi_enabled(enabled != 0.0);
1256
+ }
1257
+
1258
+ #[no_mangle]
1259
+ pub extern "C" fn bloom_set_ssgi_intensity(intensity: f64) {
1260
+ engine().renderer.set_ssgi_intensity(intensity as f32);
1261
+ }
1262
+
1263
+ #[no_mangle]
1264
+ pub extern "C" fn bloom_set_ssgi_radius(radius: f64) {
1265
+ engine().renderer.set_ssgi_radius(radius as f32);
1266
+ }
1267
+
1268
+ #[no_mangle]
1269
+ pub extern "C" fn bloom_set_dof(enabled: f64, focus_distance: f64, aperture: f64) {
1270
+ let r = &mut engine().renderer;
1271
+ r.set_dof_enabled(enabled != 0.0);
1272
+ r.set_dof_focus_distance(focus_distance as f32);
1273
+ r.set_dof_aperture(aperture as f32);
1274
+ }
1275
+
1276
+ // ============================================================
1277
+ // Render quality toggles (individual + preset)
1278
+ // ============================================================
1279
+
1280
+ #[no_mangle]
1281
+ pub extern "C" fn bloom_set_quality_preset(preset: f64) {
1282
+ engine().renderer.apply_quality_preset(preset as u32);
1283
+ }
1284
+ #[no_mangle]
1285
+ pub extern "C" fn bloom_set_shadows_enabled(on: f64) {
1286
+ engine().renderer.set_shadows_enabled(on != 0.0);
1287
+ }
1288
+ #[no_mangle]
1289
+ pub extern "C" fn bloom_set_shadows_always_fresh(on: f64) {
1290
+ engine().renderer.set_shadows_always_fresh(on != 0.0);
1291
+ }
1292
+ #[no_mangle]
1293
+ pub extern "C" fn bloom_set_bloom_enabled(on: f64) {
1294
+ engine().renderer.set_bloom_enabled(on != 0.0);
1295
+ }
1296
+ #[no_mangle]
1297
+ pub extern "C" fn bloom_set_ssao_enabled(on: f64) {
1298
+ engine().renderer.set_ssao_enabled(on != 0.0);
1299
+ }
1300
+ #[no_mangle]
1301
+ pub extern "C" fn bloom_set_ssao_intensity(value: f64) {
1302
+ engine().renderer.set_ssao_strength(value as f32);
1303
+ }
1304
+ #[no_mangle]
1305
+ pub extern "C" fn bloom_set_ssao_radius(world_radius: f64) {
1306
+ engine().renderer.set_ssao_radius(world_radius as f32);
1307
+ }
1308
+ #[no_mangle]
1309
+ pub extern "C" fn bloom_set_wind(dir_x: f64, dir_z: f64, amplitude: f64, frequency: f64) {
1310
+ engine().renderer.set_wind(dir_x as f32, dir_z as f32, amplitude as f32, frequency as f32);
1311
+ }
1312
+ #[no_mangle]
1313
+ pub extern "C" fn bloom_set_ssr_enabled(on: f64) {
1314
+ engine().renderer.set_ssr_enabled(on != 0.0);
1315
+ }
1316
+ #[no_mangle]
1317
+ pub extern "C" fn bloom_set_motion_blur_enabled(on: f64) {
1318
+ engine().renderer.set_motion_blur_enabled(on != 0.0);
1319
+ }
1320
+ #[no_mangle]
1321
+ pub extern "C" fn bloom_set_sss_enabled(on: f64) {
1322
+ engine().renderer.set_sss_enabled(on != 0.0);
1323
+ }
1324
+
1325
+ // ============================================================
1326
+ // Profiler — CPU phase timings (always available) + GPU timestamps
1327
+ // (when the adapter supports TIMESTAMP_QUERY). Disabled by default.
1328
+ // ============================================================
1329
+
1330
+ #[no_mangle]
1331
+ pub extern "C" fn bloom_set_profiler_enabled(on: f64) {
1332
+ engine().profiler.set_enabled(on != 0.0);
1333
+ }
1334
+ #[no_mangle]
1335
+ pub extern "C" fn bloom_get_profiler_frame_cpu_us() -> f64 {
1336
+ engine().profiler.avg_frame_cpu_us()
1337
+ }
1338
+ #[no_mangle]
1339
+ pub extern "C" fn bloom_get_profiler_frame_gpu_us() -> f64 {
1340
+ engine().profiler.avg_frame_gpu_us()
1341
+ }
1342
+ #[no_mangle]
1343
+ pub extern "C" fn bloom_print_profiler_summary() {
1344
+ print!("{}", engine().profiler.summary());
1345
+ }
1346
+
1347
+ /// Phase 8 — formatted per-pass overlay text. Returns a Perry-style
1348
+ /// header-prefixed string (12-byte header: len, cap, flags, then
1349
+ /// UTF-8 bytes) with one line per pass sorted by CPU time descending:
1350
+ ///
1351
+ /// "<label>|<cpu_us>|<gpu_us_or_-1>\n..."
1352
+ ///
1353
+ /// The TS overlay splits on \n then | per line. Games call this once
1354
+ /// per overlay-draw frame.
1355
+ /// Phase 7 — submit a world-space impulse. The renderer's per-frame
1356
+ /// compute pass decays + accumulates submissions into a 256×256
1357
+ /// R32Float texture covering a 128 m centred square; materials that
1358
+ /// sample `impulse_tex` (group 4 binding 4) read the result. Up to 16
1359
+ /// splats per frame are accepted — overflow is dropped silently.
1360
+ #[no_mangle]
1361
+ pub extern "C" fn bloom_splat_impulse(x: f64, z: f64, radius: f64, strength: f64) {
1362
+ engine().renderer.impulse_field.submit_splat(
1363
+ x as f32, z as f32, radius as f32, strength as f32,
1364
+ );
1365
+ }
1366
+
1367
+ /// Phase 5 — set per-material `user_params` (ABI §1.4). Bytes are
1368
+ /// uploaded verbatim to `@group(2) @binding(11)`; shaders cast them
1369
+ /// to whatever struct they declared. `param_count` is the number of
1370
+ /// f64 slots in the params array (each slot is 8 bytes in Perry's
1371
+ /// number[] layout, but we only forward the low 4 bytes as f32 since
1372
+ /// WGSL UBO scalars are 4-byte). Up to 64 floats (256 bytes).
1373
+ ///
1374
+ /// Pass `param_count == 0` to revert the material to the default
1375
+ /// zero-initialised UBO.
1376
+ #[no_mangle]
1377
+ pub extern "C" fn bloom_set_material_params(
1378
+ handle: f64,
1379
+ params_ptr: *const f64,
1380
+ param_count: f64,
1381
+ ) {
1382
+ let count = param_count as usize;
1383
+ if count > 64 {
1384
+ eprintln!("[material] set_material_params: param_count {} > 64 (256-byte UBO cap)", count);
1385
+ return;
1386
+ }
1387
+ let mut bytes = vec![0u8; count * 4];
1388
+ if !params_ptr.is_null() && count > 0 {
1389
+ let slots = unsafe { std::slice::from_raw_parts(params_ptr, count) };
1390
+ for (i, &v) in slots.iter().enumerate() {
1391
+ let f = v as f32;
1392
+ bytes[i*4..i*4+4].copy_from_slice(&f.to_le_bytes());
1393
+ }
1394
+ }
1395
+ let eng = engine();
1396
+ if let Err(e) = eng.renderer.material_system.set_user_params(
1397
+ &eng.renderer.device, &eng.renderer.queue,
1398
+ handle as u32, &bytes,
1399
+ ) {
1400
+ eprintln!("[material] set_material_params failed: {}", e);
1401
+ }
1402
+ }
1403
+
1404
+ /// Phase 8 — frame history for the histogram. Header-prefixed string,
1405
+ /// one line per frame "<cpu_us>|<gpu_us>", oldest first, up to 120
1406
+ /// lines.
1407
+ #[no_mangle]
1408
+ pub extern "C" fn bloom_profiler_frame_history() -> *const u8 {
1409
+ let hist = engine().profiler.frame_history();
1410
+ let mut s = String::with_capacity(hist.len() * 24);
1411
+ for (cpu, gpu) in &hist {
1412
+ s.push_str(&format!("{:.2}|{:.2}\n", cpu, gpu));
1413
+ }
1414
+ let bytes = s.as_bytes();
1415
+ let len = bytes.len();
1416
+ let total = 12 + len;
1417
+ let layout = std::alloc::Layout::from_size_align(total, 4).unwrap();
1418
+ unsafe {
1419
+ let ptr = std::alloc::alloc(layout);
1420
+ if ptr.is_null() { return std::ptr::null(); }
1421
+ *(ptr as *mut u32) = len as u32;
1422
+ *(ptr.add(4) as *mut u32) = len as u32;
1423
+ *(ptr.add(8) as *mut u32) = 1;
1424
+ std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.add(12), len);
1425
+ ptr
1426
+ }
1427
+ }
1428
+
1429
+ #[no_mangle]
1430
+ pub extern "C" fn bloom_profiler_overlay_text() -> *const u8 {
1431
+ let snap = engine().profiler.snapshot();
1432
+ let mut s = String::with_capacity(snap.len() * 48);
1433
+ for (label, cpu, gpu) in &snap {
1434
+ s.push_str(label);
1435
+ s.push('|');
1436
+ s.push_str(&format!("{:.2}", cpu));
1437
+ s.push('|');
1438
+ match gpu {
1439
+ Some(g) => s.push_str(&format!("{:.2}", g)),
1440
+ None => s.push_str("-1"),
1441
+ }
1442
+ s.push('\n');
1443
+ }
1444
+ let bytes = s.as_bytes();
1445
+ let len = bytes.len();
1446
+ let total = 12 + len;
1447
+ let layout = std::alloc::Layout::from_size_align(total, 4).unwrap();
1448
+ unsafe {
1449
+ let ptr = std::alloc::alloc(layout);
1450
+ if ptr.is_null() { return std::ptr::null(); }
1451
+ *(ptr as *mut u32) = len as u32;
1452
+ *(ptr.add(4) as *mut u32) = len as u32;
1453
+ *(ptr.add(8) as *mut u32) = 1;
1454
+ std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.add(12), len);
1455
+ ptr
1456
+ }
1457
+ }
1458
+
1459
+ // ============================================================
1460
+ // Models
1461
+ // ============================================================
1462
+
1463
+ #[no_mangle]
1464
+ pub extern "C" fn bloom_load_model(path_ptr: *const u8) -> f64 {
1465
+ let path = str_from_header(path_ptr);
1466
+ match std::fs::read(path) {
1467
+ Ok(data) => {
1468
+ let eng = engine();
1469
+ let EngineState { ref mut models, ref mut renderer, .. } = *eng;
1470
+ // .gltf = loose file with external .bin + image URIs; pass
1471
+ // the directory so relative paths resolve. .glb (binary)
1472
+ // embeds everything and needs no base dir.
1473
+ let p = std::path::Path::new(&path);
1474
+ let is_loose = p.extension().and_then(|e| e.to_str()) == Some("gltf");
1475
+ if is_loose {
1476
+ if let Some(dir) = p.parent() {
1477
+ return models.load_model_with_textures_from_path(&data, dir, renderer);
1478
+ }
1479
+ }
1480
+ models.load_model_with_textures(&data, renderer)
1481
+ }
1482
+ Err(_) => 0.0,
1483
+ }
1484
+ }
1485
+
1486
+ #[no_mangle]
1487
+ 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) {
1488
+ let eng = engine();
1489
+ if let Some(model) = eng.models.get(handle) {
1490
+ let position = [x as f32, y as f32, z as f32];
1491
+ let scale = scale as f32;
1492
+ let tint = [(r / 255.0) as f32, (g / 255.0) as f32, (b / 255.0) as f32, (a / 255.0) as f32];
1493
+ let handle_bits = handle.to_bits();
1494
+ if eng.renderer.cache_model_if_static(handle_bits, &model.meshes) {
1495
+ eng.renderer.draw_model_cached(handle_bits, position, scale, tint);
1496
+ } else {
1497
+ for mesh in &model.meshes {
1498
+ let tex_idx = mesh.texture_idx.unwrap_or(0);
1499
+ eng.renderer.draw_model_mesh_tinted(&mesh.vertices, &mesh.indices, position, scale, tint, tex_idx);
1500
+ }
1501
+ }
1502
+ }
1503
+ }
1504
+
1505
+ #[no_mangle]
1506
+ pub extern "C" fn bloom_draw_model_rotated(
1507
+ handle: f64, x: f64, y: f64, z: f64,
1508
+ scale: f64, rot_y: f64,
1509
+ color_packed_argb: f64,
1510
+ ) {
1511
+ // Pack RGBA into one f64 to keep the FFI to 7 args; the 9th f64
1512
+ // would otherwise be corrupted by the Perry-ARM64 stack-arg quirk.
1513
+ let bits = color_packed_argb as u32;
1514
+ let a = ((bits >> 24) & 0xff) as f32 / 255.0;
1515
+ let r = ((bits >> 16) & 0xff) as f32 / 255.0;
1516
+ let g = ((bits >> 8) & 0xff) as f32 / 255.0;
1517
+ let b = ( bits & 0xff) as f32 / 255.0;
1518
+ let eng = engine();
1519
+ if let Some(model) = eng.models.get(handle) {
1520
+ let position = [x as f32, y as f32, z as f32];
1521
+ let scale = scale as f32;
1522
+ let tint = [r, g, b, a];
1523
+ // Skip the static-mesh cache: it bakes vertex transforms with
1524
+ // identity rotation, so a rotated draw must take the dynamic
1525
+ // path. Cheap — meshes are unlikely to land in the cache twice
1526
+ // anyway when callers mix rotated + unrotated draws.
1527
+ for mesh in &model.meshes {
1528
+ let tex_idx = mesh.texture_idx.unwrap_or(0);
1529
+ eng.renderer.draw_model_mesh_tinted_rotated(
1530
+ &mesh.vertices, &mesh.indices, position, scale, tint, tex_idx, rot_y as f32,
1531
+ );
1532
+ }
1533
+ }
1534
+ }
1535
+
1536
+ #[no_mangle]
1537
+ pub extern "C" fn bloom_unload_model(handle: f64) {
1538
+ engine().models.unload_model(handle);
1539
+ }
1540
+
1541
+ #[no_mangle]
1542
+ pub extern "C" fn bloom_gen_mesh_cube(w: f64, h: f64, d: f64) -> f64 {
1543
+ engine().models.gen_mesh_cube(w as f32, h as f32, d as f32)
1544
+ }
1545
+
1546
+ #[no_mangle]
1547
+ pub extern "C" fn bloom_gen_mesh_heightmap(image_handle: f64, size_x: f64, size_y: f64, size_z: f64) -> f64 {
1548
+ let eng = engine();
1549
+ if let Some(img) = eng.textures.images.get(image_handle) {
1550
+ let data = img.data.clone();
1551
+ let w = img.width;
1552
+ let h = img.height;
1553
+ eng.models.gen_mesh_heightmap(&data, w, h, size_x as f32, size_y as f32, size_z as f32)
1554
+ } else {
1555
+ 0.0
1556
+ }
1557
+ }
1558
+
1559
+ #[no_mangle]
1560
+ pub extern "C" fn bloom_load_shader(source_ptr: *const u8) -> f64 {
1561
+ let source = str_from_header(source_ptr);
1562
+ engine().renderer.load_custom_shader(source) as f64
1563
+ }
1564
+
1565
+ // ============================================================
1566
+ // Phase 1c — material system FFI
1567
+ // ============================================================
1568
+
1569
+ #[no_mangle]
1570
+ pub extern "C" fn bloom_compile_material(source_ptr: *const u8) -> f64 {
1571
+ let source = str_from_header(source_ptr);
1572
+ match engine().renderer.compile_material(source) {
1573
+ Ok(handle) => handle as f64,
1574
+ Err(e) => {
1575
+ eprintln!("[material] compile failed: {:?}", e);
1576
+ 0.0
1577
+ }
1578
+ }
1579
+ }
1580
+
1581
+ /// Phase 4b — compile a refractive material. Shorthand for
1582
+ /// `compile_material_with_options(src, Translucent, Refractive,
1583
+ /// true)`. Perry's ARM64 FFI garbles mixed-signature calls with
1584
+ /// more than a pointer + 0-1 floats, so we ship dedicated entry
1585
+ /// points per (profile, bucket, reads_scene) combo that games
1586
+ /// actually need.
1587
+ #[no_mangle]
1588
+ pub extern "C" fn bloom_compile_material_refractive(source_ptr: *const u8) -> f64 {
1589
+ use bloom_shared::renderer::material_pipeline::{FragmentProfile, Bucket};
1590
+ let source = str_from_header(source_ptr);
1591
+ match engine().renderer.compile_material_with_options(
1592
+ source, FragmentProfile::Translucent, Bucket::Refractive, true, false,
1593
+ ) {
1594
+ Ok(handle) => handle as f64,
1595
+ Err(e) => { eprintln!("[refractive] compile failed: {:?}", e); 0.0 }
1596
+ }
1597
+ }
1598
+
1599
+ /// Phase 4b — compile a transparent (non-refractive) material.
1600
+ /// Profile=Translucent, Bucket=Transparent, reads_scene=false.
1601
+ #[no_mangle]
1602
+ pub extern "C" fn bloom_compile_material_transparent(source_ptr: *const u8) -> f64 {
1603
+ use bloom_shared::renderer::material_pipeline::{FragmentProfile, Bucket};
1604
+ let source = str_from_header(source_ptr);
1605
+ match engine().renderer.compile_material_with_options(
1606
+ source, FragmentProfile::Translucent, Bucket::Transparent, false, false,
1607
+ ) {
1608
+ Ok(handle) => handle as f64,
1609
+ Err(e) => { eprintln!("[material] compile failed: {:?}", e); 0.0 }
1610
+ }
1611
+ }
1612
+
1613
+ /// Phase 4b — compile an additive material. Profile=Translucent,
1614
+ /// Bucket=Additive, reads_scene=false.
1615
+ #[no_mangle]
1616
+ pub extern "C" fn bloom_compile_material_additive(source_ptr: *const u8) -> f64 {
1617
+ use bloom_shared::renderer::material_pipeline::{FragmentProfile, Bucket};
1618
+ let source = str_from_header(source_ptr);
1619
+ match engine().renderer.compile_material_with_options(
1620
+ source, FragmentProfile::Translucent, Bucket::Additive, false, false,
1621
+ ) {
1622
+ Ok(handle) => handle as f64,
1623
+ Err(e) => { eprintln!("[material] compile failed: {:?}", e); 0.0 }
1624
+ }
1625
+ }
1626
+
1627
+ /// EN-010 — compile a cutout (alpha-tested) material. Profile=Opaque
1628
+ /// (full G-buffer write, sun shadow, SSAO), Bucket=Cutout (rendered
1629
+ /// double-sided so foliage is visible from both sides),
1630
+ /// reads_scene=false. The fragment shader is expected to call
1631
+ /// `discard` against `material.metal_rough.w`
1632
+ /// (`MaterialFactors.alpha_cutoff`) to drop transparent texels.
1633
+ #[no_mangle]
1634
+ pub extern "C" fn bloom_compile_material_cutout(source_ptr: *const u8) -> f64 {
1635
+ use bloom_shared::renderer::material_pipeline::{FragmentProfile, Bucket};
1636
+ let source = str_from_header(source_ptr);
1637
+ match engine().renderer.compile_material_with_options(
1638
+ source, FragmentProfile::Opaque, Bucket::Cutout, false, false,
1639
+ ) {
1640
+ Ok(handle) => handle as f64,
1641
+ Err(e) => { eprintln!("[material] compile failed: {:?}", e); 0.0 }
1642
+ }
1643
+ }
1644
+
1645
+ /// EN-001 — compile a material pipeline that opts into the standard
1646
+ /// per-instance vertex layout (Opaque profile + Opaque bucket +
1647
+ /// wants_instancing). Pair with `bloom_create_instance_buffer` +
1648
+ /// `bloom_submit_material_draw_instanced` to drive per-instance
1649
+ /// rendering through a single draw_indexed call.
1650
+ ///
1651
+ /// Like the other `bloom_compile_material_*` variants this is
1652
+ /// macOS-only: the existing material-compile FFI surface lives only
1653
+ /// on macOS today (per the EN-010 precedent), and porting all five
1654
+ /// variants to ios/tvos/windows/linux/android/web is out of scope for
1655
+ /// EN-001. The runtime triple
1656
+ /// (`bloom_create_instance_buffer` / `bloom_submit_material_draw_instanced`
1657
+ /// / `bloom_destroy_instance_buffer`) follows the same scope so the
1658
+ /// surface is consistent — they're useless without the compile FFI.
1659
+ #[no_mangle]
1660
+ pub extern "C" fn bloom_compile_material_instanced(source_ptr: *const u8) -> f64 {
1661
+ let source = str_from_header(source_ptr);
1662
+ match engine().renderer.compile_material_instanced(source) {
1663
+ Ok(handle) => handle as f64,
1664
+ Err(e) => { eprintln!("[material] instanced compile failed: {:?}", e); 0.0 }
1665
+ }
1666
+ }
1667
+
1668
+ /// EN-001 — upload a flat per-instance buffer to GPU memory. Perry's
1669
+ /// `number[]` is f64-laid-out in memory; this FFI takes the pointer
1670
+ /// + the instance count and reads `instance_count * 9` f64 slots.
1671
+ /// The Rust side downcasts to f32 and pads each instance to 12 floats
1672
+ /// (48 bytes, vec4-aligned) for the GPU stride. Returns a 1-based
1673
+ /// handle, or 0 on failure (null pointer or zero count).
1674
+ #[no_mangle]
1675
+ pub extern "C" fn bloom_create_instance_buffer(
1676
+ data_ptr: *const f64, instance_count: f64,
1677
+ ) -> f64 {
1678
+ if data_ptr.is_null() || instance_count <= 0.0 { return 0.0; }
1679
+ let count = instance_count as u32;
1680
+ let slot_count = (count as usize) * 9;
1681
+ let raw_f64 = unsafe { std::slice::from_raw_parts(data_ptr, slot_count) };
1682
+ let raw_f32: Vec<f32> = raw_f64.iter().map(|&v| v as f32).collect();
1683
+ engine().renderer.create_instance_buffer(&raw_f32, count) as f64
1684
+ }
1685
+
1686
+ /// EN-001 — submit an instanced material draw. The mesh at
1687
+ /// (mesh_handle, mesh_idx) is drawn `instance_count` times via a
1688
+ /// single `draw_indexed` with the instance buffer bound at vertex
1689
+ /// slot 1. The pipeline at `material` must have been compiled via
1690
+ /// `bloom_compile_material_instanced`. Capped at 5 f64 args to dodge
1691
+ /// the Perry-ARM64 9th-arg garbling quirk.
1692
+ #[no_mangle]
1693
+ pub extern "C" fn bloom_submit_material_draw_instanced(
1694
+ material: f64, mesh_handle: f64, mesh_idx: f64,
1695
+ instance_buffer: f64, instance_count: f64,
1696
+ ) {
1697
+ let eng = engine();
1698
+ let handle_bits = mesh_handle.to_bits();
1699
+ if let Some(model) = eng.models.get(mesh_handle) {
1700
+ eng.renderer.cache_model_if_static(handle_bits, &model.meshes);
1701
+ }
1702
+ eng.renderer.submit_material_draw_instanced(
1703
+ material as u32,
1704
+ handle_bits,
1705
+ mesh_idx as usize,
1706
+ instance_buffer as u32,
1707
+ instance_count as u32,
1708
+ );
1709
+ }
1710
+
1711
+ /// EN-001 — release the GPU memory backing an instance buffer. The
1712
+ /// slot stays `None` so previously-issued handles can never alias a
1713
+ /// future allocation. Safe to call with handle 0 or stale handles.
1714
+ #[no_mangle]
1715
+ pub extern "C" fn bloom_destroy_instance_buffer(handle: f64) {
1716
+ engine().renderer.destroy_instance_buffer(handle as u32);
1717
+ }
1718
+
1719
+ /// EN-011 — create a planar reflection probe and return its 1-based
1720
+ /// handle (0 on failure). Each frame, the engine renders the world
1721
+ /// (minus materials linked to a probe) from a camera mirrored across
1722
+ /// (`plane_y`, `normal_xyz`) into a square HDR RT at `resolution²`.
1723
+ /// Pass `resolution = 0` to default to half the swapchain width.
1724
+ ///
1725
+ /// Pair with `bloom_set_material_reflection_probe` to wire the probe
1726
+ /// into a water material's `@group(2) @binding(12)` slot.
1727
+ #[no_mangle]
1728
+ pub extern "C" fn bloom_create_planar_reflection(
1729
+ plane_y: f64, nx: f64, ny: f64, nz: f64, resolution: f64,
1730
+ ) -> f64 {
1731
+ engine().renderer.create_planar_reflection(
1732
+ plane_y as f32,
1733
+ [nx as f32, ny as f32, nz as f32],
1734
+ resolution as u32,
1735
+ ) as f64
1736
+ }
1737
+
1738
+ /// EN-011 — link a material to a planar reflection probe. Subsequent
1739
+ /// draws of `material` see the probe's RT at `@group(2) @binding(12)`.
1740
+ /// Pass `probe = 0` to revert to the default 1×1 black texture.
1741
+ #[no_mangle]
1742
+ pub extern "C" fn bloom_set_material_reflection_probe(
1743
+ material: f64, probe: f64,
1744
+ ) {
1745
+ engine().renderer.set_material_reflection_probe(material as u32, probe as u32);
1746
+ }
1747
+
1748
+ /// EN-014 — create a texture array from concatenated RGBA8 byte data.
1749
+ /// `data_ptr` points to `layer_count` × `width` × `height` × 4 bytes:
1750
+ /// each layer is one slab back-to-back. All layers share the same
1751
+ /// `width` × `height` (wgpu requires uniform extent for D2Array).
1752
+ /// Layer count is capped at 16 in V1; extras are dropped.
1753
+ ///
1754
+ /// Returns a 1-based handle (0 on failure: null pointer, zero count,
1755
+ /// zero extent, or short data buffer). Pair with
1756
+ /// `bloom_set_material_texture_array` to bind the array to a
1757
+ /// material's @group(2) @binding 14/15/16 slot.
1758
+ ///
1759
+ /// V1 takes raw bytes (not pre-loaded texture handles) because
1760
+ /// existing `bloom_load_texture` uploads to GPU and drops the source
1761
+ /// bytes — there's no CPU-side image registry to read from. Games
1762
+ /// pre-decode their layer images via the standard image-loading API
1763
+ /// (`loadImage`, etc.) and pass the raw RGBA bytes here.
1764
+ #[no_mangle]
1765
+ pub extern "C" fn bloom_create_texture_array(
1766
+ data_ptr: *const u8,
1767
+ data_len: f64,
1768
+ width: f64,
1769
+ height: f64,
1770
+ layer_count: f64,
1771
+ ) -> f64 {
1772
+ // EN-014 V2 — V1 stays callable; forwards to _ex with default
1773
+ // format = sRGB (0) and mip_levels = 1 (no mips).
1774
+ bloom_create_texture_array_ex(data_ptr, data_len, width, height, layer_count, 0.0, 1.0)
1775
+ }
1776
+
1777
+ /// EN-014 V2 — create a texture array with explicit format + mip control.
1778
+ /// See `MaterialSystem::create_texture_array_ex` for semantics. The V1
1779
+ /// entry above is preserved as a thin wrapper.
1780
+ #[no_mangle]
1781
+ pub extern "C" fn bloom_create_texture_array_ex(
1782
+ data_ptr: *const u8,
1783
+ data_len: f64,
1784
+ width: f64,
1785
+ height: f64,
1786
+ layer_count: f64,
1787
+ format: f64,
1788
+ mip_levels: f64,
1789
+ ) -> f64 {
1790
+ if data_ptr.is_null() || data_len <= 0.0 { return 0.0; }
1791
+ let w = width as u32;
1792
+ let h = height as u32;
1793
+ if w == 0 || h == 0 { return 0.0; }
1794
+ let layers_count = (layer_count as u32)
1795
+ .min(bloom_shared::renderer::material_system::MAX_TEXTURE_ARRAY_LAYERS);
1796
+ if layers_count == 0 { return 0.0; }
1797
+ let layer_size = (w as usize) * (h as usize) * 4;
1798
+ let total_bytes = (data_len as usize)
1799
+ .min(layers_count as usize * layer_size);
1800
+ let bytes = unsafe { std::slice::from_raw_parts(data_ptr, total_bytes) };
1801
+ let mut layers: Vec<(&[u8], u32, u32)> = Vec::with_capacity(layers_count as usize);
1802
+ for i in 0..(layers_count as usize) {
1803
+ let start = i * layer_size;
1804
+ let end = start + layer_size;
1805
+ if end > bytes.len() { break; }
1806
+ layers.push((&bytes[start..end], w, h));
1807
+ }
1808
+ engine().renderer.create_texture_array_ex(&layers, format as u32, mip_levels as u32) as f64
1809
+ }
1810
+
1811
+ /// EN-014 — link a texture-array handle to a material at one of three
1812
+ /// slots: 0 = albedo (binding 14), 1 = normal (binding 15),
1813
+ /// 2 = MR (binding 16). Pass `array = 0` to revert to the engine's
1814
+ /// 1×1×1 stub. No-op for unknown handles or out-of-range slots.
1815
+ #[no_mangle]
1816
+ pub extern "C" fn bloom_set_material_texture_array(
1817
+ material: f64, slot: f64, array: f64,
1818
+ ) {
1819
+ engine().renderer.set_material_texture_array(
1820
+ material as u32, slot as u32, array as u32,
1821
+ );
1822
+ }
1823
+
1824
+ /// EN-012 — set the shading model for a material.
1825
+ /// `0` = default lit (standard PBR),
1826
+ /// `1` = foliage (wrap-lambert + transmission via `shade_foliage`),
1827
+ /// `2` = subsurface (V2 stub — currently behaves as default lit).
1828
+ /// Lazily allocates a per-material `MaterialFactors` UBO on first
1829
+ /// call. Game shaders branch on `material.shading_model.x` and call
1830
+ /// either `shade_pbr_standard` or `shade_foliage` from the engine
1831
+ /// helper in `common/pbr.wgsl`.
1832
+ #[no_mangle]
1833
+ pub extern "C" fn bloom_set_material_shading_model(
1834
+ material: f64, model: f64,
1835
+ ) {
1836
+ engine().renderer.set_material_shading_model(material as u32, model as u32);
1837
+ }
1838
+
1839
+ /// EN-012 — set the foliage shading parameters for a material.
1840
+ /// Only takes effect when the material's `shading_model == 1`.
1841
+ ///
1842
+ /// `trans_r/g/b`: rgb tint for back-lit foliage (1,1,1 = neutral).
1843
+ /// `trans_amount`: 0..1 — how much sun bleeds through the leaf when
1844
+ /// the camera looks at it backlit by the sun.
1845
+ /// `wrap_factor`: 0..1 — wrap-lambert intensity. 0 = standard lambert
1846
+ /// (back faces black), 1 = light wraps fully around.
1847
+ #[no_mangle]
1848
+ pub extern "C" fn bloom_set_material_foliage(
1849
+ material: f64,
1850
+ trans_r: f64, trans_g: f64, trans_b: f64,
1851
+ trans_amount: f64, wrap_factor: f64,
1852
+ ) {
1853
+ engine().renderer.set_material_foliage(
1854
+ material as u32,
1855
+ [trans_r as f32, trans_g as f32, trans_b as f32],
1856
+ trans_amount as f32, wrap_factor as f32,
1857
+ );
1858
+ }
1859
+
1860
+ /// Phase 6 — file-backed material compile. The path is registered
1861
+ /// with the hot-reload watcher; subsequent edits trigger an automatic
1862
+ /// recompile on the next end_frame. `bucket_kind` selects the
1863
+ /// (profile, bucket, reads_scene) preset:
1864
+ /// 0 = opaque
1865
+ /// 1 = transparent (translucent, no scene snapshot)
1866
+ /// 2 = refractive (translucent + scene snapshot)
1867
+ /// 3 = additive (translucent, no scene snapshot, additive blend)
1868
+ /// 4 = cutout (opaque profile, double-sided, fragment-discard
1869
+ /// against MaterialFactors.alpha_cutoff)
1870
+ #[no_mangle]
1871
+ pub extern "C" fn bloom_compile_material_from_file(
1872
+ path_ptr: *const u8,
1873
+ bucket_kind: f64,
1874
+ ) -> f64 {
1875
+ use bloom_shared::renderer::material_pipeline::{FragmentProfile, Bucket};
1876
+ let path = str_from_header(path_ptr);
1877
+ let (profile, bucket, reads_scene) = match bucket_kind as u32 {
1878
+ 0 => (FragmentProfile::Opaque, Bucket::Opaque, false),
1879
+ 1 => (FragmentProfile::Translucent, Bucket::Transparent, false),
1880
+ 2 => (FragmentProfile::Translucent, Bucket::Refractive, true),
1881
+ 3 => (FragmentProfile::Translucent, Bucket::Additive, false),
1882
+ 4 => (FragmentProfile::Opaque, Bucket::Cutout, false),
1883
+ _ => {
1884
+ eprintln!("[material] from_file: unknown bucket_kind {bucket_kind}");
1885
+ return 0.0;
1886
+ }
1887
+ };
1888
+ match engine().renderer.compile_material_from_file(
1889
+ std::path::Path::new(path), profile, bucket, reads_scene,
1890
+ ) {
1891
+ Ok(handle) => handle as f64,
1892
+ Err(e) => { eprintln!("[material] from_file failed: {e}"); 0.0 }
1893
+ }
1894
+ }
1895
+
1896
+ /// EN-017 — compile + install a fullscreen post-pass material. Runs
1897
+ /// after composite + tonemapping, before the 2D overlay (HUD stays
1898
+ /// crisp). The fragment shader must declare:
1899
+ ///
1900
+ /// @fragment fn fs_main(@location(0) uv: vec2<f32>)
1901
+ /// -> @location(0) vec4<f32> { ... }
1902
+ ///
1903
+ /// and may sample `scene_color_tex` (LDR, post-tonemap), `scene_depth_tex`
1904
+ /// from the engine-provided bind group at `@group(0)` —
1905
+ /// see `post_pass::POST_PASS_PRELUDE` for the full ABI. Returns 1.0
1906
+ /// on success, 0.0 on compile failure.
1907
+ #[no_mangle]
1908
+ pub extern "C" fn bloom_set_post_pass(source_ptr: *const u8) -> f64 {
1909
+ let source = str_from_header(source_ptr);
1910
+ match engine().renderer.set_post_pass(source) {
1911
+ Ok(()) => 1.0,
1912
+ Err(e) => { eprintln!("[post_pass] compile failed: {:?}", e); 0.0 }
1913
+ }
1914
+ }
1915
+
1916
+ /// EN-017 — uninstall the active post-pass. Composite goes back to
1917
+ /// writing the swapchain directly (zero-cost original path).
1918
+ #[no_mangle]
1919
+ pub extern "C" fn bloom_clear_post_pass() {
1920
+ engine().renderer.clear_post_pass();
1921
+ }
1922
+
1923
+ /// EN-017 V2 — append a post-pass to the stack. Returns a 1-based
1924
+ /// handle on success (so 0 means failure at the FFI layer; the TS
1925
+ /// wrapper subtracts 1 to expose a 0-based index). The fragment
1926
+ /// shader sees `scene_color_tex` (LDR, post-tonemap) +
1927
+ /// `scene_depth_tex` at `@group(0)` — same ABI as
1928
+ /// `bloom_set_post_pass`, just cumulative.
1929
+ #[no_mangle]
1930
+ pub extern "C" fn bloom_add_post_pass(source_ptr: *const u8) -> f64 {
1931
+ let source = str_from_header(source_ptr);
1932
+ match engine().renderer.add_post_pass(source) {
1933
+ Ok(h) => h as f64,
1934
+ Err(e) => { eprintln!("[post_pass] compile failed: {:?}", e); 0.0 }
1935
+ }
1936
+ }
1937
+
1938
+ /// EN-017 V2 — wipe the entire post-pass stack. Composite goes back
1939
+ /// to writing the swapchain directly (zero post-pass cost).
1940
+ #[no_mangle]
1941
+ pub extern "C" fn bloom_clear_all_post_passes() {
1942
+ engine().renderer.clear_all_post_passes();
1943
+ }
1944
+
1945
+ #[no_mangle]
1946
+ pub extern "C" fn bloom_draw_material(
1947
+ material: f64,
1948
+ mesh_handle: f64,
1949
+ mesh_idx: f64,
1950
+ x: f64, y: f64, z: f64, scale: f64,
1951
+ r: f64, g: f64, b: f64, a: f64,
1952
+ ) {
1953
+ let eng = engine();
1954
+ let handle_bits = mesh_handle.to_bits();
1955
+ if let Some(model) = eng.models.get(mesh_handle) {
1956
+ eng.renderer.cache_model_if_static(handle_bits, &model.meshes);
1957
+ }
1958
+ eng.renderer.submit_material_draw(
1959
+ material as u32,
1960
+ handle_bits,
1961
+ mesh_idx as usize,
1962
+ [x as f32, y as f32, z as f32],
1963
+ scale as f32,
1964
+ [(r / 255.0) as f32, (g / 255.0) as f32, (b / 255.0) as f32, (a / 255.0) as f32],
1965
+ );
1966
+ }
1967
+
1968
+ #[no_mangle]
1969
+ pub extern "C" fn bloom_create_mesh(vertex_ptr: *const f64, vertex_count: f64, index_ptr: *const f64, index_count: f64) -> f64 {
1970
+ // Perry's TS `number[]` is f64-laid-out in memory; Perry passes a
1971
+ // pointer to that data. A previous version of this FFI declared
1972
+ // *const f32 / *const u32, which silently read the low 4 bytes
1973
+ // of each f64 slot as garbage f32/u32 values — meshes registered
1974
+ // (non-zero handle) but were unrenderable.
1975
+ //
1976
+ // Caller must pass `vertex_count` and `index_count` derived from
1977
+ // a literal-initialized array OR from values it computed itself.
1978
+ // Don't compute these via `arr.length` after `.push()` — Perry's
1979
+ // `.length` property currently reflects the literal-init size,
1980
+ // not the post-push count (a Perry codegen bug). Workaround on
1981
+ // the TS side: track the count manually or use literal arrays.
1982
+ if vertex_ptr.is_null() || index_ptr.is_null() { return 0.0; }
1983
+ let vcount = vertex_count as usize;
1984
+ let icount = index_count as usize;
1985
+ let vertex_f64 = unsafe { std::slice::from_raw_parts(vertex_ptr, vcount * 12) };
1986
+ let index_f64 = unsafe { std::slice::from_raw_parts(index_ptr, icount) };
1987
+ let vertex_data: Vec<f32> = vertex_f64.iter().map(|&v| v as f32).collect();
1988
+ let index_data: Vec<u32> = index_f64.iter().map(|&v| v as u32).collect();
1989
+ engine().models.create_mesh(&vertex_data, &index_data)
1990
+ }
1991
+
1992
+ #[no_mangle]
1993
+ pub extern "C" fn bloom_load_model_animation(path_ptr: *const u8) -> f64 {
1994
+ let path = str_from_header(path_ptr);
1995
+ match std::fs::read(path) {
1996
+ Ok(data) => engine().models.load_model_animation(&data),
1997
+ Err(_) => 0.0,
1998
+ }
1999
+ }
2000
+
2001
+ #[no_mangle]
2002
+ 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) {
2003
+ // Take a single Y-axis angle (radians) instead of sin/cos, so the
2004
+ // engine reconstructs both with full precision + correct signs.
2005
+ // Older callers that passed (rot_sin, rot_cos) hit a Perry-ARM64
2006
+ // 9th-arg garbling bug AND a sqrt(1-sin²) workaround that lost
2007
+ // the sign of cos — model rotation was correct only on half the
2008
+ // circle. 8-arg signature dodges both issues.
2009
+ let rot_y_f = rot_y as f32;
2010
+ let rot_sin = rot_y_f.sin();
2011
+ let rot_cos = rot_y_f.cos();
2012
+ let eng = engine();
2013
+ eng.models.update_model_animation(handle, anim_index as usize, time as f32);
2014
+ if let Some(anim) = eng.models.get_animation(handle) {
2015
+ if !anim.joint_matrices.is_empty() {
2016
+ eng.renderer.set_joint_matrices_scaled(&anim.joint_matrices, scale as f32, [px as f32, py as f32, pz as f32], rot_sin, rot_cos);
2017
+ }
2018
+ }
2019
+ }
2020
+
2021
+ #[no_mangle]
2022
+ pub extern "C" fn bloom_get_model_mesh_count(handle: f64) -> f64 {
2023
+ match engine().models.get(handle) {
2024
+ Some(model) => model.meshes.len() as f64,
2025
+ None => 0.0,
2026
+ }
2027
+ }
2028
+
2029
+ #[no_mangle]
2030
+ pub extern "C" fn bloom_get_model_material_count(handle: f64) -> f64 {
2031
+ match engine().models.get(handle) {
2032
+ Some(model) => model.meshes.len() as f64, // materials roughly equal meshes
2033
+ None => 0.0,
2034
+ }
2035
+ }
2036
+
2037
+ // ============================================================
2038
+ // Audio
2039
+ // ============================================================
2040
+
2041
+ #[no_mangle]
2042
+ pub extern "C" fn bloom_init_audio() {
2043
+ unsafe {
2044
+ let desc = AudioComponentDescription {
2045
+ component_type: K_AUDIO_UNIT_TYPE_OUTPUT,
2046
+ component_sub_type: K_AUDIO_UNIT_SUB_TYPE_DEFAULT_OUTPUT,
2047
+ component_manufacturer: K_AUDIO_UNIT_MANUFACTURER_APPLE,
2048
+ component_flags: 0,
2049
+ component_flags_mask: 0,
2050
+ };
2051
+
2052
+ let component = AudioComponentFindNext(std::ptr::null_mut(), &desc);
2053
+ if component.is_null() {
2054
+ return;
2055
+ }
2056
+
2057
+ let mut unit: AudioUnit = std::ptr::null_mut();
2058
+ if AudioComponentInstanceNew(component, &mut unit) != 0 {
2059
+ return;
2060
+ }
2061
+
2062
+ // Set stream format: 44100 Hz, stereo, float32
2063
+ let stream_desc = AudioStreamBasicDescription {
2064
+ sample_rate: 44100.0,
2065
+ format_id: K_AUDIO_FORMAT_LINEAR_PCM,
2066
+ format_flags: K_AUDIO_FORMAT_FLAG_IS_FLOAT | K_AUDIO_FORMAT_FLAG_IS_PACKED,
2067
+ bytes_per_packet: 8,
2068
+ frames_per_packet: 1,
2069
+ bytes_per_frame: 8,
2070
+ channels_per_frame: 2,
2071
+ bits_per_channel: 32,
2072
+ reserved: 0,
2073
+ };
2074
+
2075
+ AudioUnitSetProperty(
2076
+ unit,
2077
+ K_AUDIO_UNIT_PROPERTY_STREAM_FORMAT,
2078
+ K_AUDIO_UNIT_SCOPE_INPUT,
2079
+ 0,
2080
+ &stream_desc as *const _ as *const std::ffi::c_void,
2081
+ std::mem::size_of::<AudioStreamBasicDescription>() as u32,
2082
+ );
2083
+
2084
+ // Set render callback
2085
+ let callback_struct = AURenderCallbackStruct {
2086
+ input_proc: audio_render_callback,
2087
+ input_proc_ref_con: std::ptr::null_mut(),
2088
+ };
2089
+
2090
+ AudioUnitSetProperty(
2091
+ unit,
2092
+ K_AUDIO_UNIT_PROPERTY_SET_RENDER_CALLBACK,
2093
+ K_AUDIO_UNIT_SCOPE_INPUT,
2094
+ 0,
2095
+ &callback_struct as *const _ as *const std::ffi::c_void,
2096
+ std::mem::size_of::<AURenderCallbackStruct>() as u32,
2097
+ );
2098
+
2099
+ AudioUnitInitialize(unit);
2100
+ AudioOutputUnitStart(unit);
2101
+
2102
+ AUDIO_UNIT = Some(AudioUnitInstance { unit });
2103
+ }
2104
+ }
2105
+
2106
+ #[no_mangle]
2107
+ pub extern "C" fn bloom_close_audio() {
2108
+ unsafe {
2109
+ if let Some(au) = AUDIO_UNIT.take() {
2110
+ AudioOutputUnitStop(au.unit);
2111
+ AudioComponentInstanceDispose(au.unit);
2112
+ }
2113
+ }
2114
+ }
2115
+
2116
+ #[no_mangle]
2117
+ pub extern "C" fn bloom_load_sound(path_ptr: *const u8) -> f64 {
2118
+ let path = str_from_header(path_ptr);
2119
+ match std::fs::read(path) {
2120
+ Ok(data) => {
2121
+ let sound_data = if path.ends_with(".ogg") || path.ends_with(".OGG") {
2122
+ parse_ogg(&data)
2123
+ } else if path.ends_with(".mp3") || path.ends_with(".MP3") {
2124
+ parse_mp3(&data)
2125
+ } else {
2126
+ parse_wav(&data)
2127
+ };
2128
+ if let Some(sound_data) = sound_data {
2129
+ engine().audio.load_sound(sound_data)
2130
+ } else {
2131
+ 0.0
2132
+ }
2133
+ }
2134
+ Err(_) => 0.0,
2135
+ }
2136
+ }
2137
+
2138
+ #[no_mangle]
2139
+ pub extern "C" fn bloom_play_sound(handle: f64) {
2140
+ engine().audio.play_sound(handle);
2141
+ }
2142
+
2143
+ #[no_mangle]
2144
+ pub extern "C" fn bloom_stop_sound(handle: f64) {
2145
+ engine().audio.stop_sound(handle);
2146
+ }
2147
+
2148
+ #[no_mangle]
2149
+ pub extern "C" fn bloom_set_sound_volume(handle: f64, volume: f64) {
2150
+ engine().audio.set_sound_volume(handle, volume as f32);
2151
+ }
2152
+
2153
+ #[no_mangle]
2154
+ pub extern "C" fn bloom_set_master_volume(volume: f64) {
2155
+ engine().audio.master_volume = volume as f32;
2156
+ }
2157
+
2158
+ #[no_mangle]
2159
+ pub extern "C" fn bloom_play_sound_3d(handle: f64, x: f64, y: f64, z: f64) {
2160
+ engine().audio.play_sound_3d(handle, x as f32, y as f32, z as f32);
2161
+ }
2162
+
2163
+ #[no_mangle]
2164
+ pub extern "C" fn bloom_set_listener_position(x: f64, y: f64, z: f64, fx: f64, fy: f64, fz: f64) {
2165
+ engine().audio.set_listener_position(x as f32, y as f32, z as f32, fx as f32, fy as f32, fz as f32);
2166
+ }
2167
+
2168
+ // ============================================================
2169
+ // Music
2170
+ // ============================================================
2171
+
2172
+ #[no_mangle]
2173
+ pub extern "C" fn bloom_load_music(path_ptr: *const u8) -> f64 {
2174
+ let path = str_from_header(path_ptr);
2175
+ match std::fs::read(path) {
2176
+ Ok(data) => {
2177
+ let sound_data = if path.ends_with(".ogg") || path.ends_with(".OGG") {
2178
+ parse_ogg(&data)
2179
+ } else if path.ends_with(".mp3") || path.ends_with(".MP3") {
2180
+ parse_mp3(&data)
2181
+ } else {
2182
+ parse_wav(&data)
2183
+ };
2184
+ if let Some(sound_data) = sound_data {
2185
+ engine().audio.load_music(sound_data)
2186
+ } else {
2187
+ 0.0
2188
+ }
2189
+ }
2190
+ Err(_) => 0.0,
2191
+ }
2192
+ }
2193
+
2194
+ #[no_mangle]
2195
+ pub extern "C" fn bloom_play_music(handle: f64) {
2196
+ engine().audio.play_music(handle);
2197
+ }
2198
+
2199
+ #[no_mangle]
2200
+ pub extern "C" fn bloom_stop_music(handle: f64) {
2201
+ engine().audio.stop_music(handle);
2202
+ }
2203
+
2204
+ #[no_mangle]
2205
+ pub extern "C" fn bloom_update_music_stream(handle: f64) {
2206
+ engine().audio.update_music_stream(handle);
2207
+ }
2208
+
2209
+ #[no_mangle]
2210
+ pub extern "C" fn bloom_set_music_volume(handle: f64, volume: f64) {
2211
+ engine().audio.set_music_volume(handle, volume as f32);
2212
+ }
2213
+
2214
+ #[no_mangle]
2215
+ pub extern "C" fn bloom_is_music_playing(handle: f64) -> f64 {
2216
+ if engine().audio.is_music_playing(handle) { 1.0 } else { 0.0 }
2217
+ }
2218
+
2219
+ // ============================================================
2220
+ // Utility
2221
+ // ============================================================
2222
+
2223
+ #[no_mangle]
2224
+ pub extern "C" fn bloom_toggle_fullscreen() {
2225
+ unsafe {
2226
+ if let Some(window) = &WINDOW {
2227
+ let _: () = msg_send![window, toggleFullScreen: std::ptr::null::<std::ffi::c_void>()];
2228
+ }
2229
+ }
2230
+ }
2231
+
2232
+ #[no_mangle]
2233
+ pub extern "C" fn bloom_set_window_title(title_ptr: *const u8) {
2234
+ let title = str_from_header(title_ptr);
2235
+ unsafe {
2236
+ if let Some(window) = &WINDOW {
2237
+ let ns_title = NSString::from_str(title);
2238
+ window.setTitle(&ns_title);
2239
+ }
2240
+ }
2241
+ }
2242
+
2243
+ #[no_mangle]
2244
+ pub extern "C" fn bloom_set_window_icon(path_ptr: *const u8) {
2245
+ let path = str_from_header(path_ptr);
2246
+ unsafe {
2247
+ let ns_path = NSString::from_str(path);
2248
+ let image_cls = objc2::runtime::AnyClass::get(c"NSImage").unwrap();
2249
+ let image: *mut objc2::runtime::AnyObject =
2250
+ msg_send![image_cls, alloc];
2251
+ if image.is_null() { return; }
2252
+ let image: *mut objc2::runtime::AnyObject =
2253
+ msg_send![image, initWithContentsOfFile: &*ns_path];
2254
+ if image.is_null() { return; }
2255
+ let app = NSApplication::sharedApplication(MainThreadMarker::new_unchecked());
2256
+ let _: () = msg_send![&*app, setApplicationIconImage: image];
2257
+ }
2258
+ }
2259
+
2260
+ extern "C" {
2261
+ fn CGDisplayHideCursor(display: u32) -> i32;
2262
+ fn CGDisplayShowCursor(display: u32) -> i32;
2263
+ fn CGAssociateMouseAndMouseCursorPosition(connected: u8) -> i32;
2264
+ }
2265
+
2266
+ #[no_mangle]
2267
+ pub extern "C" fn bloom_disable_cursor() {
2268
+ let input = &mut engine().input;
2269
+ input.cursor_disabled = true;
2270
+ input.clear_mouse_delta();
2271
+ unsafe {
2272
+ CGDisplayHideCursor(0);
2273
+ CGAssociateMouseAndMouseCursorPosition(0); // dissociate = relative mode
2274
+ }
2275
+ }
2276
+
2277
+ #[no_mangle]
2278
+ pub extern "C" fn bloom_enable_cursor() {
2279
+ engine().input.cursor_disabled = false;
2280
+ unsafe {
2281
+ CGAssociateMouseAndMouseCursorPosition(1);
2282
+ CGDisplayShowCursor(0);
2283
+ }
2284
+ }
2285
+
2286
+ #[no_mangle]
2287
+ pub extern "C" fn bloom_get_mouse_delta_x() -> f64 {
2288
+ engine().input.mouse_delta_x
2289
+ }
2290
+
2291
+ #[no_mangle]
2292
+ pub extern "C" fn bloom_get_mouse_delta_y() -> f64 {
2293
+ engine().input.mouse_delta_y
2294
+ }
2295
+
2296
+ // Accumulated scroll wheel delta since the last call. Reading consumes the
2297
+ // value (returns 0 on the next call until the user scrolls again). Used by
2298
+ // the editor's orbit camera and any scrollable UI panel.
2299
+ #[no_mangle]
2300
+ pub extern "C" fn bloom_get_mouse_wheel() -> f64 {
2301
+ engine().input.consume_mouse_wheel()
2302
+ }
2303
+
2304
+ // Dequeue the next typed character (Unicode codepoint). Returns 0 when the
2305
+ // queue is empty. Call in a loop each frame to consume all pending characters.
2306
+ // Used by the editor's in-window text-input widget.
2307
+ #[no_mangle]
2308
+ pub extern "C" fn bloom_get_char_pressed() -> f64 {
2309
+ engine().input.pop_char() as f64
2310
+ }
2311
+
2312
+ // Q2: Cursor shape
2313
+ #[no_mangle]
2314
+ pub extern "C" fn bloom_set_cursor_shape(shape: f64) {
2315
+ engine().input.cursor_shape = shape as u32;
2316
+ }
2317
+
2318
+ // E4: Clipboard
2319
+ #[no_mangle]
2320
+ pub extern "C" fn bloom_set_clipboard_text(text_ptr: *const u8) {
2321
+ let text = str_from_header(text_ptr);
2322
+ if let Ok(mut clipboard) = arboard::Clipboard::new() {
2323
+ let _ = clipboard.set_text(text.to_string());
2324
+ }
2325
+ }
2326
+
2327
+ #[no_mangle]
2328
+ pub extern "C" fn bloom_get_clipboard_text() -> *const u8 {
2329
+ match arboard::Clipboard::new() {
2330
+ Ok(mut clipboard) => {
2331
+ match clipboard.get_text() {
2332
+ Ok(text) => {
2333
+ let bytes = text.as_bytes();
2334
+ let len = bytes.len();
2335
+ let total = 12 + len;
2336
+ let layout = std::alloc::Layout::from_size_align(total, 4).unwrap();
2337
+ unsafe {
2338
+ let ptr = std::alloc::alloc(layout);
2339
+ if ptr.is_null() { return std::ptr::null(); }
2340
+ *(ptr as *mut u32) = len as u32;
2341
+ *(ptr.add(4) as *mut u32) = len as u32;
2342
+ *(ptr.add(8) as *mut u32) = 1;
2343
+ std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.add(12), len);
2344
+ ptr
2345
+ }
2346
+ }
2347
+ Err(_) => std::ptr::null(),
2348
+ }
2349
+ }
2350
+ Err(_) => std::ptr::null(),
2351
+ }
2352
+ }
2353
+
2354
+ // E5b: Native file dialogs (via rfd crate)
2355
+ #[no_mangle]
2356
+ pub extern "C" fn bloom_open_file_dialog(filter_ptr: *const u8, title_ptr: *const u8) -> *const u8 {
2357
+ let filter = str_from_header(filter_ptr);
2358
+ let title = str_from_header(title_ptr);
2359
+ let mut dialog = rfd::FileDialog::new().set_title(title);
2360
+ if !filter.is_empty() {
2361
+ dialog = dialog.add_filter("Files", &[filter]);
2362
+ }
2363
+ match dialog.pick_file() {
2364
+ Some(path) => {
2365
+ let s = path.to_string_lossy();
2366
+ let bytes = s.as_bytes();
2367
+ let len = bytes.len();
2368
+ let total = 12 + len;
2369
+ let layout = std::alloc::Layout::from_size_align(total, 4).unwrap();
2370
+ unsafe {
2371
+ let ptr = std::alloc::alloc(layout);
2372
+ if ptr.is_null() { return std::ptr::null(); }
2373
+ *(ptr as *mut u32) = len as u32;
2374
+ *(ptr.add(4) as *mut u32) = len as u32;
2375
+ *(ptr.add(8) as *mut u32) = 1;
2376
+ std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.add(12), len);
2377
+ ptr
2378
+ }
2379
+ }
2380
+ None => std::ptr::null(),
2381
+ }
2382
+ }
2383
+
2384
+ #[no_mangle]
2385
+ pub extern "C" fn bloom_save_file_dialog(default_name_ptr: *const u8, title_ptr: *const u8) -> *const u8 {
2386
+ let default_name = str_from_header(default_name_ptr);
2387
+ let title = str_from_header(title_ptr);
2388
+ let dialog = rfd::FileDialog::new()
2389
+ .set_title(title)
2390
+ .set_file_name(default_name);
2391
+ match dialog.save_file() {
2392
+ Some(path) => {
2393
+ let s = path.to_string_lossy();
2394
+ let bytes = s.as_bytes();
2395
+ let len = bytes.len();
2396
+ let total = 12 + len;
2397
+ let layout = std::alloc::Layout::from_size_align(total, 4).unwrap();
2398
+ unsafe {
2399
+ let ptr = std::alloc::alloc(layout);
2400
+ if ptr.is_null() { return std::ptr::null(); }
2401
+ *(ptr as *mut u32) = len as u32;
2402
+ *(ptr.add(4) as *mut u32) = len as u32;
2403
+ *(ptr.add(8) as *mut u32) = 1;
2404
+ std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.add(12), len);
2405
+ ptr
2406
+ }
2407
+ }
2408
+ None => std::ptr::null(),
2409
+ }
2410
+ }
2411
+
2412
+ // Model bounds accessors. Return the axis-aligned bounding box of a loaded
2413
+ // model in model-local coordinates. Editors use these to size gizmos, auto-
2414
+ // frame the camera on selection, and snap placed entities onto terrain.
2415
+ #[no_mangle]
2416
+ pub extern "C" fn bloom_get_model_bounds_min_x(model_handle: f64) -> f64 {
2417
+ engine().models.get_bounds(model_handle).0[0] as f64
2418
+ }
2419
+ #[no_mangle]
2420
+ pub extern "C" fn bloom_get_model_bounds_min_y(model_handle: f64) -> f64 {
2421
+ engine().models.get_bounds(model_handle).0[1] as f64
2422
+ }
2423
+ #[no_mangle]
2424
+ pub extern "C" fn bloom_get_model_bounds_min_z(model_handle: f64) -> f64 {
2425
+ engine().models.get_bounds(model_handle).0[2] as f64
2426
+ }
2427
+ #[no_mangle]
2428
+ pub extern "C" fn bloom_get_model_bounds_max_x(model_handle: f64) -> f64 {
2429
+ engine().models.get_bounds(model_handle).1[0] as f64
2430
+ }
2431
+ #[no_mangle]
2432
+ pub extern "C" fn bloom_get_model_bounds_max_y(model_handle: f64) -> f64 {
2433
+ engine().models.get_bounds(model_handle).1[1] as f64
2434
+ }
2435
+ #[no_mangle]
2436
+ pub extern "C" fn bloom_get_model_bounds_max_z(model_handle: f64) -> f64 {
2437
+ engine().models.get_bounds(model_handle).1[2] as f64
2438
+ }
2439
+
2440
+ #[no_mangle]
2441
+ pub extern "C" fn bloom_write_file(path_ptr: *const u8, data_ptr: *const u8) -> f64 {
2442
+ let path = str_from_header(path_ptr);
2443
+ let data = str_from_header(data_ptr);
2444
+ match std::fs::write(path, data.as_bytes()) {
2445
+ Ok(_) => 1.0,
2446
+ Err(_) => 0.0,
2447
+ }
2448
+ }
2449
+
2450
+ #[no_mangle]
2451
+ pub extern "C" fn bloom_file_exists(path_ptr: *const u8) -> f64 {
2452
+ let path = str_from_header(path_ptr);
2453
+ if std::path::Path::new(path).exists() { 1.0 } else { 0.0 }
2454
+ }
2455
+
2456
+ #[no_mangle]
2457
+ pub extern "C" fn bloom_read_file(path_ptr: *const u8) -> *const u8 {
2458
+ let path = str_from_header(path_ptr);
2459
+ match std::fs::read_to_string(path) {
2460
+ Ok(contents) => {
2461
+ // Return Perry-format string: StringHeader (length u32 + capacity u32 + refcount u32) followed by UTF-8 data
2462
+ let bytes = contents.as_bytes();
2463
+ let len = bytes.len();
2464
+ let total = 12 + len; // 12 bytes header (3 × u32) + data
2465
+ let layout = std::alloc::Layout::from_size_align(total, 4).unwrap();
2466
+ unsafe {
2467
+ let ptr = std::alloc::alloc(layout);
2468
+ if ptr.is_null() { return std::ptr::null(); }
2469
+ *(ptr as *mut u32) = len as u32; // length
2470
+ *(ptr.add(4) as *mut u32) = len as u32; // capacity
2471
+ *(ptr.add(8) as *mut u32) = 1; // refcount (unique)
2472
+ std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr.add(12), len);
2473
+ ptr
2474
+ }
2475
+ }
2476
+ Err(_) => std::ptr::null(),
2477
+ }
2478
+ }
2479
+
2480
+ // ============================================================
2481
+ // Input injection + platform detection
2482
+ // ============================================================
2483
+
2484
+ #[no_mangle]
2485
+ pub extern "C" fn bloom_inject_key_down(key: f64) {
2486
+ engine().input.set_key_down(key as usize);
2487
+ }
2488
+ #[no_mangle]
2489
+ pub extern "C" fn bloom_inject_key_up(key: f64) {
2490
+ engine().input.set_key_up(key as usize);
2491
+ }
2492
+ #[no_mangle]
2493
+ pub extern "C" fn bloom_inject_gamepad_axis(axis: f64, value: f64) {
2494
+ engine().input.set_gamepad_axis(axis as usize, value as f32);
2495
+ }
2496
+ #[no_mangle]
2497
+ pub extern "C" fn bloom_inject_gamepad_button_down(button: f64) {
2498
+ engine().input.set_gamepad_button_down(button as usize);
2499
+ }
2500
+ #[no_mangle]
2501
+ pub extern "C" fn bloom_inject_gamepad_button_up(button: f64) {
2502
+ engine().input.set_gamepad_button_up(button as usize);
2503
+ }
2504
+ #[no_mangle]
2505
+ pub extern "C" fn bloom_get_platform() -> f64 { 1.0 }
2506
+ #[no_mangle]
2507
+ pub extern "C" fn bloom_is_any_input_pressed() -> f64 {
2508
+ if engine().input.is_any_input_pressed() { 1.0 } else { 0.0 }
2509
+ }
2510
+ #[no_mangle]
2511
+ pub extern "C" fn bloom_get_crown_rotation() -> f64 {
2512
+ engine().input.consume_crown_rotation()
2513
+ }
2514
+
2515
+ // ============================================================
2516
+ // Frame callbacks
2517
+ // ============================================================
2518
+
2519
+ #[no_mangle]
2520
+ pub extern "C" fn bloom_register_frame_callback(priority: f64, callback: extern "C" fn(f64)) -> f64 {
2521
+ engine().frame_callbacks.register(priority as i32, callback) as f64
2522
+ }
2523
+
2524
+ #[no_mangle]
2525
+ pub extern "C" fn bloom_unregister_frame_callback(id: f64) {
2526
+ engine().frame_callbacks.unregister(id as u64);
2527
+ }
2528
+
2529
+ /// Emscripten-style game loop. On native, this is a no-op — the game uses
2530
+ /// a while(!windowShouldClose()) loop in TypeScript instead. The TS-level
2531
+ /// runGame() helper handles the native loop. Only used on web where the
2532
+ /// JS glue intercepts this and drives requestAnimationFrame.
2533
+ #[no_mangle]
2534
+ pub extern "C" fn bloom_run_game(_callback: extern "C" fn(f64)) {
2535
+ // No-op on native. The TypeScript runGame() helper provides the while loop.
2536
+ }
2537
+
2538
+ // ============================================================
2539
+ // Multiple lights
2540
+ // ============================================================
2541
+
2542
+ #[no_mangle]
2543
+ pub extern "C" fn bloom_add_directional_light(
2544
+ dx: f64, dy: f64, dz: f64,
2545
+ r: f64, g: f64, b: f64,
2546
+ intensity: f64,
2547
+ ) {
2548
+ engine().renderer.add_directional_light(
2549
+ dx as f32, dy as f32, dz as f32,
2550
+ r as f32, g as f32, b as f32,
2551
+ intensity as f32,
2552
+ );
2553
+ }
2554
+
2555
+ #[no_mangle]
2556
+ pub extern "C" fn bloom_add_point_light(
2557
+ x: f64, y: f64, z: f64, range: f64,
2558
+ r: f64, g: f64, b: f64,
2559
+ intensity: f64,
2560
+ ) {
2561
+ engine().renderer.add_point_light(
2562
+ x as f32, y as f32, z as f32, range as f32,
2563
+ r as f32, g as f32, b as f32,
2564
+ intensity as f32,
2565
+ );
2566
+ }
2567
+
2568
+ // ============================================================
2569
+ // Scene graph (retained mode)
2570
+ // ============================================================
2571
+
2572
+ #[no_mangle]
2573
+ pub extern "C" fn bloom_scene_create_node() -> f64 {
2574
+ engine().scene.create_node()
2575
+ }
2576
+
2577
+ #[no_mangle]
2578
+ pub extern "C" fn bloom_scene_destroy_node(handle: f64) {
2579
+ engine().scene.destroy_node(handle);
2580
+ }
2581
+
2582
+ #[no_mangle]
2583
+ pub extern "C" fn bloom_scene_set_visible(handle: f64, visible: f64) {
2584
+ engine().scene.set_visible(handle, visible != 0.0);
2585
+ }
2586
+
2587
+ #[no_mangle]
2588
+ pub extern "C" fn bloom_scene_set_cast_shadow(handle: f64, cast: f64) {
2589
+ engine().scene.set_cast_shadow(handle, cast != 0.0);
2590
+ }
2591
+
2592
+ #[no_mangle]
2593
+ pub extern "C" fn bloom_scene_set_receive_shadow(handle: f64, receive: f64) {
2594
+ engine().scene.set_receive_shadow(handle, receive != 0.0);
2595
+ }
2596
+
2597
+ #[no_mangle]
2598
+ pub extern "C" fn bloom_scene_set_parent(handle: f64, parent: f64) {
2599
+ engine().scene.set_parent(handle, parent);
2600
+ }
2601
+
2602
+ /// Set 4x4 transform matrix (16 floats passed as pointer to f64 array).
2603
+ #[no_mangle]
2604
+ pub extern "C" fn bloom_scene_set_transform(handle: f64, mat_ptr: *const f64) {
2605
+ if mat_ptr.is_null() { return; }
2606
+ let slice = unsafe { std::slice::from_raw_parts(mat_ptr, 16) };
2607
+ let mut mat = [[0.0f32; 4]; 4];
2608
+ for col in 0..4 {
2609
+ for row in 0..4 {
2610
+ mat[col][row] = slice[col * 4 + row] as f32;
2611
+ }
2612
+ }
2613
+ engine().scene.set_transform(handle, mat);
2614
+ }
2615
+
2616
+ /// Update geometry from flat vertex array (12 floats/vertex: xyz, nxnynz, rgba, uv)
2617
+ /// and index array.
2618
+ #[no_mangle]
2619
+ pub extern "C" fn bloom_scene_update_geometry(
2620
+ handle: f64,
2621
+ vert_ptr: *const f64,
2622
+ vert_count: f64,
2623
+ idx_ptr: *const f64,
2624
+ idx_count: f64,
2625
+ ) {
2626
+ if vert_ptr.is_null() || idx_ptr.is_null() { return; }
2627
+ let nv = vert_count as usize;
2628
+ let ni = idx_count as usize;
2629
+
2630
+ let vert_floats = unsafe { std::slice::from_raw_parts(vert_ptr, nv * 12) };
2631
+ let idx_floats = unsafe { std::slice::from_raw_parts(idx_ptr, ni) };
2632
+
2633
+ let mut vertices = Vec::with_capacity(nv);
2634
+ for i in 0..nv {
2635
+ let base = i * 12;
2636
+ vertices.push(bloom_shared::renderer::Vertex3D {
2637
+ position: [vert_floats[base] as f32, vert_floats[base+1] as f32, vert_floats[base+2] as f32],
2638
+ normal: [vert_floats[base+3] as f32, vert_floats[base+4] as f32, vert_floats[base+5] as f32],
2639
+ color: [vert_floats[base+6] as f32, vert_floats[base+7] as f32, vert_floats[base+8] as f32, vert_floats[base+9] as f32],
2640
+ uv: [vert_floats[base+10] as f32, vert_floats[base+11] as f32],
2641
+ joints: [0.0; 4],
2642
+ weights: [0.0; 4],
2643
+ tangent: [0.0; 4],
2644
+ });
2645
+ }
2646
+
2647
+ let indices: Vec<u32> = idx_floats.iter().map(|&v| v as u32).collect();
2648
+
2649
+ engine().scene.update_geometry(handle, vertices, indices);
2650
+ }
2651
+
2652
+ #[no_mangle]
2653
+ pub extern "C" fn bloom_scene_set_material_color(handle: f64, r: f64, g: f64, b: f64, a: f64) {
2654
+ engine().scene.set_material_color(handle, r as f32, g as f32, b as f32, a as f32);
2655
+ }
2656
+
2657
+ #[no_mangle]
2658
+ pub extern "C" fn bloom_scene_set_material_pbr(handle: f64, roughness: f64, metalness: f64) {
2659
+ engine().scene.set_material_pbr(handle, roughness as f32, metalness as f32);
2660
+ }
2661
+
2662
+ #[no_mangle]
2663
+ pub extern "C" fn bloom_scene_set_material_texture(handle: f64, texture_idx: f64) {
2664
+ engine().scene.set_material_texture(handle, texture_idx as u32);
2665
+ }
2666
+
2667
+ #[no_mangle]
2668
+ pub extern "C" fn bloom_scene_node_count() -> f64 {
2669
+ engine().scene.node_count() as f64
2670
+ }
2671
+
2672
+ /// Debug: get vertex count for a scene node (0 if not found or empty)
2673
+ #[no_mangle]
2674
+ pub extern "C" fn bloom_scene_node_vertex_count(handle: f64) -> f64 {
2675
+ match engine().scene.nodes.get(handle) {
2676
+ Some(node) => node.vertices.len() as f64,
2677
+ None => -1.0,
2678
+ }
2679
+ }
2680
+
2681
+ /// Debug: get index count for a scene node
2682
+ #[no_mangle]
2683
+ pub extern "C" fn bloom_scene_node_index_count(handle: f64) -> f64 {
2684
+ match engine().scene.nodes.get(handle) {
2685
+ Some(node) => node.indices.len() as f64,
2686
+ None => -1.0,
2687
+ }
2688
+ }
2689
+
2690
+
2691
+ // Q8: Set a water material on a scene node (translucent tint, low roughness).
2692
+ #[no_mangle]
2693
+ pub extern "C" fn bloom_scene_set_material_water(handle: f64, wave_amp: f64, wave_speed: f64, r: f64, g: f64, b: f64, a: f64) {
2694
+ engine().scene.set_material_water(handle, wave_amp as f32, wave_speed as f32, r as f32, g as f32, b as f32, a as f32);
2695
+ }
2696
+
2697
+ // Q9: Generate a ribbon mesh along a Catmull-Rom spline.
2698
+ #[no_mangle]
2699
+ pub extern "C" fn bloom_gen_mesh_spline_ribbon(points_ptr: *const u8, point_count: f64, widths_ptr: *const u8, width_count: f64) -> f64 {
2700
+ let n = point_count as usize;
2701
+ let wn = width_count as usize;
2702
+ let points = unsafe { std::slice::from_raw_parts(points_ptr as *const f32, n * 3) };
2703
+ let widths = unsafe { std::slice::from_raw_parts(widths_ptr as *const f32, wn) };
2704
+ engine().models.gen_mesh_spline_ribbon(points, widths)
2705
+ }
2706
+
2707
+ // Q1: Render texture FFI — create GPU textures for render-to-texture.
2708
+ #[no_mangle]
2709
+ pub extern "C" fn bloom_load_render_texture(width: f64, height: f64) -> f64 {
2710
+ let w = width as u32;
2711
+ let h = height as u32;
2712
+ let eng = engine();
2713
+ let rt_handle = eng.textures.load_render_texture(w, h);
2714
+
2715
+ // Create the GPU texture via the renderer's public method.
2716
+ let (bind_group_idx, _tex_vec_idx) = eng.renderer.create_render_texture(w, h);
2717
+
2718
+ // Register as a texture handle so drawTexture can sample it.
2719
+ let tex_handle = eng.textures.textures.alloc(bloom_shared::textures::TextureData {
2720
+ bind_group_idx, width: w, height: h,
2721
+ });
2722
+ eng.textures.set_render_texture_handle(rt_handle, tex_handle);
2723
+
2724
+ rt_handle
2725
+ }
2726
+ #[no_mangle]
2727
+ pub extern "C" fn bloom_unload_render_texture(handle: f64) {
2728
+ engine().textures.unload_render_texture(handle);
2729
+ }
2730
+ #[no_mangle]
2731
+ pub extern "C" fn bloom_begin_texture_mode(handle: f64) {
2732
+ let eng = engine();
2733
+ let (w, h, bg_idx) = match eng.textures.render_textures.get(handle) {
2734
+ Some(rt) => {
2735
+ let tex_handle = rt.texture_handle;
2736
+ match eng.textures.textures.get(tex_handle) {
2737
+ Some(td) => (rt.width, rt.height, td.bind_group_idx as usize),
2738
+ None => return,
2739
+ }
2740
+ }
2741
+ None => return,
2742
+ };
2743
+ if let Some(texture) = eng.renderer.get_texture_ref(bg_idx) {
2744
+ // We need to call begin_texture_mode with a reference to the texture,
2745
+ // but get_texture_ref borrows renderer immutably. Clone the texture view
2746
+ // data we need first, then call the mutable method.
2747
+ let color_view = texture.create_view(&wgpu::TextureViewDescriptor::default());
2748
+ // Create depth texture for this RT.
2749
+ let depth_tex = eng.renderer.device.create_texture(&wgpu::TextureDescriptor {
2750
+ label: Some("rt_depth"), size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
2751
+ mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2,
2752
+ format: wgpu::TextureFormat::Depth32Float, usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2753
+ view_formats: &[],
2754
+ });
2755
+ let depth_view = depth_tex.create_view(&wgpu::TextureViewDescriptor::default());
2756
+ eng.renderer.rt_color_view = Some(color_view);
2757
+ eng.renderer.rt_depth_view = Some(depth_view);
2758
+ eng.renderer.rt_depth_texture = Some(depth_tex);
2759
+ eng.renderer.rt_width = w;
2760
+ eng.renderer.rt_height = h;
2761
+ }
2762
+ }
2763
+ #[no_mangle]
2764
+ pub extern "C" fn bloom_end_texture_mode() {
2765
+ engine().renderer.end_texture_mode();
2766
+ }
2767
+ #[no_mangle]
2768
+ pub extern "C" fn bloom_get_render_texture_texture(handle: f64) -> f64 {
2769
+ engine().textures.get_render_texture_texture(handle)
2770
+ }
2771
+ // ============================================================
2772
+ // Scene graph QoL — Q4/Q5/Q6/Q7
2773
+ // ============================================================
2774
+
2775
+ /// Q4: Read back the 4x4 transform matrix of a scene node.
2776
+ /// Returns the column-major float at the specified index (0-15).
2777
+ #[no_mangle]
2778
+ pub extern "C" fn bloom_scene_get_transform(handle: f64, index: f64) -> f64 {
2779
+ let mat = engine().scene.get_transform(handle);
2780
+ let i = index as usize;
2781
+ let col = i / 4;
2782
+ let row = i % 4;
2783
+ if col < 4 && row < 4 { mat[col][row] as f64 } else { 0.0 }
2784
+ }
2785
+
2786
+ /// Q5: Get the cached AABB min of a scene node.
2787
+ #[no_mangle]
2788
+ pub extern "C" fn bloom_scene_get_bounds_min_x(handle: f64) -> f64 { engine().scene.get_bounds(handle).0[0] as f64 }
2789
+ #[no_mangle]
2790
+ pub extern "C" fn bloom_scene_get_bounds_min_y(handle: f64) -> f64 { engine().scene.get_bounds(handle).0[1] as f64 }
2791
+ #[no_mangle]
2792
+ pub extern "C" fn bloom_scene_get_bounds_min_z(handle: f64) -> f64 { engine().scene.get_bounds(handle).0[2] as f64 }
2793
+ #[no_mangle]
2794
+ pub extern "C" fn bloom_scene_get_bounds_max_x(handle: f64) -> f64 { engine().scene.get_bounds(handle).1[0] as f64 }
2795
+ #[no_mangle]
2796
+ pub extern "C" fn bloom_scene_get_bounds_max_y(handle: f64) -> f64 { engine().scene.get_bounds(handle).1[1] as f64 }
2797
+ #[no_mangle]
2798
+ pub extern "C" fn bloom_scene_get_bounds_max_z(handle: f64) -> f64 { engine().scene.get_bounds(handle).1[2] as f64 }
2799
+
2800
+ /// Q7: Set arbitrary user data on a scene node.
2801
+ #[no_mangle]
2802
+ pub extern "C" fn bloom_scene_set_user_data(handle: f64, data: f64) {
2803
+ engine().scene.set_user_data(handle, data as i64);
2804
+ }
2805
+ #[no_mangle]
2806
+ pub extern "C" fn bloom_scene_get_user_data(handle: f64) -> f64 {
2807
+ engine().scene.get_user_data(handle) as f64
2808
+ }
2809
+
2810
+ // ============================================================
2811
+ // Geometry generation
2812
+ // ============================================================
2813
+
2814
+ /// Extrude a 2D polygon along Y axis and store as a scene node's geometry.
2815
+ /// polygon_ptr: pointer to flat f64 array [x0, z0, x1, z1, ...]
2816
+ /// polygon_count: number of points (NOT number of floats)
2817
+ /// depth: extrusion height
2818
+ #[no_mangle]
2819
+ pub extern "C" fn bloom_scene_extrude_polygon(
2820
+ handle: f64,
2821
+ polygon_ptr: *const f64,
2822
+ polygon_count: f64,
2823
+ depth: f64,
2824
+ ) {
2825
+ if polygon_ptr.is_null() { return; }
2826
+ let n = polygon_count as usize;
2827
+ let polygon = unsafe { std::slice::from_raw_parts(polygon_ptr, n * 2) };
2828
+
2829
+ let geo = bloom_shared::geometry::extrude_polygon(polygon, &[], depth);
2830
+ engine().scene.update_geometry(handle, geo.vertices, geo.indices);
2831
+ }
2832
+
2833
+ /// Subtract an axis-aligned box from a scene node's geometry.
2834
+ /// Removes triangles fully inside the box (simplified CSG).
2835
+ #[no_mangle]
2836
+ pub extern "C" fn bloom_scene_subtract_box(
2837
+ handle: f64,
2838
+ min_x: f64, min_y: f64, min_z: f64,
2839
+ max_x: f64, max_y: f64, max_z: f64,
2840
+ ) {
2841
+ let eng = engine();
2842
+ if let Some(node) = eng.scene.nodes.get(handle) {
2843
+ let current = bloom_shared::geometry::GeometryData {
2844
+ vertices: node.vertices.clone(),
2845
+ indices: node.indices.clone(),
2846
+ };
2847
+ let result = bloom_shared::geometry::subtract_box(
2848
+ &current,
2849
+ [min_x as f32, min_y as f32, min_z as f32],
2850
+ [max_x as f32, max_y as f32, max_z as f32],
2851
+ );
2852
+ eng.scene.update_geometry(handle, result.vertices, result.indices);
2853
+ }
2854
+ }
2855
+
2856
+ // ============================================================
2857
+ // Shadow mapping
2858
+ // ============================================================
2859
+
2860
+ #[no_mangle]
2861
+ pub extern "C" fn bloom_enable_shadows() {
2862
+ engine().renderer.shadow_map.enable();
2863
+ }
2864
+
2865
+ #[no_mangle]
2866
+ pub extern "C" fn bloom_disable_shadows() {
2867
+ engine().renderer.shadow_map.disable();
2868
+ }
2869
+
2870
+ #[no_mangle]
2871
+ pub extern "C" fn bloom_dump_shadow_map(path_ptr: *const u8) {
2872
+ let path = str_from_header(path_ptr).to_string();
2873
+ engine().renderer.dump_shadow_map(&path);
2874
+ }
2875
+
2876
+ // ============================================================
2877
+ // Post-processing
2878
+ // ============================================================
2879
+
2880
+ #[no_mangle]
2881
+ pub extern "C" fn bloom_enable_postfx() {
2882
+ let eng = engine();
2883
+ let w = eng.renderer.width();
2884
+ let h = eng.renderer.height();
2885
+ let fmt = eng.renderer.surface_format();
2886
+ eng.postfx = Some(bloom_shared::postfx::PostFxPipeline::new(
2887
+ &eng.renderer.device, w, h, fmt,
2888
+ ));
2889
+ }
2890
+
2891
+ #[no_mangle]
2892
+ pub extern "C" fn bloom_disable_postfx() {
2893
+ engine().postfx = None;
2894
+ }
2895
+
2896
+ #[no_mangle]
2897
+ pub extern "C" fn bloom_postfx_set_selected(handle: f64) {
2898
+ if let Some(pfx) = &mut engine().postfx {
2899
+ if handle == 0.0 {
2900
+ pfx.set_selected(Vec::new());
2901
+ } else {
2902
+ pfx.set_selected(vec![handle]);
2903
+ }
2904
+ }
2905
+ }
2906
+
2907
+ #[no_mangle]
2908
+ pub extern "C" fn bloom_postfx_set_hovered(handle: f64) {
2909
+ if let Some(pfx) = &mut engine().postfx {
2910
+ pfx.set_hovered(handle);
2911
+ }
2912
+ }
2913
+
2914
+ #[no_mangle]
2915
+ pub extern "C" fn bloom_postfx_set_outline_color(r: f64, g: f64, b: f64, a: f64) {
2916
+ if let Some(pfx) = &mut engine().postfx {
2917
+ pfx.outline_params.color_selected = [r as f32, g as f32, b as f32, a as f32];
2918
+ }
2919
+ }
2920
+
2921
+ #[no_mangle]
2922
+ pub extern "C" fn bloom_postfx_set_outline_thickness(thickness: f64) {
2923
+ if let Some(pfx) = &mut engine().postfx {
2924
+ pfx.outline_params.thickness[0] = thickness as f32;
2925
+ }
2926
+ }
2927
+
2928
+ // ============================================================
2929
+ // 3D→2D Projection (for UI overlays positioned in 3D space)
2930
+ // ============================================================
2931
+
2932
+ /// Project a world-space 3D point to screen coordinates.
2933
+ /// Returns screen X. Call bloom_project_y for Y. Returns -9999 if behind camera.
2934
+ static mut LAST_PROJECT: (f64, f64) = (0.0, 0.0);
2935
+
2936
+ #[no_mangle]
2937
+ pub extern "C" fn bloom_project_to_screen(wx: f64, wy: f64, wz: f64) -> f64 {
2938
+ let eng = engine();
2939
+ let vp = eng.renderer.vp_matrix();
2940
+ let w = eng.renderer.width() as f32;
2941
+ let h = eng.renderer.height() as f32;
2942
+
2943
+ // Multiply by VP matrix
2944
+ let x = wx as f32;
2945
+ let y = wy as f32;
2946
+ let z = wz as f32;
2947
+ let clip_x = vp[0][0]*x + vp[1][0]*y + vp[2][0]*z + vp[3][0];
2948
+ let clip_y = vp[0][1]*x + vp[1][1]*y + vp[2][1]*z + vp[3][1];
2949
+ let clip_w = vp[0][3]*x + vp[1][3]*y + vp[2][3]*z + vp[3][3];
2950
+
2951
+ if clip_w <= 0.0 {
2952
+ unsafe { LAST_PROJECT = (-9999.0, -9999.0); }
2953
+ return -9999.0;
2954
+ }
2955
+
2956
+ // NDC to screen
2957
+ let ndc_x = clip_x / clip_w;
2958
+ let ndc_y = clip_y / clip_w;
2959
+ let screen_x = ((ndc_x + 1.0) * 0.5 * w) as f64;
2960
+ let screen_y = ((1.0 - ndc_y) * 0.5 * h) as f64;
2961
+
2962
+ unsafe { LAST_PROJECT = (screen_x, screen_y); }
2963
+ screen_x
2964
+ }
2965
+
2966
+ #[no_mangle]
2967
+ pub extern "C" fn bloom_project_screen_y() -> f64 {
2968
+ unsafe { LAST_PROJECT.1 }
2969
+ }
2970
+
2971
+ /// Attach a loaded GLTF model's mesh geometry to a scene node.
2972
+ /// Copies the vertex/index data from the model into the scene node.
2973
+ #[no_mangle]
2974
+ pub extern "C" fn bloom_scene_attach_model(node_handle: f64, model_handle: f64, mesh_index: f64) {
2975
+ let eng = engine();
2976
+ let mi = mesh_index as usize;
2977
+
2978
+ // Get model mesh data
2979
+ let model_data = match eng.models.models.get(model_handle) {
2980
+ Some(md) => md,
2981
+ None => return,
2982
+ };
2983
+ if mi >= model_data.meshes.len() { return; }
2984
+ let mesh = &model_data.meshes[mi];
2985
+
2986
+ // Copy vertices and indices to scene node
2987
+ let vertices = mesh.vertices.clone();
2988
+ let indices = mesh.indices.clone();
2989
+ let base_color_tex = mesh.texture_idx;
2990
+ let normal_tex = mesh.normal_texture_idx;
2991
+ let mr_tex = mesh.metallic_roughness_texture_idx;
2992
+ let emissive_tex = mesh.emissive_texture_idx;
2993
+ let emissive_factor = mesh.emissive_factor;
2994
+ eng.scene.update_geometry(node_handle, vertices, indices);
2995
+
2996
+ // Pipe PBR textures through to the scene node material so the
2997
+ // renderer's scene pipeline can sample them.
2998
+ if let Some(tex_idx) = base_color_tex {
2999
+ eng.scene.set_material_texture(node_handle, tex_idx);
3000
+ }
3001
+ if let Some(tex_idx) = normal_tex {
3002
+ eng.scene.set_material_normal_texture(node_handle, tex_idx);
3003
+ }
3004
+ if let Some(tex_idx) = mr_tex {
3005
+ eng.scene.set_material_metallic_roughness_texture(node_handle, tex_idx);
3006
+ }
3007
+ if let Some(tex_idx) = emissive_tex {
3008
+ eng.scene.set_material_emissive_texture(node_handle, tex_idx);
3009
+ }
3010
+ eng.scene.set_material_emissive_factor(
3011
+ node_handle,
3012
+ emissive_factor[0],
3013
+ emissive_factor[1],
3014
+ emissive_factor[2],
3015
+ );
3016
+ }
3017
+
3018
+ // ============================================================
3019
+ // Thread-safe staging (for async asset loading via Perry threads)
3020
+ // ============================================================
3021
+
3022
+ #[no_mangle]
3023
+ pub extern "C" fn bloom_stage_texture(path_ptr: *const u8) -> f64 {
3024
+ let path = str_from_header(path_ptr);
3025
+ match std::fs::read(path) {
3026
+ Ok(data) => bloom_shared::staging::decode_and_stage_texture(&data),
3027
+ Err(_) => 0.0,
3028
+ }
3029
+ }
3030
+
3031
+ #[no_mangle]
3032
+ pub extern "C" fn bloom_stage_model(path_ptr: *const u8) -> f64 {
3033
+ let path = str_from_header(path_ptr);
3034
+ let data = match std::fs::read(path) {
3035
+ Ok(d) => d,
3036
+ Err(_) => return 0.0,
3037
+ };
3038
+ match bloom_shared::models::load_gltf_staged(&data) {
3039
+ Some(staged) => bloom_shared::staging::stage_model(staged),
3040
+ None => 0.0,
3041
+ }
3042
+ }
3043
+
3044
+ #[no_mangle]
3045
+ pub extern "C" fn bloom_stage_sound(path_ptr: *const u8) -> f64 {
3046
+ let path = str_from_header(path_ptr);
3047
+ let data = match std::fs::read(path) {
3048
+ Ok(d) => d,
3049
+ Err(_) => return 0.0,
3050
+ };
3051
+ let sound_data = if path.ends_with(".ogg") || path.ends_with(".OGG") {
3052
+ parse_ogg(&data)
3053
+ } else if path.ends_with(".mp3") || path.ends_with(".MP3") {
3054
+ parse_mp3(&data)
3055
+ } else {
3056
+ parse_wav(&data)
3057
+ };
3058
+ match sound_data {
3059
+ Some(sd) => bloom_shared::staging::stage_sound(sd),
3060
+ None => 0.0,
3061
+ }
3062
+ }
3063
+
3064
+ #[no_mangle]
3065
+ pub extern "C" fn bloom_commit_texture(staging_handle: f64) -> f64 {
3066
+ let staged = match bloom_shared::staging::take_texture(staging_handle) {
3067
+ Some(s) => s,
3068
+ None => return 0.0,
3069
+ };
3070
+ let eng = engine();
3071
+ let bind_group_idx = eng.renderer.register_texture(staged.width, staged.height, &staged.data);
3072
+ eng.textures.textures.alloc(bloom_shared::textures::TextureData {
3073
+ bind_group_idx, width: staged.width, height: staged.height,
3074
+ })
3075
+ }
3076
+
3077
+ #[no_mangle]
3078
+ pub extern "C" fn bloom_commit_model(staging_handle: f64) -> f64 {
3079
+ let staged = match bloom_shared::staging::take_model(staging_handle) {
3080
+ Some(s) => s,
3081
+ None => return 0.0,
3082
+ };
3083
+ let eng = engine();
3084
+ // Register staged textures with GPU and build index map.
3085
+ // Staged texture_idx values are 1-based into staged.textures.
3086
+ // We map them to actual renderer bind_group_idx values.
3087
+ let mut tex_map: Vec<u32> = Vec::with_capacity(staged.textures.len());
3088
+ for tex in &staged.textures {
3089
+ tex_map.push(eng.renderer.register_texture(tex.width, tex.height, &tex.data));
3090
+ }
3091
+ let mut model = staged.model;
3092
+ for mesh in &mut model.meshes {
3093
+ if let Some(ref mut idx) = mesh.texture_idx {
3094
+ let staged_idx = *idx as usize;
3095
+ if staged_idx > 0 && staged_idx <= tex_map.len() {
3096
+ *idx = tex_map[staged_idx - 1];
3097
+ } else {
3098
+ mesh.texture_idx = None;
3099
+ }
3100
+ }
3101
+ }
3102
+ eng.models.models.alloc(model)
3103
+ }
3104
+
3105
+ #[no_mangle]
3106
+ pub extern "C" fn bloom_commit_sound(staging_handle: f64) -> f64 {
3107
+ match bloom_shared::staging::take_sound(staging_handle) {
3108
+ Some(sd) => engine().audio.load_sound(sd),
3109
+ None => 0.0,
3110
+ }
3111
+ }
3112
+
3113
+ #[no_mangle]
3114
+ pub extern "C" fn bloom_commit_music(staging_handle: f64) -> f64 {
3115
+ match bloom_shared::staging::take_sound(staging_handle) {
3116
+ Some(sd) => engine().audio.load_music(sd),
3117
+ None => 0.0,
3118
+ }
3119
+ }
3120
+
3121
+ // ============================================================
3122
+ // Simple blocking executor for wgpu async calls
3123
+ // ============================================================
3124
+
3125
+ fn pollster_block_on<F: std::future::Future>(future: F) -> F::Output {
3126
+ // Minimal block_on implementation using std::task
3127
+ use std::task::{Context, Poll, Wake, Waker};
3128
+ use std::pin::Pin;
3129
+ use std::sync::Arc;
3130
+
3131
+ struct NoopWaker;
3132
+ impl Wake for NoopWaker {
3133
+ fn wake(self: Arc<Self>) {}
3134
+ }
3135
+
3136
+ let waker = Waker::from(Arc::new(NoopWaker));
3137
+ let mut cx = Context::from_waker(&waker);
3138
+ let mut future = unsafe { Pin::new_unchecked(Box::new(future)) };
3139
+
3140
+ loop {
3141
+ match future.as_mut().poll(&mut cx) {
3142
+ Poll::Ready(result) => return result,
3143
+ Poll::Pending => std::thread::yield_now(),
3144
+ }
3145
+ }
3146
+ }
3147
+
3148
+ // ============================================================
3149
+ // Geisterhand screenshot integration
3150
+ // ============================================================
3151
+
3152
+ /// Register Bloom's GPU-based screenshot capture with perry-geisterhand.
3153
+ /// This replaces perry-ui-macos's CGWindowListCreateImage approach with
3154
+ /// direct wgpu texture readback — works for Metal/Vulkan rendered content.
3155
+ fn bloom_register_geisterhand_screenshot() {
3156
+ // Try to register with geisterhand if it's linked (weak symbol)
3157
+ extern "C" {
3158
+ fn perry_geisterhand_register_screenshot_capture(
3159
+ f: extern "C" fn(*mut usize) -> *mut u8,
3160
+ );
3161
+ }
3162
+ unsafe {
3163
+ perry_geisterhand_register_screenshot_capture(bloom_screenshot_capture);
3164
+ }
3165
+ }
3166
+
3167
+ /// Capture the Bloom framebuffer as PNG.
3168
+ /// Called from geisterhand pump BEFORE end_frame in bloom_end_drawing.
3169
+ /// The vertices_3d/2d and VP matrix from the game loop are still populated.
3170
+ /// We render to a fresh surface texture with screenshot capture, producing
3171
+ /// the same visual output as the real frame.
3172
+ extern "C" fn bloom_screenshot_capture(out_len: *mut usize) -> *mut u8 {
3173
+ let eng = engine();
3174
+
3175
+ // Set capture flag and render inline
3176
+ eng.renderer.screenshot_requested = true;
3177
+ eng.scene.prepare(
3178
+ &eng.renderer.device,
3179
+ &eng.renderer.queue,
3180
+ &eng.renderer.vp_matrix(),
3181
+ &eng.renderer.prev_vp_matrix,
3182
+ eng.renderer.uniform_3d_layout(),
3183
+ );
3184
+ eng.scene.prepare_materials(&eng.renderer);
3185
+ // Phase 1c: sync material PerFrame + PerView UBOs with the
3186
+ // current engine clock before the main HDR pass dispatches any
3187
+ // material draws that were submitted during this frame.
3188
+ {
3189
+ let t = eng.get_time() as f32;
3190
+ let dt = eng.delta_time as f32;
3191
+ eng.renderer.material_system_begin_frame(t, dt);
3192
+ }
3193
+ eng.renderer.end_frame_with_scene(&mut eng.scene, &mut eng.profiler);
3194
+
3195
+ match eng.renderer.screenshot_data.take() {
3196
+ Some((width, height, rgba)) => {
3197
+ // Encode RGBA pixels to PNG
3198
+ match encode_png(width, height, &rgba) {
3199
+ Some(png_data) => {
3200
+ let len = png_data.len();
3201
+ // Allocate with libc::malloc (caller will free with libc::free)
3202
+ let ptr = unsafe { libc::malloc(len) as *mut u8 };
3203
+ if ptr.is_null() {
3204
+ unsafe { *out_len = 0; }
3205
+ return std::ptr::null_mut();
3206
+ }
3207
+ unsafe {
3208
+ std::ptr::copy_nonoverlapping(png_data.as_ptr(), ptr, len);
3209
+ *out_len = len;
3210
+ }
3211
+ ptr
3212
+ }
3213
+ None => {
3214
+ unsafe { *out_len = 0; }
3215
+ std::ptr::null_mut()
3216
+ }
3217
+ }
3218
+ }
3219
+ None => {
3220
+ unsafe { *out_len = 0; }
3221
+ std::ptr::null_mut()
3222
+ }
3223
+ }
3224
+ }
3225
+
3226
+ /// Minimal PNG encoder (no external dependency).
3227
+ fn encode_png(width: u32, height: u32, rgba: &[u8]) -> Option<Vec<u8>> {
3228
+ use std::io::Write;
3229
+
3230
+ let mut png = Vec::new();
3231
+ // PNG signature
3232
+ png.write_all(&[137, 80, 78, 71, 13, 10, 26, 10]).ok()?;
3233
+
3234
+ // IHDR chunk
3235
+ let mut ihdr = Vec::new();
3236
+ ihdr.extend_from_slice(&width.to_be_bytes());
3237
+ ihdr.extend_from_slice(&height.to_be_bytes());
3238
+ ihdr.push(8); // bit depth
3239
+ ihdr.push(6); // color type: RGBA
3240
+ ihdr.push(0); // compression
3241
+ ihdr.push(0); // filter
3242
+ ihdr.push(0); // interlace
3243
+ write_png_chunk(&mut png, b"IHDR", &ihdr);
3244
+
3245
+ // IDAT chunk — raw pixel data with zlib
3246
+ // Build raw scanlines: each row starts with filter byte 0 (None)
3247
+ let row_bytes = (width * 4) as usize;
3248
+ let mut raw = Vec::with_capacity((row_bytes + 1) * height as usize);
3249
+ for y in 0..height as usize {
3250
+ raw.push(0); // filter: None
3251
+ let start = y * row_bytes;
3252
+ // Copy BGRA pixels, swapping B and R for PNG (which expects RGBA)
3253
+ for x in 0..width as usize {
3254
+ let idx = start + x * 4;
3255
+ // Metal Bgra8UnormSrgb: byte order is B, G, R, A
3256
+ raw.push(rgba[idx + 2]); // R (was at offset 2 in BGRA)
3257
+ raw.push(rgba[idx + 1]); // G (same position)
3258
+ raw.push(rgba[idx + 0]); // B (was at offset 0 in BGRA)
3259
+ raw.push(255); // A (force opaque — alpha from sRGB surface is unreliable)
3260
+ }
3261
+ }
3262
+
3263
+ // Compress with deflate (store blocks, no actual compression for simplicity)
3264
+ let deflated = deflate_store(&raw);
3265
+ write_png_chunk(&mut png, b"IDAT", &deflated);
3266
+
3267
+ // IEND chunk
3268
+ write_png_chunk(&mut png, b"IEND", &[]);
3269
+
3270
+ Some(png)
3271
+ }
3272
+
3273
+ fn write_png_chunk(out: &mut Vec<u8>, chunk_type: &[u8; 4], data: &[u8]) {
3274
+ let len = data.len() as u32;
3275
+ out.extend_from_slice(&len.to_be_bytes());
3276
+ out.extend_from_slice(chunk_type);
3277
+ out.extend_from_slice(data);
3278
+ // CRC32 over type + data
3279
+ let crc = crc32(&[chunk_type.as_slice(), data].concat());
3280
+ out.extend_from_slice(&crc.to_be_bytes());
3281
+ }
3282
+
3283
+ fn crc32(data: &[u8]) -> u32 {
3284
+ let mut crc: u32 = 0xFFFFFFFF;
3285
+ for &byte in data {
3286
+ crc ^= byte as u32;
3287
+ for _ in 0..8 {
3288
+ if crc & 1 != 0 {
3289
+ crc = (crc >> 1) ^ 0xEDB88320;
3290
+ } else {
3291
+ crc >>= 1;
3292
+ }
3293
+ }
3294
+ }
3295
+ !crc
3296
+ }
3297
+
3298
+ /// Minimal deflate: store blocks (no compression). Wraps in zlib format.
3299
+ fn deflate_store(data: &[u8]) -> Vec<u8> {
3300
+ let mut out = Vec::new();
3301
+ // Zlib header: CMF=0x78 (deflate, window=32K), FLG=0x01 (no dict, check bits)
3302
+ out.push(0x78);
3303
+ out.push(0x01);
3304
+
3305
+ // Split into 65535-byte store blocks
3306
+ let mut remaining = data.len();
3307
+ let mut offset = 0;
3308
+ while remaining > 0 {
3309
+ let block_size = remaining.min(65535);
3310
+ let is_last = remaining <= 65535;
3311
+ out.push(if is_last { 1 } else { 0 }); // BFINAL + BTYPE=00 (store)
3312
+ let len = block_size as u16;
3313
+ let nlen = !len;
3314
+ out.extend_from_slice(&len.to_le_bytes());
3315
+ out.extend_from_slice(&nlen.to_le_bytes());
3316
+ out.extend_from_slice(&data[offset..offset + block_size]);
3317
+ offset += block_size;
3318
+ remaining -= block_size;
3319
+ }
3320
+
3321
+ // Adler-32 checksum
3322
+ let adler = adler32(data);
3323
+ out.extend_from_slice(&adler.to_be_bytes());
3324
+ out
3325
+ }
3326
+
3327
+ fn adler32(data: &[u8]) -> u32 {
3328
+ let mut a: u32 = 1;
3329
+ let mut b: u32 = 0;
3330
+ for &byte in data {
3331
+ a = (a + byte as u32) % 65521;
3332
+ b = (b + a) % 65521;
3333
+ }
3334
+ (b << 16) | a
3335
+ }
3336
+
3337
+ // ============================================================
3338
+ // Scene picking (raycasting)
3339
+ // ============================================================
3340
+
3341
+ static mut LAST_PICK: Option<bloom_shared::picking::PickResult> = None;
3342
+
3343
+ #[no_mangle]
3344
+ pub extern "C" fn bloom_scene_pick(screen_x: f64, screen_y: f64) -> f64 {
3345
+ let eng = engine();
3346
+ let inv_vp = eng.renderer.inverse_vp_matrix();
3347
+ let cam_pos = eng.renderer.camera_pos();
3348
+ let w = eng.renderer.width() as f32;
3349
+ let h = eng.renderer.height() as f32;
3350
+
3351
+ let (origin, direction) = bloom_shared::picking::screen_to_ray(
3352
+ screen_x as f32, screen_y as f32,
3353
+ w, h, &inv_vp, &cam_pos,
3354
+ );
3355
+
3356
+ let result = bloom_shared::picking::raycast_scene(&eng.scene, &origin, &direction);
3357
+ let hit = result.hit;
3358
+ unsafe { LAST_PICK = Some(result); }
3359
+ if hit { 1.0 } else { 0.0 }
3360
+ }
3361
+
3362
+ #[no_mangle]
3363
+ pub extern "C" fn bloom_pick_hit_handle() -> f64 {
3364
+ unsafe { LAST_PICK.as_ref().map(|r| r.handle).unwrap_or(0.0) }
3365
+ }
3366
+
3367
+ #[no_mangle]
3368
+ pub extern "C" fn bloom_pick_hit_distance() -> f64 {
3369
+ unsafe { LAST_PICK.as_ref().map(|r| r.distance as f64).unwrap_or(0.0) }
3370
+ }
3371
+
3372
+ #[no_mangle]
3373
+ pub extern "C" fn bloom_pick_hit_x() -> f64 {
3374
+ unsafe { LAST_PICK.as_ref().map(|r| r.point[0] as f64).unwrap_or(0.0) }
3375
+ }
3376
+
3377
+ #[no_mangle]
3378
+ pub extern "C" fn bloom_pick_hit_y() -> f64 {
3379
+ unsafe { LAST_PICK.as_ref().map(|r| r.point[1] as f64).unwrap_or(0.0) }
3380
+ }
3381
+
3382
+ #[no_mangle]
3383
+ pub extern "C" fn bloom_pick_hit_z() -> f64 {
3384
+ unsafe { LAST_PICK.as_ref().map(|r| r.point[2] as f64).unwrap_or(0.0) }
3385
+ }
3386
+
3387
+ #[no_mangle]
3388
+ pub extern "C" fn bloom_pick_hit_normal_x() -> f64 {
3389
+ unsafe { LAST_PICK.as_ref().map(|r| r.normal[0] as f64).unwrap_or(0.0) }
3390
+ }
3391
+
3392
+ #[no_mangle]
3393
+ pub extern "C" fn bloom_pick_hit_normal_y() -> f64 {
3394
+ unsafe { LAST_PICK.as_ref().map(|r| r.normal[1] as f64).unwrap_or(0.0) }
3395
+ }
3396
+
3397
+ #[no_mangle]
3398
+ pub extern "C" fn bloom_pick_hit_normal_z() -> f64 {
3399
+ unsafe { LAST_PICK.as_ref().map(|r| r.normal[2] as f64).unwrap_or(0.0) }
3400
+ }
3401
+
3402
+ // Q6: Multi-hit picking — returns all hits sorted by distance.
3403
+ static mut LAST_PICK_ALL: Vec<bloom_shared::picking::PickResult> = Vec::new();
3404
+
3405
+ #[no_mangle]
3406
+ pub extern "C" fn bloom_scene_pick_all(screen_x: f64, screen_y: f64, max_results: f64) -> f64 {
3407
+ let eng = engine();
3408
+ let inv_vp = eng.renderer.inverse_vp_matrix();
3409
+ let cam_pos = eng.renderer.camera_pos();
3410
+ let w = eng.renderer.width() as f32;
3411
+ let h = eng.renderer.height() as f32;
3412
+ let (origin, direction) = bloom_shared::picking::screen_to_ray(
3413
+ screen_x as f32, screen_y as f32, w, h, &inv_vp, &cam_pos,
3414
+ );
3415
+ let results = bloom_shared::picking::raycast_scene_all(&eng.scene, &origin, &direction, max_results as usize);
3416
+ let count = results.len();
3417
+ unsafe { LAST_PICK_ALL = results; }
3418
+ count as f64
3419
+ }
3420
+
3421
+ #[no_mangle]
3422
+ pub extern "C" fn bloom_pick_all_handle(index: f64) -> f64 {
3423
+ let i = index as usize;
3424
+ unsafe { LAST_PICK_ALL.get(i).map(|r| r.handle).unwrap_or(0.0) }
3425
+ }
3426
+
3427
+ #[no_mangle]
3428
+ pub extern "C" fn bloom_pick_all_distance(index: f64) -> f64 {
3429
+ let i = index as usize;
3430
+ unsafe { LAST_PICK_ALL.get(i).map(|r| r.distance as f64).unwrap_or(0.0) }
3431
+ }
3432
+
3433
+ // ============================================================
3434
+ // Physics (Jolt 5.x) — FFI surface generated from shared macro
3435
+ // ============================================================
3436
+
3437
+ #[cfg(feature = "jolt")]
3438
+ #[inline]
3439
+ fn bloom_jolt_ffi_physics() -> &'static mut bloom_shared::physics_jolt::JoltPhysics {
3440
+ &mut engine().jolt
3441
+ }
3442
+
3443
+ #[cfg(feature = "jolt")]
3444
+ bloom_shared::define_physics_ffi!();