gaussian-splat-lite 1.4.3 → 1.5.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 (69) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/CONTRIBUTING.md +1 -1
  3. package/README.md +1 -1
  4. package/bin/build-lod.js +39 -3
  5. package/bin/decoder/cluster.wasm +0 -0
  6. package/bin/rad/cluster.js +756 -0
  7. package/bin/rad/parallel.js +77 -0
  8. package/bin/rad/rad.js +107 -123
  9. package/bin/rad/storage.js +4 -1
  10. package/dist/gaussian-splat-lite.cjs +685 -551
  11. package/dist/gaussian-splat-lite.cjs.map +1 -1
  12. package/dist/gaussian-splat-lite.min.cjs +118 -118
  13. package/dist/gaussian-splat-lite.min.cjs.map +1 -1
  14. package/dist/gaussian-splat-lite.module.js +685 -551
  15. package/dist/gaussian-splat-lite.module.js.map +1 -1
  16. package/dist/gaussian-splat-lite.module.min.js +3335 -3217
  17. package/dist/gaussian-splat-lite.module.min.js.map +1 -1
  18. package/dist/types/data/IndexedSplats.d.cts +11 -0
  19. package/dist/types/data/IndexedSplats.d.ts +11 -0
  20. package/dist/types/data/RadPagedSplats.d.cts +3 -23
  21. package/dist/types/data/RadPagedSplats.d.ts +3 -23
  22. package/dist/types/data/Splats.d.cts +2 -0
  23. package/dist/types/data/Splats.d.ts +2 -0
  24. package/dist/types/data/defines.d.cts +2 -0
  25. package/dist/types/data/defines.d.ts +2 -0
  26. package/dist/types/data/pendingUploads.d.cts +14 -0
  27. package/dist/types/data/pendingUploads.d.ts +14 -0
  28. package/dist/types/index.d.cts +2 -0
  29. package/dist/types/index.d.ts +2 -0
  30. package/dist/types/loaders/SplatLoader.d.cts +2 -0
  31. package/dist/types/loaders/SplatLoader.d.ts +2 -0
  32. package/dist/types/loaders/loadSplatData.d.cts +3 -0
  33. package/dist/types/loaders/loadSplatData.d.ts +3 -0
  34. package/dist/types/loaders/loadTypes.d.cts +3 -0
  35. package/dist/types/loaders/loadTypes.d.ts +3 -0
  36. package/dist/types/loaders/stream/rad-stream/RadSelectionState.d.cts +2 -2
  37. package/dist/types/loaders/stream/rad-stream/RadSelectionState.d.ts +2 -2
  38. package/dist/types/loaders/stream/rad-stream/RadStreamLoader.d.cts +42 -16
  39. package/dist/types/loaders/stream/rad-stream/RadStreamLoader.d.ts +42 -16
  40. package/dist/types/loaders/stream/rad-stream/RadStreamScheduler.d.cts +6 -3
  41. package/dist/types/loaders/stream/rad-stream/RadStreamScheduler.d.ts +6 -3
  42. package/dist/types/loaders/stream/rad-stream/radFade.d.cts +2 -2
  43. package/dist/types/loaders/stream/rad-stream/radFade.d.ts +2 -2
  44. package/dist/types/loaders/stream/rad-stream/radLod.d.cts +11 -3
  45. package/dist/types/loaders/stream/rad-stream/radLod.d.ts +11 -3
  46. package/dist/types/loaders/stream/rad-stream/workerHandlers.d.cts +9 -3
  47. package/dist/types/loaders/stream/rad-stream/workerHandlers.d.ts +9 -3
  48. package/dist/types/loaders/stream/sog-stream/SogStreamBatch.d.cts +0 -1
  49. package/dist/types/loaders/stream/sog-stream/SogStreamBatch.d.ts +0 -1
  50. package/dist/types/loaders/stream/sog-stream/SogStreamLoader.d.cts +20 -4
  51. package/dist/types/loaders/stream/sog-stream/SogStreamLoader.d.ts +20 -4
  52. package/dist/types/loaders/stream/sog-stream/SogStreamScheduler.d.cts +5 -2
  53. package/dist/types/loaders/stream/sog-stream/SogStreamScheduler.d.ts +5 -2
  54. package/dist/types/loaders/stream/sog-stream/sogLod.d.cts +5 -2
  55. package/dist/types/loaders/stream/sog-stream/sogLod.d.ts +5 -2
  56. package/dist/types/loaders/stream/sog-stream/workerHandlers.d.cts +8 -13
  57. package/dist/types/loaders/stream/sog-stream/workerHandlers.d.ts +8 -13
  58. package/dist/types/loaders/stream/streamOptions.d.cts +8 -2
  59. package/dist/types/loaders/stream/streamOptions.d.ts +8 -2
  60. package/dist/types/scene/SplatMesh.d.cts +5 -2
  61. package/dist/types/scene/SplatMesh.d.ts +5 -2
  62. package/docs/BuildRad.md +22 -1
  63. package/docs/RadStreamScheduler.md +30 -5
  64. package/docs/SogStreamScheduler.md +33 -7
  65. package/docs/SplatFileType.md +1 -1
  66. package/docs/SplatLoader.md +1 -0
  67. package/docs/SplatMesh.md +3 -2
  68. package/docs/Splats.md +1 -0
  69. package/package.json +2 -2
