@driftengine/core 4.8.7 → 4.10.0

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 (263) hide show
  1. package/CHANGELOG.json +123 -11
  2. package/README.md +43 -38
  3. package/dist/index.d.ts +8 -1
  4. package/dist/index.js +11 -3
  5. package/dist/render/ambientHarmonics.d.ts +1 -1
  6. package/dist/render/ambientHarmonics.js +1 -1
  7. package/dist/render/areaLights.d.ts +4 -4
  8. package/dist/render/areaLights.js +1 -1
  9. package/dist/render/atmosphere.d.ts +13 -0
  10. package/dist/render/atmosphere.js +9 -1
  11. package/dist/render/backend/api.d.ts +19 -4
  12. package/dist/render/backend/createRenderer.d.ts +6 -0
  13. package/dist/render/backend/createRenderer.js +4 -1
  14. package/dist/render/backend/select.js +11 -5
  15. package/dist/render/backend/webgl2/boneAnimations.d.ts +29 -0
  16. package/dist/render/backend/webgl2/boneAnimations.js +80 -0
  17. package/dist/render/backend/webgl2/instanced.d.ts +5 -1
  18. package/dist/render/backend/webgl2/instanced.js +13 -1
  19. package/dist/render/backend/webgl2/renderer.d.ts +180 -4
  20. package/dist/render/backend/webgl2/renderer.js +550 -40
  21. package/dist/render/backend/webgl2/skinPaletteTexture.d.ts +5 -3
  22. package/dist/render/backend/webgl2/skinPaletteTexture.js +6 -4
  23. package/dist/render/backend/webgpu/boneAnimations.d.ts +34 -0
  24. package/dist/render/backend/webgpu/boneAnimations.js +76 -0
  25. package/dist/render/backend/webgpu/buffers.d.ts +19 -4
  26. package/dist/render/backend/webgpu/buffers.js +18 -11
  27. package/dist/render/backend/webgpu/compressedUpload.d.ts +0 -1
  28. package/dist/render/backend/webgpu/compressedUpload.js +53 -8
  29. package/dist/render/backend/webgpu/depthPass.d.ts +59 -3
  30. package/dist/render/backend/webgpu/depthPass.js +49 -14
  31. package/dist/render/backend/webgpu/device.d.ts +7 -1
  32. package/dist/render/backend/webgpu/device.js +31 -3
  33. package/dist/render/backend/webgpu/drawCommand.d.ts +18 -4
  34. package/dist/render/backend/webgpu/drawCommand.js +10 -3
  35. package/dist/render/backend/webgpu/flatPass.d.ts +100 -42
  36. package/dist/render/backend/webgpu/flatPass.js +191 -43
  37. package/dist/render/backend/webgpu/instanced.d.ts +14 -1
  38. package/dist/render/backend/webgpu/instanced.js +24 -1
  39. package/dist/render/backend/webgpu/materialStore.d.ts +101 -0
  40. package/dist/render/backend/webgpu/materialStore.js +265 -0
  41. package/dist/render/backend/webgpu/pipelineCache.d.ts +6 -0
  42. package/dist/render/backend/webgpu/pipelineCache.js +7 -0
  43. package/dist/render/backend/webgpu/postPass.d.ts +51 -9
  44. package/dist/render/backend/webgpu/postPass.js +16 -2
  45. package/dist/render/backend/webgpu/reflectionSurfacePass.d.ts +30 -0
  46. package/dist/render/backend/webgpu/reflectionSurfacePass.js +100 -0
  47. package/dist/render/backend/webgpu/renderer.d.ts +283 -37
  48. package/dist/render/backend/webgpu/renderer.js +1363 -202
  49. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +7 -3
  50. package/dist/render/backend/webgpu/skinPaletteRing.js +13 -7
  51. package/dist/render/backend/webgpu/skinPaletteTexture.d.ts +6 -0
  52. package/dist/render/backend/webgpu/skinPaletteTexture.js +7 -1
  53. package/dist/render/backend/webgpu/staticBundles.d.ts +125 -0
  54. package/dist/render/backend/webgpu/staticBundles.js +241 -0
  55. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +10 -3
  56. package/dist/render/backend/webgpu/surfaceTexturePass.js +20 -2
  57. package/dist/render/backend/webgpu/uniformRing.d.ts +21 -0
  58. package/dist/render/backend/webgpu/uniformRing.js +9 -1
  59. package/dist/render/bloomChain.d.ts +3 -3
  60. package/dist/render/boneAnimation.d.ts +92 -0
  61. package/dist/render/boneAnimation.js +133 -0
  62. package/dist/render/compressedSource.d.ts +33 -13
  63. package/dist/render/compressedSource.js +70 -25
  64. package/dist/render/displayRange.d.ts +31 -0
  65. package/dist/render/displayRange.js +47 -0
  66. package/dist/render/filmicCurve.d.ts +74 -0
  67. package/dist/render/filmicCurve.js +203 -0
  68. package/dist/render/fog.d.ts +4 -1
  69. package/dist/render/fog.js +12 -2
  70. package/dist/render/frame/resources.d.ts +2 -2
  71. package/dist/render/frame/resources.js +6 -0
  72. package/dist/render/frameReflections.d.ts +40 -0
  73. package/dist/render/frameReflections.js +48 -0
  74. package/dist/render/glCompressed.js +83 -0
  75. package/dist/render/instances.d.ts +26 -0
  76. package/dist/render/lightBudget.d.ts +10 -0
  77. package/dist/render/lightBudget.js +10 -0
  78. package/dist/render/lightmap.d.ts +1 -1
  79. package/dist/render/mesh.d.ts +5 -0
  80. package/dist/render/mesh.js +6 -0
  81. package/dist/render/pass.d.ts +49 -0
  82. package/dist/render/pointLightSelection.d.ts +4 -4
  83. package/dist/render/reflectionSurfaceTargets.d.ts +28 -0
  84. package/dist/render/reflectionSurfaceTargets.js +99 -0
  85. package/dist/render/renderQuality.d.ts +39 -8
  86. package/dist/render/renderQuality.js +4 -0
  87. package/dist/render/sceneTarget.d.ts +5 -0
  88. package/dist/render/sceneTarget.js +24 -1
  89. package/dist/render/screenSpaceReflection.d.ts +7 -1
  90. package/dist/render/screenSpaceReflection.js +5 -0
  91. package/dist/render/shaders/arcane.d.ts +1 -1
  92. package/dist/render/shaders/bolt.d.ts +1 -1
  93. package/dist/render/shaders/boneAnimation.d.ts +19 -0
  94. package/dist/render/shaders/boneAnimation.js +63 -0
  95. package/dist/render/shaders/caustics.d.ts +1 -1
  96. package/dist/render/shaders/depth.d.ts +7 -0
  97. package/dist/render/shaders/depth.js +35 -10
  98. package/dist/render/shaders/film.d.ts +1 -1
  99. package/dist/render/shaders/filmLook.js +2 -1
  100. package/dist/render/shaders/fire.d.ts +1 -1
  101. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  102. package/dist/render/shaders/flat/glassTint.js +2 -0
  103. package/dist/render/shaders/flat/index.d.ts +16 -0
  104. package/dist/render/shaders/flat/index.js +33 -5
  105. package/dist/render/shaders/flat/main.d.ts +1 -1
  106. package/dist/render/shaders/flat/main.js +43 -5
  107. package/dist/render/shaders/flat/models.js +1 -1
  108. package/dist/render/shaders/flat/overlay.d.ts +16 -0
  109. package/dist/render/shaders/flat/overlay.js +117 -0
  110. package/dist/render/shaders/flat/pointShadow.js +3 -3
  111. package/dist/render/shaders/flat/preamble.js +38 -24
  112. package/dist/render/shaders/flat/reflectionSurface.d.ts +21 -0
  113. package/dist/render/shaders/flat/reflectionSurface.js +61 -0
  114. package/dist/render/shaders/fog.d.ts +1 -1
  115. package/dist/render/shaders/fog.js +9 -2
  116. package/dist/render/shaders/fringe.d.ts +29 -0
  117. package/dist/render/shaders/fringe.js +55 -0
  118. package/dist/render/shaders/generated/arcane.wgsl.d.ts +1 -1
  119. package/dist/render/shaders/generated/arcane.wgsl.js +1 -1
  120. package/dist/render/shaders/generated/bolt.wgsl.d.ts +1 -1
  121. package/dist/render/shaders/generated/bolt.wgsl.js +1 -1
  122. package/dist/render/shaders/generated/caustics.wgsl.d.ts +1 -1
  123. package/dist/render/shaders/generated/caustics.wgsl.js +1 -1
  124. package/dist/render/shaders/generated/depth.wgsl.d.ts +93 -0
  125. package/dist/render/shaders/generated/depth.wgsl.js +93 -0
  126. package/dist/render/shaders/generated/film.wgsl.d.ts +1 -1
  127. package/dist/render/shaders/generated/film.wgsl.js +1 -1
  128. package/dist/render/shaders/generated/fire.wgsl.d.ts +1 -1
  129. package/dist/render/shaders/generated/fire.wgsl.js +1 -1
  130. package/dist/render/shaders/generated/flat.wgsl.d.ts +3254 -2920
  131. package/dist/render/shaders/generated/flat.wgsl.js +3292 -2957
  132. package/dist/render/shaders/generated/line.wgsl.d.ts +1 -1
  133. package/dist/render/shaders/generated/line.wgsl.js +1 -1
  134. package/dist/render/shaders/generated/particle.wgsl.d.ts +4 -4
  135. package/dist/render/shaders/generated/particle.wgsl.js +4 -4
  136. package/dist/render/shaders/generated/rush.wgsl.d.ts +56 -26
  137. package/dist/render/shaders/generated/rush.wgsl.js +56 -26
  138. package/dist/render/shaders/generated/scatter.wgsl.d.ts +1 -1
  139. package/dist/render/shaders/generated/scatter.wgsl.js +1 -1
  140. package/dist/render/shaders/generated/smoke.wgsl.d.ts +1 -1
  141. package/dist/render/shaders/generated/smoke.wgsl.js +1 -1
  142. package/dist/render/shaders/generated/ssrResolve.wgsl.d.ts +29 -0
  143. package/dist/render/shaders/generated/ssrResolve.wgsl.js +29 -0
  144. package/dist/render/shaders/generated/ssrTrace.wgsl.d.ts +22 -2
  145. package/dist/render/shaders/generated/ssrTrace.wgsl.js +22 -2
  146. package/dist/render/shaders/generated/water.wgsl.d.ts +1 -1
  147. package/dist/render/shaders/generated/water.wgsl.js +1 -1
  148. package/dist/render/shaders/gpudriven/fog.wgsl.d.ts +2 -2
  149. package/dist/render/shaders/gpudriven/fog.wgsl.js +11 -3
  150. package/dist/render/shaders/line.d.ts +1 -1
  151. package/dist/render/shaders/outputTransform.d.ts +7 -0
  152. package/dist/render/shaders/outputTransform.js +28 -14
  153. package/dist/render/shaders/particle.d.ts +3 -3
  154. package/dist/render/shaders/rush.js +54 -11
  155. package/dist/render/shaders/scatter.d.ts +1 -1
  156. package/dist/render/shaders/smoke.d.ts +1 -1
  157. package/dist/render/shaders/ssrResolve.d.ts +17 -0
  158. package/dist/render/shaders/ssrResolve.js +51 -0
  159. package/dist/render/shaders/ssrTrace.js +26 -0
  160. package/dist/render/shaders/vertexChannel.d.ts +7 -0
  161. package/dist/render/shaders/vertexChannel.js +26 -0
  162. package/dist/render/shaders/water.d.ts +1 -1
  163. package/dist/render/ssrPass.d.ts +16 -0
  164. package/dist/render/ssrPass.js +106 -1
  165. package/dist/render/staticDraws.d.ts +47 -0
  166. package/dist/render/staticDraws.js +43 -0
  167. package/dist/render/surfaceOverlay.d.ts +132 -0
  168. package/dist/render/surfaceOverlay.js +85 -0
  169. package/dist/render/surfaceTexture.d.ts +57 -7
  170. package/dist/render/surfaceTexture.js +125 -17
  171. package/dist/render/temporalAa.d.ts +14 -0
  172. package/dist/render/temporalAa.js +20 -0
  173. package/dist/render/vertexDefaults.d.ts +17 -0
  174. package/dist/render/vertexDefaults.js +24 -0
  175. package/package.json +6 -6
  176. package/src/index.ts +24 -2
  177. package/src/render/ambientHarmonics.ts +1 -1
  178. package/src/render/areaLights.ts +4 -4
  179. package/src/render/atmosphere.ts +22 -1
  180. package/src/render/backend/api.ts +22 -3
  181. package/src/render/backend/createRenderer.ts +18 -1
  182. package/src/render/backend/select.ts +10 -5
  183. package/src/render/backend/webgl2/boneAnimations.ts +105 -0
  184. package/src/render/backend/webgl2/instanced.ts +20 -1
  185. package/src/render/backend/webgl2/renderer.ts +672 -37
  186. package/src/render/backend/webgl2/skinPaletteTexture.ts +6 -4
  187. package/src/render/backend/webgpu/boneAnimations.ts +97 -0
  188. package/src/render/backend/webgpu/buffers.ts +42 -16
  189. package/src/render/backend/webgpu/compressedUpload.ts +59 -9
  190. package/src/render/backend/webgpu/depthPass.ts +51 -10
  191. package/src/render/backend/webgpu/device.ts +42 -3
  192. package/src/render/backend/webgpu/drawCommand.ts +22 -4
  193. package/src/render/backend/webgpu/flatPass.ts +250 -40
  194. package/src/render/backend/webgpu/instanced.ts +32 -1
  195. package/src/render/backend/webgpu/materialStore.ts +276 -0
  196. package/src/render/backend/webgpu/pipelineCache.ts +7 -0
  197. package/src/render/backend/webgpu/postPass.ts +18 -0
  198. package/src/render/backend/webgpu/reflectionSurfacePass.ts +114 -0
  199. package/src/render/backend/webgpu/renderer.ts +1655 -230
  200. package/src/render/backend/webgpu/skinPaletteRing.ts +13 -7
  201. package/src/render/backend/webgpu/skinPaletteTexture.ts +7 -1
  202. package/src/render/backend/webgpu/staticBundles.ts +363 -0
  203. package/src/render/backend/webgpu/surfaceTexturePass.ts +29 -3
  204. package/src/render/backend/webgpu/uniformRing.ts +10 -1
  205. package/src/render/bloomChain.ts +3 -3
  206. package/src/render/boneAnimation.ts +223 -0
  207. package/src/render/compressedSource.ts +121 -29
  208. package/src/render/displayRange.ts +61 -0
  209. package/src/render/filmicCurve.ts +229 -0
  210. package/src/render/fog.ts +19 -3
  211. package/src/render/frame/resources.ts +6 -0
  212. package/src/render/frameReflections.ts +72 -0
  213. package/src/render/glCompressed.ts +83 -0
  214. package/src/render/instances.ts +27 -0
  215. package/src/render/lightBudget.ts +11 -0
  216. package/src/render/lightmap.ts +1 -1
  217. package/src/render/mesh.ts +6 -0
  218. package/src/render/pass.ts +53 -1
  219. package/src/render/pointLightSelection.ts +4 -4
  220. package/src/render/reflectionSurfaceTargets.ts +105 -0
  221. package/src/render/renderQuality.ts +44 -8
  222. package/src/render/sceneTarget.ts +30 -2
  223. package/src/render/screenSpaceReflection.ts +7 -1
  224. package/src/render/shaders/boneAnimation.ts +63 -0
  225. package/src/render/shaders/depth.ts +45 -10
  226. package/src/render/shaders/filmLook.ts +2 -1
  227. package/src/render/shaders/flat/glassTint.ts +5 -1
  228. package/src/render/shaders/flat/index.ts +49 -5
  229. package/src/render/shaders/flat/main.ts +43 -5
  230. package/src/render/shaders/flat/models.ts +1 -1
  231. package/src/render/shaders/flat/overlay.ts +117 -0
  232. package/src/render/shaders/flat/pointShadow.ts +3 -3
  233. package/src/render/shaders/flat/preamble.ts +38 -24
  234. package/src/render/shaders/flat/reflectionSurface.ts +61 -0
  235. package/src/render/shaders/fog.ts +9 -2
  236. package/src/render/shaders/fringe.ts +59 -0
  237. package/src/render/shaders/generated/arcane.wgsl.ts +1 -1
  238. package/src/render/shaders/generated/bolt.wgsl.ts +1 -1
  239. package/src/render/shaders/generated/caustics.wgsl.ts +1 -1
  240. package/src/render/shaders/generated/depth.wgsl.ts +95 -0
  241. package/src/render/shaders/generated/film.wgsl.ts +1 -1
  242. package/src/render/shaders/generated/fire.wgsl.ts +1 -1
  243. package/src/render/shaders/generated/flat.wgsl.ts +3292 -2957
  244. package/src/render/shaders/generated/line.wgsl.ts +1 -1
  245. package/src/render/shaders/generated/particle.wgsl.ts +4 -4
  246. package/src/render/shaders/generated/rush.wgsl.ts +56 -26
  247. package/src/render/shaders/generated/scatter.wgsl.ts +1 -1
  248. package/src/render/shaders/generated/smoke.wgsl.ts +1 -1
  249. package/src/render/shaders/generated/ssrResolve.wgsl.ts +30 -0
  250. package/src/render/shaders/generated/ssrTrace.wgsl.ts +22 -2
  251. package/src/render/shaders/generated/water.wgsl.ts +1 -1
  252. package/src/render/shaders/gpudriven/fog.wgsl.ts +11 -3
  253. package/src/render/shaders/outputTransform.ts +30 -14
  254. package/src/render/shaders/rush.ts +54 -11
  255. package/src/render/shaders/ssrResolve.ts +52 -0
  256. package/src/render/shaders/ssrTrace.ts +26 -0
  257. package/src/render/shaders/vertexChannel.ts +27 -0
  258. package/src/render/ssrPass.ts +138 -1
  259. package/src/render/staticDraws.ts +95 -0
  260. package/src/render/surfaceOverlay.ts +195 -0
  261. package/src/render/surfaceTexture.ts +161 -27
  262. package/src/render/temporalAa.ts +20 -0
  263. package/src/render/vertexDefaults.ts +28 -0
