@plasius/gpu-lock-free-queue 0.1.1-beta.1 → 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 CHANGED
@@ -20,38 +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.1-beta.1] - 2026-01-08
23
+ ## [0.1.2] - 2026-01-22
24
24
 
25
25
  - **Added**
26
- - (placeholder)
27
-
28
- - **Changed**
29
- - (placeholder)
30
-
31
- - **Fixed**
32
- - (placeholder)
33
-
34
- - **Security**
35
- - (placeholder)
36
-
37
- ## [0.1.1-beta.0] - 2026-01-08
38
-
39
- - **Added**
40
- - (placeholder)
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.
41
31
 
42
32
  - **Changed**
43
- - (placeholder)
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/`.
44
37
 
45
38
  - **Fixed**
46
- - (placeholder)
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.
47
42
 
48
43
  - **Security**
49
- - (placeholder)
44
+ - None.
50
45
 
51
46
  ## [0.1.0] - 2025-01-08
52
47
 
53
48
  - **Added**
54
49
  - WebGPU lock-free MPMC queue with sequence counters.
55
50
  - Demo for enqueue/dequeue, FFT spectrogram, and randomness heuristics.
56
- [0.1.1-beta.0]: https://github.com/Plasius-LTD/gpu-lock-free-queue/releases/tag/v0.1.1-beta.0
57
- [0.1.1-beta.1]: https://github.com/Plasius-LTD/gpu-lock-free-queue/releases/tag/v0.1.1-beta.1
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
@@ -1,7 +1,13 @@
1
1
  # @plasius/gpu-lock-free-queue
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@plasius/gpu-lock-free-queue)](https://www.npmjs.com/package/@plasius/gpu-lock-free-queue)
4
+ [![CI](https://github.com/Plasius-LTD/gpu-lock-free-queue/actions/workflows/ci.yml/badge.svg)](https://github.com/Plasius-LTD/gpu-lock-free-queue/actions/workflows/ci.yml)
5
+ [![license](https://img.shields.io/npm/l/@plasius/gpu-lock-free-queue)](./LICENSE)
6
+
3
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.
4
8
 
9
+ Apache-2.0. ESM + CJS builds. WGSL assets are published in `dist/`.
10
+
5
11
  ## Install
6
12
  ```
7
13
  npm install @plasius/gpu-lock-free-queue
@@ -35,6 +41,17 @@ python3 -m http.server
35
41
 
36
42
  Then open `http://localhost:8000` and check the console/output.
37
43
 
44
+ ## Build Outputs
45
+
46
+ `npm run build` emits `dist/index.js`, `dist/index.cjs`, and `dist/queue.wgsl`.
47
+
48
+ ## Tests
49
+ ```
50
+ npm run test:unit
51
+ npm run test:coverage
52
+ npm run test:e2e
53
+ ```
54
+
38
55
  ## Files
39
56
  - `demo/index.html`: Loads the demo.
40
57
  - `demo/main.js`: WebGPU setup, enqueue/dequeue test, FFT spectrogram, and randomness heuristics.
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":[]}
@@ -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.1-beta.1",
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,12 +16,20 @@
13
16
  "legal"
14
17
  ],
15
18
  "exports": {
16
- ".": "./src/index.js",
17
- "./queue.wgsl": "./src/queue.wgsl",
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": {
21
- "demo": "python3 -m http.server"
27
+ "build": "tsup && cp src/queue.wgsl dist/queue.wgsl",
28
+ "demo": "python3 -m http.server",
29
+ "test": "npm run test:unit",
30
+ "test:unit": "node --test",
31
+ "test:e2e": "npx playwright install chromium && playwright test",
32
+ "test:coverage": "c8 --reporter=lcov --reporter=text node --test"
22
33
  },
23
34
  "keywords": [
24
35
  "webgpu",
@@ -34,6 +45,12 @@
34
45
  ],
35
46
  "author": "Plasius LTD <development@plasius.co.uk>",
36
47
  "license": "Apache-2.0",
48
+ "devDependencies": {
49
+ "@playwright/test": "^1.57.0",
50
+ "c8": "^10.1.3",
51
+ "tsup": "^8.5.0",
52
+ "typescript": "^5.9.3"
53
+ },
37
54
  "repository": {
38
55
  "type": "git",
39
56
  "url": "git+https://github.com/Plasius-LTD/gpu-lock-free-queue.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 response = await fetch(queueWgslUrl);
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
- if (idx >= params.job_count) {
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
- if (idx >= params.job_count) {
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) {