gaussian-splat-lite 1.5.1 → 1.5.2
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.
- package/CHANGELOG.md +13 -1
- package/bin/build-lod.js +14 -3
- package/bin/decoder/cluster.wasm +0 -0
- package/bin/decoder/decoder.js +5 -4
- package/bin/decoder/decoder_bg.wasm +0 -0
- package/bin/rad/cluster.js +36 -6
- package/bin/rad/heap.js +1 -2
- package/bin/rad/lod.js +48 -67
- package/bin/rad/rad.js +50 -28
- package/bin/rad/splats.js +129 -37
- package/bin/rad/storage.js +147 -34
- package/bin/rad/writeback.js +100 -0
- package/dist/gaussian-splat-lite.cjs +37 -3
- package/dist/gaussian-splat-lite.cjs.map +1 -1
- package/dist/gaussian-splat-lite.min.cjs +127 -93
- package/dist/gaussian-splat-lite.min.cjs.map +1 -1
- package/dist/gaussian-splat-lite.module.js +37 -3
- package/dist/gaussian-splat-lite.module.js.map +1 -1
- package/dist/gaussian-splat-lite.module.min.js +972 -938
- package/dist/gaussian-splat-lite.module.min.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,17 @@ and this project follows [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.5.2] - 2026-10-29
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- `gsl-build-lod` reduces temporary-file reads for models larger than `--memory-budget`.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Neural denoising no longer lets a still view turn grainy again when it keeps rendering for many seconds.
|
|
19
|
+
- `gsl-build-lod` limits pending disk writes within `--memory-budget` to prevent excessive memory use on slow disks.
|
|
20
|
+
|
|
10
21
|
## [1.5.1] - 2026-10-27
|
|
11
22
|
|
|
12
23
|
### Changed
|
|
@@ -709,7 +720,8 @@ and this project follows [Semantic Versioning](https://semver.org/).
|
|
|
709
720
|
|
|
710
721
|
- Initial public release of the Three.js Gaussian Splatting renderer.
|
|
711
722
|
|
|
712
|
-
[Unreleased]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.5.
|
|
723
|
+
[Unreleased]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.5.2...HEAD
|
|
724
|
+
[1.5.2]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.5.1...v1.5.2
|
|
713
725
|
[1.5.1]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.5.0...v1.5.1
|
|
714
726
|
[1.5.0]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.4.3...v1.5.0
|
|
715
727
|
[1.4.3]: https://github.com/WilliamLiu-1997/Gaussian-Splat-Lite/compare/v1.4.2...v1.4.3
|
package/bin/build-lod.js
CHANGED
|
@@ -13,6 +13,7 @@ import { buildLod } from "./rad/lod.js";
|
|
|
13
13
|
import { Pool } from "./rad/parallel.js";
|
|
14
14
|
import { checkOutput, writeRad } from "./rad/rad.js";
|
|
15
15
|
import { GIB, Workspace } from "./rad/storage.js";
|
|
16
|
+
import { syncFiles } from "./rad/writeback.js";
|
|
16
17
|
|
|
17
18
|
const help = `Convert PLY or SPZ files to RAD with levels of detail (LOD).
|
|
18
19
|
|
|
@@ -37,7 +38,7 @@ Options:
|
|
|
37
38
|
-h, --help Show this help
|
|
38
39
|
|
|
39
40
|
SH4 inputs are accepted; coefficients above --max-sh are discarded.
|
|
40
|
-
The memory target covers the
|
|
41
|
+
The memory target covers the conversion, worker threads and pending disk writes included.
|
|
41
42
|
Requires Node.js 22.12 or later. No compilation is required.`;
|
|
42
43
|
|
|
43
44
|
function parse(args) {
|
|
@@ -139,7 +140,13 @@ async function convert(input, options, kernel, cancelled) {
|
|
|
139
140
|
`${path.basename(input, path.extname(input))}-lod.rad`,
|
|
140
141
|
);
|
|
141
142
|
checkOutput(output, options.chunked);
|
|
142
|
-
const workspace = new Workspace(
|
|
143
|
+
const workspace = new Workspace(
|
|
144
|
+
options.budget,
|
|
145
|
+
(files, state) =>
|
|
146
|
+
parentPort.postMessage({ method: "writeback", args: [files, state] }),
|
|
147
|
+
options.tempDir,
|
|
148
|
+
cancelled,
|
|
149
|
+
);
|
|
143
150
|
const start = performance.now();
|
|
144
151
|
console.log(
|
|
145
152
|
`${input} -> ${output}\nMemory target: ${(options.budget / GIB).toFixed(3)} GiB`,
|
|
@@ -222,6 +229,7 @@ async function run(options, cancelled) {
|
|
|
222
229
|
|
|
223
230
|
// Keep signal handling responsive while the conversion runs synchronous code.
|
|
224
231
|
// The worker observes cancellation and unwinds its existing catch/finally blocks.
|
|
232
|
+
// Cleanup finishes submitted writeback before closing files and exiting.
|
|
225
233
|
async function start(options) {
|
|
226
234
|
const cancelled = new Int32Array(new SharedArrayBuffer(4));
|
|
227
235
|
let progressLine = "";
|
|
@@ -230,6 +238,7 @@ async function start(options) {
|
|
|
230
238
|
progressLine = "";
|
|
231
239
|
};
|
|
232
240
|
const print = ({ method, args }) => {
|
|
241
|
+
if (method === "writeback") return syncFiles(...args, cancelled);
|
|
233
242
|
if (method === "progress") {
|
|
234
243
|
const [line, done] = args;
|
|
235
244
|
if (process.stdout.isTTY) {
|
|
@@ -249,7 +258,9 @@ async function start(options) {
|
|
|
249
258
|
const code = signal === "SIGINT" ? 130 : 143;
|
|
250
259
|
if (Atomics.compareExchange(cancelled, 0, 0, code) === 0) {
|
|
251
260
|
endProgress();
|
|
252
|
-
console.error(
|
|
261
|
+
console.error(
|
|
262
|
+
`${signal}: cancelling conversion; waiting for active disk I/O before cleanup...`,
|
|
263
|
+
);
|
|
253
264
|
}
|
|
254
265
|
};
|
|
255
266
|
process.on("SIGINT", interrupt);
|
package/bin/decoder/cluster.wasm
CHANGED
|
Binary file
|
package/bin/decoder/decoder.js
CHANGED
|
@@ -78,10 +78,6 @@ function __wbg_get_imports() {
|
|
|
78
78
|
const ret = new Error(getStringFromWasm0(arg0, arg1));
|
|
79
79
|
return ret;
|
|
80
80
|
},
|
|
81
|
-
__wbg_new_from_slice_d85ad974cf8f6f35: function(arg0, arg1) {
|
|
82
|
-
const ret = new Float32Array(getArrayF32FromWasm0(arg0, arg1));
|
|
83
|
-
return ret;
|
|
84
|
-
},
|
|
85
81
|
__wbg_stack_3b0d974bbf31e44f: function(arg0, arg1) {
|
|
86
82
|
const ret = arg1.stack;
|
|
87
83
|
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
@@ -95,6 +91,11 @@ function __wbg_get_imports() {
|
|
|
95
91
|
return ret;
|
|
96
92
|
},
|
|
97
93
|
__wbindgen_cast_0000000000000002: function(arg0, arg1) {
|
|
94
|
+
// Cast intrinsic for `Ref(Slice(F32)) -> NamedExternref("Float32Array")`.
|
|
95
|
+
const ret = getArrayF32FromWasm0(arg0, arg1);
|
|
96
|
+
return ret;
|
|
97
|
+
},
|
|
98
|
+
__wbindgen_cast_0000000000000003: function(arg0, arg1) {
|
|
98
99
|
// Cast intrinsic for `Ref(String) -> Externref`.
|
|
99
100
|
const ret = getStringFromWasm0(arg0, arg1);
|
|
100
101
|
return ret;
|
|
Binary file
|
package/bin/rad/cluster.js
CHANGED
|
@@ -6,7 +6,7 @@ import { HALF } from "./math.js";
|
|
|
6
6
|
import { WorkerGroup, clusterers } from "./parallel.js";
|
|
7
7
|
import { Progress } from "./progress.js";
|
|
8
8
|
import { SH_DIMS } from "./splats.js";
|
|
9
|
-
import { PagedArray } from "./storage.js";
|
|
9
|
+
import { PAGE_BYTES, PagedArray } from "./storage.js";
|
|
10
10
|
|
|
11
11
|
// Codebook entries at most: RAD labels are 16-bit.
|
|
12
12
|
const MAX_CODES = 65536;
|
|
@@ -323,6 +323,13 @@ class Assigner extends WorkerGroup {
|
|
|
323
323
|
}
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
+
// Rows that fit in the cache, allowing a partial page per column. Repeated
|
|
327
|
+
// scans cache this prefix and bypass the rest to avoid evicting it.
|
|
328
|
+
function fitting(bytes, row, columns) {
|
|
329
|
+
const pages = Math.floor(bytes / PAGE_BYTES) - columns;
|
|
330
|
+
return Math.max(0, Math.floor((pages * PAGE_BYTES) / row));
|
|
331
|
+
}
|
|
332
|
+
|
|
326
333
|
// Reads splats' SH as rows of half bits, the bands one after another, with
|
|
327
334
|
// their weights, area × opacity as merging weighs them.
|
|
328
335
|
class Rows {
|
|
@@ -330,16 +337,21 @@ class Rows {
|
|
|
330
337
|
this.splats = splats;
|
|
331
338
|
this.dims = dims;
|
|
332
339
|
this.length = splats.length;
|
|
340
|
+
this.kept = Number.POSITIVE_INFINITY;
|
|
333
341
|
this.column = new Uint16Array(JOB * SH_DIMS[SH_DIMS.length - 1]);
|
|
334
342
|
this.records = new Float32Array(JOB * 16);
|
|
335
343
|
}
|
|
336
344
|
|
|
345
|
+
keep(bytes) {
|
|
346
|
+
this.kept = fitting(bytes, this.dims * 2 + 64, this.splats.maxSh + 1);
|
|
347
|
+
}
|
|
348
|
+
|
|
337
349
|
read(base, count, bits, weights) {
|
|
338
|
-
const { splats, dims, column } = this;
|
|
350
|
+
const { splats, dims, column, kept } = this;
|
|
339
351
|
let offset = 0;
|
|
340
352
|
for (let band = 0; band < splats.maxSh; band++) {
|
|
341
353
|
const size = SH_DIMS[band];
|
|
342
|
-
splats.sh[band].
|
|
354
|
+
splats.sh[band].scan(base * size, column, count * size, kept * size);
|
|
343
355
|
for (let i = 0; i < count; i++)
|
|
344
356
|
for (let d = 0; d < size; d++)
|
|
345
357
|
bits[i * dims + offset + d] = column[i * size + d];
|
|
@@ -349,10 +361,11 @@ class Rows {
|
|
|
349
361
|
}
|
|
350
362
|
|
|
351
363
|
weigh(base, count, weights) {
|
|
352
|
-
const records = this.splats.records.
|
|
364
|
+
const records = this.splats.records.scan(
|
|
353
365
|
base * 16,
|
|
354
366
|
this.records,
|
|
355
367
|
count * 16,
|
|
368
|
+
this.kept * 16,
|
|
356
369
|
);
|
|
357
370
|
for (let i = 0; i < count; i++)
|
|
358
371
|
weights[i] = records[i * 16 + 15] * records[i * 16 + 3];
|
|
@@ -364,13 +377,19 @@ class Sample {
|
|
|
364
377
|
constructor(workspace, dims) {
|
|
365
378
|
this.dims = dims;
|
|
366
379
|
this.length = 0;
|
|
380
|
+
this.kept = Number.POSITIVE_INFINITY;
|
|
367
381
|
this.bits = new PagedArray(workspace, Uint16Array);
|
|
368
382
|
this.weights = new PagedArray(workspace, Float64Array);
|
|
369
383
|
}
|
|
370
384
|
|
|
385
|
+
keep(bytes) {
|
|
386
|
+
this.kept = fitting(bytes, this.dims * 2 + 8, 2);
|
|
387
|
+
}
|
|
388
|
+
|
|
371
389
|
read(base, count, bits, weights) {
|
|
372
|
-
|
|
373
|
-
this.
|
|
390
|
+
const { dims, kept } = this;
|
|
391
|
+
this.bits.scan(base * dims, bits, count * dims, kept * dims);
|
|
392
|
+
this.weights.scan(base, weights, count, kept);
|
|
374
393
|
}
|
|
375
394
|
|
|
376
395
|
dispose() {
|
|
@@ -653,11 +672,21 @@ export async function clusterSh(splats, iterations, module) {
|
|
|
653
672
|
extra,
|
|
654
673
|
Math.ceil(training.length / size),
|
|
655
674
|
);
|
|
675
|
+
// Measure worker growth between passes: bypass reads do not allocate
|
|
676
|
+
// pages or trigger measurements. Leave room for labels when the final
|
|
677
|
+
// pass shares the training cache.
|
|
678
|
+
const labelPages = sampled ? 0 : Math.ceil((length * 2) / PAGE_BYTES);
|
|
679
|
+
const keep = () => {
|
|
680
|
+
workspace.adjust();
|
|
681
|
+
training.keep(workspace.capacity - labelPages * PAGE_BYTES);
|
|
682
|
+
};
|
|
683
|
+
keep();
|
|
656
684
|
const codes = new Float64Array(count * dims);
|
|
657
685
|
const sums = new Float64Array(count * dims);
|
|
658
686
|
const totals = new Float64Array(count);
|
|
659
687
|
const bits = new Uint16Array(dims);
|
|
660
688
|
const weight = new Float64Array(1);
|
|
689
|
+
// Read seeds past the cached prefix directly, preserving training rows.
|
|
661
690
|
for (let c = 0; c < count; c++) {
|
|
662
691
|
// Spark computes the seed rows in single precision.
|
|
663
692
|
const row = Math.floor(
|
|
@@ -705,6 +734,7 @@ export async function clusterSh(splats, iterations, module) {
|
|
|
705
734
|
layout(codes, order, dims, center, blocked, norms);
|
|
706
735
|
await assigner.use("codes", { codes: blocked, norms, center });
|
|
707
736
|
}
|
|
737
|
+
keep();
|
|
708
738
|
if (!last) {
|
|
709
739
|
sums.fill(0);
|
|
710
740
|
totals.fill(0);
|
package/bin/rad/heap.js
CHANGED
|
@@ -247,7 +247,7 @@ export function sortSplats(splats, pool) {
|
|
|
247
247
|
// rank alone keeps splats of equal size in index order.
|
|
248
248
|
const items = new IndexArray(workspace);
|
|
249
249
|
for (let i = 0; i < count; i++) {
|
|
250
|
-
const key = rank(-splats.
|
|
250
|
+
const key = rank(-splats.sizes.get(i));
|
|
251
251
|
const words = items.page(i * 2, true);
|
|
252
252
|
const offset = (i * 2) & items.mask;
|
|
253
253
|
words[offset] = key;
|
|
@@ -266,5 +266,4 @@ export function sortSplats(splats, pool) {
|
|
|
266
266
|
sorted.dispose();
|
|
267
267
|
ordering.finish();
|
|
268
268
|
splats.reorder(order, pool);
|
|
269
|
-
order.dispose();
|
|
270
269
|
}
|
package/bin/rad/lod.js
CHANGED
|
@@ -49,21 +49,17 @@ export function buildLod(splats, lodBase, pool) {
|
|
|
49
49
|
splats.weight.dispose();
|
|
50
50
|
selection.finish(`${retainedCount} nodes retained`);
|
|
51
51
|
|
|
52
|
-
// Ordering reads sizes at random
|
|
53
|
-
//
|
|
54
|
-
const sizes =
|
|
55
|
-
const sizing = new Progress("Reading LOD sizes", splats.length);
|
|
56
|
-
for (let index = 0; index < splats.length; index++) {
|
|
57
|
-
sizes.set(index, splats.feature(index));
|
|
58
|
-
if ((index & 4095) === 0) sizing.update(index);
|
|
59
|
-
}
|
|
60
|
-
sizing.finish();
|
|
52
|
+
// Ordering reads sizes at random, from a column without the rest of each
|
|
53
|
+
// record, and lists them in logical order for chunkTree().
|
|
54
|
+
const sizes = splats.sizes;
|
|
61
55
|
// A splat's logical index is its position in `order`. Its children get
|
|
62
56
|
// consecutive logical indices: `first` and the `counts` - 1 after it.
|
|
63
57
|
const order = new IndexArray(workspace);
|
|
64
58
|
const first = new IndexArray(workspace);
|
|
65
59
|
const counts = new IndexArray(workspace);
|
|
60
|
+
const ordered = new PagedArray(workspace, Float32Array);
|
|
66
61
|
order.push(root);
|
|
62
|
+
ordered.push(sizes.get(root));
|
|
67
63
|
// The splats to visit, by logical index, splat and size: the frontier of a
|
|
68
64
|
// pass as triples of words, and a stack within it.
|
|
69
65
|
let frontier = new IndexArray(workspace);
|
|
@@ -120,17 +116,22 @@ export function buildLod(splats, lodBase, pool) {
|
|
|
120
116
|
const start = order.length;
|
|
121
117
|
first.set(logical, start);
|
|
122
118
|
counts.set(logical, count);
|
|
123
|
-
for (let c = 0; c < count; c++) order.push(list[c]);
|
|
124
119
|
if (top + count > logicals.length) {
|
|
125
120
|
logicals = grow(logicals, top, top + count);
|
|
126
121
|
indices = grow(indices, top, top + count);
|
|
127
122
|
lengths = grow(lengths, top, top + count);
|
|
128
123
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
124
|
+
// Each child's size is read once, at random, for both uses.
|
|
125
|
+
for (let c = 0; c < count; c++) {
|
|
126
|
+
const length = sizes.get(list[c]);
|
|
127
|
+
order.push(list[c]);
|
|
128
|
+
ordered.push(length);
|
|
129
|
+
const at = top + count - 1 - c;
|
|
130
|
+
logicals[at] = start + c;
|
|
131
|
+
indices[at] = list[c];
|
|
132
|
+
lengths[at] = length;
|
|
133
133
|
}
|
|
134
|
+
top += count;
|
|
134
135
|
}
|
|
135
136
|
}
|
|
136
137
|
frontier.dispose();
|
|
@@ -143,7 +144,7 @@ export function buildLod(splats, lodBase, pool) {
|
|
|
143
144
|
sizes.dispose();
|
|
144
145
|
kept.dispose();
|
|
145
146
|
ordering.finish();
|
|
146
|
-
chunkTree(splats, order, first, counts, pool);
|
|
147
|
+
chunkTree(splats, order, first, counts, ordered, pool);
|
|
147
148
|
order.dispose();
|
|
148
149
|
}
|
|
149
150
|
|
|
@@ -152,24 +153,8 @@ export function buildLod(splats, lodBase, pool) {
|
|
|
152
153
|
function mergeLevels(splats, pool) {
|
|
153
154
|
const initial = splats.length;
|
|
154
155
|
const workspace = splats.workspace;
|
|
155
|
-
// Merged splats lie within the leaves' bounds.
|
|
156
|
-
|
|
157
|
-
const bounds = {
|
|
158
|
-
min: [0, 1, 2].map(() => Number.POSITIVE_INFINITY),
|
|
159
|
-
max: [0, 1, 2].map(() => Number.NEGATIVE_INFINITY),
|
|
160
|
-
};
|
|
161
|
-
const record = new Float64Array(16);
|
|
162
|
-
const preparing = new Progress("Preparing LOD bounds", initial);
|
|
163
|
-
for (let i = 0; i < initial; i++) {
|
|
164
|
-
splats.read(i, record);
|
|
165
|
-
for (let d = 0; d < 3; d++) {
|
|
166
|
-
bounds.min[d] = Math.min(bounds.min[d], record[d]);
|
|
167
|
-
bounds.max[d] = Math.max(bounds.max[d], record[d]);
|
|
168
|
-
}
|
|
169
|
-
splats.weight.set(i, record[15] * record[3]);
|
|
170
|
-
if ((i & 4095) === 0) preparing.update(i);
|
|
171
|
-
}
|
|
172
|
-
preparing.finish();
|
|
156
|
+
// Merged splats lie within the leaves' bounds.
|
|
157
|
+
const { bounds, sizes } = splats;
|
|
173
158
|
const removed = new PagedArray(workspace, Uint8Array);
|
|
174
159
|
const similarities = new Similarities(splats);
|
|
175
160
|
let active = new Heap(workspace);
|
|
@@ -187,7 +172,7 @@ function mergeLevels(splats, pool) {
|
|
|
187
172
|
const added = new Cells(18);
|
|
188
173
|
const near = new Cells(20);
|
|
189
174
|
for (
|
|
190
|
-
let level = Math.ceil(Math.log2(Math.max(
|
|
175
|
+
let level = Math.ceil(Math.log2(Math.max(sizes.get(0), 0.000001)));
|
|
191
176
|
;
|
|
192
177
|
level++
|
|
193
178
|
) {
|
|
@@ -199,14 +184,14 @@ function mergeLevels(splats, pool) {
|
|
|
199
184
|
let high = initial;
|
|
200
185
|
while (low < high) {
|
|
201
186
|
const middle = Math.floor((low + high) / 2);
|
|
202
|
-
if (
|
|
187
|
+
if (sizes.get(middle) <= step) low = middle + 1;
|
|
203
188
|
else high = middle;
|
|
204
189
|
}
|
|
205
190
|
const { grid, queue } = Progress.inline(() => {
|
|
206
191
|
const adding = new Progress(`Preparing level ${level}`, low - frontier);
|
|
207
192
|
const previous = frontier;
|
|
208
193
|
while (frontier < low) {
|
|
209
|
-
active.push(frontier, -
|
|
194
|
+
active.push(frontier, -sizes.get(frontier));
|
|
210
195
|
frontier++;
|
|
211
196
|
if ((frontier & 4095) === 0) adding.update(frontier - previous);
|
|
212
197
|
}
|
|
@@ -323,7 +308,7 @@ function mergeLevels(splats, pool) {
|
|
|
323
308
|
removed.set(other, 1);
|
|
324
309
|
grid.remove(self);
|
|
325
310
|
grid.remove(best);
|
|
326
|
-
const feature =
|
|
311
|
+
const feature = sizes.get(merged);
|
|
327
312
|
if (feature > step) next.push(merged, -feature);
|
|
328
313
|
else {
|
|
329
314
|
const terms =
|
|
@@ -404,8 +389,8 @@ function sort(values, count) {
|
|
|
404
389
|
|
|
405
390
|
// Lays out the tree in chunks of 65536 splats and moves every column into
|
|
406
391
|
// that order. Children become consecutive rows: childStart is then the row of
|
|
407
|
-
// a splat's first child.
|
|
408
|
-
function chunkTree(splats, order, first, counts, pool) {
|
|
392
|
+
// a splat's first child. `sizes` are the splats' sizes in logical order.
|
|
393
|
+
function chunkTree(splats, order, first, counts, sizes, pool) {
|
|
409
394
|
const workspace = splats.workspace;
|
|
410
395
|
const nodes = splats.length;
|
|
411
396
|
for (const column of [splats.childStart, splats.childCount, splats.children])
|
|
@@ -426,13 +411,6 @@ function chunkTree(splats, order, first, counts, pool) {
|
|
|
426
411
|
"Reordering LOD nodes",
|
|
427
412
|
);
|
|
428
413
|
logical.dispose();
|
|
429
|
-
const sizes = new PagedArray(workspace, Float32Array);
|
|
430
|
-
const sizing = new Progress("Reading chunk sizes", order.length);
|
|
431
|
-
for (let index = 0; index < order.length; index++) {
|
|
432
|
-
sizes.set(index, splats.feature(index));
|
|
433
|
-
if ((index & 4095) === 0) sizing.update(index);
|
|
434
|
-
}
|
|
435
|
-
sizing.finish();
|
|
436
414
|
// The row of each splat's first child.
|
|
437
415
|
const starts = new PagedArray(workspace, Float64Array);
|
|
438
416
|
const indices = new IndexArray(workspace);
|
|
@@ -629,28 +607,31 @@ function chunkTree(splats, order, first, counts, pool) {
|
|
|
629
607
|
"Reordering child offsets",
|
|
630
608
|
);
|
|
631
609
|
chunked.dispose();
|
|
632
|
-
// The SH coefficients are still in
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
splats.sh[band] = nodeOrder.apply(
|
|
647
|
-
splats.sh[band],
|
|
648
|
-
SH_DIMS[band],
|
|
649
|
-
pool,
|
|
650
|
-
false,
|
|
651
|
-
`Reordering LOD SH${band + 1}`,
|
|
610
|
+
// The SH coefficients are still in the order of mergeSh().
|
|
611
|
+
if (splats.maxSh) {
|
|
612
|
+
const shOrder = new Progress("Collecting LOD SH order", indices.length);
|
|
613
|
+
for (let i = 0; i < indices.length; i++) {
|
|
614
|
+
indices.set(i, splats.shRow(order.get(indices.get(i))));
|
|
615
|
+
if ((i & 4095) === 0) shOrder.update(i);
|
|
616
|
+
}
|
|
617
|
+
shOrder.finish();
|
|
618
|
+
splats.shOrder.dispose();
|
|
619
|
+
const nodeOrder = new Permutation(
|
|
620
|
+
workspace,
|
|
621
|
+
indices,
|
|
622
|
+
nodes,
|
|
623
|
+
"Preparing LOD SH order",
|
|
652
624
|
);
|
|
653
|
-
|
|
625
|
+
for (let band = 0; band < splats.maxSh; band++)
|
|
626
|
+
splats.sh[band] = nodeOrder.apply(
|
|
627
|
+
splats.sh[band],
|
|
628
|
+
SH_DIMS[band],
|
|
629
|
+
pool,
|
|
630
|
+
false,
|
|
631
|
+
`Reordering LOD SH${band + 1}`,
|
|
632
|
+
);
|
|
633
|
+
nodeOrder.dispose();
|
|
634
|
+
}
|
|
654
635
|
first.dispose();
|
|
655
636
|
splats.length = indices.length;
|
|
656
637
|
indices.dispose();
|
package/bin/rad/rad.js
CHANGED
|
@@ -16,7 +16,7 @@ import { HALF, half, halfBits, opacityRadius } from "./math.js";
|
|
|
16
16
|
import { WorkerGroup, encoders } from "./parallel.js";
|
|
17
17
|
import { Progress } from "./progress.js";
|
|
18
18
|
import { SH_DIMS } from "./splats.js";
|
|
19
|
-
import { readAll, removePath
|
|
19
|
+
import { PAGE_BYTES, readAll, removePath } from "./storage.js";
|
|
20
20
|
|
|
21
21
|
const CHUNK = 65536;
|
|
22
22
|
// Fixed s8 range with SPZ's 1/128 SH step; -1 clips to -127/128.
|
|
@@ -158,27 +158,34 @@ const unorm = (value, min, max) =>
|
|
|
158
158
|
Math.round(Math.max(0, Math.min(255, ((value - min) / (max - min)) * 255)));
|
|
159
159
|
|
|
160
160
|
// The columns encodeChunk() reads. With a codebook, labels replace the SH.
|
|
161
|
-
|
|
161
|
+
// The records before `kept` come through the cache, where counting left
|
|
162
|
+
// them; the rest of them and the other columns, read once, pass it by.
|
|
163
|
+
function gather(splats, base, count, codebook, kept) {
|
|
162
164
|
return {
|
|
163
165
|
base,
|
|
164
166
|
count,
|
|
165
167
|
maxSh: splats.maxSh,
|
|
166
|
-
records: splats.records.
|
|
168
|
+
records: splats.records.scan(
|
|
169
|
+
base * 16,
|
|
170
|
+
new Float32Array(count * 16),
|
|
171
|
+
count * 16,
|
|
172
|
+
kept * 16,
|
|
173
|
+
),
|
|
167
174
|
sh: codebook
|
|
168
175
|
? []
|
|
169
176
|
: splats.sh
|
|
170
177
|
.slice(0, splats.maxSh)
|
|
171
178
|
.map((column, band) =>
|
|
172
|
-
column.
|
|
179
|
+
column.stream(
|
|
173
180
|
base * SH_DIMS[band],
|
|
174
181
|
new Uint16Array(count * SH_DIMS[band]),
|
|
175
182
|
),
|
|
176
183
|
),
|
|
177
184
|
...(codebook && {
|
|
178
|
-
labels: codebook.labels.
|
|
185
|
+
labels: codebook.labels.stream(base, new Uint16Array(count)),
|
|
179
186
|
}),
|
|
180
|
-
counts: splats.childCount.
|
|
181
|
-
starts: splats.childStart.
|
|
187
|
+
counts: splats.childCount.stream(base, new Uint32Array(count)),
|
|
188
|
+
starts: splats.childStart.stream(base, new Float64Array(count)),
|
|
182
189
|
};
|
|
183
190
|
}
|
|
184
191
|
|
|
@@ -447,15 +454,16 @@ if (!isMainThread && workerData?.coder) {
|
|
|
447
454
|
|
|
448
455
|
// With a codebook from clusterSh(), splats store SH as its labels.
|
|
449
456
|
export async function writeRad(splats, output, chunked, lodBase, codebook) {
|
|
457
|
+
const { workspace } = splats;
|
|
450
458
|
const chunks = [];
|
|
451
459
|
const stem = chunkStem(output);
|
|
452
|
-
const spool = chunked ? undefined :
|
|
460
|
+
const spool = chunked ? undefined : workspace.file();
|
|
453
461
|
// Write every file completely before publishing chunks, then the header.
|
|
454
462
|
// Track published names too, so cancellation rolls back this conversion.
|
|
455
463
|
const staged = [];
|
|
456
464
|
const created = [];
|
|
457
465
|
const stage = (file, write) => {
|
|
458
|
-
|
|
466
|
+
workspace.check();
|
|
459
467
|
const temporary = path.join(
|
|
460
468
|
path.dirname(file),
|
|
461
469
|
`.${path.basename(file)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`,
|
|
@@ -464,14 +472,15 @@ export async function writeRad(splats, output, chunked, lodBase, codebook) {
|
|
|
464
472
|
staged.push({ temporary, file });
|
|
465
473
|
try {
|
|
466
474
|
write(fd);
|
|
475
|
+
workspace.check();
|
|
467
476
|
fsyncSync(fd);
|
|
468
|
-
}
|
|
477
|
+
} finally {
|
|
469
478
|
try {
|
|
479
|
+
workspace.writeback.synced(fd);
|
|
480
|
+
} finally {
|
|
470
481
|
closeSync(fd);
|
|
471
|
-
}
|
|
472
|
-
throw error;
|
|
482
|
+
}
|
|
473
483
|
}
|
|
474
|
-
closeSync(fd);
|
|
475
484
|
};
|
|
476
485
|
let total = 0;
|
|
477
486
|
let progress;
|
|
@@ -479,11 +488,11 @@ export async function writeRad(splats, output, chunked, lodBase, codebook) {
|
|
|
479
488
|
if (chunked) {
|
|
480
489
|
const filename = `${stem}-${chunks.length}.radc`;
|
|
481
490
|
stage(path.join(path.dirname(output), filename), (fd) =>
|
|
482
|
-
|
|
491
|
+
workspace.write(fd, bytes, 0),
|
|
483
492
|
);
|
|
484
493
|
chunks.push({ offset: 0, bytes: bytes.length, filename });
|
|
485
494
|
} else {
|
|
486
|
-
|
|
495
|
+
workspace.write(spool, bytes, total);
|
|
487
496
|
chunks.push({ offset: total, bytes: bytes.length });
|
|
488
497
|
}
|
|
489
498
|
total += bytes.length;
|
|
@@ -493,21 +502,29 @@ export async function writeRad(splats, output, chunked, lodBase, codebook) {
|
|
|
493
502
|
// order. A chunk's failure is thrown when its turn comes, not as an
|
|
494
503
|
// unhandled rejection while an earlier chunk is awaited.
|
|
495
504
|
const pending = [];
|
|
496
|
-
const encoder = new Encoder(
|
|
497
|
-
splats.workspace,
|
|
498
|
-
Math.ceil(splats.length / CHUNK),
|
|
499
|
-
);
|
|
505
|
+
const encoder = new Encoder(workspace, Math.ceil(splats.length / CHUNK));
|
|
500
506
|
try {
|
|
501
|
-
// Count the encoding ranges in a first pass.
|
|
507
|
+
// Count the encoding ranges in a first pass. The records that the cache
|
|
508
|
+
// holds stay there for encoding; see gather(). Recompute the prefix
|
|
509
|
+
// after measuring worker growth so it fits a cache that has shrunk.
|
|
510
|
+
const keep = () => {
|
|
511
|
+
workspace.adjust();
|
|
512
|
+
return Math.floor(Math.max(0, workspace.capacity - PAGE_BYTES) / 64);
|
|
513
|
+
};
|
|
502
514
|
const chunkCount = Math.ceil(splats.length / CHUNK);
|
|
503
515
|
progress = new Progress("Analyzing RAD encoding", chunkCount);
|
|
504
516
|
let countedChunks = 0;
|
|
505
517
|
for (let base = 0; base < splats.length; base += CHUNK) {
|
|
506
|
-
|
|
518
|
+
workspace.check();
|
|
519
|
+
// The encoders grow as they work, and reading past the cache loads
|
|
520
|
+
// no pages, which would measure them.
|
|
521
|
+
const kept = keep();
|
|
507
522
|
const count = Math.min(CHUNK, splats.length - base);
|
|
508
|
-
const records = splats.records.
|
|
523
|
+
const records = splats.records.scan(
|
|
509
524
|
base * 16,
|
|
510
525
|
new Float32Array(count * 16),
|
|
526
|
+
count * 16,
|
|
527
|
+
kept * 16,
|
|
511
528
|
);
|
|
512
529
|
const counted = encoder
|
|
513
530
|
.count(records)
|
|
@@ -522,12 +539,14 @@ export async function writeRad(splats, output, chunked, lodBase, codebook) {
|
|
|
522
539
|
progress.finish();
|
|
523
540
|
progress = new Progress("Encoding RAD chunks", chunkCount);
|
|
524
541
|
for (let base = 0; base < splats.length; base += CHUNK) {
|
|
525
|
-
|
|
542
|
+
workspace.check();
|
|
543
|
+
const kept = keep();
|
|
526
544
|
const chunk = gather(
|
|
527
545
|
splats,
|
|
528
546
|
base,
|
|
529
547
|
Math.min(CHUNK, splats.length - base),
|
|
530
548
|
codebook,
|
|
549
|
+
kept,
|
|
531
550
|
);
|
|
532
551
|
if (codebook && base === 0)
|
|
533
552
|
chunk.codes = encodeCodes(codebook, splats.maxSh);
|
|
@@ -556,25 +575,28 @@ export async function writeRad(splats, output, chunked, lodBase, codebook) {
|
|
|
556
575
|
meta.length + (spool === undefined ? 0 : total),
|
|
557
576
|
);
|
|
558
577
|
stage(output, (fd) => {
|
|
559
|
-
|
|
578
|
+
workspace.write(fd, meta, 0);
|
|
560
579
|
if (spool !== undefined) {
|
|
561
580
|
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
562
581
|
for (let offset = 0; offset < total; offset += buffer.length) {
|
|
563
|
-
|
|
582
|
+
workspace.check();
|
|
564
583
|
const bytes = buffer.subarray(
|
|
565
584
|
0,
|
|
566
585
|
Math.min(buffer.length, total - offset),
|
|
567
586
|
);
|
|
568
587
|
readAll(spool, bytes, offset);
|
|
569
|
-
|
|
588
|
+
workspace.write(fd, bytes, meta.length + offset);
|
|
570
589
|
writing.update(meta.length + offset + bytes.length);
|
|
571
590
|
}
|
|
572
591
|
}
|
|
573
592
|
});
|
|
574
593
|
writing.finish();
|
|
594
|
+
// Closing an output waits only for a sync that includes it; a temporary
|
|
595
|
+
// file's failed writeback must surface before anything is published.
|
|
596
|
+
workspace.writeback.wait();
|
|
575
597
|
const publishing = new Progress("Publishing RAD files", staged.length);
|
|
576
598
|
for (const { temporary, file } of staged) {
|
|
577
|
-
|
|
599
|
+
workspace.check();
|
|
578
600
|
// rename can replace an existing file, so reserve the name exclusively.
|
|
579
601
|
const fd = openSync(file, "wx");
|
|
580
602
|
created.push(file);
|
|
@@ -582,7 +604,7 @@ export async function writeRad(splats, output, chunked, lodBase, codebook) {
|
|
|
582
604
|
renameSync(temporary, file);
|
|
583
605
|
publishing.update(created.length);
|
|
584
606
|
}
|
|
585
|
-
|
|
607
|
+
workspace.check();
|
|
586
608
|
publishing.finish();
|
|
587
609
|
return meta.length + total;
|
|
588
610
|
} catch (error) {
|