@plasius/gpu-lock-free-queue 0.1.2-beta.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -36
- package/README.md +6 -0
- package/dist/index.cjs +60 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/queue.wgsl +138 -0
- package/package.json +13 -4
- package/src/index.js +17 -2
- package/src/queue.wgsl +36 -2
package/CHANGELOG.md
CHANGED
|
@@ -20,53 +20,34 @@ The format is based on **[Keep a Changelog](https://keepachangelog.com/en/1.1.0/
|
|
|
20
20
|
- **Security**
|
|
21
21
|
- (placeholder)
|
|
22
22
|
|
|
23
|
-
## [0.1.2
|
|
23
|
+
## [0.1.2] - 2026-01-22
|
|
24
24
|
|
|
25
25
|
- **Added**
|
|
26
|
-
-
|
|
26
|
+
- Deterministic demo test pattern mode for stable image hashing in e2e tests.
|
|
27
|
+
- 4x4 demo grid for multi-canvas output.
|
|
28
|
+
- Timestamped demo logging.
|
|
29
|
+
- Demo FPS counter and per-image progress indicators.
|
|
30
|
+
- Loader and WGSL guard tests, plus an e2e WGSL compilation check.
|
|
27
31
|
|
|
28
32
|
- **Changed**
|
|
29
|
-
-
|
|
33
|
+
- `loadQueueWgsl` accepts `url`/`fetcher` overrides and falls back to filesystem reads for `file:` URLs.
|
|
34
|
+
- Demo renders 500 interleaved static frames using per-image queues per frame.
|
|
35
|
+
- Demo updates canvases line-by-line for progressive static output.
|
|
36
|
+
- Build outputs now ship as ESM and CJS bundles with the WGSL asset in `dist/`.
|
|
30
37
|
|
|
31
38
|
- **Fixed**
|
|
32
|
-
-
|
|
39
|
+
- WGSL entry points now validate queue configuration and clamp job counts to buffer lengths.
|
|
40
|
+
- WGSL load errors now surface with explicit HTTP status details.
|
|
41
|
+
- CD build now installs TypeScript for the tsup build step.
|
|
33
42
|
|
|
34
43
|
- **Security**
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
## [0.1.1-beta.1] - 2026-01-08
|
|
38
|
-
|
|
39
|
-
- **Added**
|
|
40
|
-
- (placeholder)
|
|
41
|
-
|
|
42
|
-
- **Changed**
|
|
43
|
-
- (placeholder)
|
|
44
|
-
|
|
45
|
-
- **Fixed**
|
|
46
|
-
- (placeholder)
|
|
47
|
-
|
|
48
|
-
- **Security**
|
|
49
|
-
- (placeholder)
|
|
50
|
-
|
|
51
|
-
## [0.1.1-beta.0] - 2026-01-08
|
|
52
|
-
|
|
53
|
-
- **Added**
|
|
54
|
-
- (placeholder)
|
|
55
|
-
|
|
56
|
-
- **Changed**
|
|
57
|
-
- (placeholder)
|
|
58
|
-
|
|
59
|
-
- **Fixed**
|
|
60
|
-
- (placeholder)
|
|
61
|
-
|
|
62
|
-
- **Security**
|
|
63
|
-
- (placeholder)
|
|
44
|
+
- None.
|
|
64
45
|
|
|
65
46
|
## [0.1.0] - 2025-01-08
|
|
66
47
|
|
|
67
48
|
- **Added**
|
|
68
49
|
- WebGPU lock-free MPMC queue with sequence counters.
|
|
69
50
|
- Demo for enqueue/dequeue, FFT spectrogram, and randomness heuristics.
|
|
70
|
-
|
|
71
|
-
[0.1.
|
|
72
|
-
[0.1.2
|
|
51
|
+
|
|
52
|
+
[0.1.0]: https://github.com/Plasius-LTD/gpu-lock-free-queue/releases/tag/v0.1.0
|
|
53
|
+
[0.1.2]: https://github.com/Plasius-LTD/gpu-lock-free-queue/releases/tag/v0.1.2
|
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
A minimal WebGPU lock-free MPMC ring queue using a per-slot sequence counter (Vyukov-style). This is a starter implementation focused on correctness, robustness, and low overhead.
|
|
8
8
|
|
|
9
|
+
Apache-2.0. ESM + CJS builds. WGSL assets are published in `dist/`.
|
|
10
|
+
|
|
9
11
|
## Install
|
|
10
12
|
```
|
|
11
13
|
npm install @plasius/gpu-lock-free-queue
|
|
@@ -39,6 +41,10 @@ python3 -m http.server
|
|
|
39
41
|
|
|
40
42
|
Then open `http://localhost:8000` and check the console/output.
|
|
41
43
|
|
|
44
|
+
## Build Outputs
|
|
45
|
+
|
|
46
|
+
`npm run build` emits `dist/index.js`, `dist/index.cjs`, and `dist/queue.wgsl`.
|
|
47
|
+
|
|
42
48
|
## Tests
|
|
43
49
|
```
|
|
44
50
|
npm run test:unit
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
|
|
29
|
+
// src/index.js
|
|
30
|
+
var index_exports = {};
|
|
31
|
+
__export(index_exports, {
|
|
32
|
+
loadQueueWgsl: () => loadQueueWgsl,
|
|
33
|
+
queueWgslUrl: () => queueWgslUrl
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
var import_meta = {};
|
|
37
|
+
var queueWgslUrl = new URL("./queue.wgsl", import_meta.url);
|
|
38
|
+
async function loadQueueWgsl(options = {}) {
|
|
39
|
+
const { url = queueWgslUrl, fetcher = globalThis.fetch } = options ?? {};
|
|
40
|
+
const wgslUrl = url instanceof URL ? url : new URL(url, queueWgslUrl);
|
|
41
|
+
if (!fetcher || wgslUrl.protocol === "file:") {
|
|
42
|
+
const { readFile } = await import("fs/promises");
|
|
43
|
+
const { fileURLToPath } = await import("url");
|
|
44
|
+
return readFile(fileURLToPath(wgslUrl), "utf8");
|
|
45
|
+
}
|
|
46
|
+
const response = await fetcher(wgslUrl);
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
const status = "status" in response ? response.status : "unknown";
|
|
49
|
+
const statusText = "statusText" in response ? response.statusText : "";
|
|
50
|
+
const detail = statusText ? `${status} ${statusText}` : `${status}`;
|
|
51
|
+
throw new Error(`Failed to load WGSL (${detail})`);
|
|
52
|
+
}
|
|
53
|
+
return response.text();
|
|
54
|
+
}
|
|
55
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
56
|
+
0 && (module.exports = {
|
|
57
|
+
loadQueueWgsl,
|
|
58
|
+
queueWgslUrl
|
|
59
|
+
});
|
|
60
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.js"],"sourcesContent":["export const queueWgslUrl = new URL(\"./queue.wgsl\", import.meta.url);\n\nexport async function loadQueueWgsl(options = {}) {\n const { url = queueWgslUrl, fetcher = globalThis.fetch } = options ?? {};\n const wgslUrl = url instanceof URL ? url : new URL(url, queueWgslUrl);\n\n if (!fetcher || wgslUrl.protocol === \"file:\") {\n const { readFile } = await import(\"node:fs/promises\");\n const { fileURLToPath } = await import(\"node:url\");\n return readFile(fileURLToPath(wgslUrl), \"utf8\");\n }\n\n const response = await fetcher(wgslUrl);\n if (!response.ok) {\n const status = \"status\" in response ? response.status : \"unknown\";\n const statusText = \"statusText\" in response ? response.statusText : \"\";\n const detail = statusText ? `${status} ${statusText}` : `${status}`;\n throw new Error(`Failed to load WGSL (${detail})`);\n }\n return response.text();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAO,IAAM,eAAe,IAAI,IAAI,gBAAgB,YAAY,GAAG;AAEnE,eAAsB,cAAc,UAAU,CAAC,GAAG;AAChD,QAAM,EAAE,MAAM,cAAc,UAAU,WAAW,MAAM,IAAI,WAAW,CAAC;AACvE,QAAM,UAAU,eAAe,MAAM,MAAM,IAAI,IAAI,KAAK,YAAY;AAEpE,MAAI,CAAC,WAAW,QAAQ,aAAa,SAAS;AAC5C,UAAM,EAAE,SAAS,IAAI,MAAM,OAAO,aAAkB;AACpD,UAAM,EAAE,cAAc,IAAI,MAAM,OAAO,KAAU;AACjD,WAAO,SAAS,cAAc,OAAO,GAAG,MAAM;AAAA,EAChD;AAEA,QAAM,WAAW,MAAM,QAAQ,OAAO;AACtC,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,SAAS,YAAY,WAAW,SAAS,SAAS;AACxD,UAAM,aAAa,gBAAgB,WAAW,SAAS,aAAa;AACpE,UAAM,SAAS,aAAa,GAAG,MAAM,IAAI,UAAU,KAAK,GAAG,MAAM;AACjE,UAAM,IAAI,MAAM,wBAAwB,MAAM,GAAG;AAAA,EACnD;AACA,SAAO,SAAS,KAAK;AACvB;","names":[]}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// src/index.js
|
|
2
|
+
var queueWgslUrl = new URL("./queue.wgsl", import.meta.url);
|
|
3
|
+
async function loadQueueWgsl(options = {}) {
|
|
4
|
+
const { url = queueWgslUrl, fetcher = globalThis.fetch } = options ?? {};
|
|
5
|
+
const wgslUrl = url instanceof URL ? url : new URL(url, queueWgslUrl);
|
|
6
|
+
if (!fetcher || wgslUrl.protocol === "file:") {
|
|
7
|
+
const { readFile } = await import("fs/promises");
|
|
8
|
+
const { fileURLToPath } = await import("url");
|
|
9
|
+
return readFile(fileURLToPath(wgslUrl), "utf8");
|
|
10
|
+
}
|
|
11
|
+
const response = await fetcher(wgslUrl);
|
|
12
|
+
if (!response.ok) {
|
|
13
|
+
const status = "status" in response ? response.status : "unknown";
|
|
14
|
+
const statusText = "statusText" in response ? response.statusText : "";
|
|
15
|
+
const detail = statusText ? `${status} ${statusText}` : `${status}`;
|
|
16
|
+
throw new Error(`Failed to load WGSL (${detail})`);
|
|
17
|
+
}
|
|
18
|
+
return response.text();
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
loadQueueWgsl,
|
|
22
|
+
queueWgslUrl
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.js"],"sourcesContent":["export const queueWgslUrl = new URL(\"./queue.wgsl\", import.meta.url);\n\nexport async function loadQueueWgsl(options = {}) {\n const { url = queueWgslUrl, fetcher = globalThis.fetch } = options ?? {};\n const wgslUrl = url instanceof URL ? url : new URL(url, queueWgslUrl);\n\n if (!fetcher || wgslUrl.protocol === \"file:\") {\n const { readFile } = await import(\"node:fs/promises\");\n const { fileURLToPath } = await import(\"node:url\");\n return readFile(fileURLToPath(wgslUrl), \"utf8\");\n }\n\n const response = await fetcher(wgslUrl);\n if (!response.ok) {\n const status = \"status\" in response ? response.status : \"unknown\";\n const statusText = \"statusText\" in response ? response.statusText : \"\";\n const detail = statusText ? `${status} ${statusText}` : `${status}`;\n throw new Error(`Failed to load WGSL (${detail})`);\n }\n return response.text();\n}\n"],"mappings":";AAAO,IAAM,eAAe,IAAI,IAAI,gBAAgB,YAAY,GAAG;AAEnE,eAAsB,cAAc,UAAU,CAAC,GAAG;AAChD,QAAM,EAAE,MAAM,cAAc,UAAU,WAAW,MAAM,IAAI,WAAW,CAAC;AACvE,QAAM,UAAU,eAAe,MAAM,MAAM,IAAI,IAAI,KAAK,YAAY;AAEpE,MAAI,CAAC,WAAW,QAAQ,aAAa,SAAS;AAC5C,UAAM,EAAE,SAAS,IAAI,MAAM,OAAO,aAAkB;AACpD,UAAM,EAAE,cAAc,IAAI,MAAM,OAAO,KAAU;AACjD,WAAO,SAAS,cAAc,OAAO,GAAG,MAAM;AAAA,EAChD;AAEA,QAAM,WAAW,MAAM,QAAQ,OAAO;AACtC,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,SAAS,YAAY,WAAW,SAAS,SAAS;AACxD,UAAM,aAAa,gBAAgB,WAAW,SAAS,aAAa;AACpE,UAAM,SAAS,aAAa,GAAG,MAAM,IAAI,UAAU,KAAK,GAAG,MAAM;AACjE,UAAM,IAAI,MAAM,wBAAwB,MAAM,GAAG;AAAA,EACnD;AACA,SAAO,SAAS,KAAK;AACvB;","names":[]}
|
package/dist/queue.wgsl
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
struct Queue {
|
|
2
|
+
head: atomic<u32>,
|
|
3
|
+
tail: atomic<u32>,
|
|
4
|
+
capacity: u32,
|
|
5
|
+
mask: u32,
|
|
6
|
+
_pad: vec2<u32>,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
struct Slot {
|
|
10
|
+
seq: atomic<u32>,
|
|
11
|
+
value: u32,
|
|
12
|
+
_pad: vec2<u32>,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
struct Params {
|
|
16
|
+
job_count: u32,
|
|
17
|
+
_pad: vec3<u32>,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
@group(0) @binding(0) var<storage, read_write> queue: Queue;
|
|
21
|
+
@group(0) @binding(1) var<storage, read_write> slots: array<Slot>;
|
|
22
|
+
@group(0) @binding(2) var<storage, read> input_jobs: array<u32>;
|
|
23
|
+
@group(0) @binding(3) var<storage, read_write> output_jobs: array<u32>;
|
|
24
|
+
@group(0) @binding(4) var<storage, read_write> status: array<u32>;
|
|
25
|
+
@group(0) @binding(5) var<uniform> params: Params;
|
|
26
|
+
|
|
27
|
+
const MAX_RETRIES: u32 = 512u;
|
|
28
|
+
|
|
29
|
+
fn queue_config_valid() -> bool {
|
|
30
|
+
if (queue.capacity == 0u) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
if ((queue.capacity & (queue.capacity - 1u)) != 0u) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
if (queue.mask != queue.capacity - 1u) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
if (queue.capacity > arrayLength(&slots)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fn enqueue_job_count() -> u32 {
|
|
46
|
+
let count = min(params.job_count, arrayLength(&input_jobs));
|
|
47
|
+
return min(count, arrayLength(&status));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fn dequeue_job_count() -> u32 {
|
|
51
|
+
let count = min(params.job_count, arrayLength(&output_jobs));
|
|
52
|
+
return min(count, arrayLength(&status));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fn enqueue(val: u32) -> u32 {
|
|
56
|
+
for (var attempt: u32 = 0u; attempt < MAX_RETRIES; attempt++) {
|
|
57
|
+
let t = atomicLoad(&queue.tail);
|
|
58
|
+
let slot_index = t & queue.mask;
|
|
59
|
+
let seq = atomicLoad(&slots[slot_index].seq);
|
|
60
|
+
let diff = i32(seq) - i32(t);
|
|
61
|
+
|
|
62
|
+
if (diff == 0) {
|
|
63
|
+
let res = atomicCompareExchangeWeak(&queue.tail, t, t + 1u);
|
|
64
|
+
if (res.exchanged) {
|
|
65
|
+
slots[slot_index].value = val;
|
|
66
|
+
atomicStore(&slots[slot_index].seq, t + 1u);
|
|
67
|
+
return 1u;
|
|
68
|
+
}
|
|
69
|
+
} else if (diff < 0) {
|
|
70
|
+
return 0u;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return 0u;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fn dequeue(idx: u32) -> u32 {
|
|
78
|
+
for (var attempt: u32 = 0u; attempt < MAX_RETRIES; attempt++) {
|
|
79
|
+
let h = atomicLoad(&queue.head);
|
|
80
|
+
let slot_index = h & queue.mask;
|
|
81
|
+
let seq = atomicLoad(&slots[slot_index].seq);
|
|
82
|
+
let diff = i32(seq) - i32(h + 1u);
|
|
83
|
+
|
|
84
|
+
if (diff == 0) {
|
|
85
|
+
let res = atomicCompareExchangeWeak(&queue.head, h, h + 1u);
|
|
86
|
+
if (res.exchanged) {
|
|
87
|
+
let val = slots[slot_index].value;
|
|
88
|
+
output_jobs[idx] = val;
|
|
89
|
+
atomicStore(&slots[slot_index].seq, h + queue.capacity);
|
|
90
|
+
return 1u;
|
|
91
|
+
}
|
|
92
|
+
} else if (diff < 0) {
|
|
93
|
+
return 0u;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return 0u;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@compute @workgroup_size(64)
|
|
101
|
+
fn enqueue_main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
102
|
+
let idx = gid.x;
|
|
103
|
+
let job_count = enqueue_job_count();
|
|
104
|
+
if (idx >= job_count) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!queue_config_valid()) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (status[idx] == 1u) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let ok = enqueue(input_jobs[idx]);
|
|
115
|
+
if (ok == 1u) {
|
|
116
|
+
status[idx] = 1u;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@compute @workgroup_size(64)
|
|
121
|
+
fn dequeue_main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
122
|
+
let idx = gid.x;
|
|
123
|
+
let job_count = dequeue_job_count();
|
|
124
|
+
if (idx >= job_count) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (!queue_config_valid()) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (status[idx] == 1u) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let ok = dequeue(idx);
|
|
135
|
+
if (ok == 1u) {
|
|
136
|
+
status[idx] = 1u;
|
|
137
|
+
}
|
|
138
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plasius/gpu-lock-free-queue",
|
|
3
|
-
"version": "0.1.2
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "WebGPU lock-free MPMC ring queue with sequence counters.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"private": false,
|
|
8
|
+
"main": "./dist/index.cjs",
|
|
9
|
+
"module": "./dist/index.js",
|
|
8
10
|
"files": [
|
|
11
|
+
"dist",
|
|
9
12
|
"src",
|
|
10
13
|
"README.md",
|
|
11
14
|
"CHANGELOG.md",
|
|
@@ -13,11 +16,15 @@
|
|
|
13
16
|
"legal"
|
|
14
17
|
],
|
|
15
18
|
"exports": {
|
|
16
|
-
".":
|
|
17
|
-
|
|
19
|
+
".": {
|
|
20
|
+
"import": "./dist/index.js",
|
|
21
|
+
"require": "./dist/index.cjs"
|
|
22
|
+
},
|
|
23
|
+
"./queue.wgsl": "./dist/queue.wgsl",
|
|
18
24
|
"./package.json": "./package.json"
|
|
19
25
|
},
|
|
20
26
|
"scripts": {
|
|
27
|
+
"build": "tsup && cp src/queue.wgsl dist/queue.wgsl",
|
|
21
28
|
"demo": "python3 -m http.server",
|
|
22
29
|
"test": "npm run test:unit",
|
|
23
30
|
"test:unit": "node --test",
|
|
@@ -40,7 +47,9 @@
|
|
|
40
47
|
"license": "Apache-2.0",
|
|
41
48
|
"devDependencies": {
|
|
42
49
|
"@playwright/test": "^1.57.0",
|
|
43
|
-
"c8": "^10.1.3"
|
|
50
|
+
"c8": "^10.1.3",
|
|
51
|
+
"tsup": "^8.5.0",
|
|
52
|
+
"typescript": "^5.9.3"
|
|
44
53
|
},
|
|
45
54
|
"repository": {
|
|
46
55
|
"type": "git",
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
export const queueWgslUrl = new URL("./queue.wgsl", import.meta.url);
|
|
2
2
|
|
|
3
|
-
export async function loadQueueWgsl() {
|
|
4
|
-
const
|
|
3
|
+
export async function loadQueueWgsl(options = {}) {
|
|
4
|
+
const { url = queueWgslUrl, fetcher = globalThis.fetch } = options ?? {};
|
|
5
|
+
const wgslUrl = url instanceof URL ? url : new URL(url, queueWgslUrl);
|
|
6
|
+
|
|
7
|
+
if (!fetcher || wgslUrl.protocol === "file:") {
|
|
8
|
+
const { readFile } = await import("node:fs/promises");
|
|
9
|
+
const { fileURLToPath } = await import("node:url");
|
|
10
|
+
return readFile(fileURLToPath(wgslUrl), "utf8");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const response = await fetcher(wgslUrl);
|
|
14
|
+
if (!response.ok) {
|
|
15
|
+
const status = "status" in response ? response.status : "unknown";
|
|
16
|
+
const statusText = "statusText" in response ? response.statusText : "";
|
|
17
|
+
const detail = statusText ? `${status} ${statusText}` : `${status}`;
|
|
18
|
+
throw new Error(`Failed to load WGSL (${detail})`);
|
|
19
|
+
}
|
|
5
20
|
return response.text();
|
|
6
21
|
}
|
package/src/queue.wgsl
CHANGED
|
@@ -26,6 +26,32 @@ struct Params {
|
|
|
26
26
|
|
|
27
27
|
const MAX_RETRIES: u32 = 512u;
|
|
28
28
|
|
|
29
|
+
fn queue_config_valid() -> bool {
|
|
30
|
+
if (queue.capacity == 0u) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
if ((queue.capacity & (queue.capacity - 1u)) != 0u) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
if (queue.mask != queue.capacity - 1u) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
if (queue.capacity > arrayLength(&slots)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fn enqueue_job_count() -> u32 {
|
|
46
|
+
let count = min(params.job_count, arrayLength(&input_jobs));
|
|
47
|
+
return min(count, arrayLength(&status));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fn dequeue_job_count() -> u32 {
|
|
51
|
+
let count = min(params.job_count, arrayLength(&output_jobs));
|
|
52
|
+
return min(count, arrayLength(&status));
|
|
53
|
+
}
|
|
54
|
+
|
|
29
55
|
fn enqueue(val: u32) -> u32 {
|
|
30
56
|
for (var attempt: u32 = 0u; attempt < MAX_RETRIES; attempt++) {
|
|
31
57
|
let t = atomicLoad(&queue.tail);
|
|
@@ -74,7 +100,11 @@ fn dequeue(idx: u32) -> u32 {
|
|
|
74
100
|
@compute @workgroup_size(64)
|
|
75
101
|
fn enqueue_main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
76
102
|
let idx = gid.x;
|
|
77
|
-
|
|
103
|
+
let job_count = enqueue_job_count();
|
|
104
|
+
if (idx >= job_count) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!queue_config_valid()) {
|
|
78
108
|
return;
|
|
79
109
|
}
|
|
80
110
|
if (status[idx] == 1u) {
|
|
@@ -90,7 +120,11 @@ fn enqueue_main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
|
90
120
|
@compute @workgroup_size(64)
|
|
91
121
|
fn dequeue_main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
92
122
|
let idx = gid.x;
|
|
93
|
-
|
|
123
|
+
let job_count = dequeue_job_count();
|
|
124
|
+
if (idx >= job_count) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (!queue_config_valid()) {
|
|
94
128
|
return;
|
|
95
129
|
}
|
|
96
130
|
if (status[idx] == 1u) {
|