gaussian-splat-lite 1.3.2 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +36 -2
  2. package/CONTRIBUTING.md +14 -2
  3. package/NOTICE +11 -3
  4. package/README.md +5 -7
  5. package/THIRD_PARTY_LICENSES.md +64 -6
  6. package/bin/build-lod.js +241 -0
  7. package/bin/decoder/decoder.js +340 -0
  8. package/bin/decoder/decoder_bg.wasm +0 -0
  9. package/bin/rad/grid.js +620 -0
  10. package/bin/rad/heap.js +270 -0
  11. package/bin/rad/input.js +87 -0
  12. package/bin/rad/lod.js +657 -0
  13. package/bin/rad/math.js +166 -0
  14. package/bin/rad/parallel.js +578 -0
  15. package/bin/rad/progress.js +50 -0
  16. package/bin/rad/rad.js +612 -0
  17. package/bin/rad/splats.js +432 -0
  18. package/bin/rad/storage.js +776 -0
  19. package/dist/gaussian-splat-lite.cjs +392 -794
  20. package/dist/gaussian-splat-lite.cjs.map +1 -1
  21. package/dist/gaussian-splat-lite.min.cjs +246 -308
  22. package/dist/gaussian-splat-lite.min.cjs.map +1 -1
  23. package/dist/gaussian-splat-lite.module.js +394 -796
  24. package/dist/gaussian-splat-lite.module.js.map +1 -1
  25. package/dist/gaussian-splat-lite.module.min.js +2518 -2802
  26. package/dist/gaussian-splat-lite.module.min.js.map +1 -1
  27. package/dist/types/addons/NeuralDenoiseNode.d.cts +1 -2
  28. package/dist/types/addons/NeuralDenoiseNode.d.ts +1 -2
  29. package/dist/types/addons/NeuralDenoiseXR.d.cts +31 -0
  30. package/dist/types/addons/NeuralDenoiseXR.d.ts +31 -0
  31. package/dist/types/addons/neuralDenoiseState.d.cts +22 -0
  32. package/dist/types/addons/neuralDenoiseState.d.ts +22 -0
  33. package/dist/types/addons/neuralDenoiseWeights.d.cts +41 -2
  34. package/dist/types/addons/neuralDenoiseWeights.d.ts +41 -2
  35. package/dist/types/data/RadPagedSplats.d.cts +1 -1
  36. package/dist/types/data/RadPagedSplats.d.ts +1 -1
  37. package/dist/types/index.d.cts +1 -2
  38. package/dist/types/index.d.ts +1 -2
  39. package/dist/types/loaders/rad/radFormat.d.cts +2 -2
  40. package/dist/types/loaders/rad/radFormat.d.ts +2 -2
  41. package/docs/BuildRad.md +91 -0
  42. package/docs/GaussianSplatRenderer.md +2 -2
  43. package/docs/NeuralDenoiseNode.md +32 -13
  44. package/docs/RadStreamScheduler.md +3 -1
  45. package/docs/StochasticRendering.md +20 -42
  46. package/package.json +9 -3
  47. package/dist/types/addons/TAANode.d.cts +0 -49
  48. package/dist/types/addons/TAANode.d.ts +0 -49
  49. package/dist/types/addons/TAAPass.d.cts +0 -57
  50. package/dist/types/addons/TAAPass.d.ts +0 -57
  51. package/dist/types/addons/taaShared.d.cts +0 -74
  52. package/dist/types/addons/taaShared.d.ts +0 -74
  53. package/docs/TAAPass.md +0 -184
