gaussian-splat-lite 1.5.0 → 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 +29 -9
- 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 +47 -14
- 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/docs/BuildRad.md +2 -2
- package/docs/RadStreamScheduler.md +3 -3
- package/docs/SogStreamScheduler.md +1 -1
- package/docs/SplatLoader.md +1 -0
- package/package.json +1 -1
package/bin/rad/storage.js
CHANGED
|
@@ -12,6 +12,7 @@ import { join } from "node:path";
|
|
|
12
12
|
import v8 from "node:v8";
|
|
13
13
|
import { runInNewContext } from "node:vm";
|
|
14
14
|
import { Progress } from "./progress.js";
|
|
15
|
+
import { Writeback } from "./writeback.js";
|
|
15
16
|
|
|
16
17
|
export const PAGE_BYTES = 65536;
|
|
17
18
|
export const GIB = 1024 ** 3;
|
|
@@ -39,7 +40,7 @@ export function collect() {
|
|
|
39
40
|
// which a SharedArrayBuffer does not on every platform. The cache takes what
|
|
40
41
|
// the budget leaves after the rest of the process, measured as it runs.
|
|
41
42
|
export class Workspace {
|
|
42
|
-
constructor(budget, parent = tmpdir(), cancelled = null) {
|
|
43
|
+
constructor(budget, sync, parent = tmpdir(), cancelled = null) {
|
|
43
44
|
this.budget = budget;
|
|
44
45
|
this.parent = parent;
|
|
45
46
|
this.cancelled = cancelled;
|
|
@@ -74,6 +75,9 @@ export class Workspace {
|
|
|
74
75
|
this.nextFile = 0;
|
|
75
76
|
this.readBytes = 0;
|
|
76
77
|
this.writeBytes = 0;
|
|
78
|
+
// File writes can remain in the system's cache after writeSync returns.
|
|
79
|
+
// Reserve room for pending writeback inside the budget on every platform.
|
|
80
|
+
this.writeback = new Writeback(64 * 1024 ** 2, sync, () => this.check());
|
|
77
81
|
// An earlier conversion's arenas return once collected.
|
|
78
82
|
this.settle();
|
|
79
83
|
}
|
|
@@ -87,13 +91,28 @@ export class Workspace {
|
|
|
87
91
|
get capacity() {
|
|
88
92
|
return Math.max(
|
|
89
93
|
this.arenaBytes,
|
|
90
|
-
this.budget -
|
|
94
|
+
this.budget -
|
|
95
|
+
MARGIN -
|
|
96
|
+
this.writeback.limit -
|
|
97
|
+
Math.max(this.outside, this.floor),
|
|
91
98
|
);
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
// Bytes the rest of the process may still grow by.
|
|
95
102
|
get room() {
|
|
96
|
-
return
|
|
103
|
+
return (
|
|
104
|
+
this.budget -
|
|
105
|
+
MARGIN -
|
|
106
|
+
this.writeback.limit -
|
|
107
|
+
this.arenaBytes -
|
|
108
|
+
this.outside
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Rebalance the cache every 64 allocations and between streaming passes.
|
|
113
|
+
adjust() {
|
|
114
|
+
this.measure();
|
|
115
|
+
this.shrink();
|
|
97
116
|
}
|
|
98
117
|
|
|
99
118
|
// Measures the memory of the rest of the process: all of it but the
|
|
@@ -117,7 +136,10 @@ export class Workspace {
|
|
|
117
136
|
expect(bytes) {
|
|
118
137
|
this.measure();
|
|
119
138
|
this.floor = this.outside + bytes;
|
|
120
|
-
if (
|
|
139
|
+
if (
|
|
140
|
+
this.budget - MARGIN - this.writeback.limit - this.floor <
|
|
141
|
+
this.arenaBytes
|
|
142
|
+
)
|
|
121
143
|
throw new Error("Memory budget too small; increase --memory-budget");
|
|
122
144
|
this.shrink();
|
|
123
145
|
}
|
|
@@ -161,9 +183,32 @@ export class Workspace {
|
|
|
161
183
|
const file = this.files.get(fd);
|
|
162
184
|
this.files.delete(fd);
|
|
163
185
|
try {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
186
|
+
this.writeback.synced(fd);
|
|
187
|
+
} finally {
|
|
188
|
+
try {
|
|
189
|
+
closeSync(fd);
|
|
190
|
+
rmSync(file);
|
|
191
|
+
} catch {}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Reserve writeback room before each write, including large output chunks.
|
|
196
|
+
// Counting whole 64 KiB pages also covers partial pages on systems whose
|
|
197
|
+
// native pages are larger than 4 KiB.
|
|
198
|
+
write(fd, bytes, position) {
|
|
199
|
+
for (let offset = 0; offset < bytes.byteLength; offset += 1024 * 1024) {
|
|
200
|
+
const part =
|
|
201
|
+
bytes.byteLength <= 1024 * 1024
|
|
202
|
+
? bytes
|
|
203
|
+
: bytes.subarray(offset, offset + 1024 * 1024);
|
|
204
|
+
const at = position + offset;
|
|
205
|
+
const size =
|
|
206
|
+
Math.ceil(((at % PAGE_BYTES) + part.byteLength) / PAGE_BYTES) *
|
|
207
|
+
PAGE_BYTES;
|
|
208
|
+
this.writeback.prepare(size);
|
|
209
|
+
writeAll(fd, part, at);
|
|
210
|
+
this.writeback.wrote(fd, size);
|
|
211
|
+
}
|
|
167
212
|
}
|
|
168
213
|
|
|
169
214
|
evict() {
|
|
@@ -198,10 +243,7 @@ export class Workspace {
|
|
|
198
243
|
}
|
|
199
244
|
|
|
200
245
|
allocate(array, index) {
|
|
201
|
-
if (++this.allocations % 64 === 0)
|
|
202
|
-
this.measure();
|
|
203
|
-
this.shrink();
|
|
204
|
-
}
|
|
246
|
+
if (++this.allocations % 64 === 0) this.adjust();
|
|
205
247
|
if (this.used + PAGE_BYTES > this.capacity && this.used) this.evict();
|
|
206
248
|
let slot = this.free.pop();
|
|
207
249
|
if (slot === undefined) {
|
|
@@ -278,25 +320,29 @@ export class Workspace {
|
|
|
278
320
|
}
|
|
279
321
|
|
|
280
322
|
dispose() {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
)
|
|
323
|
+
try {
|
|
324
|
+
this.writeback.wait(false);
|
|
325
|
+
} finally {
|
|
326
|
+
const progress = this.directory
|
|
327
|
+
? new Progress("Cleaning temporary files", this.files.size + 1)
|
|
328
|
+
: undefined;
|
|
329
|
+
let closed = 0;
|
|
330
|
+
for (const fd of this.files.keys()) {
|
|
331
|
+
this.close(fd);
|
|
332
|
+
progress?.update(++closed);
|
|
333
|
+
}
|
|
334
|
+
if (this.directory) removePath(this.directory, { recursive: true });
|
|
335
|
+
this.directory = undefined;
|
|
336
|
+
this.slots.length = 0;
|
|
337
|
+
this.reuse.length = 0;
|
|
338
|
+
this.free.length = 0;
|
|
339
|
+
this.arenas.length = 0;
|
|
340
|
+
progress?.finish();
|
|
341
|
+
if (this.readBytes || this.writeBytes) {
|
|
342
|
+
console.log(
|
|
343
|
+
`Temporary array I/O: ${(this.readBytes / 1048576).toFixed(1)} MiB read, ${(this.writeBytes / 1048576).toFixed(1)} MiB written`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
300
346
|
}
|
|
301
347
|
}
|
|
302
348
|
}
|
|
@@ -348,7 +394,7 @@ export class PagedArray {
|
|
|
348
394
|
const stripe = block % this.stripes;
|
|
349
395
|
const position = Math.floor(block / this.stripes) * PAGE_BYTES;
|
|
350
396
|
this.fds[stripe] ??= this.workspace.file();
|
|
351
|
-
|
|
397
|
+
this.workspace.write(this.fds[stripe], bytes, position);
|
|
352
398
|
this.sizes[stripe] = Math.max(
|
|
353
399
|
this.sizes[stripe] ?? 0,
|
|
354
400
|
position + PAGE_BYTES,
|
|
@@ -427,6 +473,47 @@ export class PagedArray {
|
|
|
427
473
|
return output;
|
|
428
474
|
}
|
|
429
475
|
|
|
476
|
+
// Read into the same array type, bypassing the cache on misses so a scan
|
|
477
|
+
// cannot evict cached pages.
|
|
478
|
+
stream(base, output, count = output.length, at = 0) {
|
|
479
|
+
const bytes = this.Type.BYTES_PER_ELEMENT;
|
|
480
|
+
let offset = 0;
|
|
481
|
+
while (offset < count) {
|
|
482
|
+
if ((++this.workspace.accesses & 4095) === 0) this.workspace.check();
|
|
483
|
+
const index = base + offset;
|
|
484
|
+
const block = Math.floor(index * this.inverse);
|
|
485
|
+
const within = index & this.mask;
|
|
486
|
+
const size = Math.min(count - offset, this.items - within);
|
|
487
|
+
const page = this.pages[block];
|
|
488
|
+
if (page)
|
|
489
|
+
output.set(page.values.subarray(within, within + size), at + offset);
|
|
490
|
+
else {
|
|
491
|
+
const stripe = block % this.stripes;
|
|
492
|
+
const position = Math.floor(block / this.stripes) * PAGE_BYTES;
|
|
493
|
+
const part = new Uint8Array(
|
|
494
|
+
output.buffer,
|
|
495
|
+
output.byteOffset + (at + offset) * bytes,
|
|
496
|
+
size * bytes,
|
|
497
|
+
);
|
|
498
|
+
// As in allocate(), a page that no file holds is zeros.
|
|
499
|
+
if (position >= (this.sizes[stripe] ?? 0)) part.fill(0);
|
|
500
|
+
else {
|
|
501
|
+
readAll(this.fds[stripe], part, position + within * bytes);
|
|
502
|
+
this.workspace.readBytes += part.byteLength;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
offset += size;
|
|
506
|
+
}
|
|
507
|
+
return output;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Cache the prefix before `kept`; stream the rest to preserve it for reuse.
|
|
511
|
+
scan(base, output, count, kept) {
|
|
512
|
+
const cached = Math.max(0, Math.min(count, kept - base));
|
|
513
|
+
this.read(base, output, cached);
|
|
514
|
+
return this.stream(base + cached, output, count - cached, cached);
|
|
515
|
+
}
|
|
516
|
+
|
|
430
517
|
write(base, values, count = values.length) {
|
|
431
518
|
let offset = 0;
|
|
432
519
|
while (offset < count) {
|
|
@@ -657,8 +744,17 @@ export class Permutation {
|
|
|
657
744
|
// rows into it; rows across pages are left to the main thread. The output
|
|
658
745
|
// spreads over as many files as `source`.
|
|
659
746
|
// With `words`, rows are copied as 32-bit words, which keeps any bits of
|
|
660
|
-
// floats, and the output has word views; see PagedArray.at().
|
|
661
|
-
|
|
747
|
+
// floats, and the output has word views; see PagedArray.at(). `visit` is
|
|
748
|
+
// called with the output and each window's rows [start, end) once they are
|
|
749
|
+
// in place, while their pages are still cached.
|
|
750
|
+
apply(
|
|
751
|
+
source,
|
|
752
|
+
stride,
|
|
753
|
+
pool,
|
|
754
|
+
words = false,
|
|
755
|
+
label = undefined,
|
|
756
|
+
visit = undefined,
|
|
757
|
+
) {
|
|
662
758
|
const output = new PagedArray(
|
|
663
759
|
this.workspace,
|
|
664
760
|
source.Type,
|
|
@@ -679,6 +775,11 @@ export class Permutation {
|
|
|
679
775
|
);
|
|
680
776
|
const progress =
|
|
681
777
|
label && new Progress(label, Math.ceil(this.length / window) * this.rows);
|
|
778
|
+
// The lowest and highest target + 1 of each block of source rows, found
|
|
779
|
+
// as the first window reads them: a later window that none of a block's
|
|
780
|
+
// rows land in skips the block.
|
|
781
|
+
const low = new Float64Array(Math.ceil(this.rows / block));
|
|
782
|
+
const high = new Float64Array(low.length);
|
|
682
783
|
for (let start = 0; start < this.length; start += window) {
|
|
683
784
|
const end = Math.min(this.length, start + window);
|
|
684
785
|
output.pinned = [
|
|
@@ -687,8 +788,19 @@ export class Permutation {
|
|
|
687
788
|
];
|
|
688
789
|
output.load(start * stride, end * stride, true);
|
|
689
790
|
for (let base = 0; base < this.rows; base += block) {
|
|
791
|
+
const b = base / block;
|
|
792
|
+
if (start && (high[b] <= start || low[b] > end)) continue;
|
|
690
793
|
const count = Math.min(block, this.rows - base);
|
|
691
794
|
this.targets.read(base, targets, count);
|
|
795
|
+
if (!start) {
|
|
796
|
+
low[b] = Number.POSITIVE_INFINITY;
|
|
797
|
+
for (let i = 0; i < count; i++)
|
|
798
|
+
if (targets[i]) {
|
|
799
|
+
low[b] = Math.min(low[b], targets[i]);
|
|
800
|
+
high[b] = Math.max(high[b], targets[i]);
|
|
801
|
+
}
|
|
802
|
+
if (low[b] > end) continue;
|
|
803
|
+
}
|
|
692
804
|
source.load(base * stride, (base + count) * stride);
|
|
693
805
|
pool.copy(source, output, stride, count, base, start, end, words);
|
|
694
806
|
for (let i = 0; i < count; i++) {
|
|
@@ -710,6 +822,7 @@ export class Permutation {
|
|
|
710
822
|
}
|
|
711
823
|
progress?.update(Math.floor(start / window) * this.rows + base + count);
|
|
712
824
|
}
|
|
825
|
+
visit?.(output, start, end);
|
|
713
826
|
}
|
|
714
827
|
output.pinned = [0, 0];
|
|
715
828
|
source.dispose();
|
|
@@ -763,7 +876,7 @@ export function readAll(fd, bytes, position) {
|
|
|
763
876
|
return bytes;
|
|
764
877
|
}
|
|
765
878
|
|
|
766
|
-
|
|
879
|
+
function writeAll(fd, bytes, position) {
|
|
767
880
|
let offset = 0;
|
|
768
881
|
while (offset < bytes.byteLength) {
|
|
769
882
|
const count = writeSync(
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { fdatasync } from "node:fs";
|
|
2
|
+
import { getSystemErrorName } from "node:util";
|
|
3
|
+
|
|
4
|
+
// The CLI thread can receive these requests while conversion runs synchronous
|
|
5
|
+
// code. Completion is shared so the converter need not yield its event loop.
|
|
6
|
+
export function syncFiles(files, state, cancelled) {
|
|
7
|
+
// A batch can still be in the message queue when cancellation arrives.
|
|
8
|
+
// Acknowledge it without submitting any more native I/O.
|
|
9
|
+
if (Atomics.load(cancelled, 0)) {
|
|
10
|
+
Atomics.store(state, 0, 0);
|
|
11
|
+
Atomics.notify(state, 0);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
for (const fd of files)
|
|
15
|
+
fdatasync(fd, (error) => {
|
|
16
|
+
if (error) Atomics.compareExchange(state, 1, 0, error.errno);
|
|
17
|
+
Atomics.sub(state, 0, 1);
|
|
18
|
+
Atomics.notify(state, 0);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Bounds buffered writes that have not yet been confirmed written back. One
|
|
23
|
+
// batch syncs while the next fills; only the older batch is acknowledged when
|
|
24
|
+
// its sync completes, even if the same files have been written again.
|
|
25
|
+
export class Writeback {
|
|
26
|
+
// `check` throws once the conversion is cancelled.
|
|
27
|
+
constructor(limit, submit, check) {
|
|
28
|
+
this.limit = limit;
|
|
29
|
+
this.submit = submit;
|
|
30
|
+
this.check = check;
|
|
31
|
+
this.files = new Map();
|
|
32
|
+
this.bytes = 0;
|
|
33
|
+
this.pending = undefined;
|
|
34
|
+
// Outstanding syncs, then the first negative system error number.
|
|
35
|
+
this.state = new Int32Array(new SharedArrayBuffer(8));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
complete() {
|
|
39
|
+
if (!this.pending || Atomics.load(this.state, 0)) return;
|
|
40
|
+
this.pending = undefined;
|
|
41
|
+
const error = Atomics.load(this.state, 1);
|
|
42
|
+
if (error)
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Unable to write back file data: ${getSystemErrorName(error)}`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Cancellation interrupts conversion, but keeps the batch tracked. Cleanup
|
|
49
|
+
// waits without cancellation so every fd stays open until its sync returns.
|
|
50
|
+
wait(cancellable = true) {
|
|
51
|
+
if (!this.pending) return;
|
|
52
|
+
for (;;) {
|
|
53
|
+
if (cancellable) this.check();
|
|
54
|
+
const remaining = Atomics.load(this.state, 0);
|
|
55
|
+
if (!remaining) break;
|
|
56
|
+
Atomics.wait(this.state, 0, remaining, cancellable ? 100 : undefined);
|
|
57
|
+
}
|
|
58
|
+
this.complete();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Called before writing: the write itself must fit, including while an
|
|
62
|
+
// earlier batch is still being written back.
|
|
63
|
+
prepare(bytes) {
|
|
64
|
+
this.check();
|
|
65
|
+
this.complete();
|
|
66
|
+
if (this.bytes + (this.pending?.bytes ?? 0) + bytes <= this.limit) return;
|
|
67
|
+
this.wait();
|
|
68
|
+
if (this.bytes + bytes > this.limit) {
|
|
69
|
+
this.flush();
|
|
70
|
+
this.wait();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
wrote(fd, bytes) {
|
|
75
|
+
this.files.set(fd, (this.files.get(fd) ?? 0) + bytes);
|
|
76
|
+
this.bytes += bytes;
|
|
77
|
+
// Each sync costs a call per file, so batches are large; the last
|
|
78
|
+
// quarter of the limit takes writes while the older batch syncs.
|
|
79
|
+
if (!this.pending && this.bytes >= (this.limit * 3) / 4) this.flush();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
flush() {
|
|
83
|
+
this.check();
|
|
84
|
+
const files = [...this.files.keys()];
|
|
85
|
+
Atomics.store(this.state, 0, files.length);
|
|
86
|
+
Atomics.store(this.state, 1, 0);
|
|
87
|
+
this.pending = { files: this.files, bytes: this.bytes };
|
|
88
|
+
this.submit(files, this.state);
|
|
89
|
+
this.files = new Map();
|
|
90
|
+
this.bytes = 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Before a file is closed, its outstanding syncs must finish: otherwise a
|
|
94
|
+
// queued sync could operate on a different file that reuses its descriptor.
|
|
95
|
+
synced(fd) {
|
|
96
|
+
if (this.pending?.files.has(fd)) this.wait(false);
|
|
97
|
+
this.bytes -= this.files.get(fd) ?? 0;
|
|
98
|
+
this.files.delete(fd);
|
|
99
|
+
}
|
|
100
|
+
}
|