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.
- package/CHANGELOG.md +36 -2
- package/CONTRIBUTING.md +14 -2
- package/NOTICE +11 -3
- package/README.md +5 -7
- package/THIRD_PARTY_LICENSES.md +64 -6
- package/bin/build-lod.js +241 -0
- package/bin/decoder/decoder.js +340 -0
- package/bin/decoder/decoder_bg.wasm +0 -0
- package/bin/rad/grid.js +620 -0
- package/bin/rad/heap.js +270 -0
- package/bin/rad/input.js +87 -0
- package/bin/rad/lod.js +657 -0
- package/bin/rad/math.js +166 -0
- package/bin/rad/parallel.js +578 -0
- package/bin/rad/progress.js +50 -0
- package/bin/rad/rad.js +612 -0
- package/bin/rad/splats.js +432 -0
- package/bin/rad/storage.js +776 -0
- package/dist/gaussian-splat-lite.cjs +392 -794
- package/dist/gaussian-splat-lite.cjs.map +1 -1
- package/dist/gaussian-splat-lite.min.cjs +246 -308
- package/dist/gaussian-splat-lite.min.cjs.map +1 -1
- package/dist/gaussian-splat-lite.module.js +394 -796
- package/dist/gaussian-splat-lite.module.js.map +1 -1
- package/dist/gaussian-splat-lite.module.min.js +2518 -2802
- package/dist/gaussian-splat-lite.module.min.js.map +1 -1
- package/dist/types/addons/NeuralDenoiseNode.d.cts +1 -2
- package/dist/types/addons/NeuralDenoiseNode.d.ts +1 -2
- package/dist/types/addons/NeuralDenoiseXR.d.cts +31 -0
- package/dist/types/addons/NeuralDenoiseXR.d.ts +31 -0
- package/dist/types/addons/neuralDenoiseState.d.cts +22 -0
- package/dist/types/addons/neuralDenoiseState.d.ts +22 -0
- package/dist/types/addons/neuralDenoiseWeights.d.cts +41 -2
- package/dist/types/addons/neuralDenoiseWeights.d.ts +41 -2
- package/dist/types/data/RadPagedSplats.d.cts +1 -1
- package/dist/types/data/RadPagedSplats.d.ts +1 -1
- package/dist/types/index.d.cts +1 -2
- package/dist/types/index.d.ts +1 -2
- package/dist/types/loaders/rad/radFormat.d.cts +2 -2
- package/dist/types/loaders/rad/radFormat.d.ts +2 -2
- package/docs/BuildRad.md +91 -0
- package/docs/GaussianSplatRenderer.md +2 -2
- package/docs/NeuralDenoiseNode.md +32 -13
- package/docs/RadStreamScheduler.md +3 -1
- package/docs/StochasticRendering.md +20 -42
- package/package.json +9 -3
- package/dist/types/addons/TAANode.d.cts +0 -49
- package/dist/types/addons/TAANode.d.ts +0 -49
- package/dist/types/addons/TAAPass.d.cts +0 -57
- package/dist/types/addons/TAAPass.d.ts +0 -57
- package/dist/types/addons/taaShared.d.cts +0 -74
- package/dist/types/addons/taaShared.d.ts +0 -74
- package/docs/TAAPass.md +0 -184
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
import { availableParallelism } from "node:os";
|
|
2
|
+
import {
|
|
3
|
+
Worker,
|
|
4
|
+
isMainThread,
|
|
5
|
+
parentPort,
|
|
6
|
+
receiveMessageOnPort,
|
|
7
|
+
workerData,
|
|
8
|
+
} from "node:worker_threads";
|
|
9
|
+
import { Lookup, NONE } from "./grid.js";
|
|
10
|
+
import { HALF, halfBits } from "./math.js";
|
|
11
|
+
import {
|
|
12
|
+
EMPTY,
|
|
13
|
+
INVALID,
|
|
14
|
+
KEEP,
|
|
15
|
+
LATER,
|
|
16
|
+
Merger,
|
|
17
|
+
SH_DIMS,
|
|
18
|
+
Similarities,
|
|
19
|
+
refresh,
|
|
20
|
+
validRecord,
|
|
21
|
+
} from "./splats.js";
|
|
22
|
+
import { ArenaViews, Parts, SharedColumn, collect } from "./storage.js";
|
|
23
|
+
|
|
24
|
+
// A search that the main thread does: a merge failed.
|
|
25
|
+
export const MISS = 0xfffffffe;
|
|
26
|
+
// The most searches in one round.
|
|
27
|
+
export const ROUND = 32768;
|
|
28
|
+
// Control words.
|
|
29
|
+
const GEN = 0;
|
|
30
|
+
const NEXT = 1;
|
|
31
|
+
const COUNT = 2;
|
|
32
|
+
const DONE = 3;
|
|
33
|
+
const VERSION = 4;
|
|
34
|
+
const JOB = 5;
|
|
35
|
+
const SIZE = 6;
|
|
36
|
+
const DIMS = 7;
|
|
37
|
+
// The length + 1 of a failed worker's error message in `failure`.
|
|
38
|
+
const FAILED = 8;
|
|
39
|
+
// A copy's row stride, first source row and target window.
|
|
40
|
+
const STRIDE = 10;
|
|
41
|
+
const BASE = 11;
|
|
42
|
+
const START = 12;
|
|
43
|
+
const END = 13;
|
|
44
|
+
// The workers that have collected the views of released arenas.
|
|
45
|
+
const COLLECTED = 14;
|
|
46
|
+
// Jobs.
|
|
47
|
+
const SEARCH = 0;
|
|
48
|
+
const BLEND = 1;
|
|
49
|
+
const COPY = 2;
|
|
50
|
+
const CHECK = 3;
|
|
51
|
+
const COLLECT = 4;
|
|
52
|
+
const TYPES = { Float32Array, Float64Array, Uint16Array, Uint32Array };
|
|
53
|
+
const MAX_DIMS = SH_DIMS[SH_DIMS.length - 1];
|
|
54
|
+
|
|
55
|
+
// Memory each LOD worker thread takes and each encoding worker thread with
|
|
56
|
+
// its chunk, with young generations of YOUNG_MB, measured on Linux and macOS;
|
|
57
|
+
// what the main thread grows by during the LOD; and the chunks the main
|
|
58
|
+
// thread holds for the encoders.
|
|
59
|
+
const WORKER_BYTES = 40 * 1024 ** 2;
|
|
60
|
+
const ENCODER_BYTES = 52 * 1024 ** 2;
|
|
61
|
+
const MAIN_BYTES = 64 * 1024 ** 2;
|
|
62
|
+
const CHUNKS_BYTES = 24 * 1024 ** 2;
|
|
63
|
+
export const YOUNG_MB = 8;
|
|
64
|
+
|
|
65
|
+
// Worker threads with `bytes` each: at most 8, using at most `share` of the
|
|
66
|
+
// budget and half the room left besides `extra`, as measured now.
|
|
67
|
+
function threads(workspace, bytes, extra, share = 1 / 4) {
|
|
68
|
+
workspace.settle();
|
|
69
|
+
return Math.max(
|
|
70
|
+
0,
|
|
71
|
+
Math.min(
|
|
72
|
+
availableParallelism() - 1,
|
|
73
|
+
8,
|
|
74
|
+
Math.floor((workspace.budget * share) / bytes),
|
|
75
|
+
Math.floor((workspace.room - extra) / 2 / bytes),
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Makes room for the encoding worker threads of `chunks` and returns how
|
|
81
|
+
// many there are.
|
|
82
|
+
export function encoders(workspace, chunks) {
|
|
83
|
+
const count = Math.min(
|
|
84
|
+
threads(workspace, ENCODER_BYTES, CHUNKS_BYTES),
|
|
85
|
+
chunks - 1,
|
|
86
|
+
);
|
|
87
|
+
workspace.expect(count * ENCODER_BYTES + CHUNKS_BYTES);
|
|
88
|
+
return count;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const shared = (Type, size) =>
|
|
92
|
+
new Type(new SharedArrayBuffer(size * Type.BYTES_PER_ELEMENT));
|
|
93
|
+
|
|
94
|
+
// Whether a state value is unchanged; lists of directories compare by item.
|
|
95
|
+
const same = (a, b) =>
|
|
96
|
+
a === b ||
|
|
97
|
+
(Array.isArray(a) &&
|
|
98
|
+
Array.isArray(b) &&
|
|
99
|
+
a.length === b.length &&
|
|
100
|
+
a.every((value, i) => value === b[i]));
|
|
101
|
+
|
|
102
|
+
// Runs jobs over the pages of shared PagedArrays in worker threads, with the
|
|
103
|
+
// main thread taking part: the searches and merges of a round of LOD merges,
|
|
104
|
+
// which the main thread then commits in order (see mergeLevels()), SH
|
|
105
|
+
// blending, column permutations and the checks of decoded splats. Searches
|
|
106
|
+
// read pages that are not cached from their files; the other jobs leave
|
|
107
|
+
// what is not cached to the main thread.
|
|
108
|
+
export class Pool {
|
|
109
|
+
// Workers pay off for jobs over at least a batch of `splats`.
|
|
110
|
+
constructor(workspace, splats) {
|
|
111
|
+
this.workspace = workspace;
|
|
112
|
+
// Once the LOD grid's entries, 64 bytes a splat, exceed three times the
|
|
113
|
+
// budget, reads of pages dominate: the system serves no more of them
|
|
114
|
+
// with more threads, and fewer threads leave more room for the cache.
|
|
115
|
+
// The threads' share of the budget falls from a quarter in proportion,
|
|
116
|
+
// to an eighth at twice that, below which it saves little.
|
|
117
|
+
const share = Math.max(
|
|
118
|
+
1 / 8,
|
|
119
|
+
Math.min(1 / 4, (0.75 * workspace.budget) / (splats * 64)),
|
|
120
|
+
);
|
|
121
|
+
const count =
|
|
122
|
+
splats < ROUND ? 0 : threads(workspace, WORKER_BYTES, MAIN_BYTES, share);
|
|
123
|
+
this.shared = {
|
|
124
|
+
cancelled: workspace.cancelled,
|
|
125
|
+
control: shared(Int32Array, 16),
|
|
126
|
+
// Bytes that threads read from files, for Workspace.readBytes.
|
|
127
|
+
read: new BigInt64Array(new SharedArrayBuffer(8)),
|
|
128
|
+
failure: shared(Uint8Array, 1024),
|
|
129
|
+
// Each search's splat and its entry.
|
|
130
|
+
nodes: shared(Uint32Array, ROUND),
|
|
131
|
+
entries: shared(Uint32Array, ROUND),
|
|
132
|
+
// Its candidate's entry, NONE or MISS; unless a MISS, the splat's
|
|
133
|
+
// terms and weight and its candidate's node, the merged record and its
|
|
134
|
+
// Similarities terms and the blend weights.
|
|
135
|
+
results: shared(Uint32Array, ROUND),
|
|
136
|
+
// Its score; see Lookup.find().
|
|
137
|
+
metrics: shared(Float64Array, ROUND),
|
|
138
|
+
queries: shared(Float64Array, ROUND * 14),
|
|
139
|
+
partners: shared(Uint32Array, ROUND),
|
|
140
|
+
// The slot of the merged splat's cell if found, else NONE.
|
|
141
|
+
hints: shared(Uint32Array, ROUND),
|
|
142
|
+
records: shared(Float32Array, ROUND * 16),
|
|
143
|
+
terms: shared(Float32Array, ROUND * 13),
|
|
144
|
+
blends: shared(Float64Array, ROUND * 2),
|
|
145
|
+
// SH blending, with the children in `nodes` and `entries` and their
|
|
146
|
+
// weights in `blends`: whether the main thread blends a row, as a
|
|
147
|
+
// child is in the same batch, and the blended rows.
|
|
148
|
+
later: shared(Uint8Array, ROUND),
|
|
149
|
+
blended: shared(Uint16Array, ROUND * MAX_DIMS),
|
|
150
|
+
};
|
|
151
|
+
Object.assign(this, this.shared);
|
|
152
|
+
this.generation = 0;
|
|
153
|
+
// The workspace version and the arrays last sent, and the messages sent.
|
|
154
|
+
this.version = -1;
|
|
155
|
+
this.state = {};
|
|
156
|
+
this.sent = 0;
|
|
157
|
+
this.task = new Task(this.shared);
|
|
158
|
+
workspace.expect(count * WORKER_BYTES + MAIN_BYTES);
|
|
159
|
+
this.workers = Array.from({ length: count }, () => {
|
|
160
|
+
const worker = new Worker(new URL(import.meta.url), {
|
|
161
|
+
workerData: this.shared,
|
|
162
|
+
resourceLimits: { maxYoungGenerationSizeMb: YOUNG_MB },
|
|
163
|
+
});
|
|
164
|
+
worker.unref();
|
|
165
|
+
return worker;
|
|
166
|
+
});
|
|
167
|
+
workspace.collector = () => this.collect();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Sends the workers the arrays a job reads, when they changed.
|
|
171
|
+
share(state) {
|
|
172
|
+
if (
|
|
173
|
+
this.workspace.version === this.version &&
|
|
174
|
+
Object.entries(state).every(([key, value]) =>
|
|
175
|
+
same(this.state[key], value),
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
return;
|
|
179
|
+
this.version = this.workspace.version;
|
|
180
|
+
Object.assign(this.state, state);
|
|
181
|
+
// All arrays, so that no thread keeps views of released arenas.
|
|
182
|
+
const message = {
|
|
183
|
+
version: ++this.sent,
|
|
184
|
+
arenas: this.workspace.arenas,
|
|
185
|
+
generation: this.workspace.generation,
|
|
186
|
+
...this.state,
|
|
187
|
+
};
|
|
188
|
+
for (const worker of this.workers) worker.postMessage(message);
|
|
189
|
+
this.task.use(message);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Searches and merges for the first `count` nodes and entries, against the
|
|
193
|
+
// grid as it is.
|
|
194
|
+
search(grid, count) {
|
|
195
|
+
const arrays = [grid.entries, grid.slots, grid.gone];
|
|
196
|
+
this.share({
|
|
197
|
+
entries: grid.entries.directory,
|
|
198
|
+
slots: grid.slots.directory,
|
|
199
|
+
gone: grid.gone.directory,
|
|
200
|
+
files: arrays.map((array) => array.files()),
|
|
201
|
+
capacity: grid.capacity,
|
|
202
|
+
step: grid.step,
|
|
203
|
+
});
|
|
204
|
+
this.run(SEARCH, count, 32);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Blends the rows of `column` of the first `count` children pairs in
|
|
208
|
+
// `nodes` and `entries` with `blends` into `blended`, except rows marked
|
|
209
|
+
// `later`, and marks them 0 in `results`. Rows that are not cached are left
|
|
210
|
+
// to the main thread.
|
|
211
|
+
blend(column, count, dims) {
|
|
212
|
+
this.share({ column: column.directory });
|
|
213
|
+
Atomics.store(this.control, DIMS, dims);
|
|
214
|
+
this.run(BLEND, count, 512);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Copies `count` rows of `source` from row `base` to rows of `output` at
|
|
218
|
+
// their targets + 1 in `nodes` when those lie in [start, end). Marks rows
|
|
219
|
+
// across pages or not cached in `later`, for the main thread.
|
|
220
|
+
copy(source, output, stride, count, base, start, end, words = false) {
|
|
221
|
+
this.share({
|
|
222
|
+
source: source.directory,
|
|
223
|
+
output: output.directory,
|
|
224
|
+
type: words ? "Uint32Array" : source.Type.name,
|
|
225
|
+
});
|
|
226
|
+
const control = this.control;
|
|
227
|
+
Atomics.store(control, STRIDE, stride);
|
|
228
|
+
Atomics.store(control, BASE, base);
|
|
229
|
+
Atomics.store(control, START, start);
|
|
230
|
+
Atomics.store(control, END, end);
|
|
231
|
+
this.run(COPY, count, 1024);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Refreshes and checks `count` splats from `base` in place, setting KEEP,
|
|
235
|
+
// INVALID, EMPTY or LATER in `results`; see Splats.retain().
|
|
236
|
+
check(splats, base, count) {
|
|
237
|
+
this.share({
|
|
238
|
+
splatRecords: splats.records.directory,
|
|
239
|
+
splatSh: splats.sh
|
|
240
|
+
.slice(0, splats.maxSh)
|
|
241
|
+
.map((column) => column.directory),
|
|
242
|
+
});
|
|
243
|
+
Atomics.store(this.control, BASE, base);
|
|
244
|
+
this.run(CHECK, count, 1024);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Makes the workers drop their views of arenas that the workspace
|
|
248
|
+
// released, and collect them.
|
|
249
|
+
collect() {
|
|
250
|
+
this.share({});
|
|
251
|
+
this.run(COLLECT, 0, 1);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
run(job, count, size) {
|
|
255
|
+
this.workspace.check();
|
|
256
|
+
const gen = ++this.generation;
|
|
257
|
+
const control = this.control;
|
|
258
|
+
Atomics.store(control, JOB, job);
|
|
259
|
+
Atomics.store(control, SIZE, size);
|
|
260
|
+
Atomics.store(control, COUNT, count);
|
|
261
|
+
Atomics.store(control, DONE, 0);
|
|
262
|
+
Atomics.store(control, NEXT, (gen & 0x7fff) << 16);
|
|
263
|
+
Atomics.store(control, VERSION, this.sent);
|
|
264
|
+
Atomics.store(control, COLLECTED, 0);
|
|
265
|
+
Atomics.store(control, GEN, gen);
|
|
266
|
+
Atomics.notify(control, GEN);
|
|
267
|
+
this.task.begin();
|
|
268
|
+
this.task.work(gen);
|
|
269
|
+
for (;;) {
|
|
270
|
+
this.workspace.check();
|
|
271
|
+
const done = Atomics.load(control, DONE);
|
|
272
|
+
if (done >= count) break;
|
|
273
|
+
Atomics.wait(control, DONE, done, 100);
|
|
274
|
+
}
|
|
275
|
+
this.workspace.readBytes += Number(Atomics.exchange(this.read, 0, 0n));
|
|
276
|
+
while (job === COLLECT) {
|
|
277
|
+
this.workspace.check();
|
|
278
|
+
const collected = Atomics.load(control, COLLECTED);
|
|
279
|
+
// A failed worker no longer answers; its error is thrown below.
|
|
280
|
+
if (collected >= this.workers.length || Atomics.load(control, FAILED))
|
|
281
|
+
break;
|
|
282
|
+
Atomics.wait(control, COLLECTED, collected, 100);
|
|
283
|
+
}
|
|
284
|
+
const failed = Atomics.load(control, FAILED);
|
|
285
|
+
if (failed)
|
|
286
|
+
throw new Error(
|
|
287
|
+
`LOD worker failed: ${new TextDecoder().decode(this.failure.slice(0, failed - 1))}`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Stops the workers. Their views and the main thread's would keep arenas
|
|
292
|
+
// that the workspace releases.
|
|
293
|
+
async close() {
|
|
294
|
+
Atomics.store(this.control, GEN, -1);
|
|
295
|
+
Atomics.notify(this.control, GEN);
|
|
296
|
+
await Promise.all(this.workers.map((worker) => worker.terminate()));
|
|
297
|
+
this.task = undefined;
|
|
298
|
+
this.workspace.collector = undefined;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// The slot of the cell of merged center `m` that the last find visited: the
|
|
303
|
+
// query's cell or the cell of its best, at `b`; NONE if neither.
|
|
304
|
+
export function hint(lookup, m, q, b) {
|
|
305
|
+
const inverse = lookup.inverse;
|
|
306
|
+
const x = Math.floor(m[0] * inverse);
|
|
307
|
+
const y = Math.floor(m[1] * inverse);
|
|
308
|
+
const z = Math.floor(m[2] * inverse);
|
|
309
|
+
if (
|
|
310
|
+
x === Math.floor(q[0] * inverse) &&
|
|
311
|
+
y === Math.floor(q[1] * inverse) &&
|
|
312
|
+
z === Math.floor(q[2] * inverse)
|
|
313
|
+
)
|
|
314
|
+
return lookup.home;
|
|
315
|
+
if (
|
|
316
|
+
x === Math.floor(b[0] * inverse) &&
|
|
317
|
+
y === Math.floor(b[1] * inverse) &&
|
|
318
|
+
z === Math.floor(b[2] * inverse)
|
|
319
|
+
)
|
|
320
|
+
return lookup.near;
|
|
321
|
+
return NONE;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// One thread's share of the rounds.
|
|
325
|
+
class Task {
|
|
326
|
+
constructor(shared) {
|
|
327
|
+
Object.assign(this, shared);
|
|
328
|
+
this.version = -1;
|
|
329
|
+
this.query = new Float64Array(14);
|
|
330
|
+
this.other = new Float64Array(14);
|
|
331
|
+
this.merged = new Float64Array(16);
|
|
332
|
+
this.tuple = new Float64Array(13);
|
|
333
|
+
this.merger = new Merger();
|
|
334
|
+
this.similarities = new Similarities();
|
|
335
|
+
// For the columns of searches: entries, slots and gone.
|
|
336
|
+
this.parts = [Float32Array, Uint32Array, Uint32Array].map(
|
|
337
|
+
(Type) => new Parts(Type),
|
|
338
|
+
);
|
|
339
|
+
this.views = new ArenaViews();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
use(state) {
|
|
343
|
+
this.version = state.version;
|
|
344
|
+
this.views.use(state.arenas, state.generation);
|
|
345
|
+
const views = this.views;
|
|
346
|
+
if (state.entries) {
|
|
347
|
+
const [entries, slots, gone] = [
|
|
348
|
+
[state.entries, Float32Array],
|
|
349
|
+
[state.slots, Uint32Array],
|
|
350
|
+
[state.gone, Uint32Array],
|
|
351
|
+
].map(
|
|
352
|
+
([directory, Type], i) =>
|
|
353
|
+
new SharedColumn(
|
|
354
|
+
views,
|
|
355
|
+
directory,
|
|
356
|
+
Type,
|
|
357
|
+
0,
|
|
358
|
+
state.files[i],
|
|
359
|
+
this.parts[i],
|
|
360
|
+
),
|
|
361
|
+
);
|
|
362
|
+
this.lookup = new Lookup(
|
|
363
|
+
entries,
|
|
364
|
+
slots,
|
|
365
|
+
gone,
|
|
366
|
+
state.capacity,
|
|
367
|
+
state.step,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
if (state.column)
|
|
371
|
+
this.column = new SharedColumn(views, state.column, Uint16Array);
|
|
372
|
+
if (state.splatRecords) {
|
|
373
|
+
this.splatRecords = new SharedColumn(
|
|
374
|
+
views,
|
|
375
|
+
state.splatRecords,
|
|
376
|
+
Float32Array,
|
|
377
|
+
);
|
|
378
|
+
this.splatSh = state.splatSh.map(
|
|
379
|
+
(directory) => new SharedColumn(views, directory, Uint16Array),
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
if (state.source) {
|
|
383
|
+
const Type = TYPES[state.type];
|
|
384
|
+
this.source = new SharedColumn(views, state.source, Type);
|
|
385
|
+
this.output = new SharedColumn(views, state.output, Type);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Forgets what searches read from files: pages change between jobs.
|
|
390
|
+
begin() {
|
|
391
|
+
for (const parts of this.parts) parts.reset();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Claims chunks of the round `gen` until none are left.
|
|
395
|
+
work(gen) {
|
|
396
|
+
const control = this.control;
|
|
397
|
+
for (;;) {
|
|
398
|
+
if (this.cancelled && Atomics.load(this.cancelled, 0)) return;
|
|
399
|
+
const next = Atomics.load(control, NEXT);
|
|
400
|
+
if (next >>> 16 !== (gen & 0x7fff)) return;
|
|
401
|
+
const start = next & 0xffff;
|
|
402
|
+
if (start >= Atomics.load(control, COUNT)) return;
|
|
403
|
+
const size = Atomics.load(control, SIZE);
|
|
404
|
+
if (Atomics.compareExchange(control, NEXT, next, next + size) !== next)
|
|
405
|
+
continue;
|
|
406
|
+
const count = Atomics.load(control, COUNT);
|
|
407
|
+
const end = Math.min(start + size, count);
|
|
408
|
+
const job = Atomics.load(control, JOB);
|
|
409
|
+
if (job === SEARCH)
|
|
410
|
+
for (let t = start; t < end; t++) this.results[t] = this.search(t);
|
|
411
|
+
else if (job === BLEND)
|
|
412
|
+
this.blend(start, end, Atomics.load(control, DIMS));
|
|
413
|
+
else if (job === COPY) this.copy(start, end);
|
|
414
|
+
else {
|
|
415
|
+
const base = Atomics.load(control, BASE);
|
|
416
|
+
for (let i = start; i < end; i++)
|
|
417
|
+
this.results[i] = this.check(base + i);
|
|
418
|
+
}
|
|
419
|
+
// Before the chunk counts as done, so the main thread finds it.
|
|
420
|
+
let bytes = 0;
|
|
421
|
+
for (const parts of this.parts) {
|
|
422
|
+
bytes += parts.bytesRead;
|
|
423
|
+
parts.bytesRead = 0;
|
|
424
|
+
}
|
|
425
|
+
if (bytes) Atomics.add(this.read, 0, BigInt(bytes));
|
|
426
|
+
if (Atomics.add(control, DONE, end - start) + end - start === count)
|
|
427
|
+
Atomics.notify(control, DONE);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
check(index) {
|
|
432
|
+
const { splatRecords: records, merged: record } = this;
|
|
433
|
+
records.missed = false;
|
|
434
|
+
const values = records.page(index * 16);
|
|
435
|
+
const offset = records.offset(index * 16);
|
|
436
|
+
if (records.missed) return LATER;
|
|
437
|
+
for (let d = 0; d < 16; d++) record[d] = values[offset + d];
|
|
438
|
+
refresh(record);
|
|
439
|
+
values[offset + 14] = record[14];
|
|
440
|
+
values[offset + 15] = record[15];
|
|
441
|
+
if (!validRecord(record)) return INVALID;
|
|
442
|
+
for (let band = 0; band < this.splatSh.length; band++) {
|
|
443
|
+
const dims = SH_DIMS[band];
|
|
444
|
+
const column = this.splatSh[band];
|
|
445
|
+
column.missed = false;
|
|
446
|
+
const row = column.page(index * dims);
|
|
447
|
+
const at = column.offset(index * dims);
|
|
448
|
+
if (column.missed || ((index * dims) & column.mask) > column.items - dims)
|
|
449
|
+
return LATER;
|
|
450
|
+
for (let d = 0; d < dims; d++)
|
|
451
|
+
if (!Number.isFinite(HALF[row[at + d]])) return INVALID;
|
|
452
|
+
}
|
|
453
|
+
return record[3] > 0 && record[14] > 0 ? KEEP : EMPTY;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
copy(from, to) {
|
|
457
|
+
const { source, output, nodes, later, control } = this;
|
|
458
|
+
const stride = Atomics.load(control, STRIDE);
|
|
459
|
+
const base = Atomics.load(control, BASE);
|
|
460
|
+
const start = Atomics.load(control, START);
|
|
461
|
+
const end = Atomics.load(control, END);
|
|
462
|
+
const last = source.items - stride;
|
|
463
|
+
for (let i = from; i < to; i++) {
|
|
464
|
+
later[i] = 0;
|
|
465
|
+
const target = nodes[i] - 1;
|
|
466
|
+
if (target < start || target >= end) continue;
|
|
467
|
+
const s = (base + i) * stride;
|
|
468
|
+
const t = target * stride;
|
|
469
|
+
source.missed = false;
|
|
470
|
+
output.missed = false;
|
|
471
|
+
const values = source.page(s);
|
|
472
|
+
const offset = source.offset(s);
|
|
473
|
+
const out = output.page(t);
|
|
474
|
+
const at = output.offset(t);
|
|
475
|
+
if (
|
|
476
|
+
source.missed ||
|
|
477
|
+
output.missed ||
|
|
478
|
+
(s & source.mask) > last ||
|
|
479
|
+
(t & output.mask) > last
|
|
480
|
+
) {
|
|
481
|
+
later[i] = 1;
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
for (let d = 0; d < stride; d++) out[at + d] = values[offset + d];
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
blend(start, end, dims) {
|
|
489
|
+
const { column, nodes, entries, blends, later, blended, results } = this;
|
|
490
|
+
const last = column.items - dims;
|
|
491
|
+
for (let i = start; i < end; i++) {
|
|
492
|
+
if (later[i]) continue;
|
|
493
|
+
const a = nodes[i] * dims;
|
|
494
|
+
const b = entries[i] * dims;
|
|
495
|
+
column.missed = false;
|
|
496
|
+
const left = column.page(a);
|
|
497
|
+
const l = column.offset(a);
|
|
498
|
+
const right = column.page(b);
|
|
499
|
+
const r = column.offset(b);
|
|
500
|
+
// A row across two pages is left to the main thread.
|
|
501
|
+
if (
|
|
502
|
+
column.missed ||
|
|
503
|
+
(a & column.mask) > last ||
|
|
504
|
+
(b & column.mask) > last
|
|
505
|
+
) {
|
|
506
|
+
results[i] = MISS;
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
results[i] = 0;
|
|
510
|
+
const w = blends[i * 2];
|
|
511
|
+
const v = blends[i * 2 + 1];
|
|
512
|
+
for (let d = 0; d < dims; d++)
|
|
513
|
+
blended[i * dims + d] = halfBits(
|
|
514
|
+
HALF[left[l + d]] * w + HALF[right[r + d]] * v,
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
search(t) {
|
|
520
|
+
const { lookup, query, other, merged } = this;
|
|
521
|
+
lookup.read(this.entries[t], query);
|
|
522
|
+
const best = lookup.find(query, this.nodes[t]);
|
|
523
|
+
this.metrics[t] = lookup.metric;
|
|
524
|
+
this.queries.set(query, t * 14);
|
|
525
|
+
if (best === NONE) return best;
|
|
526
|
+
this.partners[t] = lookup.read(best, other);
|
|
527
|
+
try {
|
|
528
|
+
this.merger.run(query, other, query[13], other[13], merged);
|
|
529
|
+
} catch {
|
|
530
|
+
// The main thread merges again and reports the error.
|
|
531
|
+
return MISS;
|
|
532
|
+
}
|
|
533
|
+
this.records.set(merged, t * 16);
|
|
534
|
+
this.hints[t] = hint(lookup, merged, query, other);
|
|
535
|
+
this.terms.set(this.similarities.terms(merged, this.tuple), t * 13);
|
|
536
|
+
this.blends[t * 2] = this.merger.w;
|
|
537
|
+
this.blends[t * 2 + 1] = this.merger.v;
|
|
538
|
+
return best;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (!isMainThread && workerData?.control) {
|
|
543
|
+
const task = new Task(workerData);
|
|
544
|
+
const control = task.control;
|
|
545
|
+
for (let seen = 0; ; ) {
|
|
546
|
+
Atomics.wait(control, GEN, seen);
|
|
547
|
+
const gen = Atomics.load(control, GEN);
|
|
548
|
+
if (gen < 0) break;
|
|
549
|
+
seen = gen;
|
|
550
|
+
try {
|
|
551
|
+
const version = Atomics.load(control, VERSION);
|
|
552
|
+
while (task.version < version) {
|
|
553
|
+
const received = receiveMessageOnPort(parentPort);
|
|
554
|
+
if (received) task.use(received.message);
|
|
555
|
+
}
|
|
556
|
+
task.begin();
|
|
557
|
+
if (Atomics.load(control, JOB) === COLLECT) {
|
|
558
|
+
collect();
|
|
559
|
+
Atomics.add(control, COLLECTED, 1);
|
|
560
|
+
Atomics.notify(control, COLLECTED);
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
task.work(gen);
|
|
564
|
+
} catch (error) {
|
|
565
|
+
// Report the error and release the round, so the main thread throws it.
|
|
566
|
+
// The thread ends, and its views with it, so it counts as collected.
|
|
567
|
+
const message = new TextEncoder().encode(String(error?.stack ?? error));
|
|
568
|
+
const length = Math.min(message.length, task.failure.length - 1);
|
|
569
|
+
task.failure.set(message.subarray(0, length));
|
|
570
|
+
Atomics.store(control, FAILED, length + 1);
|
|
571
|
+
Atomics.store(control, DONE, Atomics.load(control, COUNT));
|
|
572
|
+
Atomics.notify(control, DONE);
|
|
573
|
+
Atomics.add(control, COLLECTED, 1);
|
|
574
|
+
Atomics.notify(control, COLLECTED);
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { parentPort } from "node:worker_threads";
|
|
2
|
+
|
|
3
|
+
let inline = false;
|
|
4
|
+
|
|
5
|
+
// Call update at batch boundaries, not for every splat. A completed stage is
|
|
6
|
+
// the only place that reports 100%; rounding cannot finish a stage early.
|
|
7
|
+
export class Progress {
|
|
8
|
+
// Keep synchronous setup steps on the same line as the stage that follows.
|
|
9
|
+
static inline(run) {
|
|
10
|
+
inline = true;
|
|
11
|
+
try {
|
|
12
|
+
return run();
|
|
13
|
+
} finally {
|
|
14
|
+
inline = false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
constructor(label, total, detail = "") {
|
|
19
|
+
this.label = label;
|
|
20
|
+
this.total = total;
|
|
21
|
+
this.started = performance.now();
|
|
22
|
+
this.updated = this.started;
|
|
23
|
+
this.print(0, false, detail);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
update(completed, detail = "") {
|
|
27
|
+
const now = performance.now();
|
|
28
|
+
if (now - this.updated < 500) return;
|
|
29
|
+
this.updated = now;
|
|
30
|
+
const percent = this.total
|
|
31
|
+
? Math.min(99.9, Math.floor((completed * 1000) / this.total) / 10)
|
|
32
|
+
: 0;
|
|
33
|
+
this.print(percent, false, detail);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
finish(detail = "") {
|
|
37
|
+
this.print(100, true, detail);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
print(percent, done, detail) {
|
|
41
|
+
const seconds = ((performance.now() - this.started) / 1000).toFixed(1);
|
|
42
|
+
const line = `${this.label}: ${percent.toFixed(1)}%${detail ? `, ${detail}` : ""} (${seconds}s)`;
|
|
43
|
+
if (parentPort)
|
|
44
|
+
parentPort.postMessage({
|
|
45
|
+
method: "progress",
|
|
46
|
+
args: [line, done && !inline],
|
|
47
|
+
});
|
|
48
|
+
else console.log(line);
|
|
49
|
+
}
|
|
50
|
+
}
|