package/bin/rad/rad.js ADDED
@@ -0,0 +1,612 @@
1
+ // RAD v1 encoding adapted from MIT-licensed code. See THIRD_PARTY_LICENSES.md.
2
+ import { randomBytes } from "node:crypto";
3
+ import {
4
+ closeSync,
5
+ fsyncSync,
6
+ lstatSync,
7
+ openSync,
8
+ readdirSync,
9
+ renameSync,
10
+ statSync,
11
+ } from "node:fs";
12
+ import path from "node:path";
13
+ import {
14
+ Worker,
15
+ isMainThread,
16
+ parentPort,
17
+ workerData,
18
+ } from "node:worker_threads";
19
+ import { deflateRawSync } from "node:zlib";
20
+ import { HALF, half, halfBits, opacityRadius } from "./math.js";
21
+ import { YOUNG_MB, encoders } from "./parallel.js";
22
+ import { Progress } from "./progress.js";
23
+ import { SH_DIMS } from "./splats.js";
24
+ import { readAll, removePath, writeAll } from "./storage.js";
25
+
26
+ const CHUNK = 65536;
27
+ function header(magic, meta) {
28
+ const json = Buffer.from(JSON.stringify(meta));
29
+ const result = Buffer.alloc(8 + Math.ceil(json.length / 8) * 8);
30
+ result.write(magic);
31
+ result.writeUInt32LE(json.length, 4);
32
+ json.copy(result, 8);
33
+ return result;
34
+ }
35
+ function exists(file) {
36
+ try {
37
+ lstatSync(file);
38
+ return true;
39
+ } catch (error) {
40
+ if (error.code === "ENOENT") return false;
41
+ throw error;
42
+ }
43
+ }
44
+ function chunkStem(output) {
45
+ const stem = path.basename(output, path.extname(output));
46
+ // Keep the on-disk name usable as a relative URL and a resolveFile key.
47
+ // Escape '=' too so distinct stems cannot produce the same chunk names.
48
+ return Array.from(stem, (char) =>
49
+ char <= " " || "\\%#?:=".includes(char)
50
+ ? encodeURIComponent(char).replace("%", "=")
51
+ : char,
52
+ ).join("");
53
+ }
54
+ export function checkOutput(output, chunked) {
55
+ if (path.extname(output).toLowerCase() !== ".rad")
56
+ throw new Error("Output must have a .rad extension");
57
+ if (exists(output)) throw new Error(`Output already exists: ${output}`);
58
+ if (!statSync(path.dirname(output)).isDirectory())
59
+ throw new Error("Output parent is not a directory");
60
+ if (chunked) {
61
+ const prefix = `${chunkStem(output)}-`;
62
+ for (const name of readdirSync(path.dirname(output))) {
63
+ if (
64
+ name.startsWith(prefix) &&
65
+ /^\d+\.radc$/.test(name.slice(prefix.length))
66
+ )
67
+ throw new Error(`Output chunk already exists: ${name}`);
68
+ }
69
+ }
70
+ }
71
+ class Histogram {
72
+ constructor() {
73
+ this.counts = new Float64Array(65536);
74
+ this.count = 0;
75
+ }
76
+ // Adds the counts of a Histogram, also one copied from a worker.
77
+ merge(other) {
78
+ for (let i = 0; i < 65536; i++) this.counts[i] += other.counts[i];
79
+ this.count += other.count;
80
+ }
81
+ add(bits) {
82
+ this.counts[bits & 32768 ? bits ^ 65535 : bits ^ 32768]++;
83
+ this.count++;
84
+ }
85
+ percentile(fraction) {
86
+ let rank = Math.min(
87
+ this.count - 1,
88
+ Math.round(Math.fround(Math.fround(this.count) * Math.fround(fraction))),
89
+ );
90
+ for (let i = 0; i < 65536; i++) {
91
+ if (rank < this.counts[i]) return HALF[i & 32768 ? i ^ 32768 : i ^ 65535];
92
+ rank -= this.counts[i];
93
+ }
94
+ throw new Error("Empty encoding histogram");
95
+ }
96
+ }
97
+ function alpha(value) {
98
+ return value > 1
99
+ ? half(Math.min(2, 1 + 0.25 * (opacityRadius(value) - 1)))
100
+ : value;
101
+ }
102
+ // What the encoding ranges depend on, counted from gathered chunks.
103
+ class Counts {
104
+ constructor() {
105
+ this.colors = new Histogram();
106
+ this.scales = new Histogram();
107
+ this.sh = SH_DIMS.map(() => new Histogram());
108
+ this.alphaF16 = false;
109
+ }
110
+ add(chunk) {
111
+ const { records, count } = chunk;
112
+ for (let i = 0; i < count; i++) {
113
+ const offset = i * 16;
114
+ this.alphaF16 ||= alpha(records[offset + 3]) > 1;
115
+ for (let d = 4; d <= 6; d++)
116
+ this.colors.add(halfBits(records[offset + d]));
117
+ const a = records[offset + 7];
118
+ const b = records[offset + 8];
119
+ const c = records[offset + 9];
120
+ this.scales.add(
121
+ halfBits(Math.max(Math.min(a, b), Math.min(Math.max(a, b), c))),
122
+ );
123
+ this.scales.add(halfBits(Math.max(a, b, c)));
124
+ }
125
+ for (let band = 0; band < chunk.sh.length; band++)
126
+ for (const bits of chunk.sh[band]) this.sh[band].add(bits);
127
+ }
128
+ merge(other) {
129
+ this.colors.merge(other.colors);
130
+ this.scales.merge(other.scales);
131
+ for (let band = 0; band < SH_DIMS.length; band++)
132
+ this.sh[band].merge(other.sh[band]);
133
+ this.alphaF16 ||= other.alphaF16;
134
+ }
135
+ }
136
+
137
+ function encoding(counts, maxSh) {
138
+ const { colors, scales } = counts;
139
+ const rgbMin = Math.min(0, colors.percentile(0.01));
140
+ const rgbMax = Math.max(1, colors.percentile(0.99));
141
+ const lnScaleMin = Math.min(
142
+ -12,
143
+ Math.max(Math.log(1e-30), scales.percentile(0.01)),
144
+ );
145
+ const lnScaleMax = Math.max(9, scales.percentile(0.99));
146
+ const limits = [1, 1, 1];
147
+ for (let band = 0; band < maxSh; band++) {
148
+ const histogram = counts.sh[band];
149
+ limits[band] = Math.max(
150
+ 1,
151
+ Math.abs(histogram.percentile(0.05)),
152
+ Math.abs(histogram.percentile(0.95)),
153
+ );
154
+ }
155
+ // The s8 code of each half value, per band.
156
+ const sh = limits.map((limit) => {
157
+ const codes = new Uint8Array(65536);
158
+ for (let bits = 0; bits < 65536; bits++)
159
+ codes[bits] =
160
+ signedRound(Math.max(-127, Math.min(127, (HALF[bits] / limit) * 127))) &
161
+ 255;
162
+ return codes;
163
+ });
164
+ return {
165
+ alphaF16: counts.alphaF16,
166
+ rgbF16: rgbMin < -1 || rgbMax > 2,
167
+ sh,
168
+ scaleF16: lnScaleMax - lnScaleMin > 25,
169
+ limits,
170
+ meta: {
171
+ rgbMin,
172
+ rgbMax,
173
+ lnScaleMin,
174
+ lnScaleMax,
175
+ sh1Max: limits[0],
176
+ sh2Max: limits[1],
177
+ sh3Max: limits[2],
178
+ lodOpacity: true,
179
+ },
180
+ };
181
+ }
182
+ const unorm = (value, min, max) =>
183
+ Math.round(Math.max(0, Math.min(255, ((value - min) / (max - min)) * 255)));
184
+ const signedRound = (value) =>
185
+ value < 0 ? -Math.round(-value) : Math.round(value);
186
+
187
+ // The columns of the splats from `base` on that encodeChunk() reads, and
188
+ // Counts without the children.
189
+ function gather(splats, base, count, children = true) {
190
+ return {
191
+ base,
192
+ count,
193
+ records: splats.records.read(base * 16, new Float32Array(count * 16)),
194
+ sh: splats.sh
195
+ .slice(0, splats.maxSh)
196
+ .map((column, band) =>
197
+ column.read(
198
+ base * SH_DIMS[band],
199
+ new Uint16Array(count * SH_DIMS[band]),
200
+ ),
201
+ ),
202
+ ...(children && {
203
+ counts: splats.childCount.read(base, new Uint32Array(count)),
204
+ starts: splats.childStart.read(base, new Float64Array(count)),
205
+ }),
206
+ };
207
+ }
208
+
209
+ // Raw property bytes, reused: each property is compressed before the next.
210
+ const scratch = [Buffer.alloc(0), Buffer.alloc(0)];
211
+ function raw(size, slot = 0) {
212
+ if (scratch[slot].length < size) scratch[slot] = Buffer.allocUnsafe(size);
213
+ return scratch[slot].subarray(0, size);
214
+ }
215
+
216
+ function encodeChunk(chunk, encoding) {
217
+ const { base, count, records } = chunk;
218
+ const properties = [];
219
+ const payloads = [];
220
+ let payloadBytes = 0;
221
+ // Compresses a property's bytes and appends them, padded to 8 bytes.
222
+ const property = (name, codec, data, range) => {
223
+ const bytes = deflateRawSync(data, { level: 6 });
224
+ const padding = Buffer.alloc((8 - (bytes.length % 8)) % 8);
225
+ properties.push({
226
+ property: name,
227
+ encoding: codec,
228
+ compression: "gz",
229
+ ...(range && { min: range[0], max: range[1] }),
230
+ offset: payloadBytes,
231
+ bytes: bytes.length,
232
+ });
233
+ payloads.push(bytes, padding);
234
+ payloadBytes += bytes.length + padding.length;
235
+ };
236
+ const input = new Uint8Array(records.buffer);
237
+ let bytes = raw(count * 12);
238
+ for (let b = 0; b < 4; b++)
239
+ for (let d = 0; d < 3; d++)
240
+ for (let i = 0; i < count; i++)
241
+ bytes[(b * 3 + d) * count + i] = input[i * 64 + d * 4 + b];
242
+ property("center", "f32_lebytes", bytes);
243
+ bytes = raw(count * (encoding.alphaF16 ? 2 : 1));
244
+ for (let i = 0; i < count; i++) {
245
+ const value = alpha(records[i * 16 + 3]);
246
+ if (encoding.alphaF16) bytes.writeUInt16LE(halfBits(value), i * 2);
247
+ else bytes[i] = unorm(value, 0, 2);
248
+ }
249
+ property(
250
+ "alpha",
251
+ encoding.alphaF16 ? "f16" : "r8",
252
+ bytes,
253
+ encoding.alphaF16 ? undefined : [0, 2],
254
+ );
255
+ bytes = raw(count * 3 * (encoding.rgbF16 ? 2 : 1));
256
+ for (let d = 0; d < 3; d++) {
257
+ let last = 0;
258
+ for (let i = 0; i < count; i++) {
259
+ const value = records[i * 16 + 4 + d];
260
+ const offset = d * count + i;
261
+ if (encoding.rgbF16) bytes.writeUInt16LE(halfBits(value), offset * 2);
262
+ else {
263
+ const code = unorm(value, encoding.meta.rgbMin, encoding.meta.rgbMax);
264
+ bytes[offset] = (code - last) & 255;
265
+ last = code;
266
+ }
267
+ }
268
+ }
269
+ property(
270
+ "rgb",
271
+ encoding.rgbF16 ? "f16" : "r8_delta",
272
+ bytes,
273
+ encoding.rgbF16 ? undefined : [encoding.meta.rgbMin, encoding.meta.rgbMax],
274
+ );
275
+ bytes = raw(count * 3 * (encoding.scaleF16 ? 2 : 1));
276
+ for (let d = 0; d < 3; d++)
277
+ for (let i = 0; i < count; i++) {
278
+ const value = records[i * 16 + 7 + d];
279
+ const offset = d * count + i;
280
+ if (encoding.scaleF16) bytes.writeUInt16LE(halfBits(value), offset * 2);
281
+ else
282
+ bytes[offset] =
283
+ value <= -30
284
+ ? 0
285
+ : 1 +
286
+ Math.round(
287
+ Math.max(
288
+ 0,
289
+ Math.min(
290
+ 254,
291
+ ((value - encoding.meta.lnScaleMin) /
292
+ (encoding.meta.lnScaleMax - encoding.meta.lnScaleMin)) *
293
+ 254,
294
+ ),
295
+ ),
296
+ );
297
+ }
298
+ property(
299
+ "scales",
300
+ encoding.scaleF16 ? "ln_f16" : "ln_0r8",
301
+ bytes,
302
+ encoding.scaleF16
303
+ ? undefined
304
+ : [encoding.meta.lnScaleMin, encoding.meta.lnScaleMax],
305
+ );
306
+ bytes = raw(count * 3);
307
+ for (let i = 0; i < count; i++) {
308
+ const offset = i * 16;
309
+ const sign = records[offset + 13] < 0 ? -1 : 1;
310
+ const theta =
311
+ 2 * Math.acos(Math.max(0, Math.min(1, records[offset + 13] * sign)));
312
+ const s = Math.sin(theta * 0.5);
313
+ let x = Math.abs(s) < 1e-6 ? 1 : (records[offset + 10] * sign) / s;
314
+ let y = Math.abs(s) < 1e-6 ? 0 : (records[offset + 11] * sign) / s;
315
+ const z = Math.abs(s) < 1e-6 ? 0 : (records[offset + 12] * sign) / s;
316
+ const sum = Math.abs(x) + Math.abs(y) + Math.abs(z);
317
+ x /= sum;
318
+ y /= sum;
319
+ if (z < 0) {
320
+ const old = x;
321
+ x = (1 - Math.abs(y)) * (x >= 0 ? 1 : -1);
322
+ y = (1 - Math.abs(old)) * (y >= 0 ? 1 : -1);
323
+ }
324
+ bytes[i * 3] = unorm(x, -1, 1);
325
+ bytes[i * 3 + 1] = unorm(y, -1, 1);
326
+ bytes[i * 3 + 2] = unorm(theta, 0, Math.PI);
327
+ }
328
+ property("orientation", "oct88r8", bytes);
329
+ for (let band = 0; band < chunk.sh.length; band++) {
330
+ const dims = SH_DIMS[band];
331
+ const values = chunk.sh[band];
332
+ const codes = encoding.sh[band];
333
+ bytes = raw(count * dims);
334
+ for (let d = 0; d < dims; d++)
335
+ for (let i = 0; i < count; i++)
336
+ bytes[d * count + i] = codes[values[i * dims + d]];
337
+ property(`sh${band + 1}`, "s8", bytes, [
338
+ -encoding.limits[band],
339
+ encoding.limits[band],
340
+ ]);
341
+ }
342
+ const counts = raw(count * 2);
343
+ const starts = raw(count * 4, 1);
344
+ for (let i = 0; i < count; i++) {
345
+ const children = chunk.counts[i];
346
+ counts.writeUInt16LE(children, i * 2);
347
+ starts.writeUInt32LE(children ? chunk.starts[i] : 0, i * 4);
348
+ }
349
+ property("child_count", "u16", counts);
350
+ property("child_start", "u32", starts);
351
+ const meta = header("RADC", {
352
+ version: 1,
353
+ base,
354
+ count,
355
+ maxSh: chunk.sh.length,
356
+ lodTree: true,
357
+ splatEncoding: encoding.meta,
358
+ payloadBytes,
359
+ properties,
360
+ });
361
+ const size = Buffer.alloc(8);
362
+ size.writeBigUInt64LE(BigInt(payloadBytes));
363
+ return Buffer.concat([meta, size, ...payloads]);
364
+ }
365
+
366
+ // Counts and encodes chunks; in workers, each answers in order.
367
+ class Coder {
368
+ constructor() {
369
+ this.counts = new Counts();
370
+ this.format = undefined;
371
+ }
372
+ handle({ type, chunk, format }) {
373
+ if (type === "count") this.counts.add(chunk);
374
+ else if (type === "counts") return this.counts;
375
+ else if (type === "format") this.format = format;
376
+ else return encodeChunk(chunk, this.format);
377
+ return true;
378
+ }
379
+ }
380
+
381
+ // Spreads counting and encoding over worker threads.
382
+ class Encoder {
383
+ // Encodes `chunks` with workers when there is more than one and the
384
+ // workspace has room for them.
385
+ constructor(workspace, chunks) {
386
+ this.next = 0;
387
+ this.coder = new Coder();
388
+ this.workers = Array.from({ length: encoders(workspace, chunks) }, () => {
389
+ const worker = new Worker(new URL(import.meta.url), {
390
+ workerData: { coder: true },
391
+ resourceLimits: {
392
+ maxOldGenerationSizeMb: 64,
393
+ maxYoungGenerationSizeMb: YOUNG_MB,
394
+ },
395
+ });
396
+ worker.unref();
397
+ worker.waiting = [];
398
+ worker.on("message", (result) => worker.waiting.shift().resolve(result));
399
+ // A failed worker fails its pending and later requests.
400
+ const fail = (error) => {
401
+ worker.failed ??= error;
402
+ for (const job of worker.waiting.splice(0)) job.reject(worker.failed);
403
+ };
404
+ worker.on("error", fail);
405
+ worker.on("exit", (code) =>
406
+ fail(new Error(`Encoding worker exited (${code})`)),
407
+ );
408
+ return worker;
409
+ });
410
+ }
411
+ get size() {
412
+ return this.workers.length;
413
+ }
414
+ request(worker, message, transfer = []) {
415
+ if (!worker) return Promise.resolve(this.coder.handle(message));
416
+ if (worker.failed) return Promise.reject(worker.failed);
417
+ return new Promise((resolve, reject) => {
418
+ worker.waiting.push({ resolve, reject });
419
+ worker.postMessage(message, transfer);
420
+ });
421
+ }
422
+ // Sends a gathered chunk to the next worker.
423
+ send(type, chunk) {
424
+ return this.request(
425
+ this.workers[this.next++ % this.workers.length],
426
+ { type, chunk },
427
+ [
428
+ chunk.records.buffer,
429
+ ...chunk.sh.map((values) => values.buffer),
430
+ ...(chunk.counts ? [chunk.counts.buffer, chunk.starts.buffer] : []),
431
+ ],
432
+ );
433
+ }
434
+ // Sends `message` to every worker, or to this thread's coder without any.
435
+ broadcast(message) {
436
+ const all = this.workers.length ? this.workers : [undefined];
437
+ return Promise.all(all.map((worker) => this.request(worker, message)));
438
+ }
439
+ async counts() {
440
+ const counts = new Counts();
441
+ for (const result of await this.broadcast({ type: "counts" }))
442
+ counts.merge(result);
443
+ return counts;
444
+ }
445
+ use(format) {
446
+ return this.broadcast({ type: "format", format });
447
+ }
448
+ async encode(chunk) {
449
+ const bytes = await this.send("encode", chunk);
450
+ return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.length);
451
+ }
452
+ // Resolves once the workers have stopped and their memory is free.
453
+ close() {
454
+ return Promise.all(this.workers.map((worker) => worker.terminate()));
455
+ }
456
+ }
457
+
458
+ if (!isMainThread && workerData?.coder) {
459
+ const coder = new Coder();
460
+ parentPort.on("message", (message) => {
461
+ const result = coder.handle(message);
462
+ // The chunk's buffers would otherwise pile up to tens of MiB before the
463
+ // collector frees them.
464
+ gc();
465
+ // A chunk with its own memory moves to the main thread without a copy.
466
+ const own =
467
+ result instanceof Uint8Array &&
468
+ result.byteOffset === 0 &&
469
+ result.byteLength === result.buffer.byteLength;
470
+ parentPort.postMessage(result, own ? [result.buffer] : []);
471
+ });
472
+ }
473
+
474
+ export async function writeRad(splats, output, chunked, lodBase) {
475
+ const chunks = [];
476
+ const stem = chunkStem(output);
477
+ const spool = chunked ? undefined : splats.workspace.file();
478
+ // Write every file completely before publishing chunks, then the header.
479
+ // Track published names too, so cancellation rolls back this conversion.
480
+ const staged = [];
481
+ const created = [];
482
+ const stage = (file, write) => {
483
+ splats.workspace.check();
484
+ const temporary = path.join(
485
+ path.dirname(file),
486
+ `.${path.basename(file)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`,
487
+ );
488
+ const fd = openSync(temporary, "wx");
489
+ staged.push({ temporary, file });
490
+ try {
491
+ write(fd);
492
+ fsyncSync(fd);
493
+ } catch (error) {
494
+ try {
495
+ closeSync(fd);
496
+ } catch {}
497
+ throw error;
498
+ }
499
+ closeSync(fd);
500
+ };
501
+ let total = 0;
502
+ let progress;
503
+ const store = (bytes) => {
504
+ if (chunked) {
505
+ const filename = `${stem}-${chunks.length}.radc`;
506
+ stage(path.join(path.dirname(output), filename), (fd) =>
507
+ writeAll(fd, bytes, 0),
508
+ );
509
+ chunks.push({ offset: 0, bytes: bytes.length, filename });
510
+ } else {
511
+ writeAll(spool, bytes, total);
512
+ chunks.push({ offset: total, bytes: bytes.length });
513
+ }
514
+ total += bytes.length;
515
+ progress.update(chunks.length);
516
+ };
517
+ // Gather the next chunks while workers encode earlier ones; store them in
518
+ // order. A chunk's failure is thrown when its turn comes, not as an
519
+ // unhandled rejection while an earlier chunk is awaited.
520
+ const pending = [];
521
+ const encoder = new Encoder(
522
+ splats.workspace,
523
+ Math.ceil(splats.length / CHUNK),
524
+ );
525
+ try {
526
+ // Count the encoding ranges in a first pass.
527
+ const chunkCount = Math.ceil(splats.length / CHUNK);
528
+ progress = new Progress("Analyzing RAD encoding", chunkCount);
529
+ let countedChunks = 0;
530
+ for (let base = 0; base < splats.length; base += CHUNK) {
531
+ splats.workspace.check();
532
+ const counted = encoder
533
+ .send(
534
+ "count",
535
+ gather(splats, base, Math.min(CHUNK, splats.length - base), false),
536
+ )
537
+ .then(() => progress.update(++countedChunks));
538
+ counted.catch(() => {});
539
+ pending.push(counted);
540
+ if (pending.length > encoder.size + 1) await pending.shift();
541
+ }
542
+ while (pending.length) await pending.shift();
543
+ const format = encoding(await encoder.counts(), splats.maxSh);
544
+ await encoder.use(format);
545
+ progress.finish();
546
+ progress = new Progress("Encoding RAD chunks", chunkCount);
547
+ for (let base = 0; base < splats.length; base += CHUNK) {
548
+ splats.workspace.check();
549
+ const bytes = encoder.encode(
550
+ gather(splats, base, Math.min(CHUNK, splats.length - base)),
551
+ );
552
+ bytes.catch(() => {});
553
+ pending.push(bytes);
554
+ if (pending.length > encoder.size + 1) store(await pending.shift());
555
+ }
556
+ while (pending.length) store(await pending.shift());
557
+ progress.finish(`${chunks.length} chunks`);
558
+ const meta = header("RAD0", {
559
+ version: 1,
560
+ type: "gsplat",
561
+ count: splats.length,
562
+ maxSh: splats.maxSh,
563
+ lodTree: true,
564
+ chunkSize: CHUNK,
565
+ allChunkBytes: total,
566
+ chunks,
567
+ splatEncoding: format.meta,
568
+ comment: `Gaussian Splat Lite quality LOD (base ${lodBase})`,
569
+ });
570
+ const writing = new Progress(
571
+ "Writing RAD file",
572
+ meta.length + (spool === undefined ? 0 : total),
573
+ );
574
+ stage(output, (fd) => {
575
+ writeAll(fd, meta, 0);
576
+ if (spool !== undefined) {
577
+ const buffer = Buffer.allocUnsafe(1024 * 1024);
578
+ for (let offset = 0; offset < total; offset += buffer.length) {
579
+ splats.workspace.check();
580
+ const bytes = buffer.subarray(
581
+ 0,
582
+ Math.min(buffer.length, total - offset),
583
+ );
584
+ readAll(spool, bytes, offset);
585
+ writeAll(fd, bytes, meta.length + offset);
586
+ writing.update(meta.length + offset + bytes.length);
587
+ }
588
+ }
589
+ });
590
+ writing.finish();
591
+ const publishing = new Progress("Publishing RAD files", staged.length);
592
+ for (const { temporary, file } of staged) {
593
+ splats.workspace.check();
594
+ // rename can replace an existing file, so reserve the name exclusively.
595
+ const fd = openSync(file, "wx");
596
+ created.push(file);
597
+ closeSync(fd);
598
+ renameSync(temporary, file);
599
+ publishing.update(created.length);
600
+ }
601
+ splats.workspace.check();
602
+ publishing.finish();
603
+ return meta.length + total;
604
+ } catch (error) {
605
+ await Promise.allSettled(pending);
606
+ for (const { temporary } of staged) removePath(temporary);
607
+ for (const file of created) removePath(file);
608
+ throw error;
609
+ } finally {
610
+ await encoder.close();
611
+ }
612
+ }