@graphty/webgpu-graph-algorithms 0.0.0 → 0.2.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 +344 -23
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +32 -0
- package/dist/browser.js.map +1 -0
- package/dist/chunks/context-E6iKaeuJ.js +3136 -0
- package/dist/chunks/context-E6iKaeuJ.js.map +1 -0
- package/dist/node.d.ts +1 -0
- package/dist/node.js +131 -0
- package/dist/node.js.map +1 -0
- package/dist/src/accelerator.d.ts +26 -0
- package/dist/src/accelerator.d.ts.map +1 -0
- package/dist/src/accelerator.js +101 -0
- package/dist/src/accelerator.js.map +1 -0
- package/dist/src/algorithms/degree.d.ts +35 -0
- package/dist/src/algorithms/degree.d.ts.map +1 -0
- package/dist/src/algorithms/degree.js +119 -0
- package/dist/src/algorithms/degree.js.map +1 -0
- package/dist/src/browser/index.d.ts +23 -0
- package/dist/src/browser/index.d.ts.map +1 -0
- package/dist/src/browser/index.js +48 -0
- package/dist/src/browser/index.js.map +1 -0
- package/dist/src/constants.d.ts +92 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +92 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/context.d.ts +84 -0
- package/dist/src/context.d.ts.map +1 -0
- package/dist/src/context.js +304 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/device/acquire.d.ts +57 -0
- package/dist/src/device/acquire.d.ts.map +1 -0
- package/dist/src/device/acquire.js +232 -0
- package/dist/src/device/acquire.js.map +1 -0
- package/dist/src/device/caps.d.ts +43 -0
- package/dist/src/device/caps.d.ts.map +1 -0
- package/dist/src/device/caps.js +104 -0
- package/dist/src/device/caps.js.map +1 -0
- package/dist/src/device/error-scope.d.ts +75 -0
- package/dist/src/device/error-scope.d.ts.map +1 -0
- package/dist/src/device/error-scope.js +152 -0
- package/dist/src/device/error-scope.js.map +1 -0
- package/dist/src/device/lost.d.ts +51 -0
- package/dist/src/device/lost.d.ts.map +1 -0
- package/dist/src/device/lost.js +130 -0
- package/dist/src/device/lost.js.map +1 -0
- package/dist/src/device/webgpu-constants.d.ts +31 -0
- package/dist/src/device/webgpu-constants.d.ts.map +1 -0
- package/dist/src/device/webgpu-constants.js +31 -0
- package/dist/src/device/webgpu-constants.js.map +1 -0
- package/dist/src/errors.d.ts +56 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +57 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/index.d.ts +29 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +27 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/kernel/batch.d.ts +116 -0
- package/dist/src/kernel/batch.d.ts.map +1 -0
- package/dist/src/kernel/batch.js +335 -0
- package/dist/src/kernel/batch.js.map +1 -0
- package/dist/src/kernel/dispatch.d.ts +59 -0
- package/dist/src/kernel/dispatch.d.ts.map +1 -0
- package/dist/src/kernel/dispatch.js +139 -0
- package/dist/src/kernel/dispatch.js.map +1 -0
- package/dist/src/kernel/kernel.d.ts +84 -0
- package/dist/src/kernel/kernel.d.ts.map +1 -0
- package/dist/src/kernel/kernel.js +239 -0
- package/dist/src/kernel/kernel.js.map +1 -0
- package/dist/src/kernel/pipeline-cache.d.ts +90 -0
- package/dist/src/kernel/pipeline-cache.d.ts.map +1 -0
- package/dist/src/kernel/pipeline-cache.js +251 -0
- package/dist/src/kernel/pipeline-cache.js.map +1 -0
- package/dist/src/kernel/prelude.d.ts +35 -0
- package/dist/src/kernel/prelude.d.ts.map +1 -0
- package/dist/src/kernel/prelude.js +211 -0
- package/dist/src/kernel/prelude.js.map +1 -0
- package/dist/src/kernel/profiler.d.ts +64 -0
- package/dist/src/kernel/profiler.d.ts.map +1 -0
- package/dist/src/kernel/profiler.js +120 -0
- package/dist/src/kernel/profiler.js.map +1 -0
- package/dist/src/kernel/struct-block.d.ts +122 -0
- package/dist/src/kernel/struct-block.d.ts.map +1 -0
- package/dist/src/kernel/struct-block.js +353 -0
- package/dist/src/kernel/struct-block.js.map +1 -0
- package/dist/src/kernel/uniform-ring.d.ts +70 -0
- package/dist/src/kernel/uniform-ring.d.ts.map +1 -0
- package/dist/src/kernel/uniform-ring.js +146 -0
- package/dist/src/kernel/uniform-ring.js.map +1 -0
- package/dist/src/kernel/wgsl.d.ts +88 -0
- package/dist/src/kernel/wgsl.d.ts.map +1 -0
- package/dist/src/kernel/wgsl.js +390 -0
- package/dist/src/kernel/wgsl.js.map +1 -0
- package/dist/src/kernels.d.ts +81 -0
- package/dist/src/kernels.d.ts.map +1 -0
- package/dist/src/kernels.js +417 -0
- package/dist/src/kernels.js.map +1 -0
- package/dist/src/layouts/force-simulation.d.ts +498 -0
- package/dist/src/layouts/force-simulation.d.ts.map +1 -0
- package/dist/src/layouts/force-simulation.js +1650 -0
- package/dist/src/layouts/force-simulation.js.map +1 -0
- package/dist/src/layouts/forceatlas2.d.ts +210 -0
- package/dist/src/layouts/forceatlas2.d.ts.map +1 -0
- package/dist/src/layouts/forceatlas2.js +759 -0
- package/dist/src/layouts/forceatlas2.js.map +1 -0
- package/dist/src/layouts/inputs.d.ts +40 -0
- package/dist/src/layouts/inputs.d.ts.map +1 -0
- package/dist/src/layouts/inputs.js +185 -0
- package/dist/src/layouts/inputs.js.map +1 -0
- package/dist/src/layouts/repulsion-exact.d.ts +85 -0
- package/dist/src/layouts/repulsion-exact.d.ts.map +1 -0
- package/dist/src/layouts/repulsion-exact.js +134 -0
- package/dist/src/layouts/repulsion-exact.js.map +1 -0
- package/dist/src/layouts/seed.d.ts +56 -0
- package/dist/src/layouts/seed.d.ts.map +1 -0
- package/dist/src/layouts/seed.js +173 -0
- package/dist/src/layouts/seed.js.map +1 -0
- package/dist/src/memory/buffer-pool.d.ts +73 -0
- package/dist/src/memory/buffer-pool.d.ts.map +1 -0
- package/dist/src/memory/buffer-pool.js +170 -0
- package/dist/src/memory/buffer-pool.js.map +1 -0
- package/dist/src/memory/lease.d.ts +53 -0
- package/dist/src/memory/lease.d.ts.map +1 -0
- package/dist/src/memory/lease.js +85 -0
- package/dist/src/memory/lease.js.map +1 -0
- package/dist/src/memory/readback.d.ts +143 -0
- package/dist/src/memory/readback.d.ts.map +1 -0
- package/dist/src/memory/readback.js +375 -0
- package/dist/src/memory/readback.js.map +1 -0
- package/dist/src/memory/residency.d.ts +83 -0
- package/dist/src/memory/residency.d.ts.map +1 -0
- package/dist/src/memory/residency.js +573 -0
- package/dist/src/memory/residency.js.map +1 -0
- package/dist/src/memory/upload-plan.d.ts +101 -0
- package/dist/src/memory/upload-plan.d.ts.map +1 -0
- package/dist/src/memory/upload-plan.js +265 -0
- package/dist/src/memory/upload-plan.js.map +1 -0
- package/dist/src/node/index.d.ts +64 -0
- package/dist/src/node/index.d.ts.map +1 -0
- package/dist/src/node/index.js +183 -0
- package/dist/src/node/index.js.map +1 -0
- package/dist/src/primitives/reduce.d.ts +57 -0
- package/dist/src/primitives/reduce.d.ts.map +1 -0
- package/dist/src/primitives/reduce.js +161 -0
- package/dist/src/primitives/reduce.js.map +1 -0
- package/dist/src/primitives/segmented-reduce.d.ts +38 -0
- package/dist/src/primitives/segmented-reduce.d.ts.map +1 -0
- package/dist/src/primitives/segmented-reduce.js +211 -0
- package/dist/src/primitives/segmented-reduce.js.map +1 -0
- package/dist/src/types/accelerator.d.ts +209 -0
- package/dist/src/types/accelerator.d.ts.map +1 -0
- package/dist/src/types/accelerator.js +8 -0
- package/dist/src/types/accelerator.js.map +1 -0
- package/dist/src/types/context.d.ts +114 -0
- package/dist/src/types/context.d.ts.map +1 -0
- package/dist/src/types/context.js +7 -0
- package/dist/src/types/context.js.map +1 -0
- package/dist/src/types/layout.d.ts +95 -0
- package/dist/src/types/layout.d.ts.map +1 -0
- package/dist/src/types/layout.js +6 -0
- package/dist/src/types/layout.js.map +1 -0
- package/dist/src/types/memory.d.ts +22 -0
- package/dist/src/types/memory.d.ts.map +1 -0
- package/dist/src/types/memory.js +7 -0
- package/dist/src/types/memory.js.map +1 -0
- package/dist/src/types/options.d.ts +78 -0
- package/dist/src/types/options.d.ts.map +1 -0
- package/dist/src/types/options.js +7 -0
- package/dist/src/types/options.js.map +1 -0
- package/dist/src/types/run.d.ts +13 -0
- package/dist/src/types/run.d.ts.map +1 -0
- package/dist/src/types/run.js +6 -0
- package/dist/src/types/run.js.map +1 -0
- package/dist/src/wgsl/degree.wgsl.d.ts +10 -0
- package/dist/src/wgsl/degree.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/degree.wgsl.js +25 -0
- package/dist/src/wgsl/degree.wgsl.js.map +1 -0
- package/dist/src/wgsl/fa2-attraction.wgsl.d.ts +12 -0
- package/dist/src/wgsl/fa2-attraction.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/fa2-attraction.wgsl.js +37 -0
- package/dist/src/wgsl/fa2-attraction.wgsl.js.map +1 -0
- package/dist/src/wgsl/fa2-integrate.wgsl.d.ts +13 -0
- package/dist/src/wgsl/fa2-integrate.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/fa2-integrate.wgsl.js +69 -0
- package/dist/src/wgsl/fa2-integrate.wgsl.js.map +1 -0
- package/dist/src/wgsl/fa2-repulsion-exact.wgsl.d.ts +12 -0
- package/dist/src/wgsl/fa2-repulsion-exact.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/fa2-repulsion-exact.wgsl.js +79 -0
- package/dist/src/wgsl/fa2-repulsion-exact.wgsl.js.map +1 -0
- package/dist/src/wgsl/fa2-speed-finalize.wgsl.d.ts +15 -0
- package/dist/src/wgsl/fa2-speed-finalize.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/fa2-speed-finalize.wgsl.js +54 -0
- package/dist/src/wgsl/fa2-speed-finalize.wgsl.js.map +1 -0
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts +14 -0
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.js +57 -0
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.js.map +1 -0
- package/dist/src/wgsl/fa2-to-scene.wgsl.d.ts +11 -0
- package/dist/src/wgsl/fa2-to-scene.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/fa2-to-scene.wgsl.js +19 -0
- package/dist/src/wgsl/fa2-to-scene.wgsl.js.map +1 -0
- package/dist/src/wgsl/fill.wgsl.d.ts +7 -0
- package/dist/src/wgsl/fill.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/fill.wgsl.js +14 -0
- package/dist/src/wgsl/fill.wgsl.js.map +1 -0
- package/dist/src/wgsl/reduce.wgsl.d.ts +10 -0
- package/dist/src/wgsl/reduce.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/reduce.wgsl.js +63 -0
- package/dist/src/wgsl/reduce.wgsl.js.map +1 -0
- package/dist/src/wgsl/segmented-reduce.wgsl.d.ts +13 -0
- package/dist/src/wgsl/segmented-reduce.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/segmented-reduce.wgsl.js +35 -0
- package/dist/src/wgsl/segmented-reduce.wgsl.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/dist/webgpu-graph-algorithms.d.ts +1 -0
- package/dist/webgpu-graph-algorithms.js +4454 -0
- package/dist/webgpu-graph-algorithms.js.map +1 -0
- package/package.json +108 -17
- package/src/accelerator.ts +117 -0
- package/src/algorithms/degree.ts +142 -0
- package/src/browser/index.ts +57 -0
- package/src/constants.ts +116 -0
- package/src/context.ts +399 -0
- package/src/device/acquire.ts +256 -0
- package/src/device/caps.ts +122 -0
- package/src/device/error-scope.ts +171 -0
- package/src/device/lost.ts +142 -0
- package/src/device/webgpu-constants.ts +44 -0
- package/src/errors.ts +94 -0
- package/src/index.ts +102 -0
- package/src/kernel/batch.ts +427 -0
- package/src/kernel/dispatch.ts +162 -0
- package/src/kernel/kernel.ts +311 -0
- package/src/kernel/pipeline-cache.ts +288 -0
- package/src/kernel/prelude.ts +229 -0
- package/src/kernel/profiler.ts +148 -0
- package/src/kernel/struct-block.ts +439 -0
- package/src/kernel/uniform-ring.ts +184 -0
- package/src/kernel/wgsl.ts +490 -0
- package/src/kernels.ts +511 -0
- package/src/layouts/force-simulation.ts +2111 -0
- package/src/layouts/forceatlas2.ts +942 -0
- package/src/layouts/inputs.ts +252 -0
- package/src/layouts/repulsion-exact.ts +183 -0
- package/src/layouts/seed.ts +198 -0
- package/src/memory/buffer-pool.ts +204 -0
- package/src/memory/lease.ts +93 -0
- package/src/memory/readback.ts +429 -0
- package/src/memory/residency.ts +753 -0
- package/src/memory/upload-plan.ts +350 -0
- package/src/node/index.ts +230 -0
- package/src/primitives/reduce.ts +233 -0
- package/src/primitives/segmented-reduce.ts +270 -0
- package/src/types/accelerator.ts +236 -0
- package/src/types/context.ts +135 -0
- package/src/types/layout.ts +103 -0
- package/src/types/memory.ts +23 -0
- package/src/types/options.ts +84 -0
- package/src/types/run.ts +13 -0
- package/src/wgsl/degree.wgsl.ts +24 -0
- package/src/wgsl/fa2-attraction.wgsl.ts +37 -0
- package/src/wgsl/fa2-integrate.wgsl.ts +69 -0
- package/src/wgsl/fa2-repulsion-exact.wgsl.ts +78 -0
- package/src/wgsl/fa2-speed-finalize.wgsl.ts +53 -0
- package/src/wgsl/fa2-stats-finalize.wgsl.ts +57 -0
- package/src/wgsl/fa2-to-scene.wgsl.ts +19 -0
- package/src/wgsl/fill.wgsl.ts +13 -0
- package/src/wgsl/reduce.wgsl.ts +62 -0
- package/src/wgsl/segmented-reduce.wgsl.ts +35 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The staging ring of spec 4.4: a ring of MAP_READ | COPY_DST buffers (default DEFAULT_STAGING_SLOTS slots of 4 MiB,
|
|
3
|
+
* PLAN DECISION 15) through which every readback of the package flows. `read()` copies with its own encoder and
|
|
4
|
+
* submit, awaits mapAsync, copies out of the mapped range BEFORE unmap (the range is detached at unmap, design 10.7)
|
|
5
|
+
* and never polls onSubmittedWorkDone; requests above the slot size are chunked. Ownership is one-directional: the
|
|
6
|
+
* ring owns its buffers; a CommandBatch (P2) BORROWS one slot through borrowSlot, maps it through mapSlot and always
|
|
7
|
+
* returns it through returnSlot, which unmaps it. Slots at or above OOM_SCOPE_THRESHOLD_BYTES are created through the
|
|
8
|
+
* AllocationTracker (the out-of-memory scope of spec 5.7), the default-size ones directly on the device, so the
|
|
9
|
+
* allocator counts residency and pool buffers only. A mapAsync rejection is E_DEVICE_LOST (or E_DISPOSED after
|
|
10
|
+
* destroyAll).
|
|
11
|
+
*
|
|
12
|
+
* The one invariant every path keeps: a staging buffer whose mapAsync is PENDING is never unmapped and never
|
|
13
|
+
* destroyed. dawn-node 0.4.0 settles the pending promise synchronously on unmap() / destroy() and settles it a
|
|
14
|
+
* second time when Dawn's map callback arrives (a SIGSEGV in AsyncRunner::Reject); on the Vulkan backends the
|
|
15
|
+
* callback has already run inside device.destroy(), on Dawn's Metal backend it arrives after the device-lost
|
|
16
|
+
* fan-out, which is where the macOS host lane lost its vitest worker (CLAUDE.md "Verified Platform Facts"). The
|
|
17
|
+
* ring therefore tracks every map on its slots (mapPending) and defers an unmap, a return or a destroy that
|
|
18
|
+
* arrives during one to the moment the map settles.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { DEFAULT_STAGING_SLOTS, OOM_SCOPE_THRESHOLD_BYTES } from "../constants.js";
|
|
22
|
+
import { type AllocationTracker } from "../device/error-scope.js";
|
|
23
|
+
import { BufferUsage, MapMode } from "../device/webgpu-constants.js";
|
|
24
|
+
import { WebGpuGraphError } from "../errors.js";
|
|
25
|
+
|
|
26
|
+
/** Default capacity of one staging slot (PLAN DECISION 15). */
|
|
27
|
+
const DEFAULT_SLOT_BYTES = 4 * 1024 * 1024;
|
|
28
|
+
|
|
29
|
+
/** Every staging buffer is MAP_READ | COPY_DST. */
|
|
30
|
+
const STAGING_USAGE = BufferUsage.MAP_READ | BufferUsage.COPY_DST;
|
|
31
|
+
|
|
32
|
+
/** Grown slots are sized in 256-byte steps. */
|
|
33
|
+
const SLOT_GRANULE = 256;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* One MAP_READ | COPY_DST staging buffer of the ring. Exported for the borrowers of borrowSlot / returnSlot
|
|
37
|
+
* (contract 3.8): P2-T1's CommandBatch imports it as a type; nothing at P1 imports it by name.
|
|
38
|
+
* @public
|
|
39
|
+
*/
|
|
40
|
+
export interface StagingSlot {
|
|
41
|
+
readonly index: number;
|
|
42
|
+
readonly buffer: GPUBuffer;
|
|
43
|
+
readonly capacity: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The ring's bookkeeping of one slot. */
|
|
47
|
+
interface SlotEntry {
|
|
48
|
+
readonly slot: StagingSlot;
|
|
49
|
+
borrowed: boolean;
|
|
50
|
+
/** True between a successful mapAsync through the ring and the unmap of returnSlot (a fallback when mapState is absent). */
|
|
51
|
+
mapped: boolean;
|
|
52
|
+
/** True while a mapAsync through the ring (read() or mapSlot) is pending: returnSlot and destroyAll defer to the settle. */
|
|
53
|
+
mapPending: boolean;
|
|
54
|
+
/** True when returnSlot ran during a pending map: the settle finishes the return. */
|
|
55
|
+
returnRequested: boolean;
|
|
56
|
+
/** True when the buffer was created through the allocator (and is destroyed through it). */
|
|
57
|
+
readonly tracked: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Whether an ArrayBuffer-like is a SharedArrayBuffer (checked by tag so the global need not exist).
|
|
62
|
+
* @param buffer - the buffer to test
|
|
63
|
+
* @returns true for a SharedArrayBuffer
|
|
64
|
+
*/
|
|
65
|
+
function isSharedBuffer(buffer: ArrayBufferLike): boolean {
|
|
66
|
+
return Object.prototype.toString.call(buffer) === "[object SharedArrayBuffer]";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The E_INVALID_ARGUMENT error of a bad readback argument.
|
|
71
|
+
* @param argument - the argument name
|
|
72
|
+
* @param value - the value given
|
|
73
|
+
* @param expected - what was expected
|
|
74
|
+
* @returns the error to throw
|
|
75
|
+
*/
|
|
76
|
+
function invalid(argument: string, value: unknown, expected: string): WebGpuGraphError {
|
|
77
|
+
return new WebGpuGraphError("E_INVALID_ARGUMENT", `${argument}: expected ${expected}`, {
|
|
78
|
+
argument,
|
|
79
|
+
value,
|
|
80
|
+
expected,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The staging ring (spec 4.4): default 3 slots; grows when every slot is busy; always unmaps and destroys its own buffers. */
|
|
85
|
+
export class Readback {
|
|
86
|
+
private readonly device: GPUDevice;
|
|
87
|
+
private readonly allocator: AllocationTracker;
|
|
88
|
+
private readonly slotBytes: number;
|
|
89
|
+
private readonly ring: SlotEntry[] = [];
|
|
90
|
+
private disposed = false;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Creates the ring with `slots` slots of `slotBytes` each.
|
|
94
|
+
* @param device - the device the copies run on
|
|
95
|
+
* @param allocator - the context's OOM-scoped allocator (used for slots at or above OOM_SCOPE_THRESHOLD_BYTES)
|
|
96
|
+
* @param options - ring options
|
|
97
|
+
* @param options.slots - slots created up front; default DEFAULT_STAGING_SLOTS
|
|
98
|
+
* @param options.slotBytes - capacity of each slot and the chunk size of read(); default 4 MiB; a positive multiple of 4
|
|
99
|
+
*/
|
|
100
|
+
constructor(
|
|
101
|
+
device: GPUDevice,
|
|
102
|
+
allocator: AllocationTracker,
|
|
103
|
+
options?: { readonly slots?: number | undefined; readonly slotBytes?: number | undefined },
|
|
104
|
+
) {
|
|
105
|
+
const slotBytes = options?.slotBytes ?? DEFAULT_SLOT_BYTES;
|
|
106
|
+
if (!Number.isInteger(slotBytes) || slotBytes <= 0 || slotBytes % 4 !== 0) {
|
|
107
|
+
throw invalid("slotBytes", slotBytes, "a positive multiple of 4");
|
|
108
|
+
}
|
|
109
|
+
const slots = options?.slots ?? DEFAULT_STAGING_SLOTS;
|
|
110
|
+
if (!Number.isInteger(slots) || slots < 0) {
|
|
111
|
+
throw invalid("slots", slots, "a non-negative integer");
|
|
112
|
+
}
|
|
113
|
+
this.device = device;
|
|
114
|
+
this.allocator = allocator;
|
|
115
|
+
this.slotBytes = slotBytes;
|
|
116
|
+
for (let i = 0; i < slots; i++) {
|
|
117
|
+
this.addSlot(slotBytes);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Copies `byteLength` bytes from `src` at `srcOffset` (own encoder, own submit), maps, copies out BEFORE unmap;
|
|
123
|
+
* resolves the bytes (a fresh ArrayBuffer, or `dest.buffer` after `dest.set` when given). Requests above the slot
|
|
124
|
+
* size are chunked.
|
|
125
|
+
* @param src - a COPY_SRC buffer
|
|
126
|
+
* @param byteLength - bytes to read (a positive multiple of 4, within src)
|
|
127
|
+
* @param dest - an optional destination view of at least byteLength bytes over a plain ArrayBuffer
|
|
128
|
+
* @param srcOffset - the byte offset in src (default 0; a multiple of 4)
|
|
129
|
+
* @returns the bytes read
|
|
130
|
+
*/
|
|
131
|
+
async read(src: GPUBuffer, byteLength: number, dest?: ArrayBufferView, srcOffset?: number): Promise<ArrayBuffer> {
|
|
132
|
+
this.assertLive();
|
|
133
|
+
const offset = srcOffset ?? 0;
|
|
134
|
+
if (!Number.isInteger(byteLength) || byteLength <= 0 || byteLength % 4 !== 0) {
|
|
135
|
+
throw invalid("byteLength", byteLength, "a positive multiple of 4");
|
|
136
|
+
}
|
|
137
|
+
if (!Number.isInteger(offset) || offset < 0 || offset % 4 !== 0) {
|
|
138
|
+
throw invalid("srcOffset", offset, "a non-negative multiple of 4");
|
|
139
|
+
}
|
|
140
|
+
if (offset + byteLength > src.size) {
|
|
141
|
+
throw invalid("byteLength", byteLength, `srcOffset + byteLength <= src.size (${src.size})`);
|
|
142
|
+
}
|
|
143
|
+
if ((src.usage & BufferUsage.COPY_SRC) === 0) {
|
|
144
|
+
throw invalid("src", src.label, "a buffer with COPY_SRC usage");
|
|
145
|
+
}
|
|
146
|
+
let out: ArrayBuffer;
|
|
147
|
+
let outBytes: Uint8Array;
|
|
148
|
+
if (dest === undefined) {
|
|
149
|
+
out = new ArrayBuffer(byteLength);
|
|
150
|
+
outBytes = new Uint8Array(out);
|
|
151
|
+
} else {
|
|
152
|
+
if (isSharedBuffer(dest.buffer)) {
|
|
153
|
+
throw invalid("dest", "SharedArrayBuffer", "a view over a plain ArrayBuffer");
|
|
154
|
+
}
|
|
155
|
+
if (dest.byteLength < byteLength) {
|
|
156
|
+
throw invalid("dest", dest.byteLength, `at least ${byteLength} bytes`);
|
|
157
|
+
}
|
|
158
|
+
out = dest.buffer as ArrayBuffer;
|
|
159
|
+
outBytes = new Uint8Array(out, dest.byteOffset, byteLength);
|
|
160
|
+
}
|
|
161
|
+
let done = 0;
|
|
162
|
+
while (done < byteLength) {
|
|
163
|
+
const chunk = Math.min(this.slotBytes, byteLength - done);
|
|
164
|
+
const slot = this.borrowSlot(chunk);
|
|
165
|
+
const entry = this.ring[slot.index];
|
|
166
|
+
try {
|
|
167
|
+
const encoder = this.device.createCommandEncoder({ label: "readback" });
|
|
168
|
+
encoder.copyBufferToBuffer(src, offset + done, slot.buffer, 0, chunk);
|
|
169
|
+
this.device.queue.submit([encoder.finish()]);
|
|
170
|
+
await this.map(entry, chunk);
|
|
171
|
+
outBytes.set(new Uint8Array(slot.buffer.getMappedRange(0, chunk)), done);
|
|
172
|
+
} finally {
|
|
173
|
+
this.returnSlot(slot);
|
|
174
|
+
}
|
|
175
|
+
done += chunk;
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Reads one u32 counter through the same ring.
|
|
182
|
+
* @param src - a COPY_SRC buffer
|
|
183
|
+
* @param byteOffset - the counter's byte offset (a multiple of 4)
|
|
184
|
+
* @returns the counter value
|
|
185
|
+
*/
|
|
186
|
+
async readU32(src: GPUBuffer, byteOffset: number): Promise<number> {
|
|
187
|
+
const bytes = await this.read(src, 4, undefined, byteOffset);
|
|
188
|
+
return new Uint32Array(bytes)[0];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Borrows an unmapped slot of at least byteLength (grows the ring when none is free); the borrower ALWAYS
|
|
193
|
+
* returns it (spec 4.4).
|
|
194
|
+
* @param byteLength - the bytes the borrower will copy into the slot
|
|
195
|
+
* @returns the slot
|
|
196
|
+
*/
|
|
197
|
+
borrowSlot(byteLength: number): StagingSlot {
|
|
198
|
+
this.assertLive();
|
|
199
|
+
if (!Number.isInteger(byteLength) || byteLength <= 0) {
|
|
200
|
+
throw invalid("byteLength", byteLength, "a positive integer");
|
|
201
|
+
}
|
|
202
|
+
// a free slot is unmapped by construction: returnSlot unmaps, and only a borrower maps
|
|
203
|
+
for (const entry of this.ring) {
|
|
204
|
+
if (!entry.borrowed && entry.slot.capacity >= byteLength) {
|
|
205
|
+
entry.borrowed = true;
|
|
206
|
+
return entry.slot;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const capacity = Math.max(this.slotBytes, Math.ceil(byteLength / SLOT_GRANULE) * SLOT_GRANULE);
|
|
210
|
+
const entry = this.addSlot(capacity);
|
|
211
|
+
entry.borrowed = true;
|
|
212
|
+
return entry.slot;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* mapAsync(READ) on a borrowed slot, tracked by the ring: the slot counts as borrowed, and is neither unmapped
|
|
217
|
+
* nor destroyed, until the map settles (see the file comment). Returns the runtime's own promise, untranslated:
|
|
218
|
+
* the borrower classifies a rejection (a CommandBatch races it against device loss, spec 5.7).
|
|
219
|
+
* @param slot - a slot borrowed from this ring and not yet returned
|
|
220
|
+
* @param byteLength - the bytes to map from offset 0
|
|
221
|
+
* @returns the mapAsync promise
|
|
222
|
+
*/
|
|
223
|
+
mapSlot(slot: StagingSlot, byteLength: number): Promise<void> {
|
|
224
|
+
this.assertLive();
|
|
225
|
+
const entry = this.borrowedEntry(slot);
|
|
226
|
+
if (entry.mapPending || entry.mapped) {
|
|
227
|
+
throw invalid("slot", slot.index, "a borrowed slot that is not mapped and has no map pending");
|
|
228
|
+
}
|
|
229
|
+
return this.track(entry, slot.buffer.mapAsync(MapMode.READ, 0, byteLength));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Returns a borrowed slot (unmapping it if mapped). A slot whose map through the ring is still pending stays
|
|
234
|
+
* borrowed until the map settles, when the ring unmaps and returns it; a borrower that maps a slot itself must
|
|
235
|
+
* unmap it before returning it (spec 4.4; `mapState` decides when the runtime exposes it, otherwise the ring's
|
|
236
|
+
* own record does). A no-op after destroyAll() (the ring is gone; a borrower's finally must never mask the
|
|
237
|
+
* E_DISPOSED / E_DEVICE_LOST of the read it was serving).
|
|
238
|
+
* @param slot - a slot borrowed from this ring and not yet returned
|
|
239
|
+
*/
|
|
240
|
+
returnSlot(slot: StagingSlot): void {
|
|
241
|
+
if (this.disposed) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const entry = this.borrowedEntry(slot);
|
|
245
|
+
if (entry.mapPending) {
|
|
246
|
+
entry.returnRequested = true;
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
this.finishReturn(entry);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* The entry of a slot borrowed from this ring and not yet returned.
|
|
254
|
+
* @param slot - the slot
|
|
255
|
+
* @returns its entry
|
|
256
|
+
*/
|
|
257
|
+
private borrowedEntry(slot: StagingSlot): SlotEntry {
|
|
258
|
+
const entry = this.ring[slot.index];
|
|
259
|
+
if (entry === undefined || entry.slot !== slot || !entry.borrowed) {
|
|
260
|
+
throw invalid("slot", slot.index, "a slot borrowed from this ring and not yet returned");
|
|
261
|
+
}
|
|
262
|
+
return entry;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Unmaps a returned slot when it is mapped and frees it.
|
|
267
|
+
* @param entry - the slot's entry (no map pending)
|
|
268
|
+
*/
|
|
269
|
+
private finishReturn(entry: SlotEntry): void {
|
|
270
|
+
const state: string | undefined = entry.slot.buffer.mapState;
|
|
271
|
+
const mapped = state === undefined ? entry.mapped : state !== "unmapped";
|
|
272
|
+
if (mapped) {
|
|
273
|
+
entry.slot.buffer.unmap();
|
|
274
|
+
}
|
|
275
|
+
entry.mapped = false;
|
|
276
|
+
entry.borrowed = false;
|
|
277
|
+
entry.returnRequested = false;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Tracks one mapAsync on a slot: mapPending until it settles, then the deferred work -- the destruction
|
|
282
|
+
* destroyAll() left to the settle, or the return returnSlot() left to it. The handler is attached before the
|
|
283
|
+
* promise is handed out, so it runs before any borrower's continuation.
|
|
284
|
+
* @param entry - the slot's entry
|
|
285
|
+
* @param pending - the runtime's mapAsync promise
|
|
286
|
+
* @returns the same promise
|
|
287
|
+
*/
|
|
288
|
+
private track(entry: SlotEntry, pending: Promise<void>): Promise<void> {
|
|
289
|
+
entry.mapPending = true;
|
|
290
|
+
const settle = (mapped: boolean): void => {
|
|
291
|
+
entry.mapPending = false;
|
|
292
|
+
entry.mapped = mapped;
|
|
293
|
+
if (this.disposed) {
|
|
294
|
+
if (mapped) {
|
|
295
|
+
entry.slot.buffer.unmap();
|
|
296
|
+
}
|
|
297
|
+
this.destroySlot(entry);
|
|
298
|
+
entry.mapped = false;
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (entry.returnRequested) {
|
|
302
|
+
this.finishReturn(entry);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
void pending.then(
|
|
306
|
+
() => {
|
|
307
|
+
settle(true);
|
|
308
|
+
},
|
|
309
|
+
() => {
|
|
310
|
+
settle(false);
|
|
311
|
+
},
|
|
312
|
+
);
|
|
313
|
+
return pending;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Number of slots (grows).
|
|
318
|
+
* @returns the slot count
|
|
319
|
+
*/
|
|
320
|
+
get slots(): number {
|
|
321
|
+
return this.ring.length;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Slots currently borrowed.
|
|
326
|
+
* @returns the borrowed count
|
|
327
|
+
*/
|
|
328
|
+
get borrowed(): number {
|
|
329
|
+
let count = 0;
|
|
330
|
+
for (const entry of this.ring) {
|
|
331
|
+
if (entry.borrowed) {
|
|
332
|
+
count++;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return count;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Destroys every staging buffer (ctx.dispose()); idempotent. A slot whose mapAsync is still pending is NOT
|
|
340
|
+
* destroyed here (the file comment: dawn-node 0.4.0 settles the promise twice); its destruction is deferred to
|
|
341
|
+
* the moment the map settles (track()), where a pending read() still rejects with E_DISPOSED as the contract
|
|
342
|
+
* promises.
|
|
343
|
+
* @internal
|
|
344
|
+
*/
|
|
345
|
+
destroyAll(): void {
|
|
346
|
+
for (const entry of this.ring) {
|
|
347
|
+
if (entry.mapPending) {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
this.destroySlot(entry);
|
|
351
|
+
}
|
|
352
|
+
this.ring.length = 0;
|
|
353
|
+
this.disposed = true;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Destroys one slot's buffer through the allocator when it was created there.
|
|
358
|
+
* @param entry - the slot
|
|
359
|
+
*/
|
|
360
|
+
private destroySlot(entry: SlotEntry): void {
|
|
361
|
+
if (entry.tracked) {
|
|
362
|
+
this.allocator.destroy(entry.slot.buffer);
|
|
363
|
+
} else {
|
|
364
|
+
entry.slot.buffer.destroy();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Creates one slot at the end of the ring.
|
|
370
|
+
* @param capacity - the slot's byte capacity
|
|
371
|
+
* @returns the new entry
|
|
372
|
+
*/
|
|
373
|
+
private addSlot(capacity: number): SlotEntry {
|
|
374
|
+
const index = this.ring.length;
|
|
375
|
+
const descriptor: GPUBufferDescriptor = {
|
|
376
|
+
label: `readback:slot:${index}`,
|
|
377
|
+
size: capacity,
|
|
378
|
+
usage: STAGING_USAGE,
|
|
379
|
+
};
|
|
380
|
+
const tracked = capacity >= OOM_SCOPE_THRESHOLD_BYTES;
|
|
381
|
+
const buffer = tracked ? this.allocator.createBuffer(descriptor) : this.device.createBuffer(descriptor);
|
|
382
|
+
const entry: SlotEntry = {
|
|
383
|
+
slot: Object.freeze({ index, buffer, capacity }),
|
|
384
|
+
borrowed: false,
|
|
385
|
+
mapped: false,
|
|
386
|
+
mapPending: false,
|
|
387
|
+
returnRequested: false,
|
|
388
|
+
tracked,
|
|
389
|
+
};
|
|
390
|
+
this.ring.push(entry);
|
|
391
|
+
return entry;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* mapAsync(READ) on a slot for read(); a rejection becomes E_DISPOSED (after destroyAll) or E_DEVICE_LOST.
|
|
396
|
+
* @param entry - the ring entry of the borrowed slot
|
|
397
|
+
* @param byteLength - the bytes to map
|
|
398
|
+
*/
|
|
399
|
+
private async map(entry: SlotEntry, byteLength: number): Promise<void> {
|
|
400
|
+
let mapped = false;
|
|
401
|
+
let failure: unknown = null;
|
|
402
|
+
try {
|
|
403
|
+
await this.track(entry, entry.slot.buffer.mapAsync(MapMode.READ, 0, byteLength));
|
|
404
|
+
mapped = true;
|
|
405
|
+
} catch (err: unknown) {
|
|
406
|
+
failure = err;
|
|
407
|
+
}
|
|
408
|
+
if (this.disposed) {
|
|
409
|
+
// destroyAll() ran while the map was pending: track() finished the deferred destruction just before this
|
|
410
|
+
throw new WebGpuGraphError("E_DISPOSED", "the readback ring was disposed while a read was pending", {
|
|
411
|
+
label: "readback",
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
if (!mapped) {
|
|
415
|
+
const message = failure instanceof Error ? failure.message : String(failure);
|
|
416
|
+
throw new WebGpuGraphError("E_DEVICE_LOST", `mapAsync rejected: ${message}`, {
|
|
417
|
+
reason: "mapAsync",
|
|
418
|
+
message,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** Throws E_DISPOSED after destroyAll(). */
|
|
424
|
+
private assertLive(): void {
|
|
425
|
+
if (this.disposed) {
|
|
426
|
+
throw new WebGpuGraphError("E_DISPOSED", "the readback ring was disposed", { label: "readback" });
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|