@expofp/renderer 3.37.1 → 3.39.0
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/README.md +34 -0
- package/dist/controls/camera-api.d.ts +28 -0
- package/dist/controls/camera-api.js +33 -10
- package/dist/controls/hooks.js +2 -0
- package/dist/core/InstanceUniforms.d.ts +15 -0
- package/dist/core/InstanceUniforms.js +32 -0
- package/dist/core/batch/BatchedMesh.js +6 -0
- package/dist/core/instanced/instanced-material.js +5 -1
- package/dist/core/lines/BatchedLines.d.ts +9 -7
- package/dist/core/lines/BatchedLines.js +9 -7
- package/dist/core/lines/{FrustumCulling.d.ts → FrustumCullingLines.d.ts} +7 -6
- package/dist/core/lines/{FrustumCulling.js → FrustumCullingLines.js} +2 -2
- package/dist/core/lines/{Raycasting.d.ts → RaycastingLines.d.ts} +7 -6
- package/dist/core/lines/{Raycasting.js → RaycastingLines.js} +2 -2
- package/dist/core/lines/lines-material.js +10 -1
- package/dist/core/sprites/BatchedSprites.d.ts +115 -0
- package/dist/core/sprites/BatchedSprites.js +205 -0
- package/dist/core/sprites/RaycastingSprites.d.ts +41 -0
- package/dist/core/sprites/RaycastingSprites.js +71 -0
- package/dist/core/sprites/sprite-material.d.ts +59 -0
- package/dist/core/sprites/sprite-material.js +192 -0
- package/dist/core/text/BatchedText.d.ts +6 -0
- package/dist/core/text/BatchedText.js +6 -0
- package/dist/core/text/MemberTexture.js +55 -1
- package/dist/core/text/Text.d.ts +16 -1
- package/dist/core/text/Text.js +15 -0
- package/dist/core/text/batched-text-material.js +4 -2
- package/dist/objects/containers/hooks.d.ts +7 -2
- package/dist/objects/containers/hooks.js +20 -5
- package/dist/objects/containers/mesh-writes.js +3 -3
- package/dist/objects/images/ImageInstance.js +111 -43
- package/dist/objects/images/Images.d.ts +19 -7
- package/dist/objects/images/Images.js +266 -83
- package/dist/objects/images/atlas-packer.d.ts +92 -91
- package/dist/objects/images/atlas-packer.js +230 -258
- package/dist/objects/images/image-container.d.ts +14 -3
- package/dist/objects/images/image-container.js +19 -5
- package/dist/objects/images/pack-sources.d.ts +190 -0
- package/dist/objects/images/pack-sources.js +272 -0
- package/dist/objects/images/texture-budget.js +2 -2
- package/dist/objects/images/worker/atlas-worker.d.ts +56 -0
- package/dist/objects/images/worker/atlas-worker.js +123 -0
- package/dist/objects/images/worker/client.d.ts +62 -0
- package/dist/objects/images/worker/client.js +249 -0
- package/dist/objects/images/worker/index.d.ts +10 -0
- package/dist/objects/images/worker/index.js +1 -0
- package/dist/objects/layer/Layer.d.ts +7 -6
- package/dist/objects/layer/Layer.js +40 -30
- package/dist/objects/layer/layer-context.d.ts +41 -0
- package/dist/objects/layer/layer-context.js +45 -0
- package/dist/objects/materials/ColorMaterial.js +16 -3
- package/dist/objects/materials/LineMaterial.js +4 -2
- package/dist/objects/materials/TextureMaterial.js +14 -2
- package/dist/objects/materials/effects/color-dim.d.ts +1 -10
- package/dist/objects/materials/effects/color-dim.js +21 -18
- package/dist/objects/materials/effects/effects-context.d.ts +6 -5
- package/dist/objects/materials/shared.d.ts +18 -0
- package/dist/objects/materials/shared.js +21 -0
- package/dist/objects/models/Models.js +1 -8
- package/dist/objects/shapes/Shapes.js +2 -9
- package/dist/objects/text/TextInstance.js +1 -1
- package/dist/objects/text/text-layout.d.ts +13 -4
- package/dist/objects/text/text-layout.js +30 -4
- package/dist/space/Space.d.ts +15 -0
- package/dist/space/Space.js +16 -10
- package/dist/types/defs.d.ts +53 -2
- package/dist/utils/logging.d.ts +6 -3
- package/dist/utils/logging.js +15 -7
- package/dist/utils/math.d.ts +11 -0
- package/dist/utils/math.js +14 -0
- package/package.json +2 -2
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { packSources, } from '../pack-sources';
|
|
2
|
+
/**
|
|
3
|
+
* Waits for every batch to decode, packs them, and moves each atlas's pixels out of its canvas.
|
|
4
|
+
* @param id - Which pack to run.
|
|
5
|
+
* @param maxDimension - Largest atlas edge in pixels.
|
|
6
|
+
* @returns The reply to post back, or null when the pack was canceled and nothing is owed.
|
|
7
|
+
*/
|
|
8
|
+
async function pack(id, maxDimension) {
|
|
9
|
+
// `requestFor` and not `requests.get`, because a layer with no sources sends its closing message
|
|
10
|
+
// and nothing else. That is an empty pack, and it still owes a reply: without one the layer
|
|
11
|
+
// waits forever and draws nothing. The only other way to reach this with no state is a cancel
|
|
12
|
+
// that arrived first, and that happens only when the closing message was never sent, so the
|
|
13
|
+
// empty reply it produces goes to a caller that has already forgotten the id and is discarded.
|
|
14
|
+
const request = requestFor(id);
|
|
15
|
+
request.packing = true;
|
|
16
|
+
const decoded = await Promise.all(request.decoding);
|
|
17
|
+
requests.delete(id);
|
|
18
|
+
// The only yield point in a pack. `packSources` runs straight through once it starts, so this is
|
|
19
|
+
// the last moment a cancel can save the worker's time for the next pack.
|
|
20
|
+
if (request.canceled) {
|
|
21
|
+
closeDecoded(decoded);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
// `createAtlasCanvas` returns an `OffscreenCanvas` here and never takes its fallback branch. A
|
|
25
|
+
// worker runs at all only because `selectTier` found `OffscreenCanvas` on the main thread, and
|
|
26
|
+
// the constructor is exposed to both globals or to neither. No fallback is worth writing: a
|
|
27
|
+
// worker has no `document`, so without `OffscreenCanvas` it could build no canvas at all.
|
|
28
|
+
const { atlases, reports } = packSources(decoded, maxDimension);
|
|
29
|
+
// `transferToImageBitmap` moves the canvas's backing bitmap out instead of copying it, which is
|
|
30
|
+
// what makes an atlas cheap to return. The canvas is left blank, and this code drops it anyway.
|
|
31
|
+
const packed = atlases.map((atlas) => ({
|
|
32
|
+
bitmap: atlas.canvas.transferToImageBitmap(),
|
|
33
|
+
placements: atlas.placements,
|
|
34
|
+
resizable: atlas.resizable,
|
|
35
|
+
}));
|
|
36
|
+
// The atlases hold the pixels now, so every decoded bitmap is dead. This worker decoded all of
|
|
37
|
+
// them and owns all of them. Close them rather than leaving a thousand to the collector.
|
|
38
|
+
closeDecoded(decoded);
|
|
39
|
+
return { type: 'packed', id, atlases: packed, reports };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Abandons one pack.
|
|
43
|
+
*
|
|
44
|
+
* A pack that has not reached {@link pack} yet is dropped here, because nothing else will ever read
|
|
45
|
+
* its decodes. A pack that is already running is only marked: it reads the mark at its one yield
|
|
46
|
+
* point and cleans up there.
|
|
47
|
+
* @param id - Which pack to abandon.
|
|
48
|
+
*/
|
|
49
|
+
function cancel(id) {
|
|
50
|
+
const request = requests.get(id);
|
|
51
|
+
// Already replied, or never started. Either way this worker holds nothing for that pack.
|
|
52
|
+
if (!request)
|
|
53
|
+
return;
|
|
54
|
+
request.canceled = true;
|
|
55
|
+
if (request.packing)
|
|
56
|
+
return;
|
|
57
|
+
requests.delete(id);
|
|
58
|
+
void Promise.all(request.decoding).then(closeDecoded);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Frees decoded pixels this worker owns.
|
|
62
|
+
*
|
|
63
|
+
* An `ImageBitmap` holds its pixels outside the JavaScript heap, where collection does not reclaim
|
|
64
|
+
* them promptly. A canceled pack of a thousand logos would otherwise sit on a thousand of them.
|
|
65
|
+
* @param decoded - What the sources decoded into.
|
|
66
|
+
*/
|
|
67
|
+
function closeDecoded(decoded) {
|
|
68
|
+
for (const source of decoded)
|
|
69
|
+
if (source && 'close' in source)
|
|
70
|
+
source.close();
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Finds a pack's state, starting it on the first batch that arrives.
|
|
74
|
+
* @param id - Which pack the batch belongs to.
|
|
75
|
+
* @returns That pack's state.
|
|
76
|
+
*/
|
|
77
|
+
function requestFor(id) {
|
|
78
|
+
let request = requests.get(id);
|
|
79
|
+
if (!request) {
|
|
80
|
+
request = { decoding: [], canceled: false, packing: false };
|
|
81
|
+
requests.set(id, request);
|
|
82
|
+
}
|
|
83
|
+
return request;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Every pack this worker is working on, by request id.
|
|
87
|
+
*
|
|
88
|
+
* The main thread does not terminate this worker once it has returned an atlas, because those
|
|
89
|
+
* atlases are backed by GPU memory the worker owns. It stops a worker only when that worker broke
|
|
90
|
+
* before it produced anything. So one worker serves every pack of every layer, for the life of the
|
|
91
|
+
* page, and each pack's state has to be kept apart from the others'. An entry is deleted as soon
|
|
92
|
+
* as its pack replies or is canceled.
|
|
93
|
+
*/
|
|
94
|
+
const requests = new Map();
|
|
95
|
+
/** This worker's global scope. */
|
|
96
|
+
const scope = self;
|
|
97
|
+
scope.onmessage = (event) => {
|
|
98
|
+
const message = event.data;
|
|
99
|
+
if (message.type === 'sources') {
|
|
100
|
+
const request = requestFor(message.id);
|
|
101
|
+
// Null rather than a rejection, so one set of bytes that is not an image drops its own def
|
|
102
|
+
// instead of failing the whole layer's pack. `packSources` skips it as a source with no pixels.
|
|
103
|
+
for (const source of message.sources) {
|
|
104
|
+
request.decoding.push(createImageBitmap(source).catch(() => null));
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (message.type === 'cancel') {
|
|
109
|
+
cancel(message.id);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const { id } = message;
|
|
113
|
+
pack(id, message.maxDimension).then((response) => {
|
|
114
|
+
// Nothing is owed for a canceled pack. The main thread has already settled it.
|
|
115
|
+
if (!response)
|
|
116
|
+
return;
|
|
117
|
+
// The bitmaps move rather than copy. After this the worker holds nothing for this pack.
|
|
118
|
+
scope.postMessage(response, response.atlases.map((atlas) => atlas.bitmap));
|
|
119
|
+
}, (error) => {
|
|
120
|
+
requests.delete(id);
|
|
121
|
+
scope.postMessage({ type: 'failed', id, message: String(error) });
|
|
122
|
+
});
|
|
123
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type SourceReport } from '../pack-sources';
|
|
2
|
+
import { type WorkerAtlas, type WorkerSource } from './atlas-worker';
|
|
3
|
+
/** Options for {@link packInWorker}. */
|
|
4
|
+
export interface PackInWorkerOptions {
|
|
5
|
+
/** Abandons the pack. The worker is told to drop it, and a reply that still arrives is thrown away. */
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
/**
|
|
8
|
+
* Makes the pooled worker, on the first pack that needs it. Defaults to the bundled atlas worker,
|
|
9
|
+
* and exists so a spec can drive the protocol against a fake one without a browser. It is ignored
|
|
10
|
+
* once a worker is pooled.
|
|
11
|
+
*/
|
|
12
|
+
createWorker?: () => Worker;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Packs a set of sources in a web worker.
|
|
16
|
+
*
|
|
17
|
+
* **One worker serves every pack, and no pack ever stops it.** The atlases come back as
|
|
18
|
+
* `ImageBitmap`s that `OffscreenCanvas.transferToImageBitmap()` produced, and Chrome backs those
|
|
19
|
+
* with GPU memory the worker owns. `Images` re-reads an atlas's pixels on every texture-budget
|
|
20
|
+
* move, and disposing a texture forces a fresh upload, so the producing worker has to outlive every
|
|
21
|
+
* bitmap it made, not only the first upload of it. Terminating the worker used to run before this
|
|
22
|
+
* function resolved, which destroyed those pixels and left booth logos blank on real GPUs.
|
|
23
|
+
*
|
|
24
|
+
* The one `terminate()` in this file is in `retire`, and it only ever stops a worker that broke
|
|
25
|
+
* before it handed out an atlas. A worker that has produced pixels is dropped from the pool and
|
|
26
|
+
* left running. The worker script is inlined into the bundle, so starting one resolves no URL at
|
|
27
|
+
* runtime, which matters because the SDK is loaded from origins it does not control.
|
|
28
|
+
*
|
|
29
|
+
* Each pack carries an `id`, because a shared worker can hold two layers' packs at once and a reply
|
|
30
|
+
* has to find its own caller. An abandoned pack is settled here and its `id` forgotten, so a reply
|
|
31
|
+
* that still arrives is discarded — and its atlases are closed, or each superseded pack would leak
|
|
32
|
+
* a layer's worth of GPU memory.
|
|
33
|
+
*
|
|
34
|
+
* The sources go over in batches so the main thread is never blocked for long by one structured
|
|
35
|
+
* clone, and so the worker can decode the early batches while the later ones are still being sent.
|
|
36
|
+
* A closing message tells it every source has arrived.
|
|
37
|
+
* @param sources - The unique sources to pack, in the caller's order; placements address them by
|
|
38
|
+
* their index in this array.
|
|
39
|
+
* @param maxDimension - Largest atlas edge in pixels.
|
|
40
|
+
* @param options - The abort signal, and the worker factory a spec overrides.
|
|
41
|
+
* @returns The packed atlases and one report per source.
|
|
42
|
+
*/
|
|
43
|
+
export declare function packInWorker(sources: readonly WorkerSource[], maxDimension: number, options?: PackInWorkerOptions): Promise<{
|
|
44
|
+
atlases: WorkerAtlas[];
|
|
45
|
+
reports: SourceReport[];
|
|
46
|
+
}>;
|
|
47
|
+
/**
|
|
48
|
+
* Forgets the pooled worker without stopping it, and abandons every pack waiting on it.
|
|
49
|
+
*
|
|
50
|
+
* Only a spec calls this. Each spec drives its own fake worker, and the pool would otherwise hand
|
|
51
|
+
* the first spec's fake to every spec after it.
|
|
52
|
+
*/
|
|
53
|
+
export declare function resetAtlasWorkerPool(): void;
|
|
54
|
+
/**
|
|
55
|
+
* How many sources one `postMessage` carries.
|
|
56
|
+
*
|
|
57
|
+
* Sending every source in one message left 18 dropped frames at a thousand logos on a 4x-throttled
|
|
58
|
+
* CPU; sending them in batches of 25 left one. Chunking is what removes the last stall, so this is
|
|
59
|
+
* the number that matters most in this file — tune it against the benchmark, not by taste.
|
|
60
|
+
*/
|
|
61
|
+
export declare const SOURCE_CHUNK_SIZE = 25;
|
|
62
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/* eslint-disable-next-line import/default -- Vite answers this specifier with a module it
|
|
2
|
+
generates, whose default export is a `Worker` subclass. The ESLint resolver drops the query and
|
|
3
|
+
reads the worker entry instead, which exports no default of its own. `vite/client` declares the
|
|
4
|
+
real shape, so tsc sees it. */
|
|
5
|
+
import AtlasWorker from './atlas-worker?worker&inline';
|
|
6
|
+
/**
|
|
7
|
+
* Packs a set of sources in a web worker.
|
|
8
|
+
*
|
|
9
|
+
* **One worker serves every pack, and no pack ever stops it.** The atlases come back as
|
|
10
|
+
* `ImageBitmap`s that `OffscreenCanvas.transferToImageBitmap()` produced, and Chrome backs those
|
|
11
|
+
* with GPU memory the worker owns. `Images` re-reads an atlas's pixels on every texture-budget
|
|
12
|
+
* move, and disposing a texture forces a fresh upload, so the producing worker has to outlive every
|
|
13
|
+
* bitmap it made, not only the first upload of it. Terminating the worker used to run before this
|
|
14
|
+
* function resolved, which destroyed those pixels and left booth logos blank on real GPUs.
|
|
15
|
+
*
|
|
16
|
+
* The one `terminate()` in this file is in `retire`, and it only ever stops a worker that broke
|
|
17
|
+
* before it handed out an atlas. A worker that has produced pixels is dropped from the pool and
|
|
18
|
+
* left running. The worker script is inlined into the bundle, so starting one resolves no URL at
|
|
19
|
+
* runtime, which matters because the SDK is loaded from origins it does not control.
|
|
20
|
+
*
|
|
21
|
+
* Each pack carries an `id`, because a shared worker can hold two layers' packs at once and a reply
|
|
22
|
+
* has to find its own caller. An abandoned pack is settled here and its `id` forgotten, so a reply
|
|
23
|
+
* that still arrives is discarded — and its atlases are closed, or each superseded pack would leak
|
|
24
|
+
* a layer's worth of GPU memory.
|
|
25
|
+
*
|
|
26
|
+
* The sources go over in batches so the main thread is never blocked for long by one structured
|
|
27
|
+
* clone, and so the worker can decode the early batches while the later ones are still being sent.
|
|
28
|
+
* A closing message tells it every source has arrived.
|
|
29
|
+
* @param sources - The unique sources to pack, in the caller's order; placements address them by
|
|
30
|
+
* their index in this array.
|
|
31
|
+
* @param maxDimension - Largest atlas edge in pixels.
|
|
32
|
+
* @param options - The abort signal, and the worker factory a spec overrides.
|
|
33
|
+
* @returns The packed atlases and one report per source.
|
|
34
|
+
*/
|
|
35
|
+
export function packInWorker(sources, maxDimension, options = {}) {
|
|
36
|
+
const { signal, createWorker = createAtlasWorker } = options;
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
// Read the signal before anything is sent. A pack that is already abandoned must cost the
|
|
39
|
+
// worker nothing, so it posts no message at all.
|
|
40
|
+
if (signal?.aborted) {
|
|
41
|
+
reject(abandoned());
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
let worker;
|
|
45
|
+
try {
|
|
46
|
+
worker = pooledWorker(createWorker);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
// A host page whose `script-src` forbids `blob:` and `data:` makes `new Worker()` throw. The
|
|
50
|
+
// caller falls back to an inline pack.
|
|
51
|
+
reject(asError(error));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const id = ++lastRequestId;
|
|
55
|
+
// Every exit runs through here. Dropping this pack's entry is what stops delivery, because the
|
|
56
|
+
// worker is shared and goes on running.
|
|
57
|
+
const done = (settle) => {
|
|
58
|
+
signal?.removeEventListener('abort', onAbort);
|
|
59
|
+
pending.delete(id);
|
|
60
|
+
settle();
|
|
61
|
+
};
|
|
62
|
+
// A plain Error, not `signal.reason`, which Chrome 66 and Safari 15 do not have. Nothing
|
|
63
|
+
// branches on the error's type: every caller reads `signal.aborted` instead.
|
|
64
|
+
const onAbort = () => {
|
|
65
|
+
done(() => {
|
|
66
|
+
cancel(worker, id);
|
|
67
|
+
reject(abandoned());
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
pending.set(id, (message) => {
|
|
71
|
+
done(() => {
|
|
72
|
+
if (message.type === 'failed')
|
|
73
|
+
reject(new Error(message.message));
|
|
74
|
+
else
|
|
75
|
+
resolve({ atlases: message.atlases, reports: message.reports });
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
signal?.addEventListener('abort', onAbort);
|
|
79
|
+
try {
|
|
80
|
+
for (let start = 0; start < sources.length; start += SOURCE_CHUNK_SIZE) {
|
|
81
|
+
post(worker, {
|
|
82
|
+
type: 'sources',
|
|
83
|
+
id,
|
|
84
|
+
sources: sources.slice(start, start + SOURCE_CHUNK_SIZE),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
post(worker, { type: 'pack', id, maxDimension });
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
// `postMessage` throws when the platform refuses to copy a source. The worker keeps the
|
|
91
|
+
// batches that did arrive until it is told otherwise, and the closing message never comes.
|
|
92
|
+
done(() => {
|
|
93
|
+
cancel(worker, id);
|
|
94
|
+
reject(asError(error));
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Forgets the pooled worker without stopping it, and abandons every pack waiting on it.
|
|
101
|
+
*
|
|
102
|
+
* Only a spec calls this. Each spec drives its own fake worker, and the pool would otherwise hand
|
|
103
|
+
* the first spec's fake to every spec after it.
|
|
104
|
+
*/
|
|
105
|
+
export function resetAtlasWorkerPool() {
|
|
106
|
+
pool = null;
|
|
107
|
+
pending.clear();
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Hands back the one worker every pack runs on, starting it on the first call.
|
|
111
|
+
*
|
|
112
|
+
* The reply handler lives on the worker and not on a pack, because the worker outlives every pack
|
|
113
|
+
* it serves and a reply for a pack nobody wants still has pixels to free.
|
|
114
|
+
* @param createWorker - Makes the worker, if there is not one yet.
|
|
115
|
+
* @returns The pooled worker.
|
|
116
|
+
*/
|
|
117
|
+
function pooledWorker(createWorker) {
|
|
118
|
+
if (pool)
|
|
119
|
+
return pool.worker;
|
|
120
|
+
// Not stored until it exists: a factory that throws must be retried by the next pack, not cached.
|
|
121
|
+
const started = { worker: createWorker(), produced: false };
|
|
122
|
+
started.worker.onmessage = (event) => {
|
|
123
|
+
const message = event.data;
|
|
124
|
+
const deliver = pending.get(message.id);
|
|
125
|
+
if (deliver) {
|
|
126
|
+
// These pixels are about to be mounted, so from here on this worker must not be stopped.
|
|
127
|
+
if (message.type === 'packed' && message.atlases.length > 0)
|
|
128
|
+
started.produced = true;
|
|
129
|
+
deliver(message);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// Nobody is waiting: this pack was abandoned after the worker had already replied. An
|
|
133
|
+
// `ImageBitmap` holds its pixels outside the JavaScript heap and nothing else will ever close
|
|
134
|
+
// these, so every superseded pack would leak a layer's worth of GPU memory.
|
|
135
|
+
if (message.type === 'packed')
|
|
136
|
+
for (const atlas of message.atlases)
|
|
137
|
+
atlas.bitmap.close();
|
|
138
|
+
};
|
|
139
|
+
started.worker.onerror = (event) => {
|
|
140
|
+
retire(started);
|
|
141
|
+
failAll(event.message || 'atlas-worker: failed to start');
|
|
142
|
+
};
|
|
143
|
+
started.worker.onmessageerror = () => {
|
|
144
|
+
// This never fires on the primary target. `Worker`'s `messageerror` is `false` for Chrome,
|
|
145
|
+
// Chrome Android and Android WebView, and Safari only added it in 16.4, which is above the
|
|
146
|
+
// `safari15`/`ios15` floor (checked against `@mdn/browser-compat-data@6.1.5`, as installed
|
|
147
|
+
// here). Assigning it is free and Firefox has had it since 57, so it is kept for the browsers
|
|
148
|
+
// that do support it. It is not a guard the build targets can rely on.
|
|
149
|
+
//
|
|
150
|
+
// A reply that does not deserialize is lost, and it carries no id, so there is no way to tell
|
|
151
|
+
// whose it was. Without this the layer waits for a reply that never comes and stays blank.
|
|
152
|
+
retire(started);
|
|
153
|
+
failAll('atlas-worker: a reply could not be read');
|
|
154
|
+
};
|
|
155
|
+
pool = started;
|
|
156
|
+
return started.worker;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Drops a broken worker, so the next pack starts a fresh one.
|
|
160
|
+
*
|
|
161
|
+
* A worker that has handed out an atlas is dropped and left running. Stopping it would destroy the
|
|
162
|
+
* GPU memory behind every bitmap it made, which is the failure this whole file is shaped around. A
|
|
163
|
+
* worker that produced nothing is stopped, because nothing depends on it.
|
|
164
|
+
* @param broken - The pooled worker to drop.
|
|
165
|
+
*/
|
|
166
|
+
function retire(broken) {
|
|
167
|
+
if (pool === broken)
|
|
168
|
+
pool = null;
|
|
169
|
+
if (!broken.produced)
|
|
170
|
+
broken.worker.terminate();
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Abandons every pack waiting on the worker that just broke.
|
|
174
|
+
* @param message - What to reject each of them with.
|
|
175
|
+
*/
|
|
176
|
+
function failAll(message) {
|
|
177
|
+
for (const [id, deliver] of [...pending])
|
|
178
|
+
deliver({ type: 'failed', id, message });
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Tells the worker to drop a pack nobody wants any more.
|
|
182
|
+
*
|
|
183
|
+
* Termination used to do this. Now that the worker outlives its packs, a superseded pack would
|
|
184
|
+
* otherwise run to the end and hold the worker off the next one.
|
|
185
|
+
* @param worker - The pooled worker.
|
|
186
|
+
* @param id - The pack to drop.
|
|
187
|
+
*/
|
|
188
|
+
function cancel(worker, id) {
|
|
189
|
+
try {
|
|
190
|
+
post(worker, { type: 'cancel', id });
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// A worker that will not take this message is already unusable, and the pack it would have
|
|
194
|
+
// dropped is dropped either way.
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Sends one request to the worker.
|
|
199
|
+
*
|
|
200
|
+
* **Nothing is ever transferred.** A `Blob` is a handle to an immutable, reference-counted byte
|
|
201
|
+
* store, so a structured clone copies the handle and not the bytes. Being cheap to clone is the
|
|
202
|
+
* property that matters here; being transferable is not, and transferring a Blob is not a thing the
|
|
203
|
+
* platform offers. A `Blob` is also the only source that reaches this function, so no object a def
|
|
204
|
+
* owns is ever detached by a pack.
|
|
205
|
+
*
|
|
206
|
+
* The typed parameter is why this function exists: `postMessage` takes `any`, so a message built at
|
|
207
|
+
* the call site would go unchecked.
|
|
208
|
+
* @param worker - The worker to send to.
|
|
209
|
+
* @param request - The message.
|
|
210
|
+
*/
|
|
211
|
+
function post(worker, request) {
|
|
212
|
+
worker.postMessage(request);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Makes the bundled atlas worker.
|
|
216
|
+
* @returns A fresh worker.
|
|
217
|
+
*/
|
|
218
|
+
function createAtlasWorker() {
|
|
219
|
+
return new AtlasWorker();
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* The error every abandoned pack rejects with.
|
|
223
|
+
* @returns The error.
|
|
224
|
+
*/
|
|
225
|
+
function abandoned() {
|
|
226
|
+
return new Error('atlas-worker: the pack was abandoned');
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Turns whatever was thrown into an `Error`.
|
|
230
|
+
* @param error - What was thrown.
|
|
231
|
+
* @returns An error.
|
|
232
|
+
*/
|
|
233
|
+
function asError(error) {
|
|
234
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* How many sources one `postMessage` carries.
|
|
238
|
+
*
|
|
239
|
+
* Sending every source in one message left 18 dropped frames at a thousand logos on a 4x-throttled
|
|
240
|
+
* CPU; sending them in batches of 25 left one. Chunking is what removes the last stall, so this is
|
|
241
|
+
* the number that matters most in this file — tune it against the benchmark, not by taste.
|
|
242
|
+
*/
|
|
243
|
+
export const SOURCE_CHUNK_SIZE = 25;
|
|
244
|
+
/** Every pack that has been sent and is waiting for its reply, by request id. */
|
|
245
|
+
const pending = new Map();
|
|
246
|
+
/** The worker every pack runs on, once the first pack has started it. */
|
|
247
|
+
let pool = null;
|
|
248
|
+
/** The last id handed to a pack. Ids only ever climb, so no reply can find the wrong caller. */
|
|
249
|
+
let lastRequestId = 0;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The worker's public face: the main thread's half only.
|
|
3
|
+
*
|
|
4
|
+
* The worker entry is deliberately absent. It imports `packSources` and carries the
|
|
5
|
+
* `?worker&inline` specifier, and nothing outside this directory should pull either in by
|
|
6
|
+
* reaching past the barrel. Types are the exception, because they are erased at emit.
|
|
7
|
+
*/
|
|
8
|
+
export { type WorkerAtlas, type WorkerSource } from './atlas-worker';
|
|
9
|
+
export { packInWorker, type PackInWorkerOptions, SOURCE_CHUNK_SIZE } from './client';
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { packInWorker, SOURCE_CHUNK_SIZE } from './client';
|
|
@@ -5,10 +5,10 @@ export interface LayerProps {
|
|
|
5
5
|
/** The layer definition to render, together with its descendants. */
|
|
6
6
|
layer: LayerDef;
|
|
7
7
|
/**
|
|
8
|
-
* Rendered inside the root group, before the tree's own content
|
|
9
|
-
*
|
|
10
|
-
* like any layer's, and its material exists before every layer's,
|
|
11
|
-
* on.
|
|
8
|
+
* Rendered inside the root group, before the tree's own content. This sits under the root
|
|
9
|
+
* layer's dim and overlay providers. The scene's background quad mounts here for that reason:
|
|
10
|
+
* the root's dim reaches it like any layer's, and its material exists before every layer's,
|
|
11
|
+
* which its draw order relies on.
|
|
12
12
|
*/
|
|
13
13
|
children?: ReactNode;
|
|
14
14
|
}
|
|
@@ -18,8 +18,9 @@ export interface LayerProps {
|
|
|
18
18
|
* This root component owns the tree-wide concerns — in debug mode the layer-visibility GUI, a
|
|
19
19
|
* "Main layers" folder for its direct child layers and a "Content layers" folder mirroring the
|
|
20
20
|
* scheduler's leaf queue (see `visibility-gui.ts`) — and seeds the recursion {@link LayerNode}
|
|
21
|
-
* carries
|
|
22
|
-
* `
|
|
21
|
+
* carries with `hidden`, at `false`. Dim and overlay need no seed: `LayerDimContext` and
|
|
22
|
+
* `LayerOverlayContext` already default to undimmed and plan tier, which is what "no layer above
|
|
23
|
+
* me" means for either one. The whole tree's draw order lives inside `[0, 1)`.
|
|
23
24
|
* @param props - The layer props.
|
|
24
25
|
* @returns The tree's root group.
|
|
25
26
|
*/
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Suspense
|
|
2
|
+
import { Suspense } from 'react';
|
|
3
3
|
import { useLayoutTargets } from '../../events/layout-targets';
|
|
4
4
|
import { usePointerProps } from '../../events/pointer';
|
|
5
5
|
import { isImageDefs, isLayerDefs, isLineDefs, isModelDefs, isShapeDefs, isTextDefs, } from '../../types/guards';
|
|
6
6
|
import { Images } from '../images';
|
|
7
7
|
import { Lines } from '../lines';
|
|
8
|
-
import { LayerDimContext } from '../materials/effects/color-dim';
|
|
9
8
|
import { Models } from '../models';
|
|
10
9
|
import { Shapes } from '../shapes';
|
|
11
10
|
import { Texts } from '../text';
|
|
11
|
+
import { LayerDimContext, LayerOverlayContext, useLayerDim, useLayerOverlay, } from './layer-context';
|
|
12
12
|
import { useStagedChildren } from './mount-scheduler';
|
|
13
13
|
import { useVisibilityGui } from './visibility-gui';
|
|
14
14
|
/**
|
|
@@ -17,15 +17,16 @@ import { useVisibilityGui } from './visibility-gui';
|
|
|
17
17
|
* This root component owns the tree-wide concerns — in debug mode the layer-visibility GUI, a
|
|
18
18
|
* "Main layers" folder for its direct child layers and a "Content layers" folder mirroring the
|
|
19
19
|
* scheduler's leaf queue (see `visibility-gui.ts`) — and seeds the recursion {@link LayerNode}
|
|
20
|
-
* carries
|
|
21
|
-
* `
|
|
20
|
+
* carries with `hidden`, at `false`. Dim and overlay need no seed: `LayerDimContext` and
|
|
21
|
+
* `LayerOverlayContext` already default to undimmed and plan tier, which is what "no layer above
|
|
22
|
+
* me" means for either one. The whole tree's draw order lives inside `[0, 1)`.
|
|
22
23
|
* @param props - The layer props.
|
|
23
24
|
* @returns The tree's root group.
|
|
24
25
|
*/
|
|
25
26
|
export function Layer(props) {
|
|
26
27
|
const { layer, children } = props;
|
|
27
28
|
useVisibilityGui(layer);
|
|
28
|
-
return (_jsx(LayerNode, { layer: layer,
|
|
29
|
+
return (_jsx(LayerNode, { layer: layer, hidden: false, orderBase: 0, orderStep: 1, children: children }));
|
|
29
30
|
}
|
|
30
31
|
/**
|
|
31
32
|
* One layer of the tree as a three.js `<group>`, recursing into its child layers.
|
|
@@ -43,45 +44,43 @@ export function Layer(props) {
|
|
|
43
44
|
* whose membership depends on the camera re-derives it. From then on a pixel-size change reaches
|
|
44
45
|
* those defs through the frame loop, with no render of this component involved.
|
|
45
46
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* numbering the groups is all it takes to make array order the draw order. A nested group
|
|
49
|
-
* overwrites `groupOrder` for its own subtree, so the value that decides a mesh's place is its
|
|
50
|
-
* nearest enclosing layer's; the numbers on layers above it are overwritten and simply do not
|
|
51
|
-
* matter. Without this the order falls out of `material.id`, which ascends in mount order and
|
|
52
|
-
* therefore puts any material built after first paint — every model's, since they exist only once
|
|
53
|
-
* its GLB has loaded — last in the scene regardless of where its layer sits.
|
|
47
|
+
* Draw order is a pair: the tier, then the layer's own place. `overlay` adds a whole band above
|
|
48
|
+
* every plan-tier value. Array order still decides strictly within each band.
|
|
54
49
|
* @param props - The node props.
|
|
55
50
|
* @returns The `<group>` for a layer def.
|
|
56
51
|
*/
|
|
57
52
|
function LayerNode(props) {
|
|
58
|
-
const { layer, children: prelude,
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
const
|
|
53
|
+
const { layer, children: prelude, hidden: parentHidden, orderBase, orderStep } = props;
|
|
54
|
+
// What the nearest ancestor already resolved for dim and overlay. A layer cannot see its
|
|
55
|
+
// ancestors any other way.
|
|
56
|
+
const parentDim = useLayerDim();
|
|
57
|
+
const parentOverlay = useLayerOverlay();
|
|
58
|
+
// Once hidden or once in the overlay tier, a subtree stays that way. A descendant that dropped
|
|
59
|
+
// back into the plan tier would invert the nesting, and a descendant of a hidden layer is not on
|
|
60
|
+
// screen whatever it says about itself.
|
|
61
|
+
const hidden = parentHidden || layer.hidden === true;
|
|
62
|
+
const overlay = parentOverlay || layer.overlay === true;
|
|
63
|
+
// Nearest declaration wins, so only `undefined` inherits. A layer's own 0 un-dims it inside a
|
|
64
|
+
// dimmed ancestor.
|
|
65
|
+
const dim = layer.dim ?? parentDim;
|
|
66
|
+
// On screen only if nothing above hides it either. This is the value picking hangs off.
|
|
67
|
+
const visible = !hidden;
|
|
67
68
|
const { children, mounted } = useStagedChildren(layer);
|
|
68
69
|
useLayoutTargets(layer, children);
|
|
69
70
|
const pointerProps = usePointerProps(layer, visible);
|
|
70
71
|
let contents = null;
|
|
71
72
|
if (isLayerDefs(children)) {
|
|
72
73
|
const childStep = orderStep / children.length;
|
|
73
|
-
contents = children.map((child, index) => (_jsx(LayerNode, { layer: child,
|
|
74
|
+
contents = children.map((child, index) => (_jsx(LayerNode, { layer: child, hidden: hidden, orderBase: orderBase + index * childStep, orderStep: childStep }, child.name)));
|
|
74
75
|
}
|
|
75
76
|
else if (mounted > 0) {
|
|
76
77
|
contents = _jsx(LeafContent, { defs: children, mounted: mounted });
|
|
77
78
|
}
|
|
78
|
-
const group = (_jsxs("group", { name: layer.name, visible: !layer.hidden, renderOrder: orderBase, ...pointerProps, children: [prelude, contents] }));
|
|
79
|
-
// Always provided, never
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
// from not providing at all.
|
|
84
|
-
return _jsx(LayerDimContext, { value: dim, children: group });
|
|
79
|
+
const group = (_jsxs("group", { name: layer.name, visible: !layer.hidden, renderOrder: (overlay ? OVERLAY_TIER : PLAN_TIER) + orderBase, ...pointerProps, children: [prelude, contents] }));
|
|
80
|
+
// Always provided, never only when the layer declares one. Making a provider appear on first use
|
|
81
|
+
// would change this component's element type and remount the subtree, which releases and re-claims
|
|
82
|
+
// every GPU slot beneath it.
|
|
83
|
+
return (_jsx(LayerDimContext, { value: dim, children: _jsx(LayerOverlayContext, { value: overlay, children: group }) }));
|
|
85
84
|
}
|
|
86
85
|
/**
|
|
87
86
|
* The content of one leaf layer: the container for its defs' kind, inside a Suspense boundary of
|
|
@@ -112,3 +111,14 @@ function LeafContent(props) {
|
|
|
112
111
|
content = _jsx(Models, { defs: defs, mounted: mounted });
|
|
113
112
|
return _jsx(Suspense, { fallback: null, children: content });
|
|
114
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Where the plan's own layers are numbered from. The tree subdivides `[0, 1)` depth first, so the
|
|
116
|
+
* mantissa stays free for nesting.
|
|
117
|
+
*/
|
|
118
|
+
const PLAN_TIER = 0;
|
|
119
|
+
/**
|
|
120
|
+
* Where an overlay subtree is numbered from, which puts it in `[1, 2)`. That is after every
|
|
121
|
+
* plan-tier layer, whatever renderOrder those layers hold. The whole cost is one bit of mantissa for
|
|
122
|
+
* subdivision.
|
|
123
|
+
*/
|
|
124
|
+
const OVERLAY_TIER = 1;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dim amount a layer draws at, which every material beneath it reads.
|
|
3
|
+
* @returns The nearest provided amount, or `0` with no layer above.
|
|
4
|
+
*/
|
|
5
|
+
export declare function useLayerDim(): number;
|
|
6
|
+
/**
|
|
7
|
+
* Whether the current layer draws in the overlay tier.
|
|
8
|
+
* @returns `true` if this layer or an ancestor opted into the overlay tier, `false` otherwise.
|
|
9
|
+
*/
|
|
10
|
+
export declare function useLayerOverlay(): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* The dim amount the current layer hands down to its descendants.
|
|
13
|
+
*
|
|
14
|
+
* Context rather than a prop, because the value crosses the whole subtree and concerns none of the
|
|
15
|
+
* components between. A container hands its material nothing about layers, and a material that
|
|
16
|
+
* never asked about them still dims correctly. Threading it as a prop would put a `dim` on every
|
|
17
|
+
* material, every container, and every one of their inner components.
|
|
18
|
+
*
|
|
19
|
+
* Read through {@link useLayerDim} instead of directly. A `LayerNode` provides it, always, never
|
|
20
|
+
* only when it declares a `dim` of its own. See `Layer.tsx` for why.
|
|
21
|
+
*
|
|
22
|
+
* Defaults to `0`, undimmed, which is what "no layer above me" means for a component rendered
|
|
23
|
+
* outside a `Layer` tree. A fourth inherited field belongs in a context and a hook of its own here,
|
|
24
|
+
* not folded into this one.
|
|
25
|
+
*/
|
|
26
|
+
export declare const LayerDimContext: import("react").Context<number>;
|
|
27
|
+
/**
|
|
28
|
+
* Whether the current layer hands its overlay tier down to its descendants.
|
|
29
|
+
*
|
|
30
|
+
* Context rather than a prop, for the reason {@link LayerDimContext} is. It is also not a shading
|
|
31
|
+
* term, which is why `overlayParams`, the material parameters this tier unlocks, stays in
|
|
32
|
+
* `objects/materials/shared.ts` instead of moving here.
|
|
33
|
+
*
|
|
34
|
+
* Read through {@link useLayerOverlay} instead of directly. A `LayerNode` provides it, always,
|
|
35
|
+
* never only when it declares an `overlay` of its own. See `Layer.tsx` for why.
|
|
36
|
+
*
|
|
37
|
+
* Defaults to `false`, the plan tier, which is what "no layer above me" means for a component
|
|
38
|
+
* rendered outside a `Layer` tree.
|
|
39
|
+
*/
|
|
40
|
+
export declare const LayerOverlayContext: import("react").Context<boolean>;
|
|
41
|
+
//# sourceMappingURL=layer-context.d.ts.map
|