@driftengine/core 4.9.0 → 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 (135) hide show
  1. package/CHANGELOG.json +62 -11
  2. package/README.md +43 -38
  3. package/dist/index.d.ts +3 -1
  4. package/dist/index.js +4 -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/backend/api.d.ts +10 -3
  10. package/dist/render/backend/select.js +11 -5
  11. package/dist/render/backend/webgl2/boneAnimations.d.ts +29 -0
  12. package/dist/render/backend/webgl2/boneAnimations.js +80 -0
  13. package/dist/render/backend/webgl2/instanced.d.ts +5 -1
  14. package/dist/render/backend/webgl2/instanced.js +13 -1
  15. package/dist/render/backend/webgl2/renderer.d.ts +90 -2
  16. package/dist/render/backend/webgl2/renderer.js +253 -24
  17. package/dist/render/backend/webgl2/skinPaletteTexture.d.ts +5 -3
  18. package/dist/render/backend/webgl2/skinPaletteTexture.js +6 -4
  19. package/dist/render/backend/webgpu/boneAnimations.d.ts +34 -0
  20. package/dist/render/backend/webgpu/boneAnimations.js +76 -0
  21. package/dist/render/backend/webgpu/buffers.d.ts +19 -4
  22. package/dist/render/backend/webgpu/buffers.js +18 -11
  23. package/dist/render/backend/webgpu/compressedUpload.d.ts +0 -1
  24. package/dist/render/backend/webgpu/compressedUpload.js +53 -8
  25. package/dist/render/backend/webgpu/depthPass.d.ts +59 -3
  26. package/dist/render/backend/webgpu/depthPass.js +49 -14
  27. package/dist/render/backend/webgpu/drawCommand.d.ts +18 -4
  28. package/dist/render/backend/webgpu/drawCommand.js +10 -3
  29. package/dist/render/backend/webgpu/flatPass.d.ts +85 -42
  30. package/dist/render/backend/webgpu/flatPass.js +136 -20
  31. package/dist/render/backend/webgpu/instanced.d.ts +14 -1
  32. package/dist/render/backend/webgpu/instanced.js +24 -1
  33. package/dist/render/backend/webgpu/materialStore.d.ts +101 -0
  34. package/dist/render/backend/webgpu/materialStore.js +265 -0
  35. package/dist/render/backend/webgpu/pipelineCache.d.ts +6 -0
  36. package/dist/render/backend/webgpu/pipelineCache.js +7 -0
  37. package/dist/render/backend/webgpu/renderer.d.ts +152 -35
  38. package/dist/render/backend/webgpu/renderer.js +787 -142
  39. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +7 -3
  40. package/dist/render/backend/webgpu/skinPaletteRing.js +13 -7
  41. package/dist/render/backend/webgpu/skinPaletteTexture.d.ts +6 -0
  42. package/dist/render/backend/webgpu/skinPaletteTexture.js +7 -1
  43. package/dist/render/backend/webgpu/staticBundles.d.ts +125 -0
  44. package/dist/render/backend/webgpu/staticBundles.js +241 -0
  45. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +10 -3
  46. package/dist/render/backend/webgpu/surfaceTexturePass.js +20 -2
  47. package/dist/render/backend/webgpu/uniformRing.d.ts +21 -0
  48. package/dist/render/backend/webgpu/uniformRing.js +9 -1
  49. package/dist/render/bloomChain.d.ts +3 -3
  50. package/dist/render/boneAnimation.d.ts +92 -0
  51. package/dist/render/boneAnimation.js +133 -0
  52. package/dist/render/compressedSource.d.ts +33 -13
  53. package/dist/render/compressedSource.js +70 -25
  54. package/dist/render/glCompressed.js +83 -0
  55. package/dist/render/instances.d.ts +26 -0
  56. package/dist/render/lightBudget.d.ts +10 -0
  57. package/dist/render/lightBudget.js +10 -0
  58. package/dist/render/lightmap.d.ts +1 -1
  59. package/dist/render/mesh.d.ts +5 -0
  60. package/dist/render/mesh.js +6 -0
  61. package/dist/render/pointLightSelection.d.ts +4 -4
  62. package/dist/render/screenSpaceReflection.d.ts +2 -1
  63. package/dist/render/shaders/boneAnimation.d.ts +19 -0
  64. package/dist/render/shaders/boneAnimation.js +63 -0
  65. package/dist/render/shaders/depth.d.ts +7 -0
  66. package/dist/render/shaders/depth.js +35 -10
  67. package/dist/render/shaders/flat/index.d.ts +6 -0
  68. package/dist/render/shaders/flat/index.js +27 -5
  69. package/dist/render/shaders/flat/main.d.ts +1 -1
  70. package/dist/render/shaders/flat/main.js +15 -5
  71. package/dist/render/shaders/flat/models.js +1 -1
  72. package/dist/render/shaders/flat/overlay.js +1 -1
  73. package/dist/render/shaders/flat/pointShadow.js +2 -2
  74. package/dist/render/shaders/flat/preamble.js +38 -24
  75. package/dist/render/shaders/generated/depth.wgsl.d.ts +93 -0
  76. package/dist/render/shaders/generated/depth.wgsl.js +93 -0
  77. package/dist/render/shaders/generated/flat.wgsl.d.ts +3077 -2903
  78. package/dist/render/shaders/generated/flat.wgsl.js +3100 -2925
  79. package/dist/render/shaders/outputTransform.d.ts +7 -0
  80. package/dist/render/shaders/outputTransform.js +28 -14
  81. package/dist/render/shaders/vertexChannel.d.ts +7 -0
  82. package/dist/render/shaders/vertexChannel.js +26 -0
  83. package/dist/render/staticDraws.d.ts +47 -0
  84. package/dist/render/staticDraws.js +43 -0
  85. package/dist/render/surfaceTexture.d.ts +38 -7
  86. package/dist/render/surfaceTexture.js +56 -17
  87. package/package.json +6 -6
  88. package/src/index.ts +8 -2
  89. package/src/render/ambientHarmonics.ts +1 -1
  90. package/src/render/areaLights.ts +4 -4
  91. package/src/render/backend/api.ts +13 -3
  92. package/src/render/backend/select.ts +10 -5
  93. package/src/render/backend/webgl2/boneAnimations.ts +105 -0
  94. package/src/render/backend/webgl2/instanced.ts +20 -1
  95. package/src/render/backend/webgl2/renderer.ts +315 -24
  96. package/src/render/backend/webgl2/skinPaletteTexture.ts +6 -4
  97. package/src/render/backend/webgpu/boneAnimations.ts +97 -0
  98. package/src/render/backend/webgpu/buffers.ts +42 -16
  99. package/src/render/backend/webgpu/compressedUpload.ts +59 -9
  100. package/src/render/backend/webgpu/depthPass.ts +51 -10
  101. package/src/render/backend/webgpu/drawCommand.ts +22 -4
  102. package/src/render/backend/webgpu/flatPass.ts +187 -14
  103. package/src/render/backend/webgpu/instanced.ts +32 -1
  104. package/src/render/backend/webgpu/materialStore.ts +276 -0
  105. package/src/render/backend/webgpu/pipelineCache.ts +7 -0
  106. package/src/render/backend/webgpu/renderer.ts +928 -155
  107. package/src/render/backend/webgpu/skinPaletteRing.ts +13 -7
  108. package/src/render/backend/webgpu/skinPaletteTexture.ts +7 -1
  109. package/src/render/backend/webgpu/staticBundles.ts +363 -0
  110. package/src/render/backend/webgpu/surfaceTexturePass.ts +29 -3
  111. package/src/render/backend/webgpu/uniformRing.ts +10 -1
  112. package/src/render/bloomChain.ts +3 -3
  113. package/src/render/boneAnimation.ts +223 -0
  114. package/src/render/compressedSource.ts +121 -29
  115. package/src/render/glCompressed.ts +83 -0
  116. package/src/render/instances.ts +27 -0
  117. package/src/render/lightBudget.ts +11 -0
  118. package/src/render/lightmap.ts +1 -1
  119. package/src/render/mesh.ts +6 -0
  120. package/src/render/pointLightSelection.ts +4 -4
  121. package/src/render/screenSpaceReflection.ts +2 -1
  122. package/src/render/shaders/boneAnimation.ts +63 -0
  123. package/src/render/shaders/depth.ts +45 -10
  124. package/src/render/shaders/flat/index.ts +33 -5
  125. package/src/render/shaders/flat/main.ts +15 -5
  126. package/src/render/shaders/flat/models.ts +1 -1
  127. package/src/render/shaders/flat/overlay.ts +1 -1
  128. package/src/render/shaders/flat/pointShadow.ts +2 -2
  129. package/src/render/shaders/flat/preamble.ts +38 -24
  130. package/src/render/shaders/generated/depth.wgsl.ts +95 -0
  131. package/src/render/shaders/generated/flat.wgsl.ts +3100 -2925
  132. package/src/render/shaders/outputTransform.ts +30 -14
  133. package/src/render/shaders/vertexChannel.ts +27 -0
  134. package/src/render/staticDraws.ts +95 -0
  135. package/src/render/surfaceTexture.ts +94 -27
@@ -0,0 +1,101 @@
1
+ import type { BudgetCounter } from '../budget.ts';
2
+ /**
3
+ * The lit stage's material blocks, each kept in its slot for as long as later draws ask for the
4
+ * same numbers, so a material that has not changed since the last frame costs no upload at all.
5
+ *
6
+ * **Keyed by content, because nothing else names a material.** A material here is whatever the
7
+ * setters left in the block when a draw took it — a consumer never hands over an object that could
8
+ * carry an identity — so the block's own words are the key: hashed to find a bucket, compared word
9
+ * for word to accept a match, and bound by the slot's offset. What it gives up is a hash and a
10
+ * compare of a few hundred bytes per material change, against copying and uploading the whole
11
+ * block every frame, which a stage of four hundred materials measured at megabytes a frame.
12
+ *
13
+ * **A slot the frame has asked for is never given away within it**, for the reason `UniformRing`
14
+ * exists: a queue write does not interleave with recorded draws, so rewriting a slot a recorded draw
15
+ * reads would hand that draw the new numbers. A frame asking for more distinct blocks than there are
16
+ * slots is refused, counted and skipped, as the ring was. Across frames rewriting is safe, because a
17
+ * write issued after a submit is ordered after the commands in it.
18
+ *
19
+ * **What a new block displaces is a slot nobody asked for this frame or the last**, found by a hand
20
+ * that sweeps the slots in turn; only when every slot was asked for recently does it take one the
21
+ * last frame used. What would make that wrong is a scene whose distinct materials a frame approach
22
+ * the store's size: it then rewrites some of its steady materials every frame, which costs their
23
+ * upload and nothing else.
24
+ */
25
+ export declare class MaterialStore {
26
+ private readonly device;
27
+ private readonly usage;
28
+ private readonly label;
29
+ private readonly budget;
30
+ /** Replaced by `growTo`, so every bind group over it is rebuilt when that says so. */
31
+ buffer: GPUBuffer;
32
+ /** Bytes per slot, rounded up to the alignment a dynamic offset requires. */
33
+ readonly slotSize: number;
34
+ private readonly words;
35
+ private readonly stride;
36
+ private capacity;
37
+ private staging;
38
+ private ints;
39
+ /** Per slot: its block's hash, the next slot in its bucket or -1, and whether it holds one. */
40
+ private hashes;
41
+ private next;
42
+ private held;
43
+ /** Per slot: the frame that last asked for it, and whether it was written since the last flush. */
44
+ private askedIn;
45
+ private pending;
46
+ /** Per slot: how many holders pinned it. A pinned slot is never displaced. */
47
+ private pins;
48
+ /**
49
+ * How many times the store has forgotten everything it held, which growth does: a holder of an
50
+ * offset compares this, because an offset from before is a slot of a buffer that is gone.
51
+ */
52
+ epoch: number;
53
+ /** Per bucket: its first slot, or -1. A power of two at least twice the slots. */
54
+ private heads;
55
+ private mask;
56
+ private lowPending;
57
+ private highPending;
58
+ private hand;
59
+ /** Starts past zero, so a slot never asked for reads as long unasked. */
60
+ private frame;
61
+ constructor(device: GPUDevice, bytesPerBlock: number, slots: number, usage: number, label?: string, budget?: BudgetCounter | null);
62
+ /** How many distinct blocks a frame may hold. */
63
+ get slots(): number;
64
+ /** Start a frame: every slot the last one asked for may be displaced from here on, last. */
65
+ beginFrame(): void;
66
+ /**
67
+ * The byte offset of a slot holding exactly `block`, written there if no slot did, or null when
68
+ * every slot is one this frame has asked for. `block` is the whole block, word for word.
69
+ */
70
+ place(block: Int32Array): number | null;
71
+ /** Upload every slot written since the last flush, one write per run of neighbouring slots. */
72
+ flush(): void;
73
+ /**
74
+ * Make room for at least `slots`, forgetting every block held, and say whether the buffer was
75
+ * replaced. Called where `UniformRing.growTo` is, at a frame's start, for the reasons it gives,
76
+ * and refused past the same ceiling.
77
+ */
78
+ growTo(slots: number): boolean;
79
+ /**
80
+ * Keep the block at `offset` where it is until `unpin`: a holder binding it by offset frame after
81
+ * frame — a list recorded once — whether or not anything asks for it. Counted, so two holders of
82
+ * one block each let go of their own pin.
83
+ */
84
+ pin(offset: number): void;
85
+ unpin(offset: number): void;
86
+ dispose(): void;
87
+ private holds;
88
+ /**
89
+ * The slot a new block goes in: the first from the hand nobody asked for this frame or the last,
90
+ * else the first this frame has not asked for, else none. One sweep at most.
91
+ */
92
+ private displace;
93
+ private unlink;
94
+ }
95
+ /**
96
+ * MurmurHash3's 32-bit mix over the block's words. Not FNV, whose multiply carries a difference only
97
+ * upward: the bucket is the hash's low bits, and a material's words are mostly floats, whose low
98
+ * mantissa bits are zero for every value a person types — 0.5, 1, 2 — so blocks differing only in
99
+ * those would all have shared one bucket. The compare that follows a match is what decides.
100
+ */
101
+ export declare function hashWords(block: Int32Array, words: number): number;
@@ -0,0 +1,265 @@
1
+ import { DYNAMIC_ALIGNMENT, MAX_RING_BYTES } from './uniformRing.js';
2
+ /**
3
+ * The lit stage's material blocks, each kept in its slot for as long as later draws ask for the
4
+ * same numbers, so a material that has not changed since the last frame costs no upload at all.
5
+ *
6
+ * **Keyed by content, because nothing else names a material.** A material here is whatever the
7
+ * setters left in the block when a draw took it — a consumer never hands over an object that could
8
+ * carry an identity — so the block's own words are the key: hashed to find a bucket, compared word
9
+ * for word to accept a match, and bound by the slot's offset. What it gives up is a hash and a
10
+ * compare of a few hundred bytes per material change, against copying and uploading the whole
11
+ * block every frame, which a stage of four hundred materials measured at megabytes a frame.
12
+ *
13
+ * **A slot the frame has asked for is never given away within it**, for the reason `UniformRing`
14
+ * exists: a queue write does not interleave with recorded draws, so rewriting a slot a recorded draw
15
+ * reads would hand that draw the new numbers. A frame asking for more distinct blocks than there are
16
+ * slots is refused, counted and skipped, as the ring was. Across frames rewriting is safe, because a
17
+ * write issued after a submit is ordered after the commands in it.
18
+ *
19
+ * **What a new block displaces is a slot nobody asked for this frame or the last**, found by a hand
20
+ * that sweeps the slots in turn; only when every slot was asked for recently does it take one the
21
+ * last frame used. What would make that wrong is a scene whose distinct materials a frame approach
22
+ * the store's size: it then rewrites some of its steady materials every frame, which costs their
23
+ * upload and nothing else.
24
+ */
25
+ export class MaterialStore {
26
+ device;
27
+ usage;
28
+ label;
29
+ budget;
30
+ /** Replaced by `growTo`, so every bind group over it is rebuilt when that says so. */
31
+ buffer;
32
+ /** Bytes per slot, rounded up to the alignment a dynamic offset requires. */
33
+ slotSize;
34
+ words;
35
+ stride;
36
+ capacity;
37
+ staging;
38
+ ints;
39
+ /** Per slot: its block's hash, the next slot in its bucket or -1, and whether it holds one. */
40
+ hashes;
41
+ next;
42
+ held;
43
+ /** Per slot: the frame that last asked for it, and whether it was written since the last flush. */
44
+ askedIn;
45
+ pending;
46
+ /** Per slot: how many holders pinned it. A pinned slot is never displaced. */
47
+ pins;
48
+ /**
49
+ * How many times the store has forgotten everything it held, which growth does: a holder of an
50
+ * offset compares this, because an offset from before is a slot of a buffer that is gone.
51
+ */
52
+ epoch = 0;
53
+ /** Per bucket: its first slot, or -1. A power of two at least twice the slots. */
54
+ heads;
55
+ mask;
56
+ lowPending;
57
+ highPending = -1;
58
+ hand = 0;
59
+ /** Starts past zero, so a slot never asked for reads as long unasked. */
60
+ frame = 2;
61
+ constructor(device, bytesPerBlock, slots, usage, label = 'materials.store', budget = null) {
62
+ this.device = device;
63
+ this.usage = usage;
64
+ this.label = label;
65
+ this.budget = budget;
66
+ this.words = Math.ceil(bytesPerBlock / 4);
67
+ this.slotSize = Math.ceil(bytesPerBlock / DYNAMIC_ALIGNMENT) * DYNAMIC_ALIGNMENT;
68
+ this.stride = this.slotSize / 4;
69
+ this.capacity = slots;
70
+ this.lowPending = slots;
71
+ this.staging = new ArrayBuffer(this.slotSize * slots);
72
+ this.ints = new Int32Array(this.staging);
73
+ this.hashes = new Int32Array(slots);
74
+ this.next = new Int32Array(slots);
75
+ this.held = new Uint8Array(slots);
76
+ this.askedIn = new Int32Array(slots);
77
+ this.pending = new Uint8Array(slots);
78
+ this.pins = new Int32Array(slots);
79
+ this.heads = new Int32Array(bucketsFor(slots)).fill(-1);
80
+ this.mask = this.heads.length - 1;
81
+ this.buffer = device.createBuffer({ label, size: this.staging.byteLength, usage });
82
+ }
83
+ /** How many distinct blocks a frame may hold. */
84
+ get slots() {
85
+ return this.capacity;
86
+ }
87
+ /** Start a frame: every slot the last one asked for may be displaced from here on, last. */
88
+ beginFrame() {
89
+ this.frame += 1;
90
+ }
91
+ /**
92
+ * The byte offset of a slot holding exactly `block`, written there if no slot did, or null when
93
+ * every slot is one this frame has asked for. `block` is the whole block, word for word.
94
+ */
95
+ place(block) {
96
+ this.budget?.ask();
97
+ const hash = hashWords(block, this.words);
98
+ const bucket = hash & this.mask;
99
+ for (let slot = this.heads[bucket]; slot >= 0; slot = this.next[slot]) {
100
+ if (this.hashes[slot] === hash && this.holds(slot, block)) {
101
+ this.askedIn[slot] = this.frame;
102
+ return slot * this.slotSize;
103
+ }
104
+ }
105
+ const slot = this.displace();
106
+ if (slot < 0) {
107
+ this.budget?.drop();
108
+ return null;
109
+ }
110
+ if (this.held[slot] === 1)
111
+ this.unlink(slot);
112
+ this.ints.set(block, slot * this.stride);
113
+ this.hashes[slot] = hash;
114
+ this.next[slot] = this.heads[bucket];
115
+ this.heads[bucket] = slot;
116
+ this.held[slot] = 1;
117
+ this.askedIn[slot] = this.frame;
118
+ this.pending[slot] = 1;
119
+ if (slot < this.lowPending)
120
+ this.lowPending = slot;
121
+ if (slot > this.highPending)
122
+ this.highPending = slot;
123
+ return slot * this.slotSize;
124
+ }
125
+ /** Upload every slot written since the last flush, one write per run of neighbouring slots. */
126
+ flush() {
127
+ let slot = this.lowPending;
128
+ while (slot <= this.highPending) {
129
+ if (this.pending[slot] !== 1) {
130
+ slot += 1;
131
+ continue;
132
+ }
133
+ const from = slot;
134
+ while (slot <= this.highPending && this.pending[slot] === 1) {
135
+ this.pending[slot] = 0;
136
+ slot += 1;
137
+ }
138
+ const at = from * this.slotSize;
139
+ this.device.queue.writeBuffer(this.buffer, at, this.staging, at, (slot - from) * this.slotSize);
140
+ }
141
+ this.lowPending = this.capacity;
142
+ this.highPending = -1;
143
+ }
144
+ /**
145
+ * Make room for at least `slots`, forgetting every block held, and say whether the buffer was
146
+ * replaced. Called where `UniformRing.growTo` is, at a frame's start, for the reasons it gives,
147
+ * and refused past the same ceiling.
148
+ */
149
+ growTo(slots) {
150
+ if (slots <= this.capacity)
151
+ return false;
152
+ if (this.slotSize * slots > MAX_RING_BYTES)
153
+ return false;
154
+ this.buffer.destroy();
155
+ this.capacity = slots;
156
+ this.staging = new ArrayBuffer(this.slotSize * slots);
157
+ this.ints = new Int32Array(this.staging);
158
+ this.hashes = new Int32Array(slots);
159
+ this.next = new Int32Array(slots);
160
+ this.held = new Uint8Array(slots);
161
+ this.askedIn = new Int32Array(slots);
162
+ this.pending = new Uint8Array(slots);
163
+ this.pins = new Int32Array(slots);
164
+ this.heads = new Int32Array(bucketsFor(slots)).fill(-1);
165
+ this.mask = this.heads.length - 1;
166
+ this.lowPending = slots;
167
+ this.epoch += 1;
168
+ this.highPending = -1;
169
+ this.hand = 0;
170
+ this.buffer = this.device.createBuffer({
171
+ label: this.label,
172
+ size: this.staging.byteLength,
173
+ usage: this.usage,
174
+ });
175
+ return true;
176
+ }
177
+ /**
178
+ * Keep the block at `offset` where it is until `unpin`: a holder binding it by offset frame after
179
+ * frame — a list recorded once — whether or not anything asks for it. Counted, so two holders of
180
+ * one block each let go of their own pin.
181
+ */
182
+ pin(offset) {
183
+ const slot = offset / this.slotSize;
184
+ this.pins[slot] = this.pins[slot] + 1;
185
+ }
186
+ unpin(offset) {
187
+ const slot = offset / this.slotSize;
188
+ if (this.pins[slot] > 0)
189
+ this.pins[slot] = this.pins[slot] - 1;
190
+ }
191
+ dispose() {
192
+ this.buffer.destroy();
193
+ }
194
+ holds(slot, block) {
195
+ const base = slot * this.stride;
196
+ for (let word = 0; word < this.words; word++) {
197
+ if (this.ints[base + word] !== block[word])
198
+ return false;
199
+ }
200
+ return true;
201
+ }
202
+ /**
203
+ * The slot a new block goes in: the first from the hand nobody asked for this frame or the last,
204
+ * else the first this frame has not asked for, else none. One sweep at most.
205
+ */
206
+ displace() {
207
+ let fallback = -1;
208
+ for (let step = 0; step < this.capacity; step++) {
209
+ const slot = this.hand;
210
+ this.hand = slot + 1 === this.capacity ? 0 : slot + 1;
211
+ if (this.pins[slot] > 0)
212
+ continue;
213
+ const asked = this.askedIn[slot];
214
+ if (asked < this.frame - 1)
215
+ return slot;
216
+ if (fallback < 0 && asked !== this.frame)
217
+ fallback = slot;
218
+ }
219
+ return fallback;
220
+ }
221
+ unlink(slot) {
222
+ const bucket = this.hashes[slot] & this.mask;
223
+ let at = this.heads[bucket];
224
+ if (at === slot) {
225
+ this.heads[bucket] = this.next[slot];
226
+ return;
227
+ }
228
+ while (at >= 0) {
229
+ const after = this.next[at];
230
+ if (after === slot) {
231
+ this.next[at] = this.next[slot];
232
+ return;
233
+ }
234
+ at = after;
235
+ }
236
+ }
237
+ }
238
+ function bucketsFor(slots) {
239
+ let buckets = 16;
240
+ while (buckets < slots * 2)
241
+ buckets *= 2;
242
+ return buckets;
243
+ }
244
+ /**
245
+ * MurmurHash3's 32-bit mix over the block's words. Not FNV, whose multiply carries a difference only
246
+ * upward: the bucket is the hash's low bits, and a material's words are mostly floats, whose low
247
+ * mantissa bits are zero for every value a person types — 0.5, 1, 2 — so blocks differing only in
248
+ * those would all have shared one bucket. The compare that follows a match is what decides.
249
+ */
250
+ export function hashWords(block, words) {
251
+ let hash = 0x9747b28c;
252
+ for (let word = 0; word < words; word++) {
253
+ let k = Math.imul(block[word], 0xcc9e2d51);
254
+ k = (k << 15) | (k >>> 17);
255
+ hash ^= Math.imul(k, 0x1b873593);
256
+ hash = (hash << 13) | (hash >>> 19);
257
+ hash = (Math.imul(hash, 5) + 0xe6546b64) | 0;
258
+ }
259
+ hash ^= words * 4;
260
+ hash ^= hash >>> 16;
261
+ hash = Math.imul(hash, 0x85ebca6b);
262
+ hash ^= hash >>> 13;
263
+ hash = Math.imul(hash, 0xc2b2ae35);
264
+ return hash ^ (hash >>> 16);
265
+ }
@@ -52,6 +52,12 @@ export declare class PipelineCache {
52
52
  readonly glassShadows: boolean;
53
53
  /** How to describe each lit pipeline again, so `enable` can rebuild every one. */
54
54
  private readonly litDescribers;
55
+ /**
56
+ * Bumped when a switch's rebuilt set lands, which replaces pipelines already handed out: a holder
57
+ * of one — a recorded render bundle — compares this and records again rather than drawing on
58
+ * with the pipelines from before the switch.
59
+ */
60
+ version: number;
55
61
  /** Bumped by every switch, so a lit build started before one is not kept after it. */
56
62
  private litGeneration;
57
63
  /** The last rebuild, which the next one waits for so they land in the order they were asked. */
@@ -51,6 +51,12 @@ export class PipelineCache {
51
51
  glassShadows;
52
52
  /** How to describe each lit pipeline again, so `enable` can rebuild every one. */
53
53
  litDescribers = new Map();
54
+ /**
55
+ * Bumped when a switch's rebuilt set lands, which replaces pipelines already handed out: a holder
56
+ * of one — a recorded render bundle — compares this and records again rather than drawing on
57
+ * with the pipelines from before the switch.
58
+ */
59
+ version = 0;
54
60
  /** Bumped by every switch, so a lit build started before one is not kept after it. */
55
61
  litGeneration = 0;
56
62
  /** The last rebuild, which the next one waits for so they land in the order they were asked. */
@@ -201,6 +207,7 @@ export class PipelineCache {
201
207
  if (result.status === 'fulfilled')
202
208
  this.pipelines.set(result.value[0], result.value[1]);
203
209
  }
210
+ this.version += 1;
204
211
  });
205
212
  return this.litRebuild;
206
213
  }