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/bin/rad/splats.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
quaternion,
|
|
13
13
|
} from "./math.js";
|
|
14
14
|
import { Progress } from "./progress.js";
|
|
15
|
-
import { IndexArray, PagedArray, Permutation } from "./storage.js";
|
|
15
|
+
import { IndexArray, PAGE_BYTES, PagedArray, Permutation } from "./storage.js";
|
|
16
16
|
|
|
17
17
|
export const SH_DIMS = [9, 15, 21];
|
|
18
18
|
|
|
@@ -75,8 +75,15 @@ export class Splats {
|
|
|
75
75
|
// Each splat's merge weight, area × opacity, which merge() reads instead
|
|
76
76
|
// of two records.
|
|
77
77
|
this.weight = new PagedArray(workspace, Float64Array);
|
|
78
|
+
// Each splat's feature size, for the passes that read nothing else of a
|
|
79
|
+
// record, and the bounds of the centers; see reorder().
|
|
80
|
+
this.sizes = new PagedArray(workspace, Float32Array);
|
|
81
|
+
this.bounds = undefined;
|
|
78
82
|
// The two child weights of each merged splat, for mergeSh().
|
|
79
83
|
this.blend = new PagedArray(workspace, Float64Array);
|
|
84
|
+
// Each decoded splat's row in the SH columns: as decoded, and from
|
|
85
|
+
// mergeSh() on, in the order merges read them; see shRow().
|
|
86
|
+
this.shOrder = undefined;
|
|
80
87
|
this.merged = new Float64Array(16);
|
|
81
88
|
this.bits = new Uint16Array(0);
|
|
82
89
|
this.merger = new Merger();
|
|
@@ -133,9 +140,6 @@ export class Splats {
|
|
|
133
140
|
read(index, output) {
|
|
134
141
|
return this.records.read(index * 16, output, 16);
|
|
135
142
|
}
|
|
136
|
-
feature(index) {
|
|
137
|
-
return this.records.get(index * 16 + 14);
|
|
138
|
-
}
|
|
139
143
|
|
|
140
144
|
// Skips empty splats and counts invalid ones in `invalid`: NaN or infinite
|
|
141
145
|
// values, including a zero rotation, which the decoder normalizes to NaN.
|
|
@@ -192,6 +196,8 @@ export class Splats {
|
|
|
192
196
|
this.sh[band].write(count * dims, rows[band]);
|
|
193
197
|
}
|
|
194
198
|
}
|
|
199
|
+
// The record's page is cached.
|
|
200
|
+
this.sizes.set(count, this.records.get(count * 16 + 14));
|
|
195
201
|
count++;
|
|
196
202
|
}
|
|
197
203
|
progress.update(base + size);
|
|
@@ -226,6 +232,7 @@ export class Splats {
|
|
|
226
232
|
const index = this.length++;
|
|
227
233
|
this.records.write(index * 16, out);
|
|
228
234
|
this.weight.set(index, out[15] * out[3]);
|
|
235
|
+
this.sizes.set(index, out[14]);
|
|
229
236
|
this.children.push(a);
|
|
230
237
|
this.children.push(b);
|
|
231
238
|
// SH coefficients do not affect merging; mergeSh() blends them later.
|
|
@@ -236,46 +243,105 @@ export class Splats {
|
|
|
236
243
|
return index;
|
|
237
244
|
}
|
|
238
245
|
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
// rest wait for their children in the batch.
|
|
246
|
+
// Blend each SH band in creation order, children before parents. Reorder
|
|
247
|
+
// leaves for sequential reads; only merged children need random reads.
|
|
248
|
+
// Batches use at most half the cache for child pages. Workers handle
|
|
249
|
+
// earlier children; children within the batch are blended below in order.
|
|
244
250
|
mergeSh(initial, pool) {
|
|
251
|
+
if (!this.maxSh) return;
|
|
245
252
|
const { nodes, entries, later, blends, blended, results } = pool;
|
|
253
|
+
// Each leaf occurs once, as a merge child or the lone root. Replace its
|
|
254
|
+
// decoded SH row with its position in merge-read order.
|
|
255
|
+
const reads = new IndexArray(this.workspace);
|
|
256
|
+
const collecting = new Progress("Collecting SH order", this.length);
|
|
257
|
+
for (let k = 0; k < (this.length - initial) * 2; k++) {
|
|
258
|
+
const child = this.children.get(k);
|
|
259
|
+
if (child < initial) {
|
|
260
|
+
reads.push(this.shOrder.get(child));
|
|
261
|
+
this.shOrder.set(child, reads.length - 1);
|
|
262
|
+
}
|
|
263
|
+
if ((k & 4095) === 0) collecting.update(k / 2);
|
|
264
|
+
}
|
|
265
|
+
if (this.length === initial) {
|
|
266
|
+
reads.push(this.shOrder.get(0));
|
|
267
|
+
this.shOrder.set(0, 0);
|
|
268
|
+
}
|
|
269
|
+
collecting.finish();
|
|
270
|
+
const leafRows = new Permutation(
|
|
271
|
+
this.workspace,
|
|
272
|
+
reads,
|
|
273
|
+
initial,
|
|
274
|
+
"Preparing SH order",
|
|
275
|
+
);
|
|
276
|
+
reads.dispose();
|
|
246
277
|
// Splats per batch.
|
|
247
278
|
const size = later.length;
|
|
248
279
|
const rows = [this.shLeft, this.shRight];
|
|
249
280
|
for (let band = 0; band < this.maxSh; band++) {
|
|
281
|
+
const dims = SH_DIMS[band];
|
|
282
|
+
const column = leafRows.apply(
|
|
283
|
+
this.sh[band],
|
|
284
|
+
dims,
|
|
285
|
+
pool,
|
|
286
|
+
false,
|
|
287
|
+
`Reordering SH${band + 1}`,
|
|
288
|
+
);
|
|
289
|
+
this.sh[band] = column;
|
|
250
290
|
const progress = new Progress(
|
|
251
291
|
`Blending SH${band + 1}`,
|
|
252
292
|
this.length - initial,
|
|
253
293
|
);
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
294
|
+
// The batch that last counted each page of the column.
|
|
295
|
+
const counted = new Float64Array(
|
|
296
|
+
Math.ceil((this.length * dims) / column.items),
|
|
297
|
+
);
|
|
298
|
+
let batch = 0;
|
|
299
|
+
// The row of the next decoded splat that a merge reads.
|
|
300
|
+
let next = 0;
|
|
301
|
+
for (let start = initial; start < this.length; ) {
|
|
302
|
+
const limit = Math.min(size, this.length - start);
|
|
303
|
+
const pages = Math.max(
|
|
304
|
+
1,
|
|
305
|
+
Math.floor(this.workspace.capacity / PAGE_BYTES / 2),
|
|
306
|
+
);
|
|
307
|
+
batch++;
|
|
308
|
+
let taken = 0;
|
|
309
|
+
let count = 0;
|
|
258
310
|
// Load the pages of the children's rows for the workers. Consecutive
|
|
259
311
|
// splats' children often share pages; a page evicted meanwhile is a
|
|
260
312
|
// miss that the main thread blends.
|
|
261
313
|
let left = -1;
|
|
262
314
|
let right = -1;
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
315
|
+
while (count < limit) {
|
|
316
|
+
const k = (start + count - initial) * 2;
|
|
317
|
+
const a = this.children.get(k);
|
|
318
|
+
const b = this.children.get(k + 1);
|
|
319
|
+
const ra = a < initial ? next : a;
|
|
320
|
+
const rb = b < initial ? next + Number(a < initial) : b;
|
|
321
|
+
const pa = Math.floor((ra * dims) / column.items);
|
|
322
|
+
const pb = Math.floor((rb * dims) / column.items);
|
|
323
|
+
let added = Number(ra < start && counted[pa] !== batch);
|
|
324
|
+
if (rb < start && counted[pb] !== batch && !(added && pb === pa))
|
|
325
|
+
added++;
|
|
326
|
+
if (count && taken + added > pages) break;
|
|
327
|
+
taken += added;
|
|
328
|
+
if (ra < start) counted[pa] = batch;
|
|
329
|
+
if (rb < start) counted[pb] = batch;
|
|
330
|
+
next += Number(a < initial) + Number(b < initial);
|
|
331
|
+
const i = count++;
|
|
332
|
+
nodes[i] = ra;
|
|
333
|
+
entries[i] = rb;
|
|
334
|
+
blends[i * 2] = this.blend.get(k);
|
|
335
|
+
blends[i * 2 + 1] = this.blend.get(k + 1);
|
|
336
|
+
later[i] = Number(ra >= start || rb >= start);
|
|
271
337
|
if (later[i]) continue;
|
|
272
|
-
if (
|
|
273
|
-
left =
|
|
274
|
-
column.page(
|
|
338
|
+
if (pa !== left) {
|
|
339
|
+
left = pa;
|
|
340
|
+
column.page(ra * dims);
|
|
275
341
|
}
|
|
276
|
-
if (
|
|
277
|
-
right =
|
|
278
|
-
column.page(
|
|
342
|
+
if (pb !== right) {
|
|
343
|
+
right = pb;
|
|
344
|
+
column.page(rb * dims);
|
|
279
345
|
}
|
|
280
346
|
}
|
|
281
347
|
pool.blend(column, count, dims);
|
|
@@ -297,14 +363,19 @@ export class Splats {
|
|
|
297
363
|
);
|
|
298
364
|
}
|
|
299
365
|
column.write(start * dims, blended, count * dims);
|
|
300
|
-
|
|
366
|
+
start += count;
|
|
367
|
+
progress.update(start - initial);
|
|
301
368
|
}
|
|
302
369
|
progress.finish();
|
|
303
370
|
}
|
|
371
|
+
leafRows.dispose();
|
|
304
372
|
this.blend.dispose();
|
|
305
373
|
}
|
|
306
374
|
|
|
307
|
-
// Moves row indices[i] of the records
|
|
375
|
+
// Moves row indices[i] of the records to row i, and keeps `indices` for
|
|
376
|
+
// the SH rows, which move once merging has built the tree; see mergeSh().
|
|
377
|
+
// The splats' sizes and weights, and the bounds of their centers, come
|
|
378
|
+
// from the records while each window of them is cached.
|
|
308
379
|
reorder(indices, pool) {
|
|
309
380
|
const permutation = new Permutation(
|
|
310
381
|
this.workspace,
|
|
@@ -312,23 +383,43 @@ export class Splats {
|
|
|
312
383
|
this.length,
|
|
313
384
|
"Preparing splat order",
|
|
314
385
|
);
|
|
386
|
+
this.sizes.dispose();
|
|
387
|
+
const sizes = this.sizes;
|
|
388
|
+
const bounds = {
|
|
389
|
+
min: [0, 1, 2].map(() => Number.POSITIVE_INFINITY),
|
|
390
|
+
max: [0, 1, 2].map(() => Number.NEGATIVE_INFINITY),
|
|
391
|
+
};
|
|
392
|
+
const record = new Float64Array(16);
|
|
315
393
|
this.records = permutation.apply(
|
|
316
394
|
this.records,
|
|
317
395
|
16,
|
|
318
396
|
pool,
|
|
319
397
|
false,
|
|
320
398
|
"Reordering splats",
|
|
399
|
+
(records, start, end) => {
|
|
400
|
+
for (let i = start; i < end; i++) {
|
|
401
|
+
records.read(i * 16, record, 16);
|
|
402
|
+
for (let d = 0; d < 3; d++) {
|
|
403
|
+
bounds.min[d] = Math.min(bounds.min[d], record[d]);
|
|
404
|
+
bounds.max[d] = Math.max(bounds.max[d], record[d]);
|
|
405
|
+
}
|
|
406
|
+
this.weight.set(i, record[15] * record[3]);
|
|
407
|
+
sizes.set(i, record[14]);
|
|
408
|
+
}
|
|
409
|
+
},
|
|
321
410
|
);
|
|
322
|
-
|
|
323
|
-
this.sh[band] = permutation.apply(
|
|
324
|
-
this.sh[band],
|
|
325
|
-
SH_DIMS[band],
|
|
326
|
-
pool,
|
|
327
|
-
false,
|
|
328
|
-
`Reordering SH${band + 1}`,
|
|
329
|
-
);
|
|
411
|
+
this.bounds = bounds;
|
|
330
412
|
permutation.dispose();
|
|
331
413
|
this.length = indices.length;
|
|
414
|
+
if (this.maxSh) this.shOrder = indices;
|
|
415
|
+
else indices.dispose();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// The row of splat `index` in the SH columns after mergeSh(): merged
|
|
419
|
+
// splats keep theirs, and the decoded ones are in the order merges read
|
|
420
|
+
// them.
|
|
421
|
+
shRow(index) {
|
|
422
|
+
return index < this.shOrder.length ? this.shOrder.get(index) : index;
|
|
332
423
|
}
|
|
333
424
|
|
|
334
425
|
dispose() {
|
|
@@ -339,6 +430,7 @@ export class Splats {
|
|
|
339
430
|
this.childCount,
|
|
340
431
|
this.children,
|
|
341
432
|
this.weight,
|
|
433
|
+
this.sizes,
|
|
342
434
|
this.blend,
|
|
343
435
|
])
|
|
344
436
|
column.dispose();
|
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
|
+
}
|