@@ -0,0 +1,276 @@
1
+ import type { BudgetCounter } from '../budget.ts';
2
+ import { DYNAMIC_ALIGNMENT, MAX_RING_BYTES } from './uniformRing.ts';
3
+
4
+ /**
5
+ * The lit stage's material blocks, each kept in its slot for as long as later draws ask for the
6
+ * same numbers, so a material that has not changed since the last frame costs no upload at all.
7
+ *
8
+ * **Keyed by content, because nothing else names a material.** A material here is whatever the
9
+ * setters left in the block when a draw took it — a consumer never hands over an object that could
10
+ * carry an identity — so the block's own words are the key: hashed to find a bucket, compared word
11
+ * for word to accept a match, and bound by the slot's offset. What it gives up is a hash and a
12
+ * compare of a few hundred bytes per material change, against copying and uploading the whole
13
+ * block every frame, which a stage of four hundred materials measured at megabytes a frame.
14
+ *
15
+ * **A slot the frame has asked for is never given away within it**, for the reason `UniformRing`
16
+ * exists: a queue write does not interleave with recorded draws, so rewriting a slot a recorded draw
17
+ * reads would hand that draw the new numbers. A frame asking for more distinct blocks than there are
18
+ * slots is refused, counted and skipped, as the ring was. Across frames rewriting is safe, because a
19
+ * write issued after a submit is ordered after the commands in it.
20
+ *
21
+ * **What a new block displaces is a slot nobody asked for this frame or the last**, found by a hand
22
+ * that sweeps the slots in turn; only when every slot was asked for recently does it take one the
23
+ * last frame used. What would make that wrong is a scene whose distinct materials a frame approach
24
+ * the store's size: it then rewrites some of its steady materials every frame, which costs their
25
+ * upload and nothing else.
26
+ */
27
+ export class MaterialStore {
28
+ /** Replaced by `growTo`, so every bind group over it is rebuilt when that says so. */
29
+ buffer: GPUBuffer;
30
+ /** Bytes per slot, rounded up to the alignment a dynamic offset requires. */
31
+ readonly slotSize: number;
32
+
33
+ private readonly words: number;
34
+ private readonly stride: number;
35
+ private capacity: number;
36
+ private staging: ArrayBuffer;
37
+ private ints: Int32Array;
38
+ /** Per slot: its block's hash, the next slot in its bucket or -1, and whether it holds one. */
39
+ private hashes: Int32Array;
40
+ private next: Int32Array;
41
+ private held: Uint8Array;
42
+ /** Per slot: the frame that last asked for it, and whether it was written since the last flush. */
43
+ private askedIn: Int32Array;
44
+ private pending: Uint8Array;
45
+ /** Per slot: how many holders pinned it. A pinned slot is never displaced. */
46
+ private pins: Int32Array;
47
+ /**
48
+ * How many times the store has forgotten everything it held, which growth does: a holder of an
49
+ * offset compares this, because an offset from before is a slot of a buffer that is gone.
50
+ */
51
+ epoch = 0;
52
+ /** Per bucket: its first slot, or -1. A power of two at least twice the slots. */
53
+ private heads: Int32Array;
54
+ private mask: number;
55
+ private lowPending: number;
56
+ private highPending = -1;
57
+ private hand = 0;
58
+ /** Starts past zero, so a slot never asked for reads as long unasked. */
59
+ private frame = 2;
60
+
61
+ constructor(
62
+ private readonly device: GPUDevice,
63
+ bytesPerBlock: number,
64
+ slots: number,
65
+ private readonly usage: number,
66
+ private readonly label = 'materials.store',
67
+ private readonly budget: BudgetCounter | null = null,
68
+ ) {
69
+ this.words = Math.ceil(bytesPerBlock / 4);
70
+ this.slotSize = Math.ceil(bytesPerBlock / DYNAMIC_ALIGNMENT) * DYNAMIC_ALIGNMENT;
71
+ this.stride = this.slotSize / 4;
72
+ this.capacity = slots;
73
+ this.lowPending = slots;
74
+ this.staging = new ArrayBuffer(this.slotSize * slots);
75
+ this.ints = new Int32Array(this.staging);
76
+ this.hashes = new Int32Array(slots);
77
+ this.next = new Int32Array(slots);
78
+ this.held = new Uint8Array(slots);
79
+ this.askedIn = new Int32Array(slots);
80
+ this.pending = new Uint8Array(slots);
81
+ this.pins = new Int32Array(slots);
82
+ this.heads = new Int32Array(bucketsFor(slots)).fill(-1);
83
+ this.mask = this.heads.length - 1;
84
+ this.buffer = device.createBuffer({ label, size: this.staging.byteLength, usage });
85
+ }
86
+
87
+ /** How many distinct blocks a frame may hold. */
88
+ get slots(): number {
89
+ return this.capacity;
90
+ }
91
+
92
+ /** Start a frame: every slot the last one asked for may be displaced from here on, last. */
93
+ beginFrame(): void {
94
+ this.frame += 1;
95
+ }
96
+
97
+ /**
98
+ * The byte offset of a slot holding exactly `block`, written there if no slot did, or null when
99
+ * every slot is one this frame has asked for. `block` is the whole block, word for word.
100
+ */
101
+ place(block: Int32Array): number | null {
102
+ this.budget?.ask();
103
+ const hash = hashWords(block, this.words);
104
+ const bucket = hash & this.mask;
105
+ for (let slot = this.heads[bucket] as number; slot >= 0; slot = this.next[slot] as number) {
106
+ if (this.hashes[slot] === hash && this.holds(slot, block)) {
107
+ this.askedIn[slot] = this.frame;
108
+ return slot * this.slotSize;
109
+ }
110
+ }
111
+ const slot = this.displace();
112
+ if (slot < 0) {
113
+ this.budget?.drop();
114
+ return null;
115
+ }
116
+ if (this.held[slot] === 1) this.unlink(slot);
117
+ this.ints.set(block, slot * this.stride);
118
+ this.hashes[slot] = hash;
119
+ this.next[slot] = this.heads[bucket] as number;
120
+ this.heads[bucket] = slot;
121
+ this.held[slot] = 1;
122
+ this.askedIn[slot] = this.frame;
123
+ this.pending[slot] = 1;
124
+ if (slot < this.lowPending) this.lowPending = slot;
125
+ if (slot > this.highPending) this.highPending = slot;
126
+ return slot * this.slotSize;
127
+ }
128
+
129
+ /** Upload every slot written since the last flush, one write per run of neighbouring slots. */
130
+ flush(): void {
131
+ let slot = this.lowPending;
132
+ while (slot <= this.highPending) {
133
+ if (this.pending[slot] !== 1) {
134
+ slot += 1;
135
+ continue;
136
+ }
137
+ const from = slot;
138
+ while (slot <= this.highPending && this.pending[slot] === 1) {
139
+ this.pending[slot] = 0;
140
+ slot += 1;
141
+ }
142
+ const at = from * this.slotSize;
143
+ this.device.queue.writeBuffer(
144
+ this.buffer,
145
+ at,
146
+ this.staging,
147
+ at,
148
+ (slot - from) * this.slotSize,
149
+ );
150
+ }
151
+ this.lowPending = this.capacity;
152
+ this.highPending = -1;
153
+ }
154
+
155
+ /**
156
+ * Make room for at least `slots`, forgetting every block held, and say whether the buffer was
157
+ * replaced. Called where `UniformRing.growTo` is, at a frame's start, for the reasons it gives,
158
+ * and refused past the same ceiling.
159
+ */
160
+ growTo(slots: number): boolean {
161
+ if (slots <= this.capacity) return false;
162
+ if (this.slotSize * slots > MAX_RING_BYTES) return false;
163
+ this.buffer.destroy();
164
+ this.capacity = slots;
165
+ this.staging = new ArrayBuffer(this.slotSize * slots);
166
+ this.ints = new Int32Array(this.staging);
167
+ this.hashes = new Int32Array(slots);
168
+ this.next = new Int32Array(slots);
169
+ this.held = new Uint8Array(slots);
170
+ this.askedIn = new Int32Array(slots);
171
+ this.pending = new Uint8Array(slots);
172
+ this.pins = new Int32Array(slots);
173
+ this.heads = new Int32Array(bucketsFor(slots)).fill(-1);
174
+ this.mask = this.heads.length - 1;
175
+ this.lowPending = slots;
176
+ this.epoch += 1;
177
+ this.highPending = -1;
178
+ this.hand = 0;
179
+ this.buffer = this.device.createBuffer({
180
+ label: this.label,
181
+ size: this.staging.byteLength,
182
+ usage: this.usage,
183
+ });
184
+ return true;
185
+ }
186
+
187
+ /**
188
+ * Keep the block at `offset` where it is until `unpin`: a holder binding it by offset frame after
189
+ * frame — a list recorded once — whether or not anything asks for it. Counted, so two holders of
190
+ * one block each let go of their own pin.
191
+ */
192
+ pin(offset: number): void {
193
+ const slot = offset / this.slotSize;
194
+ this.pins[slot] = (this.pins[slot] as number) + 1;
195
+ }
196
+
197
+ unpin(offset: number): void {
198
+ const slot = offset / this.slotSize;
199
+ if ((this.pins[slot] as number) > 0) this.pins[slot] = (this.pins[slot] as number) - 1;
200
+ }
201
+
202
+ dispose(): void {
203
+ this.buffer.destroy();
204
+ }
205
+
206
+ private holds(slot: number, block: Int32Array): boolean {
207
+ const base = slot * this.stride;
208
+ for (let word = 0; word < this.words; word++) {
209
+ if (this.ints[base + word] !== block[word]) return false;
210
+ }
211
+ return true;
212
+ }
213
+
214
+ /**
215
+ * The slot a new block goes in: the first from the hand nobody asked for this frame or the last,
216
+ * else the first this frame has not asked for, else none. One sweep at most.
217
+ */
218
+ private displace(): number {
219
+ let fallback = -1;
220
+ for (let step = 0; step < this.capacity; step++) {
221
+ const slot = this.hand;
222
+ this.hand = slot + 1 === this.capacity ? 0 : slot + 1;
223
+ if ((this.pins[slot] as number) > 0) continue;
224
+ const asked = this.askedIn[slot] as number;
225
+ if (asked < this.frame - 1) return slot;
226
+ if (fallback < 0 && asked !== this.frame) fallback = slot;
227
+ }
228
+ return fallback;
229
+ }
230
+
231
+ private unlink(slot: number): void {
232
+ const bucket = (this.hashes[slot] as number) & this.mask;
233
+ let at = this.heads[bucket] as number;
234
+ if (at === slot) {
235
+ this.heads[bucket] = this.next[slot] as number;
236
+ return;
237
+ }
238
+ while (at >= 0) {
239
+ const after = this.next[at] as number;
240
+ if (after === slot) {
241
+ this.next[at] = this.next[slot] as number;
242
+ return;
243
+ }
244
+ at = after;
245
+ }
246
+ }
247
+ }
248
+
249
+ function bucketsFor(slots: number): number {
250
+ let buckets = 16;
251
+ while (buckets < slots * 2) buckets *= 2;
252
+ return buckets;
253
+ }
254
+
255
+ /**
256
+ * MurmurHash3's 32-bit mix over the block's words. Not FNV, whose multiply carries a difference only
257
+ * upward: the bucket is the hash's low bits, and a material's words are mostly floats, whose low
258
+ * mantissa bits are zero for every value a person types — 0.5, 1, 2 — so blocks differing only in
259
+ * those would all have shared one bucket. The compare that follows a match is what decides.
260
+ */
261
+ export function hashWords(block: Int32Array, words: number): number {
262
+ let hash = 0x9747b28c;
263
+ for (let word = 0; word < words; word++) {
264
+ let k = Math.imul(block[word] as number, 0xcc9e2d51);
265
+ k = (k << 15) | (k >>> 17);
266
+ hash ^= Math.imul(k, 0x1b873593);
267
+ hash = (hash << 13) | (hash >>> 19);
268
+ hash = (Math.imul(hash, 5) + 0xe6546b64) | 0;
269
+ }
270
+ hash ^= words * 4;
271
+ hash ^= hash >>> 16;
272
+ hash = Math.imul(hash, 0x85ebca6b);
273
+ hash ^= hash >>> 13;
274
+ hash = Math.imul(hash, 0xc2b2ae35);
275
+ return hash ^ (hash >>> 16);
276
+ }
@@ -58,6 +58,12 @@ export class PipelineCache {
58
58
 
59
59
  /** How to describe each lit pipeline again, so `enable` can rebuild every one. */
60
60
  private readonly litDescribers = new Map<string, () => GPURenderPipelineDescriptor>();
61
+ /**
62
+ * Bumped when a switch's rebuilt set lands, which replaces pipelines already handed out: a holder
63
+ * of one — a recorded render bundle — compares this and records again rather than drawing on
64
+ * with the pipelines from before the switch.
65
+ */
66
+ version = 0;
61
67
  /** Bumped by every switch, so a lit build started before one is not kept after it. */
62
68
  private litGeneration = 0;
63
69
  /** The last rebuild, which the next one waits for so they land in the order they were asked. */
@@ -220,6 +226,7 @@ export class PipelineCache {
220
226
  for (const result of results) {
221
227
  if (result.status === 'fulfilled') this.pipelines.set(result.value[0], result.value[1]);
222
228
  }
229
+ this.version += 1;
223
230
  });
224
231
  return this.litRebuild;
225
232
  }
@@ -29,6 +29,7 @@ import {
29
29
  } from '../../shaders/generated/ssrTrace.wgsl.ts';
30
30
  import {
31
31
  SSRRESOLVE_BINDINGS as SSR_RESOLVE_BINDINGS,
32
+ SSR_MATERIAL_RESOLVE_FRAG_WGSL,
32
33
  SSR_RESOLVE_FRAG_WGSL,
33
34
  } from '../../shaders/generated/ssrResolve.wgsl.ts';
34
35
  import {
@@ -331,6 +332,16 @@ export const REFLECTION_OVER_BLEND: GPUBlendState = {
331
332
  alpha: SCENE_ALPHA_KEEPS,
332
333
  };
333
334
 
335
+ /**
336
+ * The frame's materials' reflection, added: the found colour times the tint, less the probe's share,
337
+ * which is negative where the probe was brighter than what replaced it. A half-float scene takes
338
+ * the negative addend as the subtraction it is. Its alpha kept.
339
+ */
340
+ export const FRAME_REFLECTION_BLEND: GPUBlendState = {
341
+ color: { srcFactor: 'one', dstFactor: 'one', operation: 'add' },
342
+ alpha: SCENE_ALPHA_KEEPS,
343
+ };
344
+
334
345
  /**
335
346
  * The bloom pyramid's way back up: an octave added onto a level that the blend multiplies by that
336
347
  * level's tint first — the blend constant, which white makes `one, one` exactly. Into the pyramid's
@@ -450,6 +461,12 @@ export const SSR_FRAG_SIZE = SSR_BINDINGS.SSR_TRACE_FRAG.uniformSize;
450
461
  export const SSR_UNIFORMS = SSR_BINDINGS.SSR_TRACE_FRAG.uniforms;
451
462
  export const SSR_TEXTURES = SSR_BINDINGS.SSR_TRACE_FRAG.textures;
452
463
  export const SSR_RESOLVE_TEXTURES = SSR_RESOLVE_BINDINGS.SSR_RESOLVE_FRAG.textures;
464
+ /** The material resolve's block — the blur — and its three inputs. See `frameReflections.ts`. */
465
+ export const SSR_MATERIAL_FIELDS: UniformFields =
466
+ SSR_RESOLVE_BINDINGS.SSR_MATERIAL_RESOLVE_FRAG.fields;
467
+ export const SSR_MATERIAL_SIZE = SSR_RESOLVE_BINDINGS.SSR_MATERIAL_RESOLVE_FRAG.uniformSize;
468
+ export const SSR_MATERIAL_UNIFORMS = SSR_RESOLVE_BINDINGS.SSR_MATERIAL_RESOLVE_FRAG.uniforms;
469
+ export const SSR_MATERIAL_TEXTURES = SSR_RESOLVE_BINDINGS.SSR_MATERIAL_RESOLVE_FRAG.textures;
453
470
 
454
471
  /**
455
472
  * The resolve's layout: one texture and no uniform block.
@@ -513,6 +530,7 @@ export {
513
530
  DECAL_PROJECT_FRAG_WGSL,
514
531
  SSR_TRACE_FRAG_WGSL,
515
532
  SSR_RESOLVE_FRAG_WGSL,
533
+ SSR_MATERIAL_RESOLVE_FRAG_WGSL,
516
534
  };
517
535
  export { BLOOM_PREFILTER_FRAG_WGSL, BLOOM_DOWNSAMPLE_FRAG_WGSL, BLOOM_UPSAMPLE_FRAG_WGSL };
518
536
  export { RUSH_UNIFORMS, RUSH_TEXTURES };
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The surface half of a reflection traced through the frame's materials, on this backend: the
3
+ * frame's opaque lit draws again, against the depth they wrote, into two half-float targets — the
4
+ * environment as the frame shows it with the surface's roughness, and what a reflection found in the
5
+ * frame is multiplied by to land in its place. `shaders/flat/reflectionSurface.ts` is what the two
6
+ * hold and why the swap is exact; this is where they are drawn.
7
+ *
8
+ * **Why a second pass and not a second output of the frame's.** Every pipeline drawn into the
9
+ * frame's pass would have to declare a target it never writes — the sky, water, particles, text and
10
+ * every pass a consumer registers — so the frame keeps its one attachment, and the lit stage's two
11
+ * extra outputs land nowhere there. Here they land: the same pipelines with `REFLECTION_SURFACE` on,
12
+ * which leaves before the lamps, the same bind groups and ring slots, tested `equal` against the
13
+ * frame's depth so only what the frame kept writes.
14
+ *
15
+ * **The draws are commands, kept the way the skin's halves are** (`skinScatterPass.ts`): the frame
16
+ * graph's pool is spent at every flush, and these are replayed after the flush that ends the frame's
17
+ * pass. Each holds the ring slots its frame draw used, which stay valid until the frame's last
18
+ * submit, after this has run.
19
+ *
20
+ * **What it costs**: every opaque lit draw twice, the second without its lamps; two half-float
21
+ * targets the size of the frame. Single-sampled only, as the trace that reads them is.
22
+ */
23
+ import { createCommandPool, resetPool, takeCommand } from './drawCommand.ts';
24
+ import type { CommandPool, DrawCommand } from './drawCommand.ts';
25
+
26
+ /** Both targets' format: linear light and a tint, which an eight-bit target would clip and band. */
27
+ export const REFLECTION_SURFACE_FORMAT: GPUTextureFormat = 'rgba16float';
28
+
29
+ const USAGE_TARGET = 0x10 | 0x4; // RENDER_ATTACHMENT | TEXTURE_BINDING
30
+
31
+ export class ReflectionSurfacePass {
32
+ private readonly device: GPUDevice;
33
+ private readonly commands: CommandPool = createCommandPool(256);
34
+ private probe: GPUTexture | null = null;
35
+ private tint: GPUTexture | null = null;
36
+ /** The environment as the frame shows it, and the roughness in alpha. */
37
+ probeView: GPUTextureView | null = null;
38
+ /** What a found reflection is multiplied by. */
39
+ tintView: GPUTextureView | null = null;
40
+
41
+ constructor(device: GPUDevice) {
42
+ this.device = device;
43
+ }
44
+
45
+ /** A command for one opaque lit draw's surface half, filled by the caller. */
46
+ take(): DrawCommand {
47
+ return this.commands.commands[takeCommand(this.commands)] as DrawCommand;
48
+ }
49
+
50
+ /** How many draws the frame kept. */
51
+ get kept(): number {
52
+ return this.commands.taken;
53
+ }
54
+
55
+ /** A new frame keeps nothing yet. */
56
+ reset(): void {
57
+ resetPool(this.commands);
58
+ }
59
+
60
+ /** The two targets at the frame's size, made again only when it changes. */
61
+ size(width: number, height: number): void {
62
+ if (this.probe !== null && this.probe.width === width && this.probe.height === height) return;
63
+ this.release();
64
+ const make = (label: string): GPUTexture =>
65
+ this.device.createTexture({
66
+ label,
67
+ size: [width, height],
68
+ format: REFLECTION_SURFACE_FORMAT,
69
+ usage: USAGE_TARGET,
70
+ });
71
+ this.probe = make('reflection.probe');
72
+ this.tint = make('reflection.tint');
73
+ this.probeView = this.probe.createView();
74
+ this.tintView = this.tint.createView();
75
+ }
76
+
77
+ /**
78
+ * Every kept draw into the two targets against the frame's depth, which it reads and does not
79
+ * write; both cleared first, so a pixel no lit surface reached reflects nothing. Called with no
80
+ * pass open on `encoder`, and drawn even with nothing kept, since the clear is what the trace
81
+ * reads there.
82
+ */
83
+ draw(
84
+ encoder: GPUCommandEncoder,
85
+ depth: GPUTextureView,
86
+ issue: (pass: GPURenderPassEncoder, command: DrawCommand) => void,
87
+ ): void {
88
+ const probe = this.probeView;
89
+ const tint = this.tintView;
90
+ if (probe === null || tint === null) return;
91
+ const pass = encoder.beginRenderPass({
92
+ label: 'reflection.surface',
93
+ colorAttachments: [
94
+ null,
95
+ { view: probe, clearValue: [0, 0, 0, 1], loadOp: 'clear', storeOp: 'store' },
96
+ { view: tint, clearValue: [0, 0, 0, 0], loadOp: 'clear', storeOp: 'store' },
97
+ ],
98
+ depthStencilAttachment: { view: depth, depthReadOnly: true },
99
+ });
100
+ for (let i = 0; i < this.commands.taken; i += 1) {
101
+ issue(pass, this.commands.commands[i] as DrawCommand);
102
+ }
103
+ pass.end();
104
+ }
105
+
106
+ release(): void {
107
+ this.probe?.destroy();
108
+ this.tint?.destroy();
109
+ this.probe = null;
110
+ this.tint = null;
111
+ this.probeView = null;
112
+ this.tintView = null;
113
+ }
114
+ }