@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,2106 @@
1
+ // Material system — the runtime state behind Phase 1c.
2
+ //
3
+ // Owns the compiled `MaterialPipeline`s, the per-frame / per-view /
4
+ // per-material / per-draw uniform buffers, the bind groups wiring
5
+ // them to the ABI layouts, and the per-frame draw command list.
6
+ //
7
+ // The Renderer owns one `MaterialSystem` instance. Games interact via
8
+ // three methods: `compile_material`, `submit_draw`, and (internally)
9
+ // `dispatch` which runs inside the main HDR pass.
10
+
11
+ use wgpu::util::DeviceExt;
12
+
13
+ use super::material_pipeline::{
14
+ MaterialAbiLayouts, MaterialPipeline, MaterialCompileDesc, FragmentProfile,
15
+ Bucket, compile_material, MaterialCompileError,
16
+ };
17
+ use super::types::Vertex3D;
18
+
19
+ // =====================================================================
20
+ // Uniform structs — repr(C), bytemuck-Pod, mirror the WGSL in
21
+ // material_abi.wgsl. Kept local to this module so changes to ABI
22
+ // struct layouts happen in exactly one place per language.
23
+ // =====================================================================
24
+
25
+ #[repr(C)]
26
+ #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
27
+ pub struct PerFrameUniforms {
28
+ pub time: f32,
29
+ pub delta_time: f32,
30
+ pub frame_index: u32,
31
+ pub _pad0: u32,
32
+ pub screen_resolution: [f32; 2],
33
+ pub render_resolution: [f32; 2],
34
+ pub taa_jitter: [f32; 2],
35
+ pub _pad1: [f32; 2],
36
+ /// Global wind: x=dir_x, y=dir_z, z=amplitude, w=frequency.
37
+ pub wind: [f32; 4],
38
+ }
39
+
40
+ #[repr(C)]
41
+ #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
42
+ pub struct PerViewDirLight {
43
+ pub direction: [f32; 4], // xyz + intensity
44
+ pub color: [f32; 4],
45
+ }
46
+
47
+ #[repr(C)]
48
+ #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
49
+ pub struct PerViewPointLight {
50
+ pub position: [f32; 4],
51
+ pub color: [f32; 4],
52
+ }
53
+
54
+ #[repr(C)]
55
+ #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
56
+ pub struct PerViewUniforms {
57
+ pub view: [[f32; 4]; 4],
58
+ pub proj: [[f32; 4]; 4],
59
+ pub view_proj: [[f32; 4]; 4],
60
+ pub prev_view_proj: [[f32; 4]; 4],
61
+ pub inv_proj: [[f32; 4]; 4],
62
+ pub camera_pos: [f32; 4],
63
+ pub camera_dir: [f32; 4],
64
+ pub ambient: [f32; 4],
65
+ pub fog: [f32; 4],
66
+ pub sun_dir: [f32; 4],
67
+ pub sun_color: [f32; 4],
68
+ pub dir_light_count: [f32; 4],
69
+ pub dir_lights: [PerViewDirLight; 4],
70
+ pub point_light_count: [f32; 4],
71
+ pub point_lights: [PerViewPointLight; 16],
72
+ pub shadow_splits: [f32; 4],
73
+ pub shadow_view: [[f32; 4]; 4],
74
+ pub shadow_cascades: [[[f32; 4]; 4]; 3],
75
+ }
76
+
77
+ #[repr(C)]
78
+ #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
79
+ pub struct MaterialFactorsUniforms {
80
+ pub metal_rough: [f32; 4],
81
+ pub emissive: [f32; 4],
82
+ pub base_color: [f32; 4],
83
+ /// EN-012 — shading-model selector + foliage transmission tint.
84
+ /// x = shading_model enum (0 = default lit, 1 = foliage,
85
+ /// 2 = subsurface — V2 stub),
86
+ /// yzw = transmission_color (rgb tint for back-lit foliage; ignored
87
+ /// when shading_model == 0).
88
+ pub shading_model: [f32; 4],
89
+ /// EN-012 — foliage shading parameters. Only consumed when
90
+ /// `shading_model.x == 1.0`.
91
+ /// x = transmission_amount (0..1),
92
+ /// y = wrap_factor (0..1),
93
+ /// zw = reserved.
94
+ pub foliage_params: [f32; 4],
95
+ }
96
+
97
+ impl Default for MaterialFactorsUniforms {
98
+ fn default() -> Self {
99
+ Self {
100
+ metal_rough: [0.0, 1.0, 0.0, 0.0], // non-metal, rough, no MR tex, no cutoff
101
+ emissive: [0.0, 0.0, 0.0, 0.0],
102
+ base_color: [1.0, 1.0, 1.0, 1.0],
103
+ // EN-012 — default lit, white transmission tint. Materials
104
+ // that never call `setMaterialShadingModel` get standard PBR
105
+ // (shading_model.x == 0.0).
106
+ shading_model: [0.0, 1.0, 1.0, 1.0],
107
+ // EN-012 — moderate defaults so a freshly-flagged foliage
108
+ // material (shading_model = 1) looks reasonable before any
109
+ // tuning. Wrap=0.5 + transmission=0.5 gives soft back-face
110
+ // shading + a noticeable halo against the sun.
111
+ foliage_params: [0.5, 0.5, 0.0, 0.0],
112
+ }
113
+ }
114
+ }
115
+
116
+ #[repr(C)]
117
+ #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
118
+ pub struct PerDrawUniforms {
119
+ pub mvp: [[f32; 4]; 4],
120
+ pub model: [[f32; 4]; 4],
121
+ pub prev_mvp: [[f32; 4]; 4],
122
+ pub model_tint: [f32; 4],
123
+ pub skin_info: [u32; 4],
124
+ }
125
+
126
+ // =====================================================================
127
+ // Handles + draw commands
128
+ // =====================================================================
129
+
130
+ pub type MaterialHandle = u32;
131
+
132
+ pub struct MaterialDrawCommand {
133
+ pub material: MaterialHandle,
134
+ pub mesh_handle: u64, // matches model_gpu_cache keys
135
+ pub mesh_idx: usize, // sub-mesh index within that cached model
136
+ pub draw_slot: usize, // which slot in per_draw_buffers to bind
137
+ /// EN-001 — when set, the engine binds vertex slot 1 to this
138
+ /// instance buffer and emits draw_indexed with `0..count` instances.
139
+ /// `None` means a single-instance draw (the legacy path).
140
+ pub instance: Option<InstanceDrawInfo>,
141
+ }
142
+
143
+ /// Reference to an instance buffer for an instanced draw command.
144
+ /// `buffer_handle` is 1-based into `MaterialSystem::instance_buffers`;
145
+ /// `count` is the number of GPU instances to emit. Created via
146
+ /// `MaterialSystem::create_instance_buffer`, consumed by
147
+ /// `submit_draw_instanced`.
148
+ #[derive(Copy, Clone, Debug)]
149
+ pub struct InstanceDrawInfo {
150
+ pub buffer_handle: u32,
151
+ pub count: u32,
152
+ }
153
+
154
+ /// EN-001 — owned wgpu::Buffer + element count for an instance buffer.
155
+ /// Lives in the `MaterialSystem::instance_buffers` registry, indexed
156
+ /// by 1-based handle (0 = invalid).
157
+ pub struct InstanceBuffer {
158
+ pub buffer: wgpu::Buffer,
159
+ pub count: u32,
160
+ }
161
+
162
+ /// EN-014 — owned wgpu::Texture + view + layer-count for a texture
163
+ /// array. Lives in the `MaterialSystem::texture_arrays` registry,
164
+ /// indexed by 1-based handle (0 = invalid). The view is a D2Array
165
+ /// view over all `layer_count` layers (so `textureSample(arr, samp,
166
+ /// uv, layer_idx)` resolves layers 0..layer_count-1).
167
+ pub struct TextureArray {
168
+ pub texture: wgpu::Texture,
169
+ pub view: wgpu::TextureView,
170
+ pub layer_count: u32,
171
+ }
172
+
173
+ /// EN-014 — V1 cap on layers per texture array. The ticket calls this
174
+ /// "enough for any landscape shader" (UE5's typical landscape uses
175
+ /// 4–8 layers; 16 leaves headroom for foliage/debris splats).
176
+ pub const MAX_TEXTURE_ARRAY_LAYERS: u32 = 16;
177
+
178
+ /// EN-014 V2 — texture-array format codes, as exposed to the TS API
179
+ /// via `TEX_ARRAY_FORMAT_*`. Anything unrecognised falls back to sRGB
180
+ /// since albedo is the most common splat layer.
181
+ pub const TEX_ARRAY_FORMAT_SRGB: u32 = 0;
182
+ pub const TEX_ARRAY_FORMAT_LINEAR: u32 = 1;
183
+
184
+ /// Map a TS-side format code to a wgpu format. sRGB suits albedo; linear
185
+ /// is mandatory for normal/MR data textures (sRGB decoding would corrupt
186
+ /// the encoded normals or rough/metal channels).
187
+ pub fn map_texture_array_format(code: u32) -> wgpu::TextureFormat {
188
+ match code {
189
+ TEX_ARRAY_FORMAT_SRGB => wgpu::TextureFormat::Rgba8UnormSrgb,
190
+ TEX_ARRAY_FORMAT_LINEAR => wgpu::TextureFormat::Rgba8Unorm,
191
+ _ => wgpu::TextureFormat::Rgba8UnormSrgb,
192
+ }
193
+ }
194
+
195
+ // =====================================================================
196
+ // The system
197
+ // =====================================================================
198
+
199
+ pub struct MaterialSystem {
200
+ pub layouts: MaterialAbiLayouts,
201
+
202
+ // Compiled pipelines, indexed by MaterialHandle (1-based; 0 = invalid).
203
+ pub pipelines: Vec<Option<MaterialPipeline>>,
204
+
205
+ // Per-frame UBO + bind group (rewritten at the start of every frame).
206
+ pub per_frame_buffer: wgpu::Buffer,
207
+ pub per_frame_bg: wgpu::BindGroup,
208
+
209
+ // Per-view UBO — one for now (single camera). Phase 2 may add more
210
+ // for split-screen / shadow cascades.
211
+ pub per_view_buffer: wgpu::Buffer,
212
+ pub per_view_bg: wgpu::BindGroup,
213
+
214
+ // Default per-material bind group: all white 1×1 textures, default
215
+ // factors, zero-initialised user-params. Materials that don't
216
+ // provide their own share this.
217
+ pub default_per_material_bg: wgpu::BindGroup,
218
+ /// Kept alive so the BG it backs doesn't dangle.
219
+ _default_material_factors_buffer: wgpu::Buffer,
220
+ _default_user_params_buffer: wgpu::Buffer,
221
+ _default_white_tex: wgpu::Texture,
222
+ _default_white_view: wgpu::TextureView,
223
+ _default_sampler: wgpu::Sampler,
224
+ /// EN-011 — 1×1 black texture bound at @group(2) @binding(12) for
225
+ /// materials that don't have a planar reflection probe linked.
226
+ /// Lets shaders unconditionally `textureSample(planar_reflection_tex,
227
+ /// …)` without branching on probe presence.
228
+ _default_black_tex: wgpu::Texture,
229
+ pub default_black_view: wgpu::TextureView,
230
+ /// EN-014 — 1×1×1 transparent-black texture-array stub bound to
231
+ /// bindings 14/15/16 (@group(2)) for materials that don't declare
232
+ /// their own array. Has to be a real D2Array view (not a 2D
233
+ /// texture cast) so the layout's `view_dimension: D2Array`
234
+ /// matches at bind time.
235
+ _default_array_tex: wgpu::Texture,
236
+ pub default_array_view: wgpu::TextureView,
237
+
238
+ /// Phase 5 — per-material `user_params` UBOs. Indexed by
239
+ /// `MaterialHandle - 1`; `None` means the material uses the default
240
+ /// (zero-initialised) bind group. Created lazily on first
241
+ /// `set_user_params` call.
242
+ pub material_params_buffers: Vec<Option<wgpu::Buffer>>,
243
+ pub material_per_material_bgs: Vec<Option<wgpu::BindGroup>>,
244
+
245
+ /// EN-011 — link from material → planar reflection probe handle
246
+ /// (1-based; `None` → default 1×1 black texture at binding 12).
247
+ /// Indexed by `MaterialHandle - 1`. Set via
248
+ /// `Renderer::set_material_reflection_probe`; the per-material
249
+ /// bind group is rebuilt at that call so subsequent draws see
250
+ /// the probe's texture even though the engine repaints the
251
+ /// texture each frame (the wgpu Texture identity stays stable).
252
+ pub material_reflection_probe: Vec<Option<u32>>,
253
+
254
+ // Per-draw UBO pool. Buffers grow 1-by-1 as draws pile up.
255
+ // Each entry is `(PerDraw UBO, bind group binding it + the global
256
+ // joint buffer at binding 1)`.
257
+ pub per_draw_buffers: Vec<wgpu::Buffer>,
258
+ pub per_draw_bgs: Vec<wgpu::BindGroup>,
259
+
260
+ // Phase 4b — group 4 (SceneInputs) bind group. Rebuilt per-frame
261
+ // when any Refractive material is submitted and a scene-colour
262
+ // snapshot is available. `None` means "no material needs this
263
+ // group this frame" — translucent dispatch skips group 4
264
+ // binding entirely.
265
+ pub scene_inputs_bg: Option<wgpu::BindGroup>,
266
+ /// Linear sampler for scene-colour sampling (group 4 binding 1).
267
+ _scene_color_sampler: wgpu::Sampler,
268
+ /// Non-filtering sampler for scene-depth sampling (binding 3).
269
+ _scene_depth_sampler: wgpu::Sampler,
270
+ /// 1×1 default texture for impulse / motion-vectors slots when
271
+ /// no Phase 7 impulse system is wired up yet.
272
+ _scene_stub_tex: wgpu::Texture,
273
+ _scene_stub_view: wgpu::TextureView,
274
+ /// 1×1 stub depth texture — bound to scene_depth_tex in Phase 4b
275
+ /// because the live depth buffer can't be simultaneously sampled
276
+ /// and used as a depth-stencil attachment. Phase 4c will add a
277
+ /// copy-to-sample depth snapshot for shoreline-fade materials.
278
+ _scene_stub_depth: wgpu::Texture,
279
+ _scene_stub_depth_view: wgpu::TextureView,
280
+
281
+ // Frame state — commands split by bucket so the graph can
282
+ // schedule them into the right pass. Phase 4a keeps them in
283
+ // parallel lists; Phase 4b dispatches the translucent lists in
284
+ // their own sub-pass.
285
+ pub commands: Vec<MaterialDrawCommand>, // Bucket::Opaque + Bucket::Cutout
286
+ pub translucent_commands: Vec<MaterialDrawCommand>, // Transparent + Refractive + Additive
287
+ next_draw_slot: usize,
288
+
289
+ /// EN-001 — instance buffers, indexed by InstanceBufferHandle
290
+ /// (1-based; 0 = invalid). Each entry owns a wgpu Buffer + element
291
+ /// count. Created via `create_instance_buffer`, consumed by
292
+ /// `submit_draw_instanced` commands. Slots remain `None` after
293
+ /// `destroy_instance_buffer` so existing handles never collide
294
+ /// with re-issued ones.
295
+ pub instance_buffers: Vec<Option<InstanceBuffer>>,
296
+
297
+ /// EN-014 — texture arrays, indexed by 1-based handle (0 = invalid).
298
+ /// Each entry owns a wgpu D2Array texture + view + layer-count.
299
+ /// Created via `create_texture_array`, linked to a material's
300
+ /// per-material BG via `set_material_texture_array`.
301
+ pub texture_arrays: Vec<Option<TextureArray>>,
302
+
303
+ /// EN-014 — per-material → array-handle link, one slot for each
304
+ /// of the 3 array bindings (0 = albedo, 1 = normal, 2 = MR).
305
+ /// `None` means "use the default 1×1×1 stub array". Indexed by
306
+ /// `MaterialHandle - 1`, parallel to `material_per_material_bgs`
307
+ /// / `material_reflection_probe`.
308
+ pub material_texture_arrays: Vec<[Option<u32>; 3]>,
309
+
310
+ /// EN-012 — per-material `MaterialFactorsUniforms` UBO. `None` means
311
+ /// the material uses the shared `_default_material_factors_buffer`
312
+ /// (uniform default) — lazily allocated on the first
313
+ /// `set_material_shading_model` / `set_material_foliage` call.
314
+ /// Indexed by `MaterialHandle - 1`. The CPU-side mirror in
315
+ /// `material_factors_data` lets us partial-update one field at a
316
+ /// time without losing the others.
317
+ pub material_factors_buffers: Vec<Option<wgpu::Buffer>>,
318
+ pub material_factors_data: Vec<MaterialFactorsUniforms>,
319
+ }
320
+
321
+ impl MaterialSystem {
322
+ pub fn new(
323
+ device: &wgpu::Device,
324
+ queue: &wgpu::Queue,
325
+ // Joint-buffer plumbing is per-draw (used by ensure_draw_slot,
326
+ // not at construction time). Kept on the constructor's
327
+ // signature for symmetry with the per-draw path.
328
+ _joint_buffer: &wgpu::Buffer,
329
+ ) -> Self {
330
+ let layouts = MaterialAbiLayouts::create(device);
331
+
332
+ // Per-frame UBO.
333
+ let per_frame_buffer = device.create_buffer(&wgpu::BufferDescriptor {
334
+ label: Some("material_per_frame"),
335
+ size: std::mem::size_of::<PerFrameUniforms>() as u64,
336
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
337
+ mapped_at_creation: false,
338
+ });
339
+ let per_frame_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
340
+ label: Some("material_per_frame_bg"),
341
+ layout: &layouts.per_frame,
342
+ entries: &[wgpu::BindGroupEntry {
343
+ binding: 0, resource: per_frame_buffer.as_entire_binding(),
344
+ }],
345
+ });
346
+
347
+ // Default white 1×1 texture + sampler shared across optional PBR bindings.
348
+ let default_white_tex = device.create_texture_with_data(
349
+ queue,
350
+ &wgpu::TextureDescriptor {
351
+ label: Some("material_default_white"),
352
+ size: wgpu::Extent3d { width: 1, height: 1, depth_or_array_layers: 1 },
353
+ mip_level_count: 1,
354
+ sample_count: 1,
355
+ dimension: wgpu::TextureDimension::D2,
356
+ format: wgpu::TextureFormat::Rgba8UnormSrgb,
357
+ usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
358
+ view_formats: &[],
359
+ },
360
+ Default::default(),
361
+ &[255, 255, 255, 255],
362
+ );
363
+ let default_white_view = default_white_tex.create_view(&Default::default());
364
+
365
+ // EN-011 — Default 1×1 black HDR texture for the planar
366
+ // reflection slot (binding 12). Format is Rgba16Float to match
367
+ // the actual probe RT so the bind-group layout is identical
368
+ // whether a probe is bound or the default.
369
+ let black_pixel: [u16; 4] = [0, 0, 0, 0];
370
+ let default_black_tex = device.create_texture_with_data(
371
+ queue,
372
+ &wgpu::TextureDescriptor {
373
+ label: Some("material_default_black_reflection"),
374
+ size: wgpu::Extent3d { width: 1, height: 1, depth_or_array_layers: 1 },
375
+ mip_level_count: 1,
376
+ sample_count: 1,
377
+ dimension: wgpu::TextureDimension::D2,
378
+ format: super::formats::HDR_FORMAT,
379
+ usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
380
+ view_formats: &[],
381
+ },
382
+ Default::default(),
383
+ bytemuck::cast_slice(&black_pixel),
384
+ );
385
+ let default_black_view = default_black_tex.create_view(&Default::default());
386
+
387
+ // EN-014 — Default 1×1×1 transparent-black texture-array stub
388
+ // for bindings 14/15/16. Has to be a real D2Array (depth_or_
389
+ // _array_layers = 1, viewed as D2Array) so the bind-group
390
+ // layout's view_dimension matches at bind time. Format is
391
+ // Rgba8Unorm — same as a typical albedo source so a game
392
+ // dropping in albedo data without thinking gets the expected
393
+ // sRGB-vs-linear behaviour. Materials that need linear
394
+ // (normal/MR) typically encode that into the layer they upload.
395
+ let default_array_tex = device.create_texture_with_data(
396
+ queue,
397
+ &wgpu::TextureDescriptor {
398
+ label: Some("material_default_array_stub"),
399
+ size: wgpu::Extent3d { width: 1, height: 1, depth_or_array_layers: 1 },
400
+ mip_level_count: 1,
401
+ sample_count: 1,
402
+ dimension: wgpu::TextureDimension::D2,
403
+ format: wgpu::TextureFormat::Rgba8Unorm,
404
+ usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
405
+ view_formats: &[],
406
+ },
407
+ Default::default(),
408
+ &[0, 0, 0, 0],
409
+ );
410
+ let default_array_view = default_array_tex.create_view(&wgpu::TextureViewDescriptor {
411
+ label: Some("material_default_array_stub_view"),
412
+ dimension: Some(wgpu::TextureViewDimension::D2Array),
413
+ ..Default::default()
414
+ });
415
+
416
+ let default_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
417
+ label: Some("material_default_samp"),
418
+ address_mode_u: wgpu::AddressMode::Repeat,
419
+ address_mode_v: wgpu::AddressMode::Repeat,
420
+ mag_filter: wgpu::FilterMode::Linear,
421
+ min_filter: wgpu::FilterMode::Linear,
422
+ mipmap_filter: wgpu::MipmapFilterMode::Linear,
423
+ ..Default::default()
424
+ });
425
+
426
+ // Per-view UBO — zero init; write the real data each frame in `begin_frame`.
427
+ let per_view_buffer = device.create_buffer(&wgpu::BufferDescriptor {
428
+ label: Some("material_per_view"),
429
+ size: std::mem::size_of::<PerViewUniforms>() as u64,
430
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
431
+ mapped_at_creation: false,
432
+ });
433
+ // We don't have the env / BRDF LUT / shadow textures plumbed through
434
+ // the material-system layout yet — Phase 2 wires them from the
435
+ // existing renderer state. For now the PerView bind group uses the
436
+ // default white texture + sampler for the env / BRDF / shadow slots
437
+ // as placeholders so draws validate.
438
+ let white_samp = &default_sampler;
439
+ let white_view = &default_white_view;
440
+ let cmp_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
441
+ label: Some("material_default_cmp_samp"),
442
+ compare: Some(wgpu::CompareFunction::LessEqual),
443
+ ..Default::default()
444
+ });
445
+ // PerView layout wants a depth texture for shadow bindings 6-8.
446
+ // Use a 1×1 depth as a stub.
447
+ let stub_depth = device.create_texture(&wgpu::TextureDescriptor {
448
+ label: Some("material_stub_depth"),
449
+ size: wgpu::Extent3d { width: 1, height: 1, depth_or_array_layers: 1 },
450
+ mip_level_count: 1,
451
+ sample_count: 1,
452
+ dimension: wgpu::TextureDimension::D2,
453
+ format: wgpu::TextureFormat::Depth32Float,
454
+ usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
455
+ view_formats: &[],
456
+ });
457
+ let stub_depth_view = stub_depth.create_view(&Default::default());
458
+ let per_view_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
459
+ label: Some("material_per_view_bg"),
460
+ layout: &layouts.per_view,
461
+ entries: &[
462
+ wgpu::BindGroupEntry { binding: 0, resource: per_view_buffer.as_entire_binding() },
463
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(white_view) },
464
+ wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::Sampler(white_samp) },
465
+ wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(white_view) },
466
+ wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::TextureView(white_view) },
467
+ wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::Sampler(white_samp) },
468
+ wgpu::BindGroupEntry { binding: 6, resource: wgpu::BindingResource::TextureView(&stub_depth_view) },
469
+ wgpu::BindGroupEntry { binding: 7, resource: wgpu::BindingResource::TextureView(&stub_depth_view) },
470
+ wgpu::BindGroupEntry { binding: 8, resource: wgpu::BindingResource::TextureView(&stub_depth_view) },
471
+ wgpu::BindGroupEntry { binding: 9, resource: wgpu::BindingResource::Sampler(&cmp_sampler) },
472
+ ],
473
+ });
474
+ // The stub_depth texture and cmp_sampler outlive the bind group via
475
+ // wgpu internal Arc; we don't need to hold them in the struct.
476
+ std::mem::forget(stub_depth);
477
+ std::mem::forget(stub_depth_view);
478
+ std::mem::forget(cmp_sampler);
479
+
480
+ // Default MaterialFactors UBO.
481
+ let default_mf = MaterialFactorsUniforms::default();
482
+ let default_material_factors_buffer = device.create_buffer_init(
483
+ &wgpu::util::BufferInitDescriptor {
484
+ label: Some("material_default_factors"),
485
+ contents: bytemuck::bytes_of(&default_mf),
486
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
487
+ },
488
+ );
489
+ // Default user-params UBO — 256 bytes of zeros (ABI §1.4).
490
+ let default_user_params_buffer = device.create_buffer(&wgpu::BufferDescriptor {
491
+ label: Some("material_default_user_params"),
492
+ size: 256,
493
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
494
+ mapped_at_creation: false,
495
+ });
496
+ // Need a SECOND sampler here because wgpu requires distinct sampler
497
+ // handles when binding the same logical sampler to multiple slots
498
+ // within one BG — actually it doesn't, the same handle works fine.
499
+ // We reuse default_sampler for every material-tex sampler slot.
500
+ let default_per_material_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
501
+ label: Some("material_default_per_material_bg"),
502
+ layout: &layouts.per_material,
503
+ entries: &[
504
+ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&default_white_view) },
505
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&default_sampler) },
506
+ wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(&default_white_view) },
507
+ wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::Sampler(&default_sampler) },
508
+ wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::TextureView(&default_white_view) },
509
+ wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::Sampler(&default_sampler) },
510
+ wgpu::BindGroupEntry { binding: 6, resource: wgpu::BindingResource::TextureView(&default_white_view) },
511
+ wgpu::BindGroupEntry { binding: 7, resource: wgpu::BindingResource::Sampler(&default_sampler) },
512
+ wgpu::BindGroupEntry { binding: 8, resource: wgpu::BindingResource::TextureView(&default_white_view) },
513
+ wgpu::BindGroupEntry { binding: 9, resource: wgpu::BindingResource::Sampler(&default_sampler) },
514
+ wgpu::BindGroupEntry { binding: 10, resource: default_material_factors_buffer.as_entire_binding() },
515
+ wgpu::BindGroupEntry { binding: 11, resource: default_user_params_buffer.as_entire_binding() },
516
+ // EN-011 — default black 1×1 reflection texture +
517
+ // shared linear sampler. Replaced per-material when a
518
+ // game calls `set_material_reflection_probe`.
519
+ wgpu::BindGroupEntry { binding: 12, resource: wgpu::BindingResource::TextureView(&default_black_view) },
520
+ wgpu::BindGroupEntry { binding: 13, resource: wgpu::BindingResource::Sampler(&default_sampler) },
521
+ // EN-014 — default 1×1×1 stub array bound to all 3
522
+ // texture-array slots, with the shared default
523
+ // sampler at binding 17. Replaced per-slot when a
524
+ // game calls `set_material_texture_array`.
525
+ wgpu::BindGroupEntry { binding: 14, resource: wgpu::BindingResource::TextureView(&default_array_view) },
526
+ wgpu::BindGroupEntry { binding: 15, resource: wgpu::BindingResource::TextureView(&default_array_view) },
527
+ wgpu::BindGroupEntry { binding: 16, resource: wgpu::BindingResource::TextureView(&default_array_view) },
528
+ wgpu::BindGroupEntry { binding: 17, resource: wgpu::BindingResource::Sampler(&default_sampler) },
529
+ ],
530
+ });
531
+
532
+ // Phase 4b — scene-inputs scratch resources. Samplers are
533
+ // stable across frames; the bind group itself is rebuilt
534
+ // when a scene-colour snapshot becomes available.
535
+ let scene_color_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
536
+ label: Some("scene_color_samp"),
537
+ address_mode_u: wgpu::AddressMode::ClampToEdge,
538
+ address_mode_v: wgpu::AddressMode::ClampToEdge,
539
+ mag_filter: wgpu::FilterMode::Linear,
540
+ min_filter: wgpu::FilterMode::Linear,
541
+ mipmap_filter: wgpu::MipmapFilterMode::Nearest,
542
+ ..Default::default()
543
+ });
544
+ let scene_depth_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
545
+ label: Some("scene_depth_samp"),
546
+ address_mode_u: wgpu::AddressMode::ClampToEdge,
547
+ address_mode_v: wgpu::AddressMode::ClampToEdge,
548
+ mag_filter: wgpu::FilterMode::Nearest,
549
+ min_filter: wgpu::FilterMode::Nearest,
550
+ mipmap_filter: wgpu::MipmapFilterMode::Nearest,
551
+ ..Default::default()
552
+ });
553
+ // 1×1 black texture as the stub impulse / motion-vector slot
554
+ // until Phase 7 wires real sources.
555
+ let scene_stub_tex = device.create_texture_with_data(
556
+ queue,
557
+ &wgpu::TextureDescriptor {
558
+ label: Some("scene_inputs_stub"),
559
+ size: wgpu::Extent3d { width: 1, height: 1, depth_or_array_layers: 1 },
560
+ mip_level_count: 1,
561
+ sample_count: 1,
562
+ dimension: wgpu::TextureDimension::D2,
563
+ format: wgpu::TextureFormat::Rgba8Unorm,
564
+ usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
565
+ view_formats: &[],
566
+ },
567
+ Default::default(),
568
+ &[0, 0, 0, 0],
569
+ );
570
+ let scene_stub_view = scene_stub_tex.create_view(&Default::default());
571
+
572
+ // Stub depth texture — Depth32Float 1×1, cleared to 1.0 (far).
573
+ let scene_stub_depth = device.create_texture(&wgpu::TextureDescriptor {
574
+ label: Some("scene_depth_stub"),
575
+ size: wgpu::Extent3d { width: 1, height: 1, depth_or_array_layers: 1 },
576
+ mip_level_count: 1,
577
+ sample_count: 1,
578
+ dimension: wgpu::TextureDimension::D2,
579
+ format: wgpu::TextureFormat::Depth32Float,
580
+ usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
581
+ view_formats: &[],
582
+ });
583
+ let scene_stub_depth_view = scene_stub_depth.create_view(&Default::default());
584
+
585
+ Self {
586
+ layouts,
587
+ pipelines: Vec::new(),
588
+ per_frame_buffer,
589
+ per_frame_bg,
590
+ per_view_buffer,
591
+ per_view_bg,
592
+ default_per_material_bg,
593
+ _default_material_factors_buffer: default_material_factors_buffer,
594
+ _default_user_params_buffer: default_user_params_buffer,
595
+ _default_white_tex: default_white_tex,
596
+ _default_white_view: default_white_view,
597
+ _default_sampler: default_sampler,
598
+ _default_black_tex: default_black_tex,
599
+ default_black_view,
600
+ _default_array_tex: default_array_tex,
601
+ default_array_view,
602
+ material_params_buffers: Vec::new(),
603
+ material_per_material_bgs: Vec::new(),
604
+ material_reflection_probe: Vec::new(),
605
+ per_draw_buffers: Vec::new(),
606
+ per_draw_bgs: Vec::new(),
607
+ scene_inputs_bg: None,
608
+ _scene_color_sampler: scene_color_sampler,
609
+ _scene_depth_sampler: scene_depth_sampler,
610
+ _scene_stub_tex: scene_stub_tex,
611
+ _scene_stub_view: scene_stub_view,
612
+ _scene_stub_depth: scene_stub_depth,
613
+ _scene_stub_depth_view: scene_stub_depth_view,
614
+ commands: Vec::new(),
615
+ translucent_commands: Vec::new(),
616
+ next_draw_slot: 0,
617
+ instance_buffers: Vec::new(),
618
+ texture_arrays: Vec::new(),
619
+ material_texture_arrays: Vec::new(),
620
+ // EN-012 — per-material MaterialFactors UBOs are lazy.
621
+ // Until a material calls `set_shading_model` /
622
+ // `set_foliage`, it shares the default factors buffer.
623
+ material_factors_buffers: Vec::new(),
624
+ material_factors_data: Vec::new(),
625
+ }
626
+ }
627
+
628
+ // --- Material registry --------------------------------------------
629
+
630
+ /// Compile a material and return its handle. Handles are 1-based;
631
+ /// 0 is reserved for "invalid material".
632
+ pub fn compile(
633
+ &mut self,
634
+ device: &wgpu::Device,
635
+ wgsl_source: &str,
636
+ profile: FragmentProfile,
637
+ bucket: Bucket,
638
+ reads_scene: bool,
639
+ wants_instancing: bool,
640
+ hdr_format: wgpu::TextureFormat,
641
+ material_format: wgpu::TextureFormat,
642
+ velocity_format: wgpu::TextureFormat,
643
+ albedo_format: wgpu::TextureFormat,
644
+ depth_format: wgpu::TextureFormat,
645
+ ) -> Result<MaterialHandle, MaterialCompileError> {
646
+ // Inject the user's WGSL under a synthetic path so the
647
+ // preprocessor can resolve `#include "material_abi.wgsl"` etc.
648
+ let entry_path = "__user_material.wgsl";
649
+ let desc = MaterialCompileDesc {
650
+ label: "user_material",
651
+ entry_path,
652
+ extra_sources: &[(entry_path, wgsl_source)],
653
+ profile,
654
+ bucket,
655
+ reads_scene,
656
+ hdr_format,
657
+ material_format,
658
+ velocity_format,
659
+ albedo_format,
660
+ depth_format,
661
+ vertex_buffers: &[Vertex3D::desc()],
662
+ wants_instancing,
663
+ };
664
+ let pipeline = compile_material(device, &self.layouts, &desc)?;
665
+ self.pipelines.push(Some(pipeline));
666
+ Ok(self.pipelines.len() as MaterialHandle)
667
+ }
668
+
669
+ // --- Frame lifecycle ----------------------------------------------
670
+
671
+ /// Write the per-frame + per-view UBOs. Safe to call any time
672
+ /// before `dispatch`; callers should call exactly once per frame
673
+ /// to keep `PerFrame.time` accurate. Does NOT clear the commands
674
+ /// list — that's `reset_frame`'s job (called at begin_frame, not
675
+ /// end_frame, so draws submitted during the frame survive).
676
+ pub fn update_frame_uniforms(
677
+ &mut self,
678
+ queue: &wgpu::Queue,
679
+ per_frame: &PerFrameUniforms,
680
+ per_view: &PerViewUniforms,
681
+ ) {
682
+ queue.write_buffer(&self.per_frame_buffer, 0, bytemuck::bytes_of(per_frame));
683
+ queue.write_buffer(&self.per_view_buffer, 0, bytemuck::bytes_of(per_view));
684
+ }
685
+
686
+ /// Reset the per-draw slot cursor. Commands lists are cleared by
687
+ /// the Renderer from its own `begin_frame` so the order of reset
688
+ /// vs. submit is deterministic.
689
+ pub fn reset_draw_slot(&mut self) {
690
+ self.next_draw_slot = 0;
691
+ }
692
+
693
+ /// Phase 5 — set/replace `user_params` for a specific material. The
694
+ /// next dispatch of this handle binds a per-material BindGroup with
695
+ /// the given bytes uploaded to `@group(2) @binding(11)`. Materials
696
+ /// that never receive a `set_user_params` call keep using the
697
+ /// default zero-initialised UBO.
698
+ ///
699
+ /// `params.len()` must be ≤ 256 bytes (ABI §1.4 cap). The buffer
700
+ /// is allocated lazily on first call per handle and reused on
701
+ /// subsequent updates. Pass an empty slice to revert to the default.
702
+ pub fn set_user_params(
703
+ &mut self,
704
+ device: &wgpu::Device,
705
+ queue: &wgpu::Queue,
706
+ handle: MaterialHandle,
707
+ params: &[u8],
708
+ ) -> Result<(), &'static str> {
709
+ if handle == 0 { return Err("invalid material handle"); }
710
+ let idx = (handle - 1) as usize;
711
+ if idx >= self.pipelines.len() || self.pipelines[idx].is_none() {
712
+ return Err("material handle not registered");
713
+ }
714
+ if params.len() > 256 {
715
+ return Err("user_params exceeds 256-byte cap");
716
+ }
717
+
718
+ // Grow parallel vectors so handle index is valid.
719
+ while self.material_params_buffers.len() <= idx {
720
+ self.material_params_buffers.push(None);
721
+ self.material_per_material_bgs.push(None);
722
+ }
723
+
724
+ // Reverting to default — drop the per-material entries.
725
+ if params.is_empty() {
726
+ self.material_params_buffers[idx] = None;
727
+ self.material_per_material_bgs[idx] = None;
728
+ return Ok(());
729
+ }
730
+
731
+ // Allocate the per-material UBO + BG on first set. Padded to 256 B
732
+ // so the ABI cap is reflected in the buffer size and write_buffer
733
+ // never partially fills. EN-011 bindings 12/13 default to the
734
+ // shared 1×1 black reflection texture; if a probe was previously
735
+ // linked via `set_reflection_probe`, the reflection BG will be
736
+ // rebuilt by that path the next time the link changes.
737
+ if self.material_params_buffers[idx].is_none() {
738
+ let buf = device.create_buffer(&wgpu::BufferDescriptor {
739
+ label: Some("material_user_params"),
740
+ size: 256,
741
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
742
+ mapped_at_creation: false,
743
+ });
744
+ // EN-012 — resolve the per-material factors UBO so a
745
+ // material that previously called `set_shading_model` /
746
+ // `set_foliage` keeps its custom factors at binding 10
747
+ // when user_params is later set on it.
748
+ let factors_buf: &wgpu::Buffer = self.material_factors_buffers
749
+ .get(idx)
750
+ .and_then(|b| b.as_ref())
751
+ .unwrap_or(&self._default_material_factors_buffer);
752
+ let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
753
+ label: Some("material_per_material_bg_user"),
754
+ layout: &self.layouts.per_material,
755
+ entries: &[
756
+ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
757
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
758
+ wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
759
+ wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
760
+ wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
761
+ wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
762
+ wgpu::BindGroupEntry { binding: 6, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
763
+ wgpu::BindGroupEntry { binding: 7, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
764
+ wgpu::BindGroupEntry { binding: 8, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
765
+ wgpu::BindGroupEntry { binding: 9, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
766
+ wgpu::BindGroupEntry { binding: 10, resource: factors_buf.as_entire_binding() },
767
+ wgpu::BindGroupEntry { binding: 11, resource: buf.as_entire_binding() },
768
+ wgpu::BindGroupEntry { binding: 12, resource: wgpu::BindingResource::TextureView(&self.default_black_view) },
769
+ wgpu::BindGroupEntry { binding: 13, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
770
+ // EN-014 — default stub array on all 3 slots.
771
+ wgpu::BindGroupEntry { binding: 14, resource: wgpu::BindingResource::TextureView(&self.default_array_view) },
772
+ wgpu::BindGroupEntry { binding: 15, resource: wgpu::BindingResource::TextureView(&self.default_array_view) },
773
+ wgpu::BindGroupEntry { binding: 16, resource: wgpu::BindingResource::TextureView(&self.default_array_view) },
774
+ wgpu::BindGroupEntry { binding: 17, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
775
+ ],
776
+ });
777
+ self.material_params_buffers[idx] = Some(buf);
778
+ self.material_per_material_bgs[idx] = Some(bg);
779
+ }
780
+
781
+ // Pad short writes to 256 B so write_buffer doesn't read past `params`.
782
+ let mut padded = [0u8; 256];
783
+ padded[..params.len()].copy_from_slice(params);
784
+ let buf = self.material_params_buffers[idx].as_ref().unwrap();
785
+ queue.write_buffer(buf, 0, &padded);
786
+ Ok(())
787
+ }
788
+
789
+ /// Per-material BG when set, otherwise the shared default.
790
+ fn per_material_bg_for(&self, handle: MaterialHandle) -> &wgpu::BindGroup {
791
+ let idx = (handle as usize).wrapping_sub(1);
792
+ self.material_per_material_bgs.get(idx)
793
+ .and_then(|b| b.as_ref())
794
+ .unwrap_or(&self.default_per_material_bg)
795
+ }
796
+
797
+ /// EN-011 — link a material to a planar reflection probe's RT
798
+ /// view. The per-material bind group is rebuilt so binding 12
799
+ /// resolves to the probe's `color_view` instead of the default
800
+ /// 1×1 black texture. Subsequent draws of `handle` see the
801
+ /// probe's contents.
802
+ ///
803
+ /// Pass `probe_view = None` (or revert to the default by passing
804
+ /// `&self.default_black_view` from the caller side) to unlink.
805
+ /// The `probe_handle` is opaque to this method — it's stored
806
+ /// purely so `material_reflection_probe[idx]` reflects the link
807
+ /// for diagnostics + the renderer's per-frame dispatch.
808
+ ///
809
+ /// The wgpu Texture identity is stable for the probe's lifetime,
810
+ /// so we DON'T need to rebuild this bind group every frame; the
811
+ /// engine repaints the probe's texture but the same view keeps
812
+ /// pointing at it.
813
+ ///
814
+ /// Why we route through here vs direct field access: the BG
815
+ /// also needs to bind a user-params UBO (binding 11) — either
816
+ /// the per-material UBO when one was allocated by
817
+ /// `set_user_params`, or the shared default zero-UBO. This
818
+ /// helper picks the right one.
819
+ pub fn set_reflection_probe(
820
+ &mut self,
821
+ device: &wgpu::Device,
822
+ handle: MaterialHandle,
823
+ probe_handle: u32,
824
+ probe_view: &wgpu::TextureView,
825
+ ) -> Result<(), &'static str> {
826
+ if handle == 0 { return Err("invalid material handle"); }
827
+ let idx = (handle - 1) as usize;
828
+ if idx >= self.pipelines.len() || self.pipelines[idx].is_none() {
829
+ return Err("material handle not registered");
830
+ }
831
+
832
+ // Grow parallel vectors so the index is in bounds. We grow
833
+ // BOTH params + reflection registries together — they share
834
+ // an index domain (MaterialHandle - 1).
835
+ while self.material_params_buffers.len() <= idx {
836
+ self.material_params_buffers.push(None);
837
+ self.material_per_material_bgs.push(None);
838
+ }
839
+ while self.material_reflection_probe.len() <= idx {
840
+ self.material_reflection_probe.push(None);
841
+ }
842
+ self.material_reflection_probe[idx] = Some(probe_handle);
843
+
844
+ // Resolve binding 11 — per-material UBO if one's been
845
+ // allocated, else the shared zero-init default.
846
+ let user_params_buf: &wgpu::Buffer = self.material_params_buffers
847
+ .get(idx)
848
+ .and_then(|b| b.as_ref())
849
+ .unwrap_or(&self._default_user_params_buffer);
850
+
851
+ // EN-012 — resolve binding 10 to the per-material factors UBO
852
+ // when one's been allocated by `set_shading_model` /
853
+ // `set_foliage`. Otherwise the reflection-probe rebind would
854
+ // silently revert a foliage-flagged material back to the
855
+ // default factors at binding 10.
856
+ let factors_buf: &wgpu::Buffer = self.material_factors_buffers
857
+ .get(idx)
858
+ .and_then(|b| b.as_ref())
859
+ .unwrap_or(&self._default_material_factors_buffer);
860
+
861
+ // EN-014 — resolve binding 14/15/16 from any per-material
862
+ // texture-array links so a probe-bound material doesn't lose
863
+ // its splat layers when the BG gets rebuilt.
864
+ let [albedo_view, normal_view, mr_view] = self.resolve_array_views(idx);
865
+
866
+ let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
867
+ label: Some("material_per_material_bg_reflection"),
868
+ layout: &self.layouts.per_material,
869
+ entries: &[
870
+ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
871
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
872
+ wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
873
+ wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
874
+ wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
875
+ wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
876
+ wgpu::BindGroupEntry { binding: 6, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
877
+ wgpu::BindGroupEntry { binding: 7, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
878
+ wgpu::BindGroupEntry { binding: 8, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
879
+ wgpu::BindGroupEntry { binding: 9, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
880
+ wgpu::BindGroupEntry { binding: 10, resource: factors_buf.as_entire_binding() },
881
+ wgpu::BindGroupEntry { binding: 11, resource: user_params_buf.as_entire_binding() },
882
+ // EN-011 — probe's color view + a filtering sampler.
883
+ wgpu::BindGroupEntry { binding: 12, resource: wgpu::BindingResource::TextureView(probe_view) },
884
+ wgpu::BindGroupEntry { binding: 13, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
885
+ // EN-014 — resolved texture arrays + shared sampler.
886
+ wgpu::BindGroupEntry { binding: 14, resource: wgpu::BindingResource::TextureView(albedo_view) },
887
+ wgpu::BindGroupEntry { binding: 15, resource: wgpu::BindingResource::TextureView(normal_view) },
888
+ wgpu::BindGroupEntry { binding: 16, resource: wgpu::BindingResource::TextureView(mr_view) },
889
+ wgpu::BindGroupEntry { binding: 17, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
890
+ ],
891
+ });
892
+ self.material_per_material_bgs[idx] = Some(bg);
893
+ Ok(())
894
+ }
895
+
896
+ /// EN-014 — resolve the 3 D2Array texture views for a material
897
+ /// (albedo / normal / MR), falling back to the shared 1×1×1 stub
898
+ /// when the slot has no link. Returns borrowed views that share
899
+ /// the lifetime of `self`. Callers use these to populate bindings
900
+ /// 14/15/16 when (re)building a per-material BG.
901
+ fn resolve_array_views(&self, idx: usize) -> [&wgpu::TextureView; 3] {
902
+ let mut out: [&wgpu::TextureView; 3] = [
903
+ &self.default_array_view,
904
+ &self.default_array_view,
905
+ &self.default_array_view,
906
+ ];
907
+ if let Some(slots) = self.material_texture_arrays.get(idx) {
908
+ for (slot, link) in slots.iter().enumerate() {
909
+ if let Some(handle) = link {
910
+ let h = *handle as usize;
911
+ if h > 0 {
912
+ if let Some(Some(arr)) = self.texture_arrays.get(h - 1) {
913
+ out[slot] = &arr.view;
914
+ }
915
+ }
916
+ }
917
+ }
918
+ }
919
+ out
920
+ }
921
+
922
+ /// EN-012 — resolve the per-material `MaterialFactors` UBO for
923
+ /// `idx`, falling back to the shared default buffer when no
924
+ /// per-material factors UBO has been allocated yet. Used by every
925
+ /// per-material BG-build path so EN-011 / EN-014 rebinds don't
926
+ /// silently revert a material's foliage / shading-model state to
927
+ /// the default factors.
928
+ fn resolve_factors_buffer(&self, idx: usize) -> &wgpu::Buffer {
929
+ self.material_factors_buffers
930
+ .get(idx)
931
+ .and_then(|b| b.as_ref())
932
+ .unwrap_or(&self._default_material_factors_buffer)
933
+ }
934
+
935
+ /// EN-012 — ensure a per-material `MaterialFactorsUniforms` UBO
936
+ /// exists for `idx`, allocating + initialising from the default
937
+ /// values on first use. Returns mutable access to the CPU mirror
938
+ /// so the caller can mutate one field then write back via
939
+ /// `flush_material_factors`. Grows `material_factors_buffers` /
940
+ /// `material_factors_data` so `idx` is in bounds.
941
+ fn ensure_material_factors(
942
+ &mut self, device: &wgpu::Device, idx: usize,
943
+ ) -> &mut MaterialFactorsUniforms {
944
+ while self.material_factors_buffers.len() <= idx {
945
+ self.material_factors_buffers.push(None);
946
+ self.material_factors_data.push(MaterialFactorsUniforms::default());
947
+ }
948
+ if self.material_factors_buffers[idx].is_none() {
949
+ // Allocate a fresh per-material factors UBO seeded with the
950
+ // current CPU-side data (defaults on first call).
951
+ let init = self.material_factors_data[idx];
952
+ let buf = device.create_buffer_init(
953
+ &wgpu::util::BufferInitDescriptor {
954
+ label: Some("material_factors_per_material"),
955
+ contents: bytemuck::bytes_of(&init),
956
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
957
+ },
958
+ );
959
+ self.material_factors_buffers[idx] = Some(buf);
960
+ }
961
+ &mut self.material_factors_data[idx]
962
+ }
963
+
964
+ /// EN-012 — write the current CPU-side `MaterialFactorsUniforms`
965
+ /// for `idx` to the per-material UBO. Caller must have previously
966
+ /// called `ensure_material_factors` so the buffer exists.
967
+ fn flush_material_factors(&self, queue: &wgpu::Queue, idx: usize) {
968
+ if let Some(Some(buf)) = self.material_factors_buffers.get(idx) {
969
+ let data = &self.material_factors_data[idx];
970
+ queue.write_buffer(buf, 0, bytemuck::bytes_of(data));
971
+ }
972
+ }
973
+
974
+ /// EN-012 — rebuild the per-material BG for `idx` after a
975
+ /// MaterialFactors mutation. Resolves the per-material UBO,
976
+ /// user_params UBO, reflection-probe view, and texture-array
977
+ /// views from the current state so we don't clobber any of the
978
+ /// EN-005/EN-011/EN-014 wiring. Caller is responsible for
979
+ /// passing `probe_view` (the renderer wrapper looks the linked
980
+ /// probe up via `material_reflection_probe_handle` and supplies
981
+ /// either the probe's RT view or the default 1×1 black view).
982
+ fn rebuild_per_material_bg(
983
+ &mut self,
984
+ device: &wgpu::Device,
985
+ idx: usize,
986
+ probe_view: &wgpu::TextureView,
987
+ ) {
988
+ // Grow parallel BG vector so `idx` is in bounds.
989
+ while self.material_per_material_bgs.len() <= idx {
990
+ self.material_params_buffers.push(None);
991
+ self.material_per_material_bgs.push(None);
992
+ }
993
+
994
+ let factors_buf: &wgpu::Buffer = self.resolve_factors_buffer(idx);
995
+ let user_params_buf: &wgpu::Buffer = self.material_params_buffers
996
+ .get(idx)
997
+ .and_then(|b| b.as_ref())
998
+ .unwrap_or(&self._default_user_params_buffer);
999
+ let [albedo_view, normal_view, mr_view] = self.resolve_array_views(idx);
1000
+
1001
+ let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1002
+ label: Some("material_per_material_bg_factors"),
1003
+ layout: &self.layouts.per_material,
1004
+ entries: &[
1005
+ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
1006
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
1007
+ wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
1008
+ wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
1009
+ wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
1010
+ wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
1011
+ wgpu::BindGroupEntry { binding: 6, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
1012
+ wgpu::BindGroupEntry { binding: 7, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
1013
+ wgpu::BindGroupEntry { binding: 8, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
1014
+ wgpu::BindGroupEntry { binding: 9, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
1015
+ wgpu::BindGroupEntry { binding: 10, resource: factors_buf.as_entire_binding() },
1016
+ wgpu::BindGroupEntry { binding: 11, resource: user_params_buf.as_entire_binding() },
1017
+ wgpu::BindGroupEntry { binding: 12, resource: wgpu::BindingResource::TextureView(probe_view) },
1018
+ wgpu::BindGroupEntry { binding: 13, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
1019
+ wgpu::BindGroupEntry { binding: 14, resource: wgpu::BindingResource::TextureView(albedo_view) },
1020
+ wgpu::BindGroupEntry { binding: 15, resource: wgpu::BindingResource::TextureView(normal_view) },
1021
+ wgpu::BindGroupEntry { binding: 16, resource: wgpu::BindingResource::TextureView(mr_view) },
1022
+ wgpu::BindGroupEntry { binding: 17, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
1023
+ ],
1024
+ });
1025
+ self.material_per_material_bgs[idx] = Some(bg);
1026
+ }
1027
+
1028
+ /// EN-012 — set the shading model for a material (`0` = default
1029
+ /// lit, `1` = foliage, `2` = subsurface — V2 stub). Lazily
1030
+ /// allocates a per-material `MaterialFactors` UBO on first call,
1031
+ /// then rebuilds the per-material BG so subsequent draws sample
1032
+ /// the new shading model. Caller passes `probe_view` so EN-011
1033
+ /// reflection links survive — see `Renderer::set_material_shading_model`.
1034
+ pub fn set_material_shading_model(
1035
+ &mut self,
1036
+ device: &wgpu::Device,
1037
+ queue: &wgpu::Queue,
1038
+ material: MaterialHandle,
1039
+ model: u32,
1040
+ probe_view: &wgpu::TextureView,
1041
+ ) -> Result<(), &'static str> {
1042
+ if material == 0 { return Err("invalid material handle"); }
1043
+ let idx = (material - 1) as usize;
1044
+ if idx >= self.pipelines.len() || self.pipelines[idx].is_none() {
1045
+ return Err("material handle not registered");
1046
+ }
1047
+ {
1048
+ let factors = self.ensure_material_factors(device, idx);
1049
+ factors.shading_model[0] = model as f32;
1050
+ }
1051
+ self.flush_material_factors(queue, idx);
1052
+ self.rebuild_per_material_bg(device, idx, probe_view);
1053
+ Ok(())
1054
+ }
1055
+
1056
+ /// EN-012 — set the foliage shading parameters for a material.
1057
+ /// Only takes effect when `shading_model == 1` (foliage). Updates
1058
+ /// `MaterialFactors.shading_model.yzw` (transmission_color) and
1059
+ /// `foliage_params.xy` (transmission_amount, wrap_factor). Lazily
1060
+ /// allocates a per-material UBO on first call.
1061
+ pub fn set_material_foliage(
1062
+ &mut self,
1063
+ device: &wgpu::Device,
1064
+ queue: &wgpu::Queue,
1065
+ material: MaterialHandle,
1066
+ trans_color: [f32; 3],
1067
+ trans_amount: f32,
1068
+ wrap_factor: f32,
1069
+ probe_view: &wgpu::TextureView,
1070
+ ) -> Result<(), &'static str> {
1071
+ if material == 0 { return Err("invalid material handle"); }
1072
+ let idx = (material - 1) as usize;
1073
+ if idx >= self.pipelines.len() || self.pipelines[idx].is_none() {
1074
+ return Err("material handle not registered");
1075
+ }
1076
+ {
1077
+ let factors = self.ensure_material_factors(device, idx);
1078
+ factors.shading_model[1] = trans_color[0];
1079
+ factors.shading_model[2] = trans_color[1];
1080
+ factors.shading_model[3] = trans_color[2];
1081
+ factors.foliage_params[0] = trans_amount;
1082
+ factors.foliage_params[1] = wrap_factor;
1083
+ }
1084
+ self.flush_material_factors(queue, idx);
1085
+ self.rebuild_per_material_bg(device, idx, probe_view);
1086
+ Ok(())
1087
+ }
1088
+
1089
+ /// EN-014 — create a 2D texture array from a slice of layer data.
1090
+ /// Each `(bytes, w, h)` describes one layer's RGBA8 source. All
1091
+ /// layers must share `w × h` (wgpu requires a uniform extent for
1092
+ /// D2Array). V1 panics on mismatch — V2 may resize. Layer count
1093
+ /// is capped at `MAX_TEXTURE_ARRAY_LAYERS`; extra layers are
1094
+ /// dropped. Returns a 1-based handle (0 on failure: empty layers
1095
+ /// or zero extent).
1096
+ ///
1097
+ /// Defaults: `format = 0` (Rgba8UnormSrgb, suitable for albedo),
1098
+ /// `mip_levels = 1` (no mips). For data textures (normal / MR)
1099
+ /// or auto-mip generation, see `create_texture_array_ex`.
1100
+ pub fn create_texture_array(
1101
+ &mut self,
1102
+ device: &wgpu::Device,
1103
+ queue: &wgpu::Queue,
1104
+ layers: &[(&[u8], u32, u32)],
1105
+ ) -> u32 {
1106
+ // V1 default: sRGB albedo, no mips. V2 callers use the _ex
1107
+ // variant directly.
1108
+ self.create_texture_array_ex(device, queue, layers, 0, 1)
1109
+ }
1110
+
1111
+ /// EN-014 V2 — create a 2D texture array with explicit pixel format
1112
+ /// and mip-level control. Layer extent / count rules match V1.
1113
+ ///
1114
+ /// `format`:
1115
+ /// 0 → `Rgba8UnormSrgb` (albedo / colour textures; default)
1116
+ /// 1 → `Rgba8Unorm` (normal / MR / data textures — linear)
1117
+ /// _ → falls back to `Rgba8UnormSrgb`
1118
+ ///
1119
+ /// `mip_levels`:
1120
+ /// 1 → no mips (matches V1 behaviour)
1121
+ /// 0 → auto-generate `floor(log2(max(w,h))) + 1` mips, filled
1122
+ /// by point-downsample (`copy_texture_to_texture` halving
1123
+ /// the previous mip). Cheap, correct sized, but aliased
1124
+ /// — a render-pass box filter is the V2.5 follow-up.
1125
+ /// N > 1 → not yet supported (game-supplied per-mip bytes); V2
1126
+ /// treats this as auto-generate.
1127
+ pub fn create_texture_array_ex(
1128
+ &mut self,
1129
+ device: &wgpu::Device,
1130
+ queue: &wgpu::Queue,
1131
+ layers: &[(&[u8], u32, u32)],
1132
+ format: u32,
1133
+ mip_levels: u32,
1134
+ ) -> u32 {
1135
+ let layer_count = (layers.len() as u32).min(MAX_TEXTURE_ARRAY_LAYERS);
1136
+ if layer_count == 0 { return 0; }
1137
+ let (_first_bytes, w, h) = layers[0];
1138
+ if w == 0 || h == 0 { return 0; }
1139
+ // Uniform extent check — V1 hard-fail surfaces obvious bugs at
1140
+ // creation rather than during silent GPU truncation later.
1141
+ for (i, (_, lw, lh)) in layers.iter().enumerate().take(layer_count as usize) {
1142
+ if *lw != w || *lh != h {
1143
+ eprintln!(
1144
+ "[texture_array] layer {} extent {}×{} does not match layer 0 ({}×{}); aborting create",
1145
+ i, lw, lh, w, h,
1146
+ );
1147
+ return 0;
1148
+ }
1149
+ }
1150
+ let wgpu_format = map_texture_array_format(format);
1151
+ // Resolve mip count. mip_levels = 1 → single mip. mip_levels = 0
1152
+ // → engine-generated max. Anything else (game-supplied per-mip)
1153
+ // is V2.5; for now treat N > 1 as auto so games opting into mips
1154
+ // don't silently regress to no-mips.
1155
+ let max_mips = (w.max(h) as f32).log2().floor() as u32 + 1;
1156
+ let auto_generate = mip_levels == 0 || mip_levels > 1;
1157
+ let mip_level_count = if mip_levels == 1 { 1 } else { max_mips.max(1) };
1158
+ // Auto-gen needs COPY_SRC on the texture so we can ping-pong each
1159
+ // mip into the next via copy_texture_to_texture.
1160
+ let mut usage = wgpu::TextureUsages::TEXTURE_BINDING
1161
+ | wgpu::TextureUsages::COPY_DST;
1162
+ if auto_generate && mip_level_count > 1 {
1163
+ usage |= wgpu::TextureUsages::COPY_SRC;
1164
+ }
1165
+ let bytes_per_layer = (w as usize) * (h as usize) * 4;
1166
+ let texture = device.create_texture(&wgpu::TextureDescriptor {
1167
+ label: Some("material_texture_array"),
1168
+ size: wgpu::Extent3d {
1169
+ width: w,
1170
+ height: h,
1171
+ depth_or_array_layers: layer_count,
1172
+ },
1173
+ mip_level_count,
1174
+ sample_count: 1,
1175
+ dimension: wgpu::TextureDimension::D2,
1176
+ format: wgpu_format,
1177
+ usage,
1178
+ view_formats: &[],
1179
+ });
1180
+ for (i, (bytes, _, _)) in layers.iter().enumerate().take(layer_count as usize) {
1181
+ // Defensive — a short layer slice would panic in
1182
+ // write_texture; skip and emit a diagnostic instead.
1183
+ if bytes.len() < bytes_per_layer {
1184
+ eprintln!(
1185
+ "[texture_array] layer {} short: {} B < {} B (skipping)",
1186
+ i, bytes.len(), bytes_per_layer,
1187
+ );
1188
+ continue;
1189
+ }
1190
+ queue.write_texture(
1191
+ wgpu::TexelCopyTextureInfo {
1192
+ texture: &texture,
1193
+ mip_level: 0,
1194
+ origin: wgpu::Origin3d { x: 0, y: 0, z: i as u32 },
1195
+ aspect: wgpu::TextureAspect::All,
1196
+ },
1197
+ &bytes[..bytes_per_layer],
1198
+ wgpu::TexelCopyBufferLayout {
1199
+ offset: 0,
1200
+ bytes_per_row: Some(w * 4),
1201
+ rows_per_image: Some(h),
1202
+ },
1203
+ wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
1204
+ );
1205
+ }
1206
+ // EN-014 V2 — auto-generate the mip chain via point-sample copies.
1207
+ // For each mip > 0, copy a half-size region of mip-1 into mip,
1208
+ // for every layer. wgpu's copy_texture_to_texture covers a single
1209
+ // mip level + array layer per call. This is point-filtered (not
1210
+ // box-filtered): correct sizes, sampleable at distance, but
1211
+ // aliased. V2.5 follow-up upgrades this to a render-pass box
1212
+ // filter (one fullscreen draw per (mip, layer)).
1213
+ if auto_generate && mip_level_count > 1 {
1214
+ let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1215
+ label: Some("material_texture_array_mipgen"),
1216
+ });
1217
+ for mip in 1..mip_level_count {
1218
+ let src_w = (w >> (mip - 1)).max(1);
1219
+ let src_h = (h >> (mip - 1)).max(1);
1220
+ let dst_w = (w >> mip).max(1);
1221
+ let dst_h = (h >> mip).max(1);
1222
+ // Copy region is dst-sized so we read the top-left
1223
+ // 2x2 reduction implicitly. Truly we'd want a filter,
1224
+ // but copy is the cheapest "mips exist" path.
1225
+ let copy_w = dst_w.min(src_w);
1226
+ let copy_h = dst_h.min(src_h);
1227
+ for layer in 0..layer_count {
1228
+ encoder.copy_texture_to_texture(
1229
+ wgpu::TexelCopyTextureInfo {
1230
+ texture: &texture,
1231
+ mip_level: mip - 1,
1232
+ origin: wgpu::Origin3d { x: 0, y: 0, z: layer },
1233
+ aspect: wgpu::TextureAspect::All,
1234
+ },
1235
+ wgpu::TexelCopyTextureInfo {
1236
+ texture: &texture,
1237
+ mip_level: mip,
1238
+ origin: wgpu::Origin3d { x: 0, y: 0, z: layer },
1239
+ aspect: wgpu::TextureAspect::All,
1240
+ },
1241
+ wgpu::Extent3d {
1242
+ width: copy_w,
1243
+ height: copy_h,
1244
+ depth_or_array_layers: 1,
1245
+ },
1246
+ );
1247
+ }
1248
+ }
1249
+ queue.submit(std::iter::once(encoder.finish()));
1250
+ }
1251
+ let view = texture.create_view(&wgpu::TextureViewDescriptor {
1252
+ label: Some("material_texture_array_view"),
1253
+ dimension: Some(wgpu::TextureViewDimension::D2Array),
1254
+ ..Default::default()
1255
+ });
1256
+ self.texture_arrays.push(Some(TextureArray {
1257
+ texture, view, layer_count,
1258
+ }));
1259
+ self.texture_arrays.len() as u32
1260
+ }
1261
+
1262
+ /// EN-014 — link a texture array to a material's per-material
1263
+ /// bind-group at one of three slots (0 = albedo, 1 = normal,
1264
+ /// 2 = MR). Pass `array = 0` to revert the slot to the default
1265
+ /// 1×1×1 stub. The per-material BG is rebuilt so subsequent draws
1266
+ /// see the new array view at @binding 14/15/16.
1267
+ ///
1268
+ /// `probe_view` is the binding-12 source — the caller (typically
1269
+ /// `Renderer::set_material_texture_array`) resolves the
1270
+ /// material's currently-linked reflection probe and passes its
1271
+ /// `color_view` here, or `&self.default_black_view` to default.
1272
+ /// This indirection keeps EN-011 reflection links live across
1273
+ /// EN-014 rebinds without the MaterialSystem having to reach
1274
+ /// into the Renderer's probe registry. Out-of-range slots /
1275
+ /// unknown handles are no-ops with a diagnostic.
1276
+ pub fn set_material_texture_array(
1277
+ &mut self,
1278
+ device: &wgpu::Device,
1279
+ material: MaterialHandle,
1280
+ slot: u32,
1281
+ array: u32,
1282
+ probe_view: &wgpu::TextureView,
1283
+ ) {
1284
+ if material == 0 { return; }
1285
+ let idx = (material - 1) as usize;
1286
+ if idx >= self.pipelines.len() || self.pipelines[idx].is_none() {
1287
+ eprintln!("[texture_array] unknown material handle {material}");
1288
+ return;
1289
+ }
1290
+ if slot >= 3 {
1291
+ eprintln!("[texture_array] slot {slot} out of range (0..=2)");
1292
+ return;
1293
+ }
1294
+ if array != 0 {
1295
+ let h = array as usize;
1296
+ if h == 0 || h > self.texture_arrays.len()
1297
+ || self.texture_arrays[h - 1].is_none()
1298
+ {
1299
+ eprintln!("[texture_array] unknown array handle {array}");
1300
+ return;
1301
+ }
1302
+ }
1303
+
1304
+ // Grow the link table so `idx` is in bounds. Parallel to
1305
+ // the params + reflection registries but not strictly tied
1306
+ // to either — texture-array links are independent.
1307
+ while self.material_texture_arrays.len() <= idx {
1308
+ self.material_texture_arrays.push([None, None, None]);
1309
+ }
1310
+ let link = if array == 0 { None } else { Some(array) };
1311
+ self.material_texture_arrays[idx][slot as usize] = link;
1312
+
1313
+ // Rebuild the per-material BG. Resolve user_params from
1314
+ // existing state so we don't clobber EN-005 links.
1315
+ while self.material_params_buffers.len() <= idx {
1316
+ self.material_params_buffers.push(None);
1317
+ self.material_per_material_bgs.push(None);
1318
+ }
1319
+ let user_params_buf: &wgpu::Buffer = self.material_params_buffers
1320
+ .get(idx)
1321
+ .and_then(|b| b.as_ref())
1322
+ .unwrap_or(&self._default_user_params_buffer);
1323
+ // EN-012 — preserve any per-material MaterialFactors UBO
1324
+ // across an EN-014 array rebind.
1325
+ let factors_buf: &wgpu::Buffer = self.material_factors_buffers
1326
+ .get(idx)
1327
+ .and_then(|b| b.as_ref())
1328
+ .unwrap_or(&self._default_material_factors_buffer);
1329
+
1330
+ let [albedo_view, normal_view, mr_view] = self.resolve_array_views(idx);
1331
+
1332
+ let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1333
+ label: Some("material_per_material_bg_array"),
1334
+ layout: &self.layouts.per_material,
1335
+ entries: &[
1336
+ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
1337
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
1338
+ wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
1339
+ wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
1340
+ wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
1341
+ wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
1342
+ wgpu::BindGroupEntry { binding: 6, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
1343
+ wgpu::BindGroupEntry { binding: 7, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
1344
+ wgpu::BindGroupEntry { binding: 8, resource: wgpu::BindingResource::TextureView(&self._default_white_view) },
1345
+ wgpu::BindGroupEntry { binding: 9, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
1346
+ wgpu::BindGroupEntry { binding: 10, resource: factors_buf.as_entire_binding() },
1347
+ wgpu::BindGroupEntry { binding: 11, resource: user_params_buf.as_entire_binding() },
1348
+ wgpu::BindGroupEntry { binding: 12, resource: wgpu::BindingResource::TextureView(probe_view) },
1349
+ wgpu::BindGroupEntry { binding: 13, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
1350
+ wgpu::BindGroupEntry { binding: 14, resource: wgpu::BindingResource::TextureView(albedo_view) },
1351
+ wgpu::BindGroupEntry { binding: 15, resource: wgpu::BindingResource::TextureView(normal_view) },
1352
+ wgpu::BindGroupEntry { binding: 16, resource: wgpu::BindingResource::TextureView(mr_view) },
1353
+ wgpu::BindGroupEntry { binding: 17, resource: wgpu::BindingResource::Sampler(&self._default_sampler) },
1354
+ ],
1355
+ });
1356
+ self.material_per_material_bgs[idx] = Some(bg);
1357
+ }
1358
+
1359
+ /// EN-014 — accessor for `material_reflection_probe[idx]`. The
1360
+ /// Renderer wrapper uses this to resolve the currently-linked
1361
+ /// probe handle when rebuilding the BG via
1362
+ /// `set_material_texture_array`. Returns `None` for unset / out-
1363
+ /// of-range / unlinked materials.
1364
+ pub fn material_reflection_probe_handle(&self, material: MaterialHandle) -> Option<u32> {
1365
+ if material == 0 { return None; }
1366
+ let idx = (material as usize).checked_sub(1)?;
1367
+ self.material_reflection_probe.get(idx).copied().flatten()
1368
+ }
1369
+
1370
+ /// Convenience — true if either bucket has queued work this frame.
1371
+ pub fn any_commands(&self) -> bool {
1372
+ !self.commands.is_empty() || !self.translucent_commands.is_empty()
1373
+ }
1374
+
1375
+ /// Submit a draw against a compiled material. Allocates (or reuses)
1376
+ /// a per-draw UBO slot, writes the MVP / model / tint / skin info,
1377
+ /// and queues the command for dispatch.
1378
+ pub fn submit_draw(
1379
+ &mut self,
1380
+ device: &wgpu::Device,
1381
+ queue: &wgpu::Queue,
1382
+ joint_buffer: &wgpu::Buffer,
1383
+ material: MaterialHandle,
1384
+ mesh_handle: u64,
1385
+ mesh_idx: usize,
1386
+ mvp: [[f32; 4]; 4],
1387
+ model: [[f32; 4]; 4],
1388
+ prev_mvp: [[f32; 4]; 4],
1389
+ tint: [f32; 4],
1390
+ skin_info: [u32; 4],
1391
+ ) {
1392
+ let idx = material as usize;
1393
+ if material == 0 || idx > self.pipelines.len() { return; }
1394
+ let bucket = match self.pipelines[idx - 1].as_ref() {
1395
+ Some(p) => p.bucket,
1396
+ None => return,
1397
+ };
1398
+
1399
+ let slot = self.next_draw_slot;
1400
+ self.next_draw_slot += 1;
1401
+ self.ensure_draw_slot(device, joint_buffer, slot);
1402
+
1403
+ let per_draw = PerDrawUniforms { mvp, model, prev_mvp, model_tint: tint, skin_info };
1404
+ queue.write_buffer(&self.per_draw_buffers[slot], 0, bytemuck::bytes_of(&per_draw));
1405
+
1406
+ let cmd = MaterialDrawCommand {
1407
+ material, mesh_handle, mesh_idx, draw_slot: slot,
1408
+ instance: None,
1409
+ };
1410
+ if bucket.is_translucent() {
1411
+ self.translucent_commands.push(cmd);
1412
+ } else {
1413
+ self.commands.push(cmd);
1414
+ }
1415
+ }
1416
+
1417
+ /// EN-001 — submit an instanced draw. Identical to `submit_draw`
1418
+ /// except the engine binds vertex slot 1 to the registered
1419
+ /// instance buffer and emits `draw_indexed(.., 0..count)`. The
1420
+ /// pipeline must have been compiled with `wants_instancing=true`
1421
+ /// (use `compile_material_instanced` on the renderer).
1422
+ ///
1423
+ /// `model` / `mvp` here are the instance-local→world fallback
1424
+ /// transform — the per-instance buffer's `instance_pos`/`rot_y`/
1425
+ /// `scale` typically dominate, so callers usually pass identity
1426
+ /// for `model` and the camera VP for `mvp`. `tint` is multiplied
1427
+ /// per-draw (in addition to the per-instance tint).
1428
+ pub fn submit_draw_instanced(
1429
+ &mut self,
1430
+ device: &wgpu::Device,
1431
+ queue: &wgpu::Queue,
1432
+ joint_buffer: &wgpu::Buffer,
1433
+ material: MaterialHandle,
1434
+ mesh_handle: u64,
1435
+ mesh_idx: usize,
1436
+ instance_buffer: u32,
1437
+ instance_count: u32,
1438
+ mvp: [[f32; 4]; 4],
1439
+ model: [[f32; 4]; 4],
1440
+ prev_mvp: [[f32; 4]; 4],
1441
+ tint: [f32; 4],
1442
+ skin_info: [u32; 4],
1443
+ ) {
1444
+ let idx = material as usize;
1445
+ if material == 0 || idx > self.pipelines.len() { return; }
1446
+ let bucket = match self.pipelines[idx - 1].as_ref() {
1447
+ Some(p) => p.bucket,
1448
+ None => return,
1449
+ };
1450
+
1451
+ let slot = self.next_draw_slot;
1452
+ self.next_draw_slot += 1;
1453
+ self.ensure_draw_slot(device, joint_buffer, slot);
1454
+
1455
+ let per_draw = PerDrawUniforms { mvp, model, prev_mvp, model_tint: tint, skin_info };
1456
+ queue.write_buffer(&self.per_draw_buffers[slot], 0, bytemuck::bytes_of(&per_draw));
1457
+
1458
+ let cmd = MaterialDrawCommand {
1459
+ material, mesh_handle, mesh_idx, draw_slot: slot,
1460
+ instance: Some(InstanceDrawInfo {
1461
+ buffer_handle: instance_buffer,
1462
+ count: instance_count,
1463
+ }),
1464
+ };
1465
+ if bucket.is_translucent() {
1466
+ self.translucent_commands.push(cmd);
1467
+ } else {
1468
+ self.commands.push(cmd);
1469
+ }
1470
+ }
1471
+
1472
+ /// EN-001 — create a persistent instance buffer from CPU-side
1473
+ /// floats. The data layout matches `InstanceData3D` (9 floats per
1474
+ /// instance: pos.xyz, rot_y, scale, tint.rgba); this method pads
1475
+ /// each instance to 12 floats at upload time so the GPU side gets
1476
+ /// the correct 48-byte stride. Returns a 1-based handle to use
1477
+ /// with `submit_draw_instanced`. Pair with `destroy_instance_buffer`
1478
+ /// when the buffer's no longer needed.
1479
+ pub fn create_instance_buffer(
1480
+ &mut self,
1481
+ device: &wgpu::Device,
1482
+ queue: &wgpu::Queue,
1483
+ raw: &[f32],
1484
+ instance_count: u32,
1485
+ ) -> u32 {
1486
+ let count = instance_count as usize;
1487
+ let mut packed: Vec<f32> = Vec::with_capacity(count * 12);
1488
+ for i in 0..count {
1489
+ let off = i * 9;
1490
+ if off + 9 > raw.len() { break; }
1491
+ packed.extend_from_slice(&raw[off..off + 3]); // pos.xyz
1492
+ packed.push(raw[off + 3]); // rot_y
1493
+ packed.push(raw[off + 4]); // scale
1494
+ packed.extend_from_slice(&raw[off + 5..off + 9]); // tint.rgba
1495
+ packed.extend_from_slice(&[0.0, 0.0, 0.0]); // pad to 48 bytes
1496
+ }
1497
+ let size = (packed.len() * std::mem::size_of::<f32>()) as u64;
1498
+ // Empty buffers can't be created (size 0 is invalid in wgpu).
1499
+ // Reserve at least one stride so the BG/binding remains valid.
1500
+ let buffer_size = size.max(48);
1501
+ let buffer = device.create_buffer(&wgpu::BufferDescriptor {
1502
+ label: Some("material_instance_buffer"),
1503
+ size: buffer_size,
1504
+ usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1505
+ mapped_at_creation: false,
1506
+ });
1507
+ if !packed.is_empty() {
1508
+ queue.write_buffer(&buffer, 0, bytemuck::cast_slice(&packed));
1509
+ }
1510
+ self.instance_buffers.push(Some(InstanceBuffer { buffer, count: instance_count }));
1511
+ self.instance_buffers.len() as u32
1512
+ }
1513
+
1514
+ /// EN-001 — drop an instance buffer slot. The slot is left as
1515
+ /// `None` so previously-issued handles never alias a future
1516
+ /// allocation. No-op for `handle == 0` or out-of-range handles.
1517
+ pub fn destroy_instance_buffer(&mut self, handle: u32) {
1518
+ if handle == 0 { return; }
1519
+ let idx = handle as usize - 1;
1520
+ if idx < self.instance_buffers.len() {
1521
+ self.instance_buffers[idx] = None;
1522
+ }
1523
+ }
1524
+
1525
+ fn ensure_draw_slot(
1526
+ &mut self,
1527
+ device: &wgpu::Device,
1528
+ joint_buffer: &wgpu::Buffer,
1529
+ slot: usize,
1530
+ ) {
1531
+ while self.per_draw_buffers.len() <= slot {
1532
+ let buf = device.create_buffer(&wgpu::BufferDescriptor {
1533
+ label: Some("material_per_draw"),
1534
+ size: std::mem::size_of::<PerDrawUniforms>() as u64,
1535
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1536
+ mapped_at_creation: false,
1537
+ });
1538
+ let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1539
+ label: Some("material_per_draw_bg"),
1540
+ layout: &self.layouts.per_draw,
1541
+ entries: &[
1542
+ wgpu::BindGroupEntry { binding: 0, resource: buf.as_entire_binding() },
1543
+ wgpu::BindGroupEntry { binding: 1, resource: joint_buffer.as_entire_binding() },
1544
+ ],
1545
+ });
1546
+ self.per_draw_buffers.push(buf);
1547
+ self.per_draw_bgs.push(bg);
1548
+ }
1549
+ }
1550
+
1551
+ /// Dispatch all queued material draws. Caller owns the render pass;
1552
+ /// this method binds the pipelines + groups + meshes and issues
1553
+ /// indexed draws. `mesh_fetch` is a closure that returns
1554
+ /// `(vertex_buffer, index_buffer, index_count)` for a given
1555
+ /// (mesh_handle, mesh_idx) — lets the renderer hand over its
1556
+ /// `model_gpu_cache` without this module taking a dependency on it.
1557
+ pub fn dispatch<'pass, F>(
1558
+ &'pass self,
1559
+ pass: &mut wgpu::RenderPass<'pass>,
1560
+ mesh_fetch: F,
1561
+ )
1562
+ where F: FnMut(u64, usize) -> Option<(&'pass wgpu::Buffer, &'pass wgpu::Buffer, u32)>
1563
+ {
1564
+ self.dispatch_with_view(pass, &self.per_view_bg, |_| true, false, mesh_fetch);
1565
+ }
1566
+
1567
+ /// EN-011 — like `dispatch`, but uses a caller-supplied PerView
1568
+ /// bind group (so the planar-reflection pass can render the same
1569
+ /// draws against a mirrored camera UBO) and skips any command
1570
+ /// whose material handle the `accept` predicate rejects (so the
1571
+ /// reflection pass can drop hardcoded-excluded materials —
1572
+ /// foliage, particles, grass — without touching the original
1573
+ /// command list).
1574
+ ///
1575
+ /// `accept` is consulted once per command; the engine passes a
1576
+ /// closure that closes over a small `HashSet<MaterialHandle>` of
1577
+ /// excluded material handles.
1578
+ ///
1579
+ /// EN-011 V2 — `use_reflection_pipeline` swaps in the material's
1580
+ /// sibling pipeline (cull_mode flipped) when one was compiled.
1581
+ /// Falls back to the main pipeline if no reflection variant
1582
+ /// exists (translucent / cutout materials, where the original
1583
+ /// pipeline already cull-mode = None and no flip is needed).
1584
+ pub fn dispatch_with_view<'pass, F, A>(
1585
+ &'pass self,
1586
+ pass: &mut wgpu::RenderPass<'pass>,
1587
+ per_view_bg: &'pass wgpu::BindGroup,
1588
+ mut accept: A,
1589
+ use_reflection_pipeline: bool,
1590
+ mut mesh_fetch: F,
1591
+ )
1592
+ where
1593
+ F: FnMut(u64, usize) -> Option<(&'pass wgpu::Buffer, &'pass wgpu::Buffer, u32)>,
1594
+ A: FnMut(MaterialHandle) -> bool,
1595
+ {
1596
+ if self.commands.is_empty() { return; }
1597
+
1598
+ let mut last_material: MaterialHandle = 0;
1599
+ for cmd in &self.commands {
1600
+ if !accept(cmd.material) { continue; }
1601
+ if cmd.material != last_material {
1602
+ let mat = match self.pipelines.get(cmd.material as usize - 1) {
1603
+ Some(Some(m)) => m,
1604
+ _ => continue,
1605
+ };
1606
+ let pipeline = if use_reflection_pipeline {
1607
+ mat.reflection_pipeline.as_ref().unwrap_or(&mat.pipeline)
1608
+ } else {
1609
+ &mat.pipeline
1610
+ };
1611
+ pass.set_pipeline(pipeline);
1612
+ pass.set_bind_group(0, &self.per_frame_bg, &[]);
1613
+ pass.set_bind_group(1, per_view_bg, &[]);
1614
+ pass.set_bind_group(2, self.per_material_bg_for(cmd.material), &[]);
1615
+ last_material = cmd.material;
1616
+ }
1617
+ if let Some((vb, ib, icount)) = mesh_fetch(cmd.mesh_handle, cmd.mesh_idx) {
1618
+ pass.set_bind_group(3, &self.per_draw_bgs[cmd.draw_slot], &[]);
1619
+ pass.set_vertex_buffer(0, vb.slice(..));
1620
+ pass.set_index_buffer(ib.slice(..), wgpu::IndexFormat::Uint32);
1621
+ let instance_range = self.bind_instance_buffer(pass, &cmd.instance);
1622
+ if instance_range.end > instance_range.start {
1623
+ pass.draw_indexed(0..icount, 0, instance_range);
1624
+ }
1625
+ }
1626
+ }
1627
+ }
1628
+
1629
+ /// EN-001 — resolve an instanced draw command's vertex slot 1 binding
1630
+ /// and return the instance range. For non-instanced draws (`info` is
1631
+ /// None) this is a no-op and returns `0..1`. For instanced draws
1632
+ /// with a missing/destroyed buffer slot we return an empty range
1633
+ /// so the caller skips the draw rather than crashing on a stale
1634
+ /// handle.
1635
+ fn bind_instance_buffer<'pass>(
1636
+ &'pass self,
1637
+ pass: &mut wgpu::RenderPass<'pass>,
1638
+ info: &Option<InstanceDrawInfo>,
1639
+ ) -> std::ops::Range<u32> {
1640
+ match info {
1641
+ None => 0..1,
1642
+ Some(inst) => {
1643
+ if inst.buffer_handle == 0 { return 0..1; }
1644
+ let slot_idx = inst.buffer_handle as usize - 1;
1645
+ match self.instance_buffers.get(slot_idx).and_then(|s| s.as_ref()) {
1646
+ Some(ib_slot) => {
1647
+ pass.set_vertex_buffer(1, ib_slot.buffer.slice(..));
1648
+ 0..inst.count
1649
+ }
1650
+ None => 0..0,
1651
+ }
1652
+ }
1653
+ }
1654
+ }
1655
+
1656
+ /// Phase 4b — rebuild the SceneInputs (group 4) bind group with
1657
+ /// the current frame's snapshot textures. Called by the Renderer
1658
+ /// once per frame when translucent draws exist and a SceneColor
1659
+ /// transient has been allocated. `scene_color_view` is the
1660
+ /// copy-to-sample snapshot from `hdr_rt`; `scene_depth_view` is
1661
+ /// the live depth buffer the opaque pass wrote. Other slots
1662
+ /// (impulse, motion vectors) bind to internal stub textures
1663
+ /// until Phase 7 wires them.
1664
+ pub fn update_scene_inputs(
1665
+ &mut self,
1666
+ device: &wgpu::Device,
1667
+ scene_color_view: &wgpu::TextureView,
1668
+ scene_depth_view: Option<&wgpu::TextureView>,
1669
+ impulse_view: Option<(&wgpu::TextureView, &wgpu::Sampler)>,
1670
+ ) {
1671
+ let depth_view = scene_depth_view.unwrap_or(&self._scene_stub_depth_view);
1672
+ // Layout entry 5 is NonFiltering — fallback uses the depth
1673
+ // sampler (which is also NonFiltering) rather than the
1674
+ // filtering color sampler so the layout matches either way.
1675
+ let (imp_view, imp_samp): (&wgpu::TextureView, &wgpu::Sampler) = match impulse_view {
1676
+ Some((v, s)) => (v, s),
1677
+ None => (&self._scene_stub_view, &self._scene_depth_sampler),
1678
+ };
1679
+ // Phase 4c — group 4 binding 2 receives a COPY_DST snapshot of
1680
+ // the opaque depth buffer, rather than the live depth-stencil
1681
+ // attachment (wgpu rejects read+write aliasing in the same
1682
+ // pass). Callers that don't need depth pass the stub view
1683
+ // already held here; `Renderer::end_frame_with_scene` acquires
1684
+ // a transient depth texture when any translucent material
1685
+ // declares a depth read.
1686
+ let bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1687
+ label: Some("scene_inputs_bg"),
1688
+ layout: &self.layouts.scene_inputs,
1689
+ entries: &[
1690
+ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(scene_color_view) },
1691
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&self._scene_color_sampler) },
1692
+ wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(depth_view) },
1693
+ wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::Sampler(&self._scene_depth_sampler) },
1694
+ wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::TextureView(imp_view) },
1695
+ wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::Sampler(imp_samp) },
1696
+ wgpu::BindGroupEntry { binding: 6, resource: wgpu::BindingResource::TextureView(&self._scene_stub_view) },
1697
+ ],
1698
+ });
1699
+ self.scene_inputs_bg = Some(bg);
1700
+ }
1701
+
1702
+ /// Dispatch translucent-bucket draws (Transparent, Refractive,
1703
+ /// Additive). Caller owns a render pass set up with a single HDR
1704
+ /// attachment (LoadOp::Load) + depth read-only. Refractive
1705
+ /// materials additionally receive the SceneInputs bind group at
1706
+ /// group 4 — `update_scene_inputs` must have been called this
1707
+ /// frame for that to be non-None.
1708
+ pub fn dispatch_translucent<'pass, F>(
1709
+ &'pass self,
1710
+ pass: &mut wgpu::RenderPass<'pass>,
1711
+ mut mesh_fetch: F,
1712
+ )
1713
+ where F: FnMut(u64, usize) -> Option<(&'pass wgpu::Buffer, &'pass wgpu::Buffer, u32)>
1714
+ {
1715
+ if self.translucent_commands.is_empty() { return; }
1716
+
1717
+ let mut last_material: MaterialHandle = 0;
1718
+ let mut last_reads_scene: bool = false;
1719
+ for cmd in &self.translucent_commands {
1720
+ if cmd.material != last_material {
1721
+ let mat = match self.pipelines.get(cmd.material as usize - 1) {
1722
+ Some(Some(m)) => m,
1723
+ _ => continue,
1724
+ };
1725
+ pass.set_pipeline(&mat.pipeline);
1726
+ pass.set_bind_group(0, &self.per_frame_bg, &[]);
1727
+ pass.set_bind_group(1, &self.per_view_bg, &[]);
1728
+ pass.set_bind_group(2, self.per_material_bg_for(cmd.material), &[]);
1729
+ if mat.reads_scene {
1730
+ if let Some(bg) = self.scene_inputs_bg.as_ref() {
1731
+ pass.set_bind_group(4, bg, &[]);
1732
+ }
1733
+ }
1734
+ last_material = cmd.material;
1735
+ last_reads_scene = mat.reads_scene;
1736
+ }
1737
+ // Re-bind group 4 if the material switches its reads_scene
1738
+ // between subsequent draws — rarely happens with a
1739
+ // stable bucket but keeps the state machine honest.
1740
+ let _ = last_reads_scene;
1741
+
1742
+ if let Some((vb, ib, icount)) = mesh_fetch(cmd.mesh_handle, cmd.mesh_idx) {
1743
+ pass.set_bind_group(3, &self.per_draw_bgs[cmd.draw_slot], &[]);
1744
+ pass.set_vertex_buffer(0, vb.slice(..));
1745
+ pass.set_index_buffer(ib.slice(..), wgpu::IndexFormat::Uint32);
1746
+ let instance_range = self.bind_instance_buffer(pass, &cmd.instance);
1747
+ if instance_range.end > instance_range.start {
1748
+ pass.draw_indexed(0..icount, 0, instance_range);
1749
+ }
1750
+ }
1751
+ }
1752
+ }
1753
+ }
1754
+
1755
+ // =====================================================================
1756
+ // Tests
1757
+ // =====================================================================
1758
+
1759
+ #[cfg(test)]
1760
+ mod tests {
1761
+ use super::*;
1762
+ use super::super::formats;
1763
+ use super::super::types::Vertex3D;
1764
+
1765
+ /// Headless wgpu device. See sibling helpers in `transient.rs` /
1766
+ /// `impulse_field.rs` — same fallback adapter pattern. Returns None
1767
+ /// (test skips gracefully) when no GPU is available.
1768
+ fn try_create_device() -> Option<(wgpu::Device, wgpu::Queue)> {
1769
+ let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
1770
+ backends: wgpu::Backends::all(),
1771
+ ..wgpu::InstanceDescriptor::new_without_display_handle()
1772
+ });
1773
+ let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
1774
+ power_preference: wgpu::PowerPreference::LowPower,
1775
+ compatible_surface: None,
1776
+ force_fallback_adapter: true,
1777
+ })).ok()?;
1778
+ let (device, queue) = pollster::block_on(adapter.request_device(
1779
+ &wgpu::DeviceDescriptor {
1780
+ label: Some("material-test-device"),
1781
+ required_features: wgpu::Features::empty(),
1782
+ required_limits: wgpu::Limits::downlevel_defaults(),
1783
+ ..Default::default()
1784
+ },
1785
+ )).ok()?;
1786
+ Some((device, queue))
1787
+ }
1788
+
1789
+ /// Minimal translucent (Bucket::Refractive) WGSL material. Writes
1790
+ /// a constant red+0.5α colour through the alpha-blended HDR target.
1791
+ /// Uses #include "material_abi.wgsl" so the same pipeline-layout
1792
+ /// / per-frame / per-view bindings are validated as production
1793
+ /// materials. Vertex stage transforms via `draw.mvp` so we can
1794
+ /// emit a fullscreen-ish triangle from any geometry.
1795
+ const TRANSLUCENT_WGSL: &str = r#"
1796
+ #include "material_abi.wgsl"
1797
+
1798
+ struct VsOut {
1799
+ @builtin(position) clip_position: vec4<f32>,
1800
+ };
1801
+
1802
+ @vertex
1803
+ fn vs_main(in: VertexInput) -> VsOut {
1804
+ var out: VsOut;
1805
+ out.clip_position = draw.mvp * vec4<f32>(in.position, 1.0);
1806
+ return out;
1807
+ }
1808
+
1809
+ @fragment
1810
+ fn fs_main(_in: VsOut) -> TranslucentOut {
1811
+ var out: TranslucentOut;
1812
+ // Red, half alpha — alpha-blended onto whatever the load-op set.
1813
+ out.hdr = vec4<f32>(1.0, 0.0, 0.0, 0.5);
1814
+ return out;
1815
+ }
1816
+ "#;
1817
+
1818
+ /// Create a tiny joint buffer so MaterialSystem::new is happy. The
1819
+ /// per_draw layout binds it at @binding(1); the test material
1820
+ /// doesn't read it but the bind group still has to validate.
1821
+ fn make_joint_buffer(device: &wgpu::Device) -> wgpu::Buffer {
1822
+ device.create_buffer(&wgpu::BufferDescriptor {
1823
+ label: Some("test_joint_buffer"),
1824
+ // 1024 mat4s × 64 B = 64 KiB. Same size as production.
1825
+ size: 65536,
1826
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1827
+ mapped_at_creation: false,
1828
+ })
1829
+ }
1830
+
1831
+ /// Build a single fullscreen triangle covering NDC: three vertices
1832
+ /// at (-1,-1), (3,-1), (-1,3). The pipeline's MVP starts as
1833
+ /// identity (we override it below) so the triangle covers the
1834
+ /// whole viewport.
1835
+ fn make_fullscreen_tri(device: &wgpu::Device) -> (wgpu::Buffer, wgpu::Buffer, u32) {
1836
+ let mut verts: [Vertex3D; 3] = [Vertex3D::default(); 3];
1837
+ verts[0].position = [-1.0, -1.0, 0.5];
1838
+ verts[1].position = [ 3.0, -1.0, 0.5];
1839
+ verts[2].position = [-1.0, 3.0, 0.5];
1840
+ // The MaterialPipeline's depth-stencil uses Less; the load-op
1841
+ // for a translucent pass clears to 1.0 (far) by default in
1842
+ // production but in this test we use a depth attachment with
1843
+ // the CLEAR op and clear value 1.0, so anything < 1.0 passes.
1844
+ let vb = device.create_buffer(&wgpu::BufferDescriptor {
1845
+ label: Some("test_tri_vb"),
1846
+ size: std::mem::size_of_val(&verts) as u64,
1847
+ usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1848
+ mapped_at_creation: false,
1849
+ });
1850
+ let indices: [u32; 3] = [0, 1, 2];
1851
+ let ib = device.create_buffer(&wgpu::BufferDescriptor {
1852
+ label: Some("test_tri_ib"),
1853
+ size: std::mem::size_of_val(&indices) as u64,
1854
+ usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1855
+ mapped_at_creation: false,
1856
+ });
1857
+ (vb, ib, 3)
1858
+ }
1859
+
1860
+ /// EN-006 — translucent dispatch path. Compiles a refractive
1861
+ /// material via `MaterialSystem::compile`, submits one draw,
1862
+ /// dispatches into a 64×64 Rgba16Float HDR render target via
1863
+ /// `dispatch_translucent`, then reads back the HDR and verifies
1864
+ /// the alpha-blended red shows through the cyan background.
1865
+ ///
1866
+ /// This is the scoped-down version of the acceptance scenario:
1867
+ /// it exercises the dispatch site and pipeline-layout / blend-state
1868
+ /// wiring without standing up the full `Renderer` (which requires
1869
+ /// a surface and ~30 other resources). What this DOES cover:
1870
+ /// - MaterialSystem::compile against a refractive WGSL source
1871
+ /// - dispatch_translucent's pipeline / bind-group binding loop
1872
+ /// - alpha blending via the translucent target's BlendState
1873
+ /// - the per-draw / per-view / per-frame UBO writes
1874
+ /// What it does NOT cover:
1875
+ /// - The scene-color snapshot (test material has reads_scene=false
1876
+ /// so group 4 doesn't bind; the e2e copy_texture_to_texture
1877
+ /// before this dispatch is exercised by the depth-snapshot test
1878
+ /// in transient.rs).
1879
+ /// - Sort order across multiple translucent draws (single-draw test).
1880
+ /// Skipped on adapters where `try_create_device` returns None.
1881
+ #[test]
1882
+ fn dispatch_translucent_alpha_blends_into_hdr() {
1883
+ let Some((device, queue)) = try_create_device() else { return; };
1884
+ let joint_buf = make_joint_buffer(&device);
1885
+ let mut sys = MaterialSystem::new(&device, &queue, &joint_buf);
1886
+
1887
+ // Compile a refractive (translucent) material. Use the engine's
1888
+ // production format constants so the pipeline matches what
1889
+ // Renderer::new would have produced.
1890
+ let handle = sys.compile(
1891
+ &device,
1892
+ TRANSLUCENT_WGSL,
1893
+ FragmentProfile::Translucent,
1894
+ Bucket::Transparent,
1895
+ false, // reads_scene
1896
+ false, // wants_instancing
1897
+ wgpu::TextureFormat::Rgba16Float, // hdr_format
1898
+ wgpu::TextureFormat::Rg8Unorm, // material_format (unused in translucent)
1899
+ wgpu::TextureFormat::Rg16Float, // velocity_format (unused)
1900
+ wgpu::TextureFormat::Rgba8Unorm, // albedo_format (unused)
1901
+ formats::DEPTH_FORMAT,
1902
+ ).expect("translucent material compiles");
1903
+ assert!(handle != 0, "compile returns a 1-based handle");
1904
+
1905
+ // Frame uniforms — zeros are fine for a constant-colour shader.
1906
+ let pf = PerFrameUniforms {
1907
+ time: 0.0, delta_time: 0.0, frame_index: 0, _pad0: 0,
1908
+ screen_resolution: [64.0, 64.0], render_resolution: [64.0, 64.0],
1909
+ taa_jitter: [0.0; 2], _pad1: [0.0; 2], wind: [0.0; 4],
1910
+ };
1911
+ let pv = bytemuck::Zeroable::zeroed();
1912
+ sys.update_frame_uniforms(&queue, &pf, &pv);
1913
+ sys.reset_draw_slot();
1914
+
1915
+ // MVP = identity so the fullscreen tri stays in NDC.
1916
+ let identity = [
1917
+ [1.0, 0.0, 0.0, 0.0],
1918
+ [0.0, 1.0, 0.0, 0.0],
1919
+ [0.0, 0.0, 1.0, 0.0],
1920
+ [0.0, 0.0, 0.0, 1.0],
1921
+ ];
1922
+ let (vb, ib, icount) = make_fullscreen_tri(&device);
1923
+
1924
+ sys.submit_draw(
1925
+ &device, &queue, &joint_buf,
1926
+ handle, /* mesh_handle */ 1, /* mesh_idx */ 0,
1927
+ identity, identity, identity,
1928
+ [1.0; 4], [0; 4],
1929
+ );
1930
+ assert_eq!(sys.translucent_commands.len(), 1, "draw queued in translucent bucket");
1931
+
1932
+ // Build the HDR + depth render targets for the dispatch.
1933
+ let (rt_w, rt_h) = (64u32, 64u32);
1934
+ let hdr_rt = device.create_texture(&wgpu::TextureDescriptor {
1935
+ label: Some("test_hdr_rt"),
1936
+ size: wgpu::Extent3d { width: rt_w, height: rt_h, depth_or_array_layers: 1 },
1937
+ mip_level_count: 1, sample_count: 1,
1938
+ dimension: wgpu::TextureDimension::D2,
1939
+ format: wgpu::TextureFormat::Rgba16Float,
1940
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
1941
+ view_formats: &[],
1942
+ });
1943
+ let hdr_view = hdr_rt.create_view(&Default::default());
1944
+ let depth_rt = device.create_texture(&wgpu::TextureDescriptor {
1945
+ label: Some("test_depth_rt"),
1946
+ size: wgpu::Extent3d { width: rt_w, height: rt_h, depth_or_array_layers: 1 },
1947
+ mip_level_count: 1, sample_count: 1,
1948
+ dimension: wgpu::TextureDimension::D2,
1949
+ format: formats::DEPTH_FORMAT,
1950
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1951
+ view_formats: &[],
1952
+ });
1953
+ let depth_view = depth_rt.create_view(&Default::default());
1954
+
1955
+ // Pre-clear HDR to opaque cyan so we can detect alpha-blended red:
1956
+ // (1, 0, 0, 0.5) over (0, 1, 1, 1) → (0.5, 0.5, 0.5, 1.0).
1957
+ let bg_color = wgpu::Color { r: 0.0, g: 1.0, b: 1.0, a: 1.0 };
1958
+ let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1959
+ label: Some("test_translucent_encoder"),
1960
+ });
1961
+ {
1962
+ // Clear HDR + depth in one pass.
1963
+ let _clear = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1964
+ label: Some("test_clear_pass"),
1965
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1966
+ view: &hdr_view,
1967
+ resolve_target: None,
1968
+ depth_slice: None,
1969
+ ops: wgpu::Operations {
1970
+ load: wgpu::LoadOp::Clear(bg_color),
1971
+ store: wgpu::StoreOp::Store,
1972
+ },
1973
+ })],
1974
+ depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
1975
+ view: &depth_view,
1976
+ depth_ops: Some(wgpu::Operations {
1977
+ load: wgpu::LoadOp::Clear(1.0),
1978
+ store: wgpu::StoreOp::Store,
1979
+ }),
1980
+ stencil_ops: None,
1981
+ }),
1982
+ timestamp_writes: None,
1983
+ occlusion_query_set: None,
1984
+ multiview_mask: None,
1985
+ });
1986
+ }
1987
+ // Translucent dispatch — Load (don't clear) HDR; depth read-only.
1988
+ {
1989
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1990
+ label: Some("test_translucent_pass"),
1991
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1992
+ view: &hdr_view,
1993
+ resolve_target: None,
1994
+ depth_slice: None,
1995
+ ops: wgpu::Operations {
1996
+ load: wgpu::LoadOp::Load,
1997
+ store: wgpu::StoreOp::Store,
1998
+ },
1999
+ })],
2000
+ depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2001
+ view: &depth_view,
2002
+ depth_ops: Some(wgpu::Operations {
2003
+ load: wgpu::LoadOp::Load,
2004
+ store: wgpu::StoreOp::Store,
2005
+ }),
2006
+ stencil_ops: None,
2007
+ }),
2008
+ timestamp_writes: None,
2009
+ occlusion_query_set: None,
2010
+ multiview_mask: None,
2011
+ });
2012
+ sys.dispatch_translucent(&mut pass, |mh, _idx| {
2013
+ if mh == 1 { Some((&vb, &ib, icount)) } else { None }
2014
+ });
2015
+ }
2016
+
2017
+ // Read back the HDR target (Rgba16Float = 8 B / texel).
2018
+ let bpr_unpadded = rt_w * 8;
2019
+ let bpr = (bpr_unpadded + 255) & !255;
2020
+ let buf_size = (bpr * rt_h) as u64;
2021
+ let staging = device.create_buffer(&wgpu::BufferDescriptor {
2022
+ label: Some("test_hdr_staging"),
2023
+ size: buf_size,
2024
+ usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
2025
+ mapped_at_creation: false,
2026
+ });
2027
+ encoder.copy_texture_to_buffer(
2028
+ wgpu::TexelCopyTextureInfo {
2029
+ texture: &hdr_rt,
2030
+ mip_level: 0,
2031
+ origin: wgpu::Origin3d::ZERO,
2032
+ aspect: wgpu::TextureAspect::All,
2033
+ },
2034
+ wgpu::TexelCopyBufferInfo {
2035
+ buffer: &staging,
2036
+ layout: wgpu::TexelCopyBufferLayout {
2037
+ offset: 0,
2038
+ bytes_per_row: Some(bpr),
2039
+ rows_per_image: Some(rt_h),
2040
+ },
2041
+ },
2042
+ wgpu::Extent3d { width: rt_w, height: rt_h, depth_or_array_layers: 1 },
2043
+ );
2044
+ queue.submit(std::iter::once(encoder.finish()));
2045
+
2046
+ let slice = staging.slice(..);
2047
+ let (tx, rx) = std::sync::mpsc::channel();
2048
+ slice.map_async(wgpu::MapMode::Read, move |r| { let _ = tx.send(r); });
2049
+ let _ = device.poll(wgpu::PollType::Wait { submission_index: None, timeout: None });
2050
+ rx.recv().expect("map sender").expect("map failed");
2051
+ let data = slice.get_mapped_range();
2052
+
2053
+ // Sample the centre texel. Rgba16Float = 4 × half = 8 bytes.
2054
+ // Use half::f16 via bytemuck cast through u16 then manual decode.
2055
+ let cx = rt_w / 2;
2056
+ let cy = rt_h / 2;
2057
+ let row_start = (cy * bpr) as usize;
2058
+ let texel_start = row_start + (cx as usize) * 8;
2059
+ let halfs: [u16; 4] = [
2060
+ u16::from_le_bytes([data[texel_start], data[texel_start + 1]]),
2061
+ u16::from_le_bytes([data[texel_start + 2], data[texel_start + 3]]),
2062
+ u16::from_le_bytes([data[texel_start + 4], data[texel_start + 5]]),
2063
+ u16::from_le_bytes([data[texel_start + 6], data[texel_start + 7]]),
2064
+ ];
2065
+ drop(data);
2066
+ staging.unmap();
2067
+
2068
+ let r = f16_to_f32(halfs[0]);
2069
+ let g = f16_to_f32(halfs[1]);
2070
+ let b = f16_to_f32(halfs[2]);
2071
+ let _a = f16_to_f32(halfs[3]);
2072
+
2073
+ // Expected SrcAlpha/OneMinusSrcAlpha blend:
2074
+ // src = (1, 0, 0, 0.5)
2075
+ // dst = (0, 1, 1, 1)
2076
+ // out.rgb = src.rgb * src.a + dst.rgb * (1 - src.a)
2077
+ // = (0.5, 0.5, 0.5)
2078
+ // Allow 1/256 tolerance for half-precision round-trip.
2079
+ let eps = 0.02;
2080
+ assert!((r - 0.5).abs() < eps, "red channel = {} (expected ~0.5)", r);
2081
+ assert!((g - 0.5).abs() < eps, "green channel = {} (expected ~0.5)", g);
2082
+ assert!((b - 0.5).abs() < eps, "blue channel = {} (expected ~0.5)", b);
2083
+ }
2084
+
2085
+ /// IEEE-754 binary16 → binary32. We don't pull in the `half` crate
2086
+ /// for a single readback; the manual decode is short and exact for
2087
+ /// the values this test produces (no NaN / Inf / subnormal cases).
2088
+ fn f16_to_f32(bits: u16) -> f32 {
2089
+ let sign = (bits >> 15) & 0x1;
2090
+ let exp = (bits >> 10) & 0x1f;
2091
+ let frac = bits & 0x3ff;
2092
+ if exp == 0 {
2093
+ if frac == 0 {
2094
+ return if sign == 1 { -0.0 } else { 0.0 };
2095
+ }
2096
+ // Subnormal — not expected in this test; decode for completeness.
2097
+ let f = (frac as f32) / 1024.0 * (2.0f32).powi(-14);
2098
+ return if sign == 1 { -f } else { f };
2099
+ }
2100
+ if exp == 0x1f {
2101
+ return f32::NAN; // Inf or NaN — unexpected in this test.
2102
+ }
2103
+ let f = (1.0 + (frac as f32) / 1024.0) * (2.0f32).powi(exp as i32 - 15);
2104
+ if sign == 1 { -f } else { f }
2105
+ }
2106
+ }