@@ -0,0 +1,756 @@
1
+ // SH codebook clustering adapted from MIT-licensed code. See THIRD_PARTY_LICENSES.md.
2
+ import { readFileSync } from "node:fs";
3
+ import { availableParallelism } from "node:os";
4
+ import { isMainThread, parentPort, workerData } from "node:worker_threads";
5
+ import { HALF } from "./math.js";
6
+ import { WorkerGroup, clusterers } from "./parallel.js";
7
+ import { Progress } from "./progress.js";
8
+ import { SH_DIMS } from "./splats.js";
9
+ import { PagedArray } from "./storage.js";
10
+
11
+ // Codebook entries at most: RAD labels are 16-bit.
12
+ const MAX_CODES = 65536;
13
+ // Training reads SAMPLE_SHARE of the splats, and at least SAMPLES_PER_CODE
14
+ // per entry; larger models train on a sample by weight, and only the final
15
+ // assignment reads all. Entries start at evenly spaced training splats,
16
+ // best at heavy ones: on Elevator (3.9M splats after LOD) 16 per entry fit
17
+ // better than all splats, and on C-NorthSouth (111M) 5% fit 8.5% closer
18
+ // than 16 per entry, for 5% more conversion time.
19
+ const SAMPLES_PER_CODE = 16;
20
+ const SAMPLE_SHARE = 0.05;
21
+ // Splats per job of a thread, and per call of the kernel.
22
+ const JOB = 4096;
23
+ const BATCH = 1024;
24
+ // Jobs per thread that are read and not yet consumed, at most; a thread
25
+ // computes one at a time.
26
+ const WINDOW = 3;
27
+ // Codes per block of the kernel's layout.
28
+ const BLOCK = 8;
29
+ // With at least GROUPS × 32 entries, each pass searches only part of them:
30
+ // the entries are clustered into GROUPS groups, and each splat searches the
31
+ // entries of the groups with the nearest centers, TRAIN_PROBES of them in
32
+ // iterations and FINAL_PROBES in the final pass. Grouping takes GROUP_STEPS
33
+ // steps from evenly spaced entries, then one from the last pass's groups.
34
+ // On Elevator this fit the entries better than comparing all of them, as
35
+ // with few probes entries stay near their groups, in a twentieth of the
36
+ // time; the final pass gave 99% of splats their nearest entry.
37
+ const GROUPS = 512;
38
+ const TRAIN_PROBES = 2;
39
+ const FINAL_PROBES = 24;
40
+ const GROUP_STEPS = 4;
41
+
42
+ let kernel;
43
+ // The compiled nearest-code kernel, loaded before converting.
44
+ export function codebookKernel() {
45
+ if (kernel) return kernel;
46
+ let bytes;
47
+ try {
48
+ bytes = readFileSync(new URL("../decoder/cluster.wasm", import.meta.url));
49
+ } catch (error) {
50
+ if (error.code !== "ENOENT") throw error;
51
+ throw new Error(
52
+ "SH clustering module not found; build it with npm run build:wasm:lod",
53
+ );
54
+ }
55
+ kernel = new WebAssembly.Module(bytes);
56
+ return kernel;
57
+ }
58
+
59
+ // Bytes of a kernel instance's memory for `blocks` blocks of codes, with the
60
+ // module's own 1 MiB, and for the groups of the final assignment, if any:
61
+ // each group may add a block, and `scratch` bytes for the search.
62
+ function kernelBytes(dims, blocks, groups, scratch) {
63
+ return (
64
+ ((blocks + groups) * BLOCK + groups) * (dims + 1) * 4 +
65
+ groups * 8 +
66
+ scratch +
67
+ BATCH * (dims * 4 + 64 + 6) +
68
+ 1024 ** 2
69
+ );
70
+ }
71
+
72
+ // A thread's instance of the kernel, with the codes in blocks of 8, their
73
+ // squared lengths, and room for a batch of points, the kernel's state and
74
+ // the labels; with `groups`, also room for the grouped codes, the groups'
75
+ // centers and table, and the search's scratch memory.
76
+ class Nearest {
77
+ constructor(module, dims, blocks, groups) {
78
+ const { exports } = new WebAssembly.Instance(module, {});
79
+ const memory = exports.memory;
80
+ const align = (bytes) => Math.ceil(bytes / 16) * 16;
81
+ const slots = (blocks + groups) * BLOCK;
82
+ const centers = Math.ceil(groups / BLOCK) * BLOCK;
83
+ this.exports = exports;
84
+ this.dims = dims;
85
+ this.blocks = blocks;
86
+ this.groups = 0;
87
+ this.codes = align(memory.buffer.byteLength);
88
+ this.norms = this.codes + slots * dims * 4;
89
+ this.centers = align(this.norms + slots * 4);
90
+ this.centerNorms = this.centers + centers * dims * 4;
91
+ this.starts = align(this.centerNorms + centers * 4);
92
+ this.sizes = this.starts + groups * 4;
93
+ this.points = align(this.sizes + groups * 4);
94
+ this.state = align(this.points + BATCH * dims * 4);
95
+ this.labels = this.state + (BATCH / 4) * 256;
96
+ this.slots = align(this.labels + BATCH * 2);
97
+ this.scratch = this.slots + BATCH * 4;
98
+ const end =
99
+ this.scratch +
100
+ (groups ? exports.probed_scratch(BATCH, dims, groups, FINAL_PROBES) : 0);
101
+ memory.grow(Math.ceil((end - memory.buffer.byteLength) / 65536));
102
+ const { buffer } = memory;
103
+ this.codeView = new Float32Array(buffer, this.codes, slots * dims);
104
+ this.normView = new Float32Array(buffer, this.norms, slots);
105
+ this.centerView = new Float32Array(buffer, this.centers, centers * dims);
106
+ this.centerNormView = new Float32Array(buffer, this.centerNorms, centers);
107
+ this.startView = new Uint32Array(buffer, this.starts, groups);
108
+ this.sizeView = new Uint32Array(buffer, this.sizes, groups);
109
+ this.pointView = new Float32Array(buffer, this.points, BATCH * dims);
110
+ this.labelView = new Uint16Array(buffer, this.labels, BATCH);
111
+ this.slotView = new Uint32Array(buffer, this.slots, BATCH);
112
+ this.center = new Float64Array(dims);
113
+ this.warm = false;
114
+ }
115
+
116
+ handle(message) {
117
+ if (message.type === "codes") {
118
+ this.codeView.set(message.codes);
119
+ this.normView.set(message.norms);
120
+ this.center.set(message.center);
121
+ // A first small call makes the engine optimize the kernel before the
122
+ // next: a call that runs long stays unoptimized to its end.
123
+ if (!this.warm) this.call(4);
124
+ this.warm = true;
125
+ return true;
126
+ }
127
+ if (message.type === "groups") {
128
+ // From now on, codes are searched by group; `order` maps their
129
+ // positions to their indices.
130
+ this.codeView.set(message.codes);
131
+ this.normView.set(message.norms);
132
+ this.centerView.set(message.centers);
133
+ this.centerNormView.set(message.centerNorms);
134
+ this.startView.set(message.starts);
135
+ this.sizeView.set(message.sizes);
136
+ this.center.set(message.center);
137
+ this.order = message.order;
138
+ this.probes = message.probes;
139
+ this.groups = message.starts.length;
140
+ this.call(4);
141
+ return true;
142
+ }
143
+ if (message.type === "group") {
144
+ // For grouping, codes are the points: labels `count` of them from
145
+ // `first` with their nearest centers, in shared memory.
146
+ const { points, centers, norms, labels, first, count } = message;
147
+ const { dims, pointView, labelView } = this;
148
+ this.centerView.set(centers);
149
+ this.centerNormView.set(norms);
150
+ for (let start = first; start < first + count; start += BATCH) {
151
+ const size = Math.min(BATCH, first + count - start);
152
+ const rows = Math.ceil(size / 4) * 4;
153
+ pointView.set(points.subarray(start * dims, (start + size) * dims));
154
+ for (let i = size; i < rows; i++)
155
+ pointView.copyWithin(i * dims, (size - 1) * dims, size * dims);
156
+ this.exports.nearest(
157
+ this.points,
158
+ rows,
159
+ dims,
160
+ this.centers,
161
+ this.centerNorms,
162
+ centers.length / (BLOCK * dims),
163
+ this.state,
164
+ this.labels,
165
+ );
166
+ labels.set(labelView.subarray(0, size), start);
167
+ }
168
+ return true;
169
+ }
170
+ const { bits, labels, count } = message;
171
+ const { dims, center, pointView, labelView, slotView, order } = this;
172
+ for (let start = 0; start < count; start += BATCH) {
173
+ const size = Math.min(BATCH, count - start);
174
+ // Points relative to the center; the last fills the final group of 4.
175
+ const rows = Math.ceil(size / 4) * 4;
176
+ for (let i = 0; i < rows; i++) {
177
+ const row = (start + Math.min(i, size - 1)) * dims;
178
+ for (let d = 0; d < dims; d++)
179
+ pointView[i * dims + d] = HALF[bits[row + d]] - center[d];
180
+ }
181
+ this.call(rows);
182
+ if (!this.groups) labels.set(labelView.subarray(0, size), start);
183
+ else
184
+ for (let i = 0; i < size; i++) labels[start + i] = order[slotView[i]];
185
+ }
186
+ return { labels, bits };
187
+ }
188
+
189
+ call(rows) {
190
+ if (!this.groups) {
191
+ this.exports.nearest(
192
+ this.points,
193
+ rows,
194
+ this.dims,
195
+ this.codes,
196
+ this.norms,
197
+ this.blocks,
198
+ this.state,
199
+ this.labels,
200
+ );
201
+ return;
202
+ }
203
+ this.exports.probed(
204
+ this.points,
205
+ rows,
206
+ this.dims,
207
+ this.centers,
208
+ this.centerNorms,
209
+ this.groups,
210
+ this.codes,
211
+ this.norms,
212
+ this.starts,
213
+ this.sizes,
214
+ this.probes,
215
+ this.scratch,
216
+ this.slots,
217
+ );
218
+ }
219
+ }
220
+
221
+ if (!isMainThread && workerData?.codebook) {
222
+ const { module, dims, blocks, groups } = workerData;
223
+ const nearest = new Nearest(module, dims, blocks, groups);
224
+ parentPort.on("message", (message) => {
225
+ const result = nearest.handle(message);
226
+ parentPort.postMessage(
227
+ result,
228
+ result === true ? [] : [result.labels.buffer, result.bits.buffer],
229
+ );
230
+ });
231
+ }
232
+
233
+ // Spreads nearest-code jobs over worker threads, or runs them on this thread
234
+ // without any.
235
+ class Assigner extends WorkerGroup {
236
+ constructor(module, dims, blocks, groups, count) {
237
+ super(
238
+ new URL(import.meta.url),
239
+ count,
240
+ { codebook: true, module, dims, blocks, groups },
241
+ count ? undefined : new Nearest(module, dims, blocks, groups),
242
+ "SH clustering",
243
+ );
244
+ }
245
+
246
+ // Gives every thread the codes, or their groups, in a message of `type`;
247
+ // workers copy them from shared memory.
248
+ use(type, layout) {
249
+ return this.broadcast({ type, ...layout });
250
+ }
251
+
252
+ // Labels codes with their nearest group centers for grouping, the codes
253
+ // spread evenly over the threads.
254
+ group(work) {
255
+ const { threads } = this;
256
+ const each = Math.ceil(work.count / threads.length);
257
+ return Promise.all(
258
+ threads.map((worker, t) => {
259
+ const first = Math.min(t * each, work.count);
260
+ const count = Math.min(each, work.count - first);
261
+ return this.request(worker, { type: "group", ...work, first, count });
262
+ }),
263
+ );
264
+ }
265
+
266
+ // Runs `jobs` jobs, each from `read(index)`, and passes each with its
267
+ // labels to `consume` in job order. The buffers of a job travel to its
268
+ // thread and back. No more than WINDOW jobs per thread are read but not
269
+ // consumed: a slow job holds back the next instead of letting finished
270
+ // ones pile up. A thread has one job at a time, as a job takes far longer
271
+ // than its messages, so a slow thread holds back only that one. After a
272
+ // failure no more start.
273
+ run(jobs, read, consume) {
274
+ const { threads } = this;
275
+ const busy = threads.map(() => false);
276
+ const results = new Map();
277
+ let next = 0;
278
+ let done = 0;
279
+ let stopped = false;
280
+ return new Promise((resolve, reject) => {
281
+ const stop = (error) => {
282
+ if (!stopped) reject(error);
283
+ stopped = true;
284
+ };
285
+ const pump = () => {
286
+ if (stopped) return;
287
+ try {
288
+ for (; results.has(done); done++) {
289
+ consume(results.get(done));
290
+ results.delete(done);
291
+ }
292
+ if (done === jobs) {
293
+ stopped = true;
294
+ resolve();
295
+ return;
296
+ }
297
+ for (let t = 0; t < threads.length && next < jobs; t++) {
298
+ if (busy[t]) continue;
299
+ if (next - done >= WINDOW * threads.length) break;
300
+ const job = read(next++);
301
+ const { bits, labels, count } = job;
302
+ busy[t] = true;
303
+ this.request(threads[t], { bits, labels, count }, [
304
+ bits.buffer,
305
+ labels.buffer,
306
+ ]).then((result) => {
307
+ busy[t] = false;
308
+ job.bits = result.bits;
309
+ job.labels = result.labels;
310
+ results.set(job.index, job);
311
+ pump();
312
+ }, stop);
313
+ }
314
+ } catch (error) {
315
+ stop(error);
316
+ }
317
+ };
318
+ pump();
319
+ });
320
+ }
321
+ }
322
+
323
+ // Reads splats' SH as rows of half bits, the bands one after another, with
324
+ // their weights, area × opacity as merging weighs them.
325
+ class Rows {
326
+ constructor(splats, dims) {
327
+ this.splats = splats;
328
+ this.dims = dims;
329
+ this.length = splats.length;
330
+ this.column = new Uint16Array(JOB * SH_DIMS[SH_DIMS.length - 1]);
331
+ this.records = new Float32Array(JOB * 16);
332
+ }
333
+
334
+ read(base, count, bits, weights) {
335
+ const { splats, dims, column } = this;
336
+ let offset = 0;
337
+ for (let band = 0; band < splats.maxSh; band++) {
338
+ const size = SH_DIMS[band];
339
+ splats.sh[band].read(base * size, column, count * size);
340
+ for (let i = 0; i < count; i++)
341
+ for (let d = 0; d < size; d++)
342
+ bits[i * dims + offset + d] = column[i * size + d];
343
+ offset += size;
344
+ }
345
+ this.weigh(base, count, weights);
346
+ }
347
+
348
+ weigh(base, count, weights) {
349
+ const records = this.splats.records.read(
350
+ base * 16,
351
+ this.records,
352
+ count * 16,
353
+ );
354
+ for (let i = 0; i < count; i++)
355
+ weights[i] = records[i * 16 + 15] * records[i * 16 + 3];
356
+ }
357
+ }
358
+
359
+ // Rows and weights sampled from other rows.
360
+ class Sample {
361
+ constructor(workspace, dims) {
362
+ this.dims = dims;
363
+ this.length = 0;
364
+ this.bits = new PagedArray(workspace, Uint16Array);
365
+ this.weights = new PagedArray(workspace, Float64Array);
366
+ }
367
+
368
+ read(base, count, bits, weights) {
369
+ this.bits.read(base * this.dims, bits, count * this.dims);
370
+ this.weights.read(base, weights, count);
371
+ }
372
+
373
+ dispose() {
374
+ this.bits.dispose();
375
+ this.weights.dispose();
376
+ }
377
+ }
378
+
379
+ // The histogram bucket of a weight: its top 16 bits in single precision,
380
+ // which order nonnegative values.
381
+ const float = new Float32Array(1);
382
+ const word = new Uint32Array(float.buffer);
383
+ function bucket(weight) {
384
+ float[0] = weight;
385
+ return word[0] >>> 16;
386
+ }
387
+
388
+ // Samples about `size` rows, each with probability min(1, weight / step):
389
+ // rows weighing at least the step stay with their weights, and the others
390
+ // are taken where the running total of their weights crosses each multiple
391
+ // of the step, weighing one step. The weighted means of the sample then
392
+ // estimate those of all rows, also when a few rows hold most of the weight,
393
+ // as coarse LOD splats do. The step that makes about `size` rows follows
394
+ // from a histogram of the weights.
395
+ function sample(rows, size, workspace) {
396
+ const { dims, length } = rows;
397
+ const result = new Sample(workspace, dims);
398
+ const bits = new Uint16Array(JOB * dims);
399
+ const weights = new Float64Array(JOB);
400
+ const picked = new Uint16Array(JOB * dims);
401
+ const pickedWeights = new Float64Array(JOB);
402
+ const progress = new Progress("Sampling SH for clustering", 2 * length);
403
+ const counts = new Float64Array(32768);
404
+ const sums = new Float64Array(32768);
405
+ let total = 0;
406
+ for (let base = 0; base < length; base += JOB) {
407
+ const count = Math.min(JOB, length - base);
408
+ rows.weigh(base, count, weights);
409
+ for (let i = 0; i < count; i++) {
410
+ const b = bucket(weights[i]);
411
+ counts[b]++;
412
+ sums[b] += weights[i];
413
+ total += weights[i];
414
+ }
415
+ progress.update(base + count);
416
+ }
417
+ // The expected count falls as the step grows; at total / size it is at
418
+ // most `size`.
419
+ const expected = (step) => {
420
+ let sum = 0;
421
+ for (let b = 0; b < counts.length; b++)
422
+ if (counts[b])
423
+ sum += sums[b] >= step * counts[b] ? counts[b] : sums[b] / step;
424
+ return sum;
425
+ };
426
+ let low = 0;
427
+ let step = total / size;
428
+ for (let i = 0; i < 64; i++) {
429
+ const middle = (low + step) / 2;
430
+ if (expected(middle) > size) low = middle;
431
+ else step = middle;
432
+ }
433
+ let light = 0;
434
+ let next = step / 2;
435
+ for (let base = 0; base < length; base += JOB) {
436
+ const count = Math.min(JOB, length - base);
437
+ rows.read(base, count, bits, weights);
438
+ let n = 0;
439
+ for (let i = 0; i < count; i++) {
440
+ let w = weights[i];
441
+ if (w < step) {
442
+ light += w;
443
+ if (light <= next) continue;
444
+ next += step;
445
+ w = step;
446
+ }
447
+ for (let d = 0; d < dims; d++) picked[n * dims + d] = bits[i * dims + d];
448
+ pickedWeights[n++] = w;
449
+ }
450
+ result.bits.write(result.length * dims, picked, n * dims);
451
+ result.weights.write(result.length, pickedWeights, n);
452
+ result.length += n;
453
+ progress.update(length + base + count);
454
+ }
455
+ progress.finish(`${result.length} of ${length} splats`);
456
+ return result;
457
+ }
458
+
459
+ // Writes `codes` relative to `center` in the kernel's blocks of 8 and their
460
+ // squared lengths: position i holds code order[i]. Positions without a
461
+ // code, negative or past `order`, stay infinitely far.
462
+ function layout(codes, order, dims, center, blocks, norms) {
463
+ norms.fill(Number.POSITIVE_INFINITY);
464
+ for (let i = 0; i < order.length; i++) {
465
+ const c = order[i];
466
+ if (c < 0) continue;
467
+ const block = Math.floor(i / BLOCK) * dims * BLOCK + (i % BLOCK);
468
+ let norm = 0;
469
+ for (let d = 0; d < dims; d++) {
470
+ const value = Math.fround(codes[c * dims + d] - center[d]);
471
+ blocks[block + d * BLOCK] = value;
472
+ norm += value * value;
473
+ }
474
+ norms[i] = norm;
475
+ }
476
+ }
477
+
478
+ const shared = (Type, items) =>
479
+ new Type(new SharedArrayBuffer(items * Type.BYTES_PER_ELEMENT));
480
+ const identity = (count) => Int32Array.from({ length: count }, (_, i) => i);
481
+
482
+ // Groups codes for searches by group: k-means without weights, first from
483
+ // evenly spaced codes, then one step from the last groups, as codes move
484
+ // little between passes. Each step labels the codes with their nearest
485
+ // centers on the threads, then moves the centers to the means of their
486
+ // groups. Lays the codes out by group: the codes of each group fill whole
487
+ // blocks, and `order` maps positions to codes. Empty groups have infinitely
488
+ // far centers.
489
+ class Grouper {
490
+ constructor(count, dims, center) {
491
+ const blocks = Math.ceil(GROUPS / BLOCK);
492
+ this.count = count;
493
+ this.dims = dims;
494
+ this.center = center;
495
+ // The codes relative to the center, as points, and their groups.
496
+ this.points = shared(Float32Array, count * dims);
497
+ this.labels = shared(Uint16Array, count);
498
+ this.means = undefined;
499
+ this.sums = new Float64Array(GROUPS * dims);
500
+ this.sizes = new Uint32Array(GROUPS);
501
+ // Each group adds at most a block of fillers.
502
+ const slots = (Math.ceil(count / BLOCK) + GROUPS) * BLOCK;
503
+ this.layout = {
504
+ codes: shared(Float32Array, slots * dims),
505
+ norms: shared(Float32Array, slots),
506
+ centers: shared(Float32Array, blocks * BLOCK * dims),
507
+ centerNorms: shared(Float32Array, blocks * BLOCK),
508
+ center,
509
+ };
510
+ }
511
+
512
+ // Groups `codes` on the threads of `assigner` and returns their layout.
513
+ async group(codes, assigner) {
514
+ const { count, dims, center, points, labels, sums, sizes } = this;
515
+ const result = this.layout;
516
+ for (let c = 0; c < count; c++)
517
+ for (let d = 0; d < dims; d++)
518
+ points[c * dims + d] = codes[c * dims + d] - center[d];
519
+ const steps = this.means ? 1 : GROUP_STEPS;
520
+ if (!this.means) {
521
+ this.means = new Float64Array(GROUPS * dims);
522
+ for (let g = 0; g < GROUPS; g++) {
523
+ const c = Math.floor((g * count) / GROUPS);
524
+ this.means.set(codes.subarray(c * dims, (c + 1) * dims), g * dims);
525
+ }
526
+ }
527
+ const { means } = this;
528
+ for (let step = 0; step < steps; step++) {
529
+ layout(
530
+ means,
531
+ identity(GROUPS),
532
+ dims,
533
+ center,
534
+ result.centers,
535
+ result.centerNorms,
536
+ );
537
+ await assigner.group({
538
+ points,
539
+ centers: result.centers,
540
+ norms: result.centerNorms,
541
+ labels,
542
+ count,
543
+ });
544
+ sums.fill(0);
545
+ sizes.fill(0);
546
+ for (let c = 0; c < count; c++) {
547
+ const g = labels[c];
548
+ sizes[g]++;
549
+ for (let d = 0; d < dims; d++)
550
+ sums[g * dims + d] += codes[c * dims + d];
551
+ }
552
+ for (let g = 0; g < GROUPS; g++)
553
+ if (sizes[g])
554
+ for (let d = 0; d < dims; d++)
555
+ means[g * dims + d] = sums[g * dims + d] / sizes[g];
556
+ }
557
+ const starts = new Uint32Array(GROUPS);
558
+ const groupBlocks = new Uint32Array(GROUPS);
559
+ let slots = 0;
560
+ for (let g = 0; g < GROUPS; g++) {
561
+ starts[g] = slots / BLOCK;
562
+ groupBlocks[g] = Math.ceil(sizes[g] / BLOCK);
563
+ slots += groupBlocks[g] * BLOCK;
564
+ }
565
+ const order = new Int32Array(slots).fill(-1);
566
+ const filled = new Uint32Array(GROUPS);
567
+ for (let c = 0; c < count; c++) {
568
+ const g = labels[c];
569
+ order[starts[g] * BLOCK + filled[g]++] = c;
570
+ }
571
+ layout(codes, order, dims, center, result.codes, result.norms);
572
+ const nonempty = Int32Array.from({ length: GROUPS }, (_, g) =>
573
+ sizes[g] ? g : -1,
574
+ );
575
+ layout(means, nonempty, dims, center, result.centers, result.centerNorms);
576
+ return {
577
+ ...result,
578
+ starts,
579
+ sizes: groupBlocks,
580
+ // Positions that hold no code are never nearest.
581
+ order: Uint16Array.from(order, (c) => Math.max(c, 0)),
582
+ };
583
+ }
584
+ }
585
+
586
+ // Clusters the splats' SH into a codebook of at most 65536 entries with
587
+ // weighted k-means as Spark does: entries start at evenly spaced splats,
588
+ // each of `iterations` moves them to the weighted mean of their splats, an
589
+ // entry without any stays, and a final pass labels each splat with its
590
+ // nearest entry. Weights are area × opacity. Training uses 5% of the
591
+ // splats, at least 16 per entry, sampled in proportion to their weights;
592
+ // with many entries, passes search only groups of them. Returns the
593
+ // entries, `count` rows of `dims` SH coefficients, and the labels.
594
+ export async function clusterSh(splats, iterations, module) {
595
+ const { workspace, length } = splats;
596
+ const dims = SH_DIMS.slice(0, splats.maxSh).reduce((a, b) => a + b, 0);
597
+ const count = Math.min(length, MAX_CODES);
598
+ const all = new Rows(splats, dims);
599
+ const samples = Math.max(
600
+ count * SAMPLES_PER_CODE,
601
+ Math.round(length * SAMPLE_SHARE),
602
+ );
603
+ const sampled = length > samples;
604
+ const training = sampled ? sample(all, samples, workspace) : all;
605
+ const labels = new PagedArray(workspace, Uint16Array);
606
+ let assigner;
607
+ try {
608
+ const blocks = Math.ceil(count / BLOCK);
609
+ const groups = count >= GROUPS * 32 ? GROUPS : 0;
610
+ // Jobs are small enough for every thread to take several.
611
+ const size = Math.max(
612
+ 4,
613
+ Math.min(
614
+ JOB,
615
+ Math.ceil(training.length / (availableParallelism() * 8) / 4) * 4,
616
+ ),
617
+ );
618
+ // Each thread's kernel and the buffers of its jobs: half bits, labels
619
+ // and weights of their rows.
620
+ const scratch = groups
621
+ ? new WebAssembly.Instance(module, {}).exports.probed_scratch(
622
+ BATCH,
623
+ dims,
624
+ groups,
625
+ FINAL_PROBES,
626
+ )
627
+ : 0;
628
+ const bytes =
629
+ kernelBytes(dims, blocks, groups, scratch) +
630
+ WINDOW * size * (dims * 2 + 10);
631
+ // This thread's arrays, its own kernel and jobs when there is no room for
632
+ // workers, and the layout of the codes; for groups, the codes as points
633
+ // with their groups, the groups' means and sums, and the grouped layout
634
+ // with its order.
635
+ const slots = (blocks + groups) * BLOCK;
636
+ const extra =
637
+ count * (dims * 16 + 8) +
638
+ bytes +
639
+ (groups
640
+ ? count * (dims * 4 + 2) +
641
+ groups * dims * 16 +
642
+ (slots + groups) * (dims + 1) * 4 +
643
+ slots * 6
644
+ : blocks * BLOCK * (dims + 1) * 4);
645
+ // Reserve before allocating the codes and accumulators, so their memory
646
+ // is not already in the RSS measurement that clusterers() adds to.
647
+ const threads = clusterers(
648
+ workspace,
649
+ bytes,
650
+ extra,
651
+ Math.ceil(training.length / size),
652
+ );
653
+ const codes = new Float64Array(count * dims);
654
+ const sums = new Float64Array(count * dims);
655
+ const totals = new Float64Array(count);
656
+ const bits = new Uint16Array(dims);
657
+ const weight = new Float64Array(1);
658
+ for (let c = 0; c < count; c++) {
659
+ // Spark computes the seed rows in single precision.
660
+ const row = Math.floor(
661
+ Math.fround(Math.fround(c / count) * Math.fround(training.length)),
662
+ );
663
+ training.read(Math.min(row, training.length - 1), 1, bits, weight);
664
+ for (let d = 0; d < dims; d++) codes[c * dims + d] = HALF[bits[d]];
665
+ }
666
+ // Points and codes are taken relative to the seeds' mean: the expanded
667
+ // distances then round with the spread of the coefficients, not their
668
+ // offset from zero.
669
+ const center = new Float64Array(dims);
670
+ for (let c = 0; c < count; c++)
671
+ for (let d = 0; d < dims; d++) center[d] += codes[c * dims + d];
672
+ for (let d = 0; d < dims; d++) center[d] /= count;
673
+ assigner = new Assigner(module, dims, blocks, groups, threads);
674
+ const grouper = groups && new Grouper(count, dims, center);
675
+ const order = !groups && identity(count);
676
+ const blocked = !groups && shared(Float32Array, blocks * BLOCK * dims);
677
+ const norms = !groups && shared(Float32Array, blocks * BLOCK);
678
+ // Jobs come back with their buffers, which are reused.
679
+ const free = [];
680
+ const take = (rows, index) => {
681
+ workspace.check();
682
+ const job = free.pop() ?? {
683
+ bits: new Uint16Array(size * dims),
684
+ labels: new Uint16Array(size),
685
+ weights: new Float64Array(size),
686
+ };
687
+ job.index = index;
688
+ job.base = index * size;
689
+ job.count = Math.min(size, rows.length - job.base);
690
+ rows.read(job.base, job.count, job.bits, job.weights);
691
+ return job;
692
+ };
693
+ for (let pass = 0; pass <= iterations; pass++) {
694
+ const last = pass === iterations;
695
+ const rows = last ? all : training;
696
+ if (grouper)
697
+ await assigner.use("groups", {
698
+ ...(await grouper.group(codes, assigner)),
699
+ probes: last ? FINAL_PROBES : TRAIN_PROBES,
700
+ });
701
+ else {
702
+ layout(codes, order, dims, center, blocked, norms);
703
+ await assigner.use("codes", { codes: blocked, norms, center });
704
+ }
705
+ if (!last) {
706
+ sums.fill(0);
707
+ totals.fill(0);
708
+ }
709
+ let total = 0;
710
+ let distance = 0;
711
+ const progress = new Progress(
712
+ `Clustering SH ${pass + 1}/${iterations + 1}`,
713
+ rows.length,
714
+ );
715
+ await assigner.run(
716
+ Math.ceil(rows.length / size),
717
+ (index) => take(rows, index),
718
+ (done) => {
719
+ const { bits, weights, labels: nearest } = done;
720
+ for (let i = 0; i < done.count; i++) {
721
+ const c = nearest[i];
722
+ const w = weights[i];
723
+ let squared = 0;
724
+ for (let d = 0; d < dims; d++) {
725
+ const value = HALF[bits[i * dims + d]];
726
+ const offset = value - codes[c * dims + d];
727
+ squared += offset * offset;
728
+ if (!last) sums[c * dims + d] += w * value;
729
+ }
730
+ total += w;
731
+ if (!last) totals[c] += w;
732
+ distance += w * squared;
733
+ }
734
+ if (last) labels.write(done.base, nearest, done.count);
735
+ free.push(done);
736
+ progress.update(done.base + done.count);
737
+ },
738
+ );
739
+ progress.finish(
740
+ `mean squared distance ${(distance / total).toExponential(3)}`,
741
+ );
742
+ if (last) break;
743
+ for (let c = 0; c < count; c++)
744
+ if (totals[c] > 0)
745
+ for (let d = 0; d < dims; d++)
746
+ codes[c * dims + d] = sums[c * dims + d] / totals[c];
747
+ }
748
+ return { count, dims, codes, labels };
749
+ } catch (error) {
750
+ labels.dispose();
751
+ throw error;
752
+ } finally {
753
+ await assigner?.close();
754
+ if (sampled) training.dispose();
755
+ }
756
+ }