@driftengine/texture 4.0.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/LICENSE +202 -0
- package/NOTICE +29 -0
- package/README.md +106 -0
- package/dist/decodeCpu.d.ts +59 -0
- package/dist/decodeCpu.js +234 -0
- package/dist/decodeGraph.d.ts +105 -0
- package/dist/decodeGraph.js +180 -0
- package/dist/half.d.ts +24 -0
- package/dist/half.js +86 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +55 -0
- package/dist/inference.d.ts +53 -0
- package/dist/inference.js +243 -0
- package/dist/materialArray.d.ts +38 -0
- package/dist/materialArray.js +40 -0
- package/dist/mipNdf.d.ts +29 -0
- package/dist/mipNdf.js +53 -0
- package/dist/overlay/journal.d.ts +78 -0
- package/dist/overlay/journal.js +171 -0
- package/dist/overlay/sparse.d.ts +68 -0
- package/dist/overlay/sparse.js +212 -0
- package/dist/progressive.d.ts +30 -0
- package/dist/progressive.js +56 -0
- package/dist/residency/pageCache.d.ts +103 -0
- package/dist/residency/pageCache.js +184 -0
- package/dist/residency/predict.d.ts +55 -0
- package/dist/residency/predict.js +51 -0
- package/dist/residency/predictor.d.ts +16 -0
- package/dist/residency/predictor.js +44 -0
- package/dist/residency/queue.d.ts +26 -0
- package/dist/residency/queue.js +52 -0
- package/dist/residency/stream.d.ts +66 -0
- package/dist/residency/stream.js +142 -0
- package/dist/residency/table.d.ts +36 -0
- package/dist/residency/table.js +72 -0
- package/dist/residency/viewTiles.d.ts +108 -0
- package/dist/residency/viewTiles.js +419 -0
- package/dist/semantics.d.ts +52 -0
- package/dist/semantics.js +76 -0
- package/dist/tensor/architecture.d.ts +53 -0
- package/dist/tensor/architecture.js +96 -0
- package/dist/tensor/attention.d.ts +5 -0
- package/dist/tensor/attention.js +62 -0
- package/dist/tensor/denseOperators.d.ts +2 -0
- package/dist/tensor/denseOperators.js +136 -0
- package/dist/tensor/graph.d.ts +83 -0
- package/dist/tensor/graph.js +175 -0
- package/dist/tensor/linear.d.ts +49 -0
- package/dist/tensor/linear.js +136 -0
- package/dist/tensor/operatorKit.d.ts +27 -0
- package/dist/tensor/operatorKit.js +45 -0
- package/dist/tensor/operators.d.ts +3 -0
- package/dist/tensor/operators.js +24 -0
- package/dist/tensor/resize.d.ts +6 -0
- package/dist/tensor/resize.js +107 -0
- package/dist/tensor/reuse.d.ts +33 -0
- package/dist/tensor/reuse.js +59 -0
- package/dist/tensor/shapeOperators.d.ts +3 -0
- package/dist/tensor/shapeOperators.js +173 -0
- package/dist/tensor/spatial.d.ts +34 -0
- package/dist/tensor/spatial.js +131 -0
- package/dist/tensor/spatialOperators.d.ts +2 -0
- package/dist/tensor/spatialOperators.js +138 -0
- package/dist/tileHash.d.ts +29 -0
- package/dist/tileHash.js +50 -0
- package/dist/timeNodes.d.ts +26 -0
- package/dist/timeNodes.js +48 -0
- package/package.json +59 -0
- package/src/decodeCpu.ts +308 -0
- package/src/decodeGraph.ts +214 -0
- package/src/half.ts +86 -0
- package/src/index.ts +175 -0
- package/src/inference.ts +278 -0
- package/src/materialArray.ts +67 -0
- package/src/mipNdf.ts +63 -0
- package/src/overlay/journal.ts +218 -0
- package/src/overlay/sparse.ts +275 -0
- package/src/progressive.ts +60 -0
- package/src/residency/pageCache.ts +233 -0
- package/src/residency/predict.ts +74 -0
- package/src/residency/predictor.ts +62 -0
- package/src/residency/queue.ts +78 -0
- package/src/residency/stream.ts +194 -0
- package/src/residency/table.ts +89 -0
- package/src/residency/viewTiles.ts +553 -0
- package/src/semantics.ts +114 -0
- package/src/tensor/architecture.ts +140 -0
- package/src/tensor/attention.ts +75 -0
- package/src/tensor/denseOperators.ts +153 -0
- package/src/tensor/graph.ts +244 -0
- package/src/tensor/linear.ts +153 -0
- package/src/tensor/operatorKit.ts +76 -0
- package/src/tensor/operators.ts +28 -0
- package/src/tensor/resize.ts +140 -0
- package/src/tensor/shapeOperators.ts +173 -0
- package/src/tensor/spatial.ts +178 -0
- package/src/tensor/spatialOperators.ts +182 -0
- package/src/tileHash.ts +60 -0
- package/src/timeNodes.ts +60 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Running the world forward to see what it will need, then putting it back.
|
|
3
|
+
*
|
|
4
|
+
* **This is the part no competitor can copy, and the reason is not the code.** The state of the art
|
|
5
|
+
* in texture streaming is reactive: the documentation for the most widely deployed implementation
|
|
6
|
+
* says so in as many words — streaming is *reactive by nature*, because the processor cannot know a
|
|
7
|
+
* tile is needed until after a frame has already needed it. Hence pop-in, and hence the further
|
|
8
|
+
* limitation that passes which write no feedback can sample tiles they are unable to request.
|
|
9
|
+
*
|
|
10
|
+
* That is true only of an engine that cannot run its simulation forward and put it back. This one
|
|
11
|
+
* can, and already does every frame for the netcode. So residency is decided by looking at where
|
|
12
|
+
* the camera *will* be:
|
|
13
|
+
*
|
|
14
|
+
* ```text
|
|
15
|
+
* every engine DriftEngine
|
|
16
|
+
* ──────────── ───────────
|
|
17
|
+
* render frame N save the simulation
|
|
18
|
+
* read the feedback buffer advance it N frames, deterministic, no rendering
|
|
19
|
+
* ↓ two to three frames late sample which tiles those views want
|
|
20
|
+
* request the tiles restore
|
|
21
|
+
* pop-in prefetch → resident before it is ever visible
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* **The simulation is taken as a handle the caller supplies**, in the save/restore shape
|
|
25
|
+
* `@driftengine/network`'s `Snapshotter` already uses, for two reasons: this package must not
|
|
26
|
+
* import a simulation, and a consumer with their own loop has to be able to supply their own.
|
|
27
|
+
*
|
|
28
|
+
* **A misprediction costs a wasted fetch and nothing else.** The tile arrives late, exactly as it
|
|
29
|
+
* would in a reactive engine — so the floor of this mechanism is everyone else's ceiling.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* What prediction needs from a simulation.
|
|
33
|
+
*
|
|
34
|
+
* Save and restore rather than a snapshot value, matching `Snapshotter` in the network package:
|
|
35
|
+
* the buffer is the handle's own and is reused, so predicting allocates nothing.
|
|
36
|
+
*/
|
|
37
|
+
export interface SimulationHandle {
|
|
38
|
+
/** Capture the current state into the handle's own storage. */
|
|
39
|
+
save(): void;
|
|
40
|
+
/** Put back what `save` captured. */
|
|
41
|
+
restore(): void;
|
|
42
|
+
/** Advance one fixed step. Deterministic, and the same step the simulation really runs. */
|
|
43
|
+
advance(dt: number): void;
|
|
44
|
+
/** Write the current view matrix into `out`, sixteen floats. */
|
|
45
|
+
viewAt(out: Float32Array): void;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Fill `out` with one view matrix per predicted frame, and leave the simulation exactly as found.
|
|
49
|
+
*
|
|
50
|
+
* Returns how many views were written — fewer than `frames` when `out` cannot hold them all.
|
|
51
|
+
*
|
|
52
|
+
* **Saved once and restored once**, however many frames are predicted: the intermediate states are
|
|
53
|
+
* not wanted, only the views they imply.
|
|
54
|
+
*/
|
|
55
|
+
export declare function predictViews(sim: SimulationHandle, frames: number, dt: number, out: Float32Array): number;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Running the world forward to see what it will need, then putting it back.
|
|
3
|
+
*
|
|
4
|
+
* **This is the part no competitor can copy, and the reason is not the code.** The state of the art
|
|
5
|
+
* in texture streaming is reactive: the documentation for the most widely deployed implementation
|
|
6
|
+
* says so in as many words — streaming is *reactive by nature*, because the processor cannot know a
|
|
7
|
+
* tile is needed until after a frame has already needed it. Hence pop-in, and hence the further
|
|
8
|
+
* limitation that passes which write no feedback can sample tiles they are unable to request.
|
|
9
|
+
*
|
|
10
|
+
* That is true only of an engine that cannot run its simulation forward and put it back. This one
|
|
11
|
+
* can, and already does every frame for the netcode. So residency is decided by looking at where
|
|
12
|
+
* the camera *will* be:
|
|
13
|
+
*
|
|
14
|
+
* ```text
|
|
15
|
+
* every engine DriftEngine
|
|
16
|
+
* ──────────── ───────────
|
|
17
|
+
* render frame N save the simulation
|
|
18
|
+
* read the feedback buffer advance it N frames, deterministic, no rendering
|
|
19
|
+
* ↓ two to three frames late sample which tiles those views want
|
|
20
|
+
* request the tiles restore
|
|
21
|
+
* pop-in prefetch → resident before it is ever visible
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* **The simulation is taken as a handle the caller supplies**, in the save/restore shape
|
|
25
|
+
* `@driftengine/network`'s `Snapshotter` already uses, for two reasons: this package must not
|
|
26
|
+
* import a simulation, and a consumer with their own loop has to be able to supply their own.
|
|
27
|
+
*
|
|
28
|
+
* **A misprediction costs a wasted fetch and nothing else.** The tile arrives late, exactly as it
|
|
29
|
+
* would in a reactive engine — so the floor of this mechanism is everyone else's ceiling.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Fill `out` with one view matrix per predicted frame, and leave the simulation exactly as found.
|
|
33
|
+
*
|
|
34
|
+
* Returns how many views were written — fewer than `frames` when `out` cannot hold them all.
|
|
35
|
+
*
|
|
36
|
+
* **Saved once and restored once**, however many frames are predicted: the intermediate states are
|
|
37
|
+
* not wanted, only the views they imply.
|
|
38
|
+
*/
|
|
39
|
+
export function predictViews(sim, frames, dt, out) {
|
|
40
|
+
const capacity = Math.floor(out.length / 16);
|
|
41
|
+
const wanted = Math.min(frames, capacity);
|
|
42
|
+
if (wanted <= 0)
|
|
43
|
+
return 0;
|
|
44
|
+
sim.save();
|
|
45
|
+
for (let i = 0; i < wanted; i += 1) {
|
|
46
|
+
sim.advance(dt);
|
|
47
|
+
sim.viewAt(out.subarray(i * 16, i * 16 + 16));
|
|
48
|
+
}
|
|
49
|
+
sim.restore();
|
|
50
|
+
return wanted;
|
|
51
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SimulationHandle } from './predict.ts';
|
|
2
|
+
export interface Predictor<T> {
|
|
3
|
+
/** What this view needs, most important first. Fills `out` and returns how many. */
|
|
4
|
+
needs(view: Float32Array, out: T[], budget: number): number;
|
|
5
|
+
/** Whether this item is already here. */
|
|
6
|
+
resident(item: T): boolean;
|
|
7
|
+
/** Ask for it. Lower priority is sooner. */
|
|
8
|
+
request(item: T, priority: number): void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Advance the simulation, ask the predictor what each predicted view needs, and put it back.
|
|
12
|
+
*
|
|
13
|
+
* Returns how many requests were made. `budget` bounds the total across every predicted frame
|
|
14
|
+
* rather than per frame, so a near frame cannot be starved by a distant one filling the queue.
|
|
15
|
+
*/
|
|
16
|
+
export declare function runPrediction<T>(sim: SimulationHandle, predictor: Predictor<T>, frames: number, dt: number, budget: number): number;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prediction, generic over what is being streamed.
|
|
3
|
+
*
|
|
4
|
+
* **Written generic now rather than generalised later, and the reason is a contract rather than
|
|
5
|
+
* tidiness.** Wave 4B streams world cells by exactly this mechanism. Writing the tile case against
|
|
6
|
+
* a concrete type would mean a second speculative-advance implementation there — and therefore a
|
|
7
|
+
* second place for "put the simulation back exactly" to be got wrong, in a subsystem where being
|
|
8
|
+
* wrong means the world runs at double speed only when streaming is enabled.
|
|
9
|
+
*
|
|
10
|
+
* One prediction, two consumers.
|
|
11
|
+
*/
|
|
12
|
+
import { predictViews } from './predict.js';
|
|
13
|
+
const VIEWS = new Float32Array(16 * 32);
|
|
14
|
+
const WANTED = [];
|
|
15
|
+
/**
|
|
16
|
+
* Advance the simulation, ask the predictor what each predicted view needs, and put it back.
|
|
17
|
+
*
|
|
18
|
+
* Returns how many requests were made. `budget` bounds the total across every predicted frame
|
|
19
|
+
* rather than per frame, so a near frame cannot be starved by a distant one filling the queue.
|
|
20
|
+
*/
|
|
21
|
+
export function runPrediction(sim, predictor, frames, dt, budget) {
|
|
22
|
+
const horizon = Math.min(frames, Math.floor(VIEWS.length / 16));
|
|
23
|
+
const count = predictViews(sim, horizon, dt, VIEWS);
|
|
24
|
+
let requested = 0;
|
|
25
|
+
const out = WANTED;
|
|
26
|
+
for (let frame = 0; frame < count && requested < budget; frame += 1) {
|
|
27
|
+
const view = VIEWS.subarray(frame * 16, frame * 16 + 16);
|
|
28
|
+
const wanted = predictor.needs(view, out, budget - requested);
|
|
29
|
+
for (let i = 0; i < wanted && requested < budget; i += 1) {
|
|
30
|
+
const item = out[i];
|
|
31
|
+
if (predictor.resident(item))
|
|
32
|
+
continue;
|
|
33
|
+
/*
|
|
34
|
+
* Priority is how far ahead the frame is, so nearer frames come first — and within a frame,
|
|
35
|
+
* the place the predictor named the item in, as a fraction that never reaches the next
|
|
36
|
+
* frame. A frame's priority alone would tie its items, and a queue breaks a tie however it
|
|
37
|
+
* likes; a budget that cut the frame short would then keep an arbitrary part of it.
|
|
38
|
+
*/
|
|
39
|
+
predictor.request(item, frame + i / wanted);
|
|
40
|
+
requested += 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return requested;
|
|
44
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What to fetch next, ordered by how soon it will be needed.
|
|
3
|
+
*
|
|
4
|
+
* **That ordering is not available to a reactive system at all.** Everything a feedback buffer
|
|
5
|
+
* reports is already needed; there is nothing to rank. Prediction produces a horizon, so a tile
|
|
6
|
+
* wanted in one frame can outrank one wanted in eight — which is the difference between spending a
|
|
7
|
+
* limited byte budget well and spending it arbitrarily.
|
|
8
|
+
*
|
|
9
|
+
* **A full queue drops its worst entry rather than refusing the best one.** Refusing on arrival
|
|
10
|
+
* would discard exactly the tiles a late prediction found most urgent.
|
|
11
|
+
*/
|
|
12
|
+
export interface PrefetchQueue {
|
|
13
|
+
/** Priority per hash. Lower is sooner, so it sorts naturally. */
|
|
14
|
+
priority: Map<string, number>;
|
|
15
|
+
capacity: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function createPrefetchQueue(capacity: number): PrefetchQueue;
|
|
18
|
+
export declare function enqueue(queue: PrefetchQueue, hash: string, priority: number): void;
|
|
19
|
+
export declare function queueSize(queue: PrefetchQueue): number;
|
|
20
|
+
/**
|
|
21
|
+
* Take the next batch, soonest first, within a byte budget.
|
|
22
|
+
*
|
|
23
|
+
* **Residency is checked here rather than at enqueue**, because it may have changed in between —
|
|
24
|
+
* a tile requested eight frames ahead may well have arrived by the time its turn comes.
|
|
25
|
+
*/
|
|
26
|
+
export declare function takeBatch(queue: PrefetchQueue, isResident: (hash: string) => boolean, bytesBudget: number, tileBytes: number, out: string[]): number;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export function createPrefetchQueue(capacity) {
|
|
2
|
+
return { priority: new Map(), capacity };
|
|
3
|
+
}
|
|
4
|
+
export function enqueue(queue, hash, priority) {
|
|
5
|
+
const existing = queue.priority.get(hash);
|
|
6
|
+
/* One entry per tile, keeping the better claim on it. */
|
|
7
|
+
if (existing !== undefined) {
|
|
8
|
+
if (priority < existing)
|
|
9
|
+
queue.priority.set(hash, priority);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
queue.priority.set(hash, priority);
|
|
13
|
+
if (queue.priority.size <= queue.capacity)
|
|
14
|
+
return;
|
|
15
|
+
let worst = '';
|
|
16
|
+
let worstPriority = -Infinity;
|
|
17
|
+
for (const [key, value] of queue.priority) {
|
|
18
|
+
if (value > worstPriority) {
|
|
19
|
+
worstPriority = value;
|
|
20
|
+
worst = key;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
queue.priority.delete(worst);
|
|
24
|
+
}
|
|
25
|
+
export function queueSize(queue) {
|
|
26
|
+
return queue.priority.size;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Take the next batch, soonest first, within a byte budget.
|
|
30
|
+
*
|
|
31
|
+
* **Residency is checked here rather than at enqueue**, because it may have changed in between —
|
|
32
|
+
* a tile requested eight frames ahead may well have arrived by the time its turn comes.
|
|
33
|
+
*/
|
|
34
|
+
export function takeBatch(queue, isResident, bytesBudget, tileBytes, out) {
|
|
35
|
+
const ordered = [...queue.priority.entries()].sort((a, b) => a[1] - b[1] || (a[0] < b[0] ? -1 : 1));
|
|
36
|
+
let spent = 0;
|
|
37
|
+
let written = 0;
|
|
38
|
+
for (const [hash] of ordered) {
|
|
39
|
+
if (isResident(hash)) {
|
|
40
|
+
queue.priority.delete(hash);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (spent + tileBytes > bytesBudget)
|
|
44
|
+
break;
|
|
45
|
+
out[written] = hash;
|
|
46
|
+
written += 1;
|
|
47
|
+
spent += tileBytes;
|
|
48
|
+
queue.priority.delete(hash);
|
|
49
|
+
}
|
|
50
|
+
out.length = written;
|
|
51
|
+
return written;
|
|
52
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Getting the bytes, and giving up on the ones that are not coming.
|
|
3
|
+
*
|
|
4
|
+
* **A tile that will not arrive stops being waited for.** That is the whole of this file's
|
|
5
|
+
* difficulty. A fetch that resolves with nothing — a tile the container does not hold, a name from
|
|
6
|
+
* a stale index, a range the server refused — leaves the tile marked requested and its slot
|
|
7
|
+
* occupied forever if nobody handles it. A handful of those and the in-flight budget is gone, so
|
|
8
|
+
* every subsequent fetch is refused and **streaming stops entirely**, with no error anywhere: the
|
|
9
|
+
* world simply never finishes loading, and the last thing that happened was unrelated.
|
|
10
|
+
*
|
|
11
|
+
* So a missing tile is remembered as missing. Not evicted-and-forgotten, which asks for it again on
|
|
12
|
+
* the next pump and turns one absent tile into a fetch every frame; and not left requested, which
|
|
13
|
+
* is the leak. `forgetMissing` exists because "missing" is a fact about a container and a caller
|
|
14
|
+
* that mounts a different one is entitled to ask again.
|
|
15
|
+
*
|
|
16
|
+
* **The source is the caller's.** What a tile's bytes are behind — a `.drft` container, a range
|
|
17
|
+
* request, a file — is not this package's business, and a streamer that knew would be a streamer
|
|
18
|
+
* every host had to agree with. It is one method returning a promise of bytes or nothing.
|
|
19
|
+
*
|
|
20
|
+
* **Nothing here awaits.** `pumpStreamer` starts work and returns; a source that never resolves
|
|
21
|
+
* costs an in-flight slot and nothing else. A streamer that awaited its own fetches would stall the
|
|
22
|
+
* frame that called it, which is the failure streaming exists to avoid.
|
|
23
|
+
*/
|
|
24
|
+
import { type ResidencyTable } from './table.ts';
|
|
25
|
+
import { type PageCache } from './pageCache.ts';
|
|
26
|
+
/** Where a tile's bytes come from. Resolving `null` means the source does not have it. */
|
|
27
|
+
export interface TileSource {
|
|
28
|
+
fetch(hash: string): Promise<Uint8Array | null>;
|
|
29
|
+
}
|
|
30
|
+
export interface StreamerOptions {
|
|
31
|
+
/**
|
|
32
|
+
* How many fetches may be outstanding at once.
|
|
33
|
+
*
|
|
34
|
+
* A bound rather than none, because an unbounded streamer asked for a thousand tiles opens a
|
|
35
|
+
* thousand connections and finishes none of them sooner.
|
|
36
|
+
*/
|
|
37
|
+
readonly maxInFlight?: number;
|
|
38
|
+
}
|
|
39
|
+
export interface Streamer {
|
|
40
|
+
readonly source: TileSource;
|
|
41
|
+
readonly table: ResidencyTable;
|
|
42
|
+
readonly cache: PageCache;
|
|
43
|
+
readonly maxInFlight: number;
|
|
44
|
+
/** Tiles asked for and not yet answered. */
|
|
45
|
+
readonly inFlight: Set<string>;
|
|
46
|
+
/** Tiles the source answered `null` for. Not asked for again until forgotten. */
|
|
47
|
+
readonly missing: Set<string>;
|
|
48
|
+
/** Fetches that landed after their tile had been evicted. Counted, not silently dropped. */
|
|
49
|
+
stale: number;
|
|
50
|
+
/** Fetches that landed with no page free to put them in. The tile stays absent and is retried. */
|
|
51
|
+
deferred: number;
|
|
52
|
+
/** Fetches whose bytes were larger than a page. A container and a cache disagreeing. */
|
|
53
|
+
oversize: number;
|
|
54
|
+
}
|
|
55
|
+
export declare function createStreamer(source: TileSource, table: ResidencyTable, cache: PageCache, options?: StreamerOptions): Streamer;
|
|
56
|
+
export declare function streamerInFlight(streamer: Streamer): number;
|
|
57
|
+
/** Stop calling a tile missing, so the next pump asks for it again. */
|
|
58
|
+
export declare function forgetMissing(streamer: Streamer, hash: string): void;
|
|
59
|
+
export declare function clearMissing(streamer: Streamer): void;
|
|
60
|
+
/**
|
|
61
|
+
* Ask for the first `count` tiles of `batch` that are worth asking for. Returns how many were sent.
|
|
62
|
+
*
|
|
63
|
+
* Skips anything already resident, already in flight, or known missing — which is what makes
|
|
64
|
+
* calling this every frame with the same batch correct rather than a fetch storm.
|
|
65
|
+
*/
|
|
66
|
+
export declare function pumpStreamer(streamer: Streamer, batch: readonly string[], count: number): number;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Getting the bytes, and giving up on the ones that are not coming.
|
|
3
|
+
*
|
|
4
|
+
* **A tile that will not arrive stops being waited for.** That is the whole of this file's
|
|
5
|
+
* difficulty. A fetch that resolves with nothing — a tile the container does not hold, a name from
|
|
6
|
+
* a stale index, a range the server refused — leaves the tile marked requested and its slot
|
|
7
|
+
* occupied forever if nobody handles it. A handful of those and the in-flight budget is gone, so
|
|
8
|
+
* every subsequent fetch is refused and **streaming stops entirely**, with no error anywhere: the
|
|
9
|
+
* world simply never finishes loading, and the last thing that happened was unrelated.
|
|
10
|
+
*
|
|
11
|
+
* So a missing tile is remembered as missing. Not evicted-and-forgotten, which asks for it again on
|
|
12
|
+
* the next pump and turns one absent tile into a fetch every frame; and not left requested, which
|
|
13
|
+
* is the leak. `forgetMissing` exists because "missing" is a fact about a container and a caller
|
|
14
|
+
* that mounts a different one is entitled to ask again.
|
|
15
|
+
*
|
|
16
|
+
* **The source is the caller's.** What a tile's bytes are behind — a `.drft` container, a range
|
|
17
|
+
* request, a file — is not this package's business, and a streamer that knew would be a streamer
|
|
18
|
+
* every host had to agree with. It is one method returning a promise of bytes or nothing.
|
|
19
|
+
*
|
|
20
|
+
* **Nothing here awaits.** `pumpStreamer` starts work and returns; a source that never resolves
|
|
21
|
+
* costs an in-flight slot and nothing else. A streamer that awaited its own fetches would stall the
|
|
22
|
+
* frame that called it, which is the failure streaming exists to avoid.
|
|
23
|
+
*/
|
|
24
|
+
import { TILE_ABSENT, TILE_REQUESTED, evict, markRequested, markResident, tileState, } from './table.js';
|
|
25
|
+
import { acquirePage, takeEvicted } from './pageCache.js';
|
|
26
|
+
export function createStreamer(source, table, cache, options = {}) {
|
|
27
|
+
return {
|
|
28
|
+
source,
|
|
29
|
+
table,
|
|
30
|
+
cache,
|
|
31
|
+
maxInFlight: Math.max(1, Math.floor(options.maxInFlight ?? 8)),
|
|
32
|
+
inFlight: new Set(),
|
|
33
|
+
missing: new Set(),
|
|
34
|
+
stale: 0,
|
|
35
|
+
deferred: 0,
|
|
36
|
+
oversize: 0,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function streamerInFlight(streamer) {
|
|
40
|
+
return streamer.inFlight.size;
|
|
41
|
+
}
|
|
42
|
+
/** Stop calling a tile missing, so the next pump asks for it again. */
|
|
43
|
+
export function forgetMissing(streamer, hash) {
|
|
44
|
+
streamer.missing.delete(hash);
|
|
45
|
+
}
|
|
46
|
+
export function clearMissing(streamer) {
|
|
47
|
+
streamer.missing.clear();
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Ask for the first `count` tiles of `batch` that are worth asking for. Returns how many were sent.
|
|
51
|
+
*
|
|
52
|
+
* Skips anything already resident, already in flight, or known missing — which is what makes
|
|
53
|
+
* calling this every frame with the same batch correct rather than a fetch storm.
|
|
54
|
+
*/
|
|
55
|
+
export function pumpStreamer(streamer, batch, count) {
|
|
56
|
+
let sent = 0;
|
|
57
|
+
/* A bound on the loop rather than a correctness check — the `undefined` guard below is what
|
|
58
|
+
makes a short batch behave, and a perturbation removing this one fails no test. It stays
|
|
59
|
+
because without it `pumpStreamer(s, [], 1e9)` spins a billion times to do nothing. */
|
|
60
|
+
const wanted = Math.min(count, batch.length);
|
|
61
|
+
for (let at = 0; at < wanted; at += 1) {
|
|
62
|
+
if (streamer.inFlight.size >= streamer.maxInFlight)
|
|
63
|
+
break;
|
|
64
|
+
const hash = batch[at];
|
|
65
|
+
if (hash === undefined)
|
|
66
|
+
continue;
|
|
67
|
+
if (streamer.missing.has(hash))
|
|
68
|
+
continue;
|
|
69
|
+
/* Already asked for. Usually the table says so too, but not after an eviction while the fetch
|
|
70
|
+
was still out: the tile is absent again and a second fetch of bytes already on their way is
|
|
71
|
+
exactly what a content-addressed store makes pointless. */
|
|
72
|
+
if (streamer.inFlight.has(hash))
|
|
73
|
+
continue;
|
|
74
|
+
if (tileState(streamer.table, hash) !== TILE_ABSENT)
|
|
75
|
+
continue;
|
|
76
|
+
markRequested(streamer.table, hash);
|
|
77
|
+
streamer.inFlight.add(hash);
|
|
78
|
+
sent += 1;
|
|
79
|
+
/*
|
|
80
|
+
* Not awaited: this returns to the frame that called it. A rejection is treated exactly as a
|
|
81
|
+
* missing tile, because from here they are the same fact — the bytes are not coming — and a
|
|
82
|
+
* streamer that let one through would be the leak this file is about, arriving by the one path
|
|
83
|
+
* nobody writes a test for.
|
|
84
|
+
*/
|
|
85
|
+
void streamer.source
|
|
86
|
+
.fetch(hash)
|
|
87
|
+
.then((bytes) => {
|
|
88
|
+
land(streamer, hash, bytes);
|
|
89
|
+
})
|
|
90
|
+
.catch(() => {
|
|
91
|
+
land(streamer, hash, null);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return sent;
|
|
95
|
+
}
|
|
96
|
+
function land(streamer, hash, bytes) {
|
|
97
|
+
streamer.inFlight.delete(hash);
|
|
98
|
+
/*
|
|
99
|
+
* Evicted while in flight. Its slot belongs to somebody else now, so these bytes go nowhere —
|
|
100
|
+
* and the tile is left absent rather than resurrected, because whatever evicted it decided it
|
|
101
|
+
* was not wanted and this arriving late does not change that.
|
|
102
|
+
*/
|
|
103
|
+
if (tileState(streamer.table, hash) !== TILE_REQUESTED) {
|
|
104
|
+
streamer.stale += 1;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (bytes === null) {
|
|
108
|
+
evict(streamer.table, hash);
|
|
109
|
+
streamer.missing.add(hash);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (bytes.length > streamer.cache.pageBytes) {
|
|
113
|
+
/* A truncation here would be a tile that decodes to something nobody authored, found weeks
|
|
114
|
+
later as a corrupt-looking texture. Refused, counted, and left absent. */
|
|
115
|
+
streamer.oversize += 1;
|
|
116
|
+
evict(streamer.table, hash);
|
|
117
|
+
streamer.missing.add(hash);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const slot = acquirePage(streamer.cache, hash);
|
|
121
|
+
if (slot < 0) {
|
|
122
|
+
/* Every page is in use by this frame. The tile stays absent, so the next pump asks again. */
|
|
123
|
+
streamer.deferred += 1;
|
|
124
|
+
evict(streamer.table, hash);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
drainEvictions(streamer);
|
|
128
|
+
streamer.cache.bytes.set(bytes, slot * streamer.cache.pageBytes);
|
|
129
|
+
markResident(streamer.table, hash, slot);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Tell the table about pages the cache took, so nothing is called resident in a slot it lost.
|
|
133
|
+
*
|
|
134
|
+
* The one piece of wiring between the two structures, and the reason `takeEvicted` exists: a table
|
|
135
|
+
* still pointing at a reused slot hands a frame another tile's bytes.
|
|
136
|
+
*/
|
|
137
|
+
function drainEvictions(streamer) {
|
|
138
|
+
const taken = [];
|
|
139
|
+
takeEvicted(streamer.cache, taken);
|
|
140
|
+
for (const hash of taken)
|
|
141
|
+
evict(streamer.table, hash);
|
|
142
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What is here, what is on its way, and what is not.
|
|
3
|
+
*
|
|
4
|
+
* **The tile the current frame is sampling is never evicted.** That is the one rule this structure
|
|
5
|
+
* exists to enforce: an eviction policy that can reclaim a tile in use produces a frame drawn
|
|
6
|
+
* against a page somebody else is writing, which is a corruption rather than a stall and does not
|
|
7
|
+
* look like a streaming problem at all.
|
|
8
|
+
*/
|
|
9
|
+
export declare const TILE_ABSENT = 0;
|
|
10
|
+
export declare const TILE_REQUESTED = 1;
|
|
11
|
+
export declare const TILE_RESIDENT = 2;
|
|
12
|
+
export interface ResidencyTable {
|
|
13
|
+
state: Map<string, number>;
|
|
14
|
+
slot: Map<string, number>;
|
|
15
|
+
/** Monotonic counter standing in for time, so eviction needs no clock. */
|
|
16
|
+
touchedAt: Map<string, number>;
|
|
17
|
+
clock: number;
|
|
18
|
+
capacity: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function createResidencyTable(capacity: number): ResidencyTable;
|
|
21
|
+
export declare function tileState(table: ResidencyTable, hash: string): number;
|
|
22
|
+
export declare function markRequested(table: ResidencyTable, hash: string): void;
|
|
23
|
+
export declare function markResident(table: ResidencyTable, hash: string, slot: number): void;
|
|
24
|
+
/** Say this tile is in use now, so eviction will not take it. */
|
|
25
|
+
export declare function touchTile(table: ResidencyTable, hash: string): void;
|
|
26
|
+
export declare function evict(table: ResidencyTable, hash: string): void;
|
|
27
|
+
export declare function slotFor(table: ResidencyTable, hash: string): number;
|
|
28
|
+
export declare function residentCount(table: ResidencyTable): number;
|
|
29
|
+
/**
|
|
30
|
+
* The least recently used resident tiles, oldest first, **excluding anything touched since
|
|
31
|
+
* `since`**.
|
|
32
|
+
*
|
|
33
|
+
* The exclusion is the point. A caller passes the clock value from the start of the frame, and
|
|
34
|
+
* everything this frame has sampled is off the table.
|
|
35
|
+
*/
|
|
36
|
+
export declare function leastRecentlyUsed(table: ResidencyTable, since: number, out: string[], count: number): number;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What is here, what is on its way, and what is not.
|
|
3
|
+
*
|
|
4
|
+
* **The tile the current frame is sampling is never evicted.** That is the one rule this structure
|
|
5
|
+
* exists to enforce: an eviction policy that can reclaim a tile in use produces a frame drawn
|
|
6
|
+
* against a page somebody else is writing, which is a corruption rather than a stall and does not
|
|
7
|
+
* look like a streaming problem at all.
|
|
8
|
+
*/
|
|
9
|
+
export const TILE_ABSENT = 0;
|
|
10
|
+
export const TILE_REQUESTED = 1;
|
|
11
|
+
export const TILE_RESIDENT = 2;
|
|
12
|
+
export function createResidencyTable(capacity) {
|
|
13
|
+
return { state: new Map(), slot: new Map(), touchedAt: new Map(), clock: 0, capacity };
|
|
14
|
+
}
|
|
15
|
+
export function tileState(table, hash) {
|
|
16
|
+
return table.state.get(hash) ?? TILE_ABSENT;
|
|
17
|
+
}
|
|
18
|
+
export function markRequested(table, hash) {
|
|
19
|
+
/* An already-resident tile is not re-requested: that is a second fetch of bytes we hold. */
|
|
20
|
+
if (tileState(table, hash) !== TILE_ABSENT)
|
|
21
|
+
return;
|
|
22
|
+
table.state.set(hash, TILE_REQUESTED);
|
|
23
|
+
}
|
|
24
|
+
export function markResident(table, hash, slot) {
|
|
25
|
+
table.state.set(hash, TILE_RESIDENT);
|
|
26
|
+
table.slot.set(hash, slot);
|
|
27
|
+
touchTile(table, hash);
|
|
28
|
+
}
|
|
29
|
+
/** Say this tile is in use now, so eviction will not take it. */
|
|
30
|
+
export function touchTile(table, hash) {
|
|
31
|
+
table.clock += 1;
|
|
32
|
+
table.touchedAt.set(hash, table.clock);
|
|
33
|
+
}
|
|
34
|
+
export function evict(table, hash) {
|
|
35
|
+
table.state.delete(hash);
|
|
36
|
+
table.slot.delete(hash);
|
|
37
|
+
table.touchedAt.delete(hash);
|
|
38
|
+
}
|
|
39
|
+
export function slotFor(table, hash) {
|
|
40
|
+
return table.slot.get(hash) ?? -1;
|
|
41
|
+
}
|
|
42
|
+
export function residentCount(table) {
|
|
43
|
+
let count = 0;
|
|
44
|
+
for (const state of table.state.values())
|
|
45
|
+
if (state === TILE_RESIDENT)
|
|
46
|
+
count += 1;
|
|
47
|
+
return count;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The least recently used resident tiles, oldest first, **excluding anything touched since
|
|
51
|
+
* `since`**.
|
|
52
|
+
*
|
|
53
|
+
* The exclusion is the point. A caller passes the clock value from the start of the frame, and
|
|
54
|
+
* everything this frame has sampled is off the table.
|
|
55
|
+
*/
|
|
56
|
+
export function leastRecentlyUsed(table, since, out, count) {
|
|
57
|
+
const candidates = [];
|
|
58
|
+
for (const [hash, state] of table.state) {
|
|
59
|
+
if (state !== TILE_RESIDENT)
|
|
60
|
+
continue;
|
|
61
|
+
const at = table.touchedAt.get(hash) ?? 0;
|
|
62
|
+
if (at > since)
|
|
63
|
+
continue;
|
|
64
|
+
candidates.push({ hash, at });
|
|
65
|
+
}
|
|
66
|
+
candidates.sort((a, b) => a.at - b.at || (a.hash < b.hash ? -1 : 1));
|
|
67
|
+
const written = Math.min(count, candidates.length);
|
|
68
|
+
for (let i = 0; i < written; i += 1)
|
|
69
|
+
out[i] = candidates[i].hash;
|
|
70
|
+
out.length = written;
|
|
71
|
+
return written;
|
|
72
|
+
}
|