@next2d/builder 0.1.7 → 0.1.8
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/dist/index.js +62 -26
- package/package.json +3 -2
- package/templates/xbox/CMakeLists.txt +69 -0
- package/templates/xbox/README.md +16 -8
- package/templates/xbox/cmake/FetchDawn.cmake +47 -6
- package/templates/xbox/cmake/FindV8.cmake +15 -0
- package/templates/xbox/js/bootstrap.js +31 -28
- package/templates/xbox/js/selftest.js +740 -90
- package/templates/xbox/src/EventLoop.cpp +28 -1
- package/templates/xbox/src/EventLoop.h +12 -0
- package/templates/xbox/src/bindings/Audio.cpp +99 -15
- package/templates/xbox/src/bindings/Bindings.h +10 -0
- package/templates/xbox/src/bindings/Canvas.cpp +45 -0
- package/templates/xbox/src/bindings/Canvas2D.cpp +2 -5
- package/templates/xbox/src/bindings/DomShims.cpp +127 -0
- package/templates/xbox/src/bindings/EventTarget.cpp +2 -0
- package/templates/xbox/src/bindings/Fetch.cpp +2 -8
- package/templates/xbox/src/bindings/Image.cpp +212 -10
- package/templates/xbox/src/bindings/Polyfills.cpp +791 -0
- package/templates/xbox/src/bindings/Video.cpp +96 -7
- package/templates/xbox/src/bindings/webgpu/WebGPU.cpp +815 -15
- package/templates/xbox/src/bindings/webgpu/WebGPUCommon.h +3 -8
- package/templates/xbox/src/gpu/DawnContext.cpp +90 -3
- package/templates/xbox/src/gpu/DawnContext.h +18 -0
- package/templates/xbox/src/main.cpp +88 -6
- package/templates/xbox/src/platform/AudioEngine.cpp +9 -3
- package/templates/xbox/src/v8/V8Runtime.cpp +69 -5
- package/templates/xbox/src/v8/V8Util.h +29 -0
- package/templates/xbox/src/v8/WeakHandle.h +45 -0
- package/templates/xbox/src/worker/WorkerRuntime.cpp +325 -41
- package/templates/xbox/src/worker/WorkerRuntime.h +20 -10
- package/templates/xbox/tests/check_local.sh +2 -1
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
(function () {
|
|
17
17
|
|
|
18
|
-
const results = { pass: 0, fail: 0, skip: 0 };
|
|
18
|
+
const results = { "pass": 0, "fail": 0, "skip": 0 };
|
|
19
19
|
const failures = [];
|
|
20
20
|
|
|
21
21
|
function tap(status, name, detail) {
|
|
@@ -27,16 +27,16 @@
|
|
|
27
27
|
console.log("[selftest] " + line);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
function pass(name) { results.pass++; tap("ok", name)
|
|
30
|
+
function pass(name) { results.pass++; tap("ok", name) }
|
|
31
31
|
function fail(name, e) {
|
|
32
32
|
results.fail++;
|
|
33
33
|
const msg = e && e.message ? e.message : String(e);
|
|
34
34
|
failures.push(name + ": " + msg);
|
|
35
35
|
tap("fail", name, msg);
|
|
36
36
|
}
|
|
37
|
-
function skip(name, why) { results.skip++; tap("skip", name, why)
|
|
37
|
+
function skip(name, why) { results.skip++; tap("skip", name, why) }
|
|
38
38
|
|
|
39
|
-
function assert(cond, msg) { if (!cond) throw new Error(msg || "assertion failed")
|
|
39
|
+
function assert(cond, msg) { if (!cond) { throw new Error(msg || "assertion failed") } }
|
|
40
40
|
|
|
41
41
|
function withTimeout(promise, ms, what) {
|
|
42
42
|
return Promise.race([
|
|
@@ -109,18 +109,18 @@
|
|
|
109
109
|
const order = [];
|
|
110
110
|
await new Promise((resolve) => {
|
|
111
111
|
setTimeout(() => order.push("t10"), 10);
|
|
112
|
-
setTimeout(() => { order.push("t30"); resolve()
|
|
112
|
+
setTimeout(() => { order.push("t30"); resolve() }, 30);
|
|
113
113
|
Promise.resolve().then(() => order.push("micro"));
|
|
114
114
|
order.push("sync");
|
|
115
115
|
});
|
|
116
116
|
assert(order[0] === "sync" && order[1] === "micro",
|
|
117
|
-
|
|
117
|
+
"microtask order: " + order.join(","));
|
|
118
118
|
assert(order.indexOf("t10") < order.indexOf("t30"), "timer order");
|
|
119
119
|
|
|
120
120
|
let ticks = 0;
|
|
121
121
|
await new Promise((resolve) => {
|
|
122
122
|
const id = setInterval(() => {
|
|
123
|
-
if (++ticks >= 2) { clearInterval(id); resolve()
|
|
123
|
+
if (++ticks >= 2) { clearInterval(id); resolve() }
|
|
124
124
|
}, 5);
|
|
125
125
|
});
|
|
126
126
|
assert(ticks === 2, "interval stopped after clearInterval");
|
|
@@ -135,22 +135,101 @@
|
|
|
135
135
|
|
|
136
136
|
await test("EventTarget: addEventListener / dispatchEvent", () => {
|
|
137
137
|
let got = null;
|
|
138
|
-
const handler = (e) => { got = e
|
|
138
|
+
const handler = (e) => { got = e };
|
|
139
139
|
window.addEventListener("selftest-event", handler);
|
|
140
|
-
window.dispatchEvent({ type: "selftest-event", value: 42 });
|
|
140
|
+
window.dispatchEvent({ "type": "selftest-event", "value": 42 });
|
|
141
141
|
assert(got && got.value === 42, "listener called");
|
|
142
142
|
window.removeEventListener("selftest-event", handler);
|
|
143
143
|
got = null;
|
|
144
|
-
window.dispatchEvent({ type: "selftest-event" });
|
|
144
|
+
window.dispatchEvent({ "type": "selftest-event" });
|
|
145
145
|
assert(got === null, "listener removed");
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
+
await test("DOM: player boot パス (body/parentElement/getElementById)", () => {
|
|
149
|
+
// @next2d/player の起動シーケンスが要求する DOM API を検証する
|
|
150
|
+
const div = document.createElement("div");
|
|
151
|
+
div.id = "selftest-main";
|
|
152
|
+
div.tabIndex = -1;
|
|
153
|
+
document.body.appendChild(div);
|
|
154
|
+
assert(div.parentElement === document.body, "parentElement === body");
|
|
155
|
+
assert(div.parentElement.tagName === "BODY", "body.tagName === BODY");
|
|
156
|
+
div.setAttribute("style", "display:flex;width:640px;height:480px;");
|
|
157
|
+
assert(div.clientWidth === 640 && div.clientHeight === 480,
|
|
158
|
+
"clientWidth/Height from style attr");
|
|
159
|
+
div.innerHTML = "<style></style><div id=\"loading\"></div>";
|
|
160
|
+
while (div.firstChild) { div.removeChild(div.firstChild) }
|
|
161
|
+
|
|
162
|
+
const canvas = document.createElement("canvas");
|
|
163
|
+
div.appendChild(canvas);
|
|
164
|
+
assert(div.children[0] === canvas, "children[0] === canvas");
|
|
165
|
+
assert(canvas.localName === "canvas", "canvas.localName");
|
|
166
|
+
assert(canvas.parentElement === div, "canvas.parentElement");
|
|
167
|
+
|
|
168
|
+
assert(document.getElementById("selftest-main") === div, "getElementById");
|
|
169
|
+
assert(typeof Event === "function", "Event constructor");
|
|
170
|
+
let fired = false;
|
|
171
|
+
div.addEventListener("selftest-dom", () => { fired = true });
|
|
172
|
+
div.dispatchEvent(new Event("selftest-dom"));
|
|
173
|
+
assert(fired, "element dispatchEvent");
|
|
174
|
+
div.remove();
|
|
175
|
+
assert(document.getElementById("selftest-main") === null, "removed from tree");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await test("DOM: history / location (framework gotoView パス)", () => {
|
|
179
|
+
// framework の gotoView は SPA 設定時に history.pushState を呼ぶ
|
|
180
|
+
assert(typeof history === "object" && typeof history.pushState === "function",
|
|
181
|
+
"history.pushState");
|
|
182
|
+
const beforePath = location.pathname;
|
|
183
|
+
history.pushState("", "", location.origin + "/selftest-view?q=1");
|
|
184
|
+
assert(location.pathname === "/selftest-view", "pushState -> location.pathname");
|
|
185
|
+
assert(location.search === "?q=1", "pushState -> location.search");
|
|
186
|
+
history.replaceState("", "", beforePath);
|
|
187
|
+
assert(location.pathname === beforePath, "replaceState で復元");
|
|
188
|
+
assert(typeof location.origin === "string" && location.origin.length > 0,
|
|
189
|
+
"location.origin");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
await test("DOM: 未処理 Promise 拒否が握り潰されない (レポータ設置済み)", async () => {
|
|
193
|
+
// SetPromiseRejectCallback の存在自体は JS から観測できないため、
|
|
194
|
+
// 拒否→後付けハンドラの一連が例外なく動くことのみ確認する
|
|
195
|
+
const p = Promise.reject(new Error("selftest-rejection (expected log)"));
|
|
196
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
197
|
+
let caught = false;
|
|
198
|
+
await p.catch(() => { caught = true });
|
|
199
|
+
assert(caught, "late handler attached");
|
|
200
|
+
});
|
|
201
|
+
|
|
148
202
|
// ==================== 画像 ====================
|
|
149
203
|
let bitmap = null;
|
|
150
204
|
await test("createImageBitmap: PNG デコード (WIC)", async () => {
|
|
151
205
|
bitmap = await createImageBitmap(TEST_PNG.buffer);
|
|
152
206
|
assert(bitmap.width === 2 && bitmap.height === 2,
|
|
153
|
-
|
|
207
|
+
"2x2 png -> " + bitmap.width + "x" + bitmap.height);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
await test("Image 要素: new Image + src(data:) -> load -> drawImage", async () => {
|
|
211
|
+
// player の ShapeLoadSrcUseCase と同じ利用パターン
|
|
212
|
+
const img = new Image();
|
|
213
|
+
img.crossOrigin = "anonymous";
|
|
214
|
+
const loaded = new Promise((resolve, reject) => {
|
|
215
|
+
img.addEventListener("load", resolve);
|
|
216
|
+
img.addEventListener("error", () => reject(new Error("image load error")));
|
|
217
|
+
setTimeout(() => reject(new Error("image load timeout")), 3000);
|
|
218
|
+
});
|
|
219
|
+
img.src = "data:image/png;base64," + btoa(String.fromCharCode.apply(null, TEST_PNG));
|
|
220
|
+
await loaded;
|
|
221
|
+
assert(img.complete === true, "complete = true");
|
|
222
|
+
assert(img.width === 2 && img.height === 2,
|
|
223
|
+
"size: " + img.width + "x" + img.height);
|
|
224
|
+
assert(img.naturalWidth === 2, "naturalWidth");
|
|
225
|
+
|
|
226
|
+
const c = new OffscreenCanvas(2, 2);
|
|
227
|
+
const ctx = c.getContext("2d", { "willReadFrequently": true });
|
|
228
|
+
ctx.drawImage(img, 0, 0, 2, 2);
|
|
229
|
+
const d = ctx.getImageData(0, 0, 2, 2).data;
|
|
230
|
+
assert(d[0] === 255 && d[1] === 0 && d[2] === 0 && d[3] === 255,
|
|
231
|
+
"(0,0) red: " + d[0] + "," + d[1] + "," + d[2] + "," + d[3]);
|
|
232
|
+
assert(d[5] === 255, "(1,0) green");
|
|
154
233
|
});
|
|
155
234
|
|
|
156
235
|
// ==================== Canvas2D ====================
|
|
@@ -207,7 +286,7 @@
|
|
|
207
286
|
ctx.fillText("A", 4, 24);
|
|
208
287
|
const d = ctx.getImageData(0, 0, 64, 32).data;
|
|
209
288
|
let opaque = 0;
|
|
210
|
-
for (let i = 3; i < d.length; i += 4) if (d[i] > 128) opaque
|
|
289
|
+
for (let i = 3; i < d.length; i += 4) { if (d[i] > 128) { opaque++ } }
|
|
211
290
|
assert(opaque > 5, "glyph pixels=" + opaque);
|
|
212
291
|
});
|
|
213
292
|
|
|
@@ -221,13 +300,103 @@
|
|
|
221
300
|
assert(d[4] === 0 && d[5] === 255, "(1,0) green");
|
|
222
301
|
});
|
|
223
302
|
|
|
303
|
+
// ==================== エンジン/グローバル (player の V8 機能全数調査ぶん) ====================
|
|
304
|
+
await test("engine: crypto / ImageData→ImageBitmap / Proxy", async () => {
|
|
305
|
+
// crypto.randomUUID (player の WebGLUtil が ID 生成に使用)
|
|
306
|
+
const uuid = crypto.randomUUID();
|
|
307
|
+
assert(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(uuid),
|
|
308
|
+
"uuid=" + uuid);
|
|
309
|
+
const rnd = crypto.getRandomValues(new Uint8Array(16));
|
|
310
|
+
assert(rnd.length === 16 && rnd.some((b) => b !== 0), "getRandomValues");
|
|
311
|
+
|
|
312
|
+
// createImageBitmap(ImageData) — webgpu レンダラのキャプチャ/フィルタ経路
|
|
313
|
+
const data = new Uint8ClampedArray(2 * 2 * 4);
|
|
314
|
+
data[0] = 255; data[3] = 255; // (0,0) = 赤
|
|
315
|
+
const bmp = await createImageBitmap(new ImageData(data, 2, 2));
|
|
316
|
+
assert(bmp.width === 2 && bmp.height === 2, "ImageData bitmap " + bmp.width + "x" + bmp.height);
|
|
317
|
+
const c = new OffscreenCanvas(2, 2);
|
|
318
|
+
const ctx = c.getContext("2d");
|
|
319
|
+
ctx.drawImage(bmp, 0, 0);
|
|
320
|
+
const px = ctx.getImageData(0, 0, 1, 1).data;
|
|
321
|
+
assert(px[0] === 255, "ImageData pixel roundtrip");
|
|
322
|
+
|
|
323
|
+
// Proxy (player が4ファイルで使用。jitless V8 のコア機能)
|
|
324
|
+
const p = new Proxy({}, { "get": (_, k) => k === "x" ? 42 : undefined });
|
|
325
|
+
assert(p.x === 42, "Proxy");
|
|
326
|
+
|
|
327
|
+
// ゲームコードが使うグローバル (UserSettings=localStorage, boot=readyState, confirm)
|
|
328
|
+
localStorage.setItem("__selftest", "1");
|
|
329
|
+
assert(localStorage.getItem("__selftest") === "1", "localStorage roundtrip");
|
|
330
|
+
localStorage.removeItem("__selftest");
|
|
331
|
+
assert(localStorage.getItem("__selftest") === null, "localStorage remove");
|
|
332
|
+
assert(document.readyState === "complete", "document.readyState");
|
|
333
|
+
assert(typeof confirm("?") === "boolean", "confirm");
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
await test("engine: WebAssembly (DrumBrake インタープリタ実行)", async () => {
|
|
337
|
+
// (module (func $add (param i32 i32) (result i32) ...) (export "add"))
|
|
338
|
+
// prebuilt V8 r2 (wasm + DrumBrake) が必要。実行時は --wasm-jitless で解釈実行。
|
|
339
|
+
assert(typeof WebAssembly === "object", "WebAssembly global (V8 r2 required)");
|
|
340
|
+
const wasmBytes = new Uint8Array([
|
|
341
|
+
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
|
|
342
|
+
0x01, 0x07, 0x01, 0x60, 0x02, 0x7f, 0x7f, 0x01, 0x7f,
|
|
343
|
+
0x03, 0x02, 0x01, 0x00,
|
|
344
|
+
0x07, 0x07, 0x01, 0x03, 0x61, 0x64, 0x64, 0x00, 0x00,
|
|
345
|
+
0x0a, 0x09, 0x01, 0x07, 0x00, 0x20, 0x00, 0x20, 0x01, 0x6a, 0x0b
|
|
346
|
+
]);
|
|
347
|
+
const { instance } = await WebAssembly.instantiate(wasmBytes);
|
|
348
|
+
assert(instance.exports.add(19, 23) === 42, "wasm add(19,23)");
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
await test("engine: indexedDB / atob / btoa", async () => {
|
|
352
|
+
assert(atob(btoa("Next2D!")) === "Next2D!", "atob/btoa roundtrip");
|
|
353
|
+
|
|
354
|
+
const openReq = indexedDB.open("selftest-db", 1);
|
|
355
|
+
const db = await new Promise((resolve, reject) => {
|
|
356
|
+
openReq.onupgradeneeded = (e) => e.target.result.createObjectStore("kv");
|
|
357
|
+
openReq.onsuccess = (e) => resolve(e.target.result);
|
|
358
|
+
openReq.onerror = () => reject(new Error("open failed"));
|
|
359
|
+
});
|
|
360
|
+
const store = db.transaction(["kv"], "readwrite").objectStore("kv");
|
|
361
|
+
await new Promise((resolve, reject) => {
|
|
362
|
+
const r = store.put({ "n": 1, "bin": new Uint8Array([7, 8]) }, "k");
|
|
363
|
+
r.onsuccess = resolve;
|
|
364
|
+
r.onerror = () => reject(new Error("put failed"));
|
|
365
|
+
});
|
|
366
|
+
const v = await new Promise((resolve, reject) => {
|
|
367
|
+
const r = db.transaction(["kv"]).objectStore("kv").get("k");
|
|
368
|
+
r.onsuccess = (e) => resolve(e.target.result);
|
|
369
|
+
r.onerror = () => reject(new Error("get failed"));
|
|
370
|
+
});
|
|
371
|
+
assert(v && v.n === 1 && v.bin instanceof Uint8Array && v.bin[1] === 8,
|
|
372
|
+
"indexedDB roundtrip (binary 値含む)");
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
await test("engine: Worker 内の TextDecoder / location / crypto", async () => {
|
|
376
|
+
const src = "self.onmessage = function() { " +
|
|
377
|
+
"self.postMessage({ " +
|
|
378
|
+
" td: typeof TextDecoder, loc: typeof location, cr: typeof crypto, " +
|
|
379
|
+
" dec: new TextDecoder().decode(new TextEncoder().encode('abc')) " +
|
|
380
|
+
"}); };";
|
|
381
|
+
const url = URL.createObjectURL(new Blob([src], { "type": "text/javascript" }));
|
|
382
|
+
const worker = new Worker(url);
|
|
383
|
+
const r = await new Promise((resolve) => {
|
|
384
|
+
worker.onmessage = (e) => resolve(e.data);
|
|
385
|
+
worker.postMessage(0);
|
|
386
|
+
});
|
|
387
|
+
assert(r.td === "function" && r.loc === "object" && r.cr === "object",
|
|
388
|
+
JSON.stringify(r));
|
|
389
|
+
assert(r.dec === "abc", "worker TextDecoder roundtrip");
|
|
390
|
+
if (worker.terminate) { worker.terminate() }
|
|
391
|
+
});
|
|
392
|
+
|
|
224
393
|
// ==================== ネットワーク (Blob / URL / XHR) ====================
|
|
225
394
|
let blobUrl = null;
|
|
226
395
|
await test("Blob / URL.createObjectURL / revokeObjectURL", () => {
|
|
227
|
-
const blob = new Blob(["self.__marker = 1;"], { type: "text/javascript" });
|
|
396
|
+
const blob = new Blob(["self.__marker = 1;"], { "type": "text/javascript" });
|
|
228
397
|
blobUrl = URL.createObjectURL(blob);
|
|
229
398
|
assert(typeof blobUrl === "string" && blobUrl.indexOf("blob:") === 0,
|
|
230
|
-
|
|
399
|
+
"url=" + blobUrl);
|
|
231
400
|
});
|
|
232
401
|
|
|
233
402
|
await softTest("XMLHttpRequest: アセット読込 (assets/app)", async () => {
|
|
@@ -245,17 +414,49 @@
|
|
|
245
414
|
const src = "self.onmessage = function(e) { " +
|
|
246
415
|
" self.postMessage({ echo: e.data.value * 2, arr: e.data.arr }); " +
|
|
247
416
|
"};";
|
|
248
|
-
const url = URL.createObjectURL(new Blob([src], { type: "text/javascript" }));
|
|
417
|
+
const url = URL.createObjectURL(new Blob([src], { "type": "text/javascript" }));
|
|
249
418
|
const worker = new Worker(url);
|
|
250
419
|
const reply = await new Promise((resolve, reject) => {
|
|
251
420
|
worker.onmessage = (e) => resolve(e.data);
|
|
252
421
|
worker.onerror = (e) => reject(new Error("worker error: " + (e && e.message)));
|
|
253
|
-
worker.postMessage({ value: 21, arr: new Float32Array([1.5, 2.5]) });
|
|
422
|
+
worker.postMessage({ "value": 21, "arr": new Float32Array([1.5, 2.5]) });
|
|
254
423
|
});
|
|
255
424
|
assert(reply.echo === 42, "echo=" + reply.echo);
|
|
256
425
|
assert(reply.arr instanceof Float32Array && reply.arr[1] === 2.5,
|
|
257
|
-
|
|
258
|
-
if (worker.terminate) worker.terminate()
|
|
426
|
+
"structured clone TypedArray");
|
|
427
|
+
if (worker.terminate) { worker.terminate() }
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
await test("Worker: ArrayBuffer transfer + ImageBitmap clone (render 経路)", async () => {
|
|
431
|
+
// player の render キューと同じく、大きな buffer は transfer で移譲し、
|
|
432
|
+
// ImageBitmap はホストオブジェクトとして複製して往復させる
|
|
433
|
+
const src = "self.onmessage = function(e) { " +
|
|
434
|
+
" var view = new Float32Array(e.data.buffer); " +
|
|
435
|
+
" self.postMessage({ " +
|
|
436
|
+
" sum: view[0] + view[1], " +
|
|
437
|
+
" len: e.data.buffer.byteLength, " +
|
|
438
|
+
" bmp: e.data.bmp, " +
|
|
439
|
+
" buffer: e.data.buffer " +
|
|
440
|
+
" }, [e.data.buffer]); " +
|
|
441
|
+
"};";
|
|
442
|
+
const url = URL.createObjectURL(new Blob([src], { "type": "text/javascript" }));
|
|
443
|
+
const worker = new Worker(url);
|
|
444
|
+
const view = new Float32Array(1024);
|
|
445
|
+
view[0] = 1.5;
|
|
446
|
+
view[1] = 2.25;
|
|
447
|
+
const reply = await new Promise((resolve, reject) => {
|
|
448
|
+
worker.onmessage = (e) => resolve(e.data);
|
|
449
|
+
worker.onerror = (e) => reject(new Error("worker error: " + (e && e.message)));
|
|
450
|
+
worker.postMessage({ "buffer": view.buffer, "bmp": bitmap }, [view.buffer]);
|
|
451
|
+
assert(view.buffer.byteLength === 0, "送信側は detach される (transferable)");
|
|
452
|
+
});
|
|
453
|
+
assert(reply.sum === 3.75, "worker 側で中身が読める: " + reply.sum);
|
|
454
|
+
assert(reply.len === 4096, "worker 側 byteLength: " + reply.len);
|
|
455
|
+
assert(reply.buffer instanceof ArrayBuffer && reply.buffer.byteLength === 4096,
|
|
456
|
+
"worker→main への transfer 返却");
|
|
457
|
+
assert(reply.bmp && reply.bmp.width === 2 && reply.bmp.height === 2,
|
|
458
|
+
"ImageBitmap クローン往復");
|
|
459
|
+
if (worker.terminate) { worker.terminate() }
|
|
259
460
|
});
|
|
260
461
|
|
|
261
462
|
await test("OffscreenCanvas: transferControlToOffscreen + Worker 転送", async () => {
|
|
@@ -267,20 +468,22 @@
|
|
|
267
468
|
" var c = e.data.canvas; " +
|
|
268
469
|
" self.postMessage({ w: c ? c.width : -1 }); " +
|
|
269
470
|
"};";
|
|
270
|
-
const url = URL.createObjectURL(new Blob([src], { type: "text/javascript" }));
|
|
471
|
+
const url = URL.createObjectURL(new Blob([src], { "type": "text/javascript" }));
|
|
271
472
|
const worker = new Worker(url);
|
|
272
473
|
const reply = await new Promise((resolve, reject) => {
|
|
273
474
|
worker.onmessage = (e) => resolve(e.data);
|
|
274
|
-
worker.onerror = (
|
|
275
|
-
worker.postMessage({ canvas: off }, [off]);
|
|
475
|
+
worker.onerror = (_e) => reject(new Error("worker error"));
|
|
476
|
+
worker.postMessage({ "canvas": off }, [off]);
|
|
276
477
|
});
|
|
277
478
|
assert(reply.w === 8, "transferred canvas width=" + reply.w);
|
|
278
|
-
if (worker.terminate) worker.terminate()
|
|
479
|
+
if (worker.terminate) { worker.terminate() }
|
|
279
480
|
});
|
|
280
481
|
|
|
281
482
|
// ==================== WebGPU ====================
|
|
483
|
+
// GPU の無い環境 (CI ランナー等) では adapter が取れず skip になる (softTest)。
|
|
484
|
+
// 実機・GPU あり PC では全て実行される。
|
|
282
485
|
let device = null;
|
|
283
|
-
await
|
|
486
|
+
await softTest("WebGPU: requestAdapter / requestDevice / limits", async () => {
|
|
284
487
|
assert(navigator.gpu, "navigator.gpu");
|
|
285
488
|
const format = navigator.gpu.getPreferredCanvasFormat();
|
|
286
489
|
assert(typeof format === "string" && format.length > 0, "preferred format=" + format);
|
|
@@ -289,13 +492,13 @@
|
|
|
289
492
|
device = await adapter.requestDevice();
|
|
290
493
|
assert(device, "device");
|
|
291
494
|
assert(device.limits && device.limits.maxTextureDimension2D >= 4096,
|
|
292
|
-
|
|
495
|
+
"maxTextureDimension2D=" + (device.limits && device.limits.maxTextureDimension2D));
|
|
293
496
|
});
|
|
294
497
|
|
|
295
|
-
await
|
|
498
|
+
await softTest("WebGPU: writeBuffer -> copyBufferToBuffer -> mapAsync 読み戻し", async () => {
|
|
296
499
|
assert(device, "needs device");
|
|
297
|
-
const src = device.createBuffer({ size: 16, usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST });
|
|
298
|
-
const dst = device.createBuffer({ size: 16, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
|
|
500
|
+
const src = device.createBuffer({ "size": 16, "usage": GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST });
|
|
501
|
+
const dst = device.createBuffer({ "size": 16, "usage": GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
|
|
299
502
|
device.queue.writeBuffer(src, 0, new Uint32Array([1, 2, 3, 4]));
|
|
300
503
|
const enc = device.createCommandEncoder();
|
|
301
504
|
enc.copyBufferToBuffer(src, 0, dst, 0, 16);
|
|
@@ -306,20 +509,20 @@
|
|
|
306
509
|
assert(view[0] === 1 && view[3] === 4, "readback=" + Array.from(view).join(","));
|
|
307
510
|
});
|
|
308
511
|
|
|
309
|
-
await
|
|
512
|
+
await softTest("WebGPU: copyExternalImageToTexture -> copyTextureToBuffer 読み戻し", async () => {
|
|
310
513
|
assert(device && bitmap, "needs device+bitmap");
|
|
311
514
|
const tex = device.createTexture({
|
|
312
|
-
size: { width: 2, height: 2 },
|
|
313
|
-
format: "rgba8unorm",
|
|
314
|
-
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT
|
|
515
|
+
"size": { "width": 2, "height": 2 },
|
|
516
|
+
"format": "rgba8unorm",
|
|
517
|
+
"usage": GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT
|
|
315
518
|
});
|
|
316
519
|
device.queue.copyExternalImageToTexture(
|
|
317
|
-
{ source: bitmap }, { texture: tex }, { width: 2, height: 2 });
|
|
520
|
+
{ "source": bitmap }, { "texture": tex }, { "width": 2, "height": 2 });
|
|
318
521
|
// bytesPerRow は 256 アライン必須
|
|
319
|
-
const buf = device.createBuffer({ size: 256 * 2, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
|
|
522
|
+
const buf = device.createBuffer({ "size": 256 * 2, "usage": GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
|
|
320
523
|
const enc = device.createCommandEncoder();
|
|
321
524
|
enc.copyTextureToBuffer(
|
|
322
|
-
{ texture: tex }, { buffer: buf, bytesPerRow: 256 }, { width: 2, height: 2 });
|
|
525
|
+
{ "texture": tex }, { "buffer": buf, "bytesPerRow": 256 }, { "width": 2, "height": 2 });
|
|
323
526
|
device.queue.submit([enc.finish()]);
|
|
324
527
|
await buf.mapAsync(GPUMapMode.READ);
|
|
325
528
|
const px = new Uint8Array(buf.getMappedRange().slice(0));
|
|
@@ -328,9 +531,9 @@
|
|
|
328
531
|
assert(px[4] === 0 && px[5] === 255, "(1,0) green");
|
|
329
532
|
});
|
|
330
533
|
|
|
331
|
-
await
|
|
534
|
+
await softTest("WebGPU: dynamic offset 付き uniform で描画 (塗りの中核経路)", async () => {
|
|
332
535
|
assert(device, "needs device");
|
|
333
|
-
const shader = device.createShaderModule({ code: `
|
|
536
|
+
const shader = device.createShaderModule({ "code": `
|
|
334
537
|
struct U { color: vec4f };
|
|
335
538
|
@group(0) @binding(0) var<uniform> u: U;
|
|
336
539
|
@vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
|
|
@@ -339,96 +542,251 @@
|
|
|
339
542
|
}
|
|
340
543
|
@fragment fn fs() -> @location(0) vec4f { return u.color; }
|
|
341
544
|
` });
|
|
342
|
-
const bgl = device.createBindGroupLayout({ entries: [{
|
|
343
|
-
binding: 0, visibility: GPUShaderStage.FRAGMENT,
|
|
344
|
-
buffer: { type: "uniform", hasDynamicOffset: true }
|
|
545
|
+
const bgl = device.createBindGroupLayout({ "entries": [{
|
|
546
|
+
"binding": 0, "visibility": GPUShaderStage.FRAGMENT,
|
|
547
|
+
"buffer": { "type": "uniform", "hasDynamicOffset": true }
|
|
345
548
|
}] });
|
|
346
549
|
const pipeline = device.createRenderPipeline({
|
|
347
|
-
layout: device.createPipelineLayout({ bindGroupLayouts: [bgl] }),
|
|
348
|
-
vertex: { module: shader, entryPoint: "vs" },
|
|
349
|
-
fragment: { module: shader, entryPoint: "fs",
|
|
350
|
-
|
|
351
|
-
primitive: { topology: "triangle-list" }
|
|
550
|
+
"layout": device.createPipelineLayout({ "bindGroupLayouts": [bgl] }),
|
|
551
|
+
"vertex": { "module": shader, "entryPoint": "vs" },
|
|
552
|
+
"fragment": { "module": shader, "entryPoint": "fs",
|
|
553
|
+
"targets": [{ "format": "rgba8unorm" }] },
|
|
554
|
+
"primitive": { "topology": "triangle-list" }
|
|
352
555
|
});
|
|
353
556
|
// offset 0 = 赤 / offset 256 = 緑。dynamic offset 256 で緑が描かれるべき。
|
|
354
|
-
const ubo = device.createBuffer({ size: 512, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
557
|
+
const ubo = device.createBuffer({ "size": 512, "usage": GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
355
558
|
device.queue.writeBuffer(ubo, 0, new Float32Array([1, 0, 0, 1]));
|
|
356
559
|
device.queue.writeBuffer(ubo, 256, new Float32Array([0, 1, 0, 1]));
|
|
357
|
-
const bindGroup = device.createBindGroup({ layout: bgl, entries: [{
|
|
358
|
-
binding: 0, resource: { buffer: ubo, offset: 0, size: 16 }
|
|
560
|
+
const bindGroup = device.createBindGroup({ "layout": bgl, "entries": [{
|
|
561
|
+
"binding": 0, "resource": { "buffer": ubo, "offset": 0, "size": 16 }
|
|
359
562
|
}] });
|
|
360
563
|
const target = device.createTexture({
|
|
361
|
-
size: { width: 4, height: 4 }, format: "rgba8unorm",
|
|
362
|
-
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC
|
|
564
|
+
"size": { "width": 4, "height": 4 }, "format": "rgba8unorm",
|
|
565
|
+
"usage": GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC
|
|
363
566
|
});
|
|
364
567
|
const enc = device.createCommandEncoder();
|
|
365
|
-
const pass = enc.beginRenderPass({ colorAttachments: [{
|
|
366
|
-
view: target.createView(), loadOp: "clear", storeOp: "store",
|
|
367
|
-
clearValue: [0, 0, 0, 0]
|
|
568
|
+
const pass = enc.beginRenderPass({ "colorAttachments": [{
|
|
569
|
+
"view": target.createView(), "loadOp": "clear", "storeOp": "store",
|
|
570
|
+
"clearValue": [0, 0, 0, 0]
|
|
368
571
|
}] });
|
|
369
572
|
pass.setPipeline(pipeline);
|
|
370
573
|
pass.setBindGroup(0, bindGroup, [256]); // ← dynamic offset
|
|
371
574
|
pass.draw(3);
|
|
372
575
|
pass.end();
|
|
373
|
-
const buf = device.createBuffer({ size: 256 * 4, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
|
|
576
|
+
const buf = device.createBuffer({ "size": 256 * 4, "usage": GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
|
|
374
577
|
const enc2 = device.createCommandEncoder();
|
|
375
|
-
enc2.copyTextureToBuffer({ texture: target }, { buffer: buf, bytesPerRow: 256 }, { width: 4, height: 4 });
|
|
578
|
+
enc2.copyTextureToBuffer({ "texture": target }, { "buffer": buf, "bytesPerRow": 256 }, { "width": 4, "height": 4 });
|
|
376
579
|
device.queue.submit([enc.finish(), enc2.finish()]);
|
|
377
580
|
await buf.mapAsync(GPUMapMode.READ);
|
|
378
581
|
const px = new Uint8Array(buf.getMappedRange().slice(0));
|
|
379
582
|
buf.unmap();
|
|
380
583
|
assert(px[1] === 255 && px[0] === 0,
|
|
381
|
-
|
|
584
|
+
"dynamic offset 256 -> green, got rgba=" + px[0] + "," + px[1] + "," + px[2] + "," + px[3]);
|
|
382
585
|
});
|
|
383
586
|
|
|
384
|
-
await
|
|
587
|
+
await softTest("WebGPU: MSAA (sampleCount=4) + resolveTarget 解決 (player 全描画経路)", async () => {
|
|
385
588
|
assert(device, "needs device");
|
|
386
|
-
|
|
589
|
+
// player は全描画を MSAA テクスチャへ行い resolveTarget で本体へ解決する。
|
|
590
|
+
// resolveTarget が無視されると描画結果がどこにも現れない (黒画面の実原因だった)
|
|
591
|
+
const shader = device.createShaderModule({ "code": `
|
|
387
592
|
@vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
|
|
388
593
|
var p = array<vec2f, 3>(vec2f(-1,-3), vec2f(3,1), vec2f(-1,1));
|
|
389
594
|
return vec4f(p[i], 0, 1);
|
|
390
595
|
}
|
|
391
596
|
@fragment fn fs() -> @location(0) vec4f { return vec4f(0, 0, 1, 1); }
|
|
392
597
|
` });
|
|
393
|
-
const
|
|
598
|
+
const pipeline = device.createRenderPipeline({
|
|
599
|
+
"layout": "auto",
|
|
600
|
+
"vertex": { "module": shader, "entryPoint": "vs" },
|
|
601
|
+
"fragment": { "module": shader, "entryPoint": "fs",
|
|
602
|
+
"targets": [{ "format": "rgba8unorm" }] },
|
|
603
|
+
"primitive": { "topology": "triangle-list" },
|
|
604
|
+
"multisample": { "count": 4 }
|
|
605
|
+
});
|
|
606
|
+
const msaa = device.createTexture({
|
|
607
|
+
"size": { "width": 4, "height": 4 }, "format": "rgba8unorm",
|
|
608
|
+
"sampleCount": 4,
|
|
609
|
+
"usage": GPUTextureUsage.RENDER_ATTACHMENT
|
|
610
|
+
});
|
|
611
|
+
const resolve = device.createTexture({
|
|
612
|
+
"size": { "width": 4, "height": 4 }, "format": "rgba8unorm",
|
|
613
|
+
"usage": GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC
|
|
614
|
+
});
|
|
615
|
+
const enc = device.createCommandEncoder();
|
|
616
|
+
const pass = enc.beginRenderPass({ "colorAttachments": [{
|
|
617
|
+
"view": msaa.createView(),
|
|
618
|
+
"resolveTarget": resolve.createView(),
|
|
619
|
+
"loadOp": "clear", "storeOp": "store",
|
|
620
|
+
"clearValue": [0, 0, 0, 0]
|
|
621
|
+
}] });
|
|
622
|
+
pass.setPipeline(pipeline);
|
|
623
|
+
pass.draw(3);
|
|
624
|
+
pass.end();
|
|
625
|
+
const buf = device.createBuffer({ "size": 256 * 4, "usage": GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
|
|
626
|
+
const enc2 = device.createCommandEncoder();
|
|
627
|
+
enc2.copyTextureToBuffer({ "texture": resolve }, { "buffer": buf, "bytesPerRow": 256 }, { "width": 4, "height": 4 });
|
|
628
|
+
device.queue.submit([enc.finish(), enc2.finish()]);
|
|
629
|
+
await buf.mapAsync(GPUMapMode.READ);
|
|
630
|
+
const px = new Uint8Array(buf.getMappedRange().slice(0));
|
|
631
|
+
buf.unmap();
|
|
632
|
+
assert(px[2] === 255 && px[3] === 255,
|
|
633
|
+
"resolve target has blue, got rgba=" + px[0] + "," + px[1] + "," + px[2] + "," + px[3]);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
await softTest("WebGPU: MSAA ステンシルマスク (player clip 経路の忠実再現)", async () => {
|
|
637
|
+
assert(device, "needs device");
|
|
638
|
+
// player の mask: (1) writeMask=0 でステンシルへ形状を書く (resolveTarget なし)
|
|
639
|
+
// (2) stencil compare=equal で内容を描く (resolveTarget あり)
|
|
640
|
+
// これが崩れるとマスクが効かず、隠されるべきレイヤーが全面に描かれる
|
|
641
|
+
const W = 8, H = 8;
|
|
642
|
+
const shader = device.createShaderModule({ "code": `
|
|
643
|
+
@vertex fn vsLeft(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
|
|
644
|
+
// 左半分を覆う矩形 (2 三角形)
|
|
645
|
+
var p = array<vec2f, 6>(
|
|
646
|
+
vec2f(-1,-1), vec2f(0,-1), vec2f(-1,1),
|
|
647
|
+
vec2f(-1,1), vec2f(0,-1), vec2f(0,1));
|
|
648
|
+
return vec4f(p[i], 0, 1);
|
|
649
|
+
}
|
|
650
|
+
@vertex fn vsFull(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
|
|
651
|
+
var p = array<vec2f, 3>(vec2f(-1,-3), vec2f(3,1), vec2f(-1,1));
|
|
652
|
+
return vec4f(p[i], 0, 1);
|
|
653
|
+
}
|
|
654
|
+
@fragment fn fs() -> @location(0) vec4f { return vec4f(1, 0, 0, 1); }
|
|
655
|
+
@fragment fn fsBlue() -> @location(0) vec4f { return vec4f(0, 0, 1, 1); }
|
|
656
|
+
` });
|
|
657
|
+
const msaaColor = device.createTexture({
|
|
658
|
+
"size": { "width": W, "height": H }, "format": "rgba8unorm",
|
|
659
|
+
"sampleCount": 4, "usage": GPUTextureUsage.RENDER_ATTACHMENT
|
|
660
|
+
});
|
|
661
|
+
const resolve = device.createTexture({
|
|
662
|
+
"size": { "width": W, "height": H }, "format": "rgba8unorm",
|
|
663
|
+
"usage": GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC
|
|
664
|
+
});
|
|
665
|
+
const msaaStencil = device.createTexture({
|
|
666
|
+
"size": { "width": W, "height": H }, "format": "stencil8",
|
|
667
|
+
"sampleCount": 4, "usage": GPUTextureUsage.RENDER_ATTACHMENT
|
|
668
|
+
});
|
|
669
|
+
const clipPipeline = device.createRenderPipeline({
|
|
670
|
+
"layout": "auto",
|
|
671
|
+
"vertex": { "module": shader, "entryPoint": "vsLeft" },
|
|
672
|
+
"fragment": { "module": shader, "entryPoint": "fs",
|
|
673
|
+
"targets": [{ "format": "rgba8unorm", "writeMask": 0 }] },
|
|
674
|
+
"primitive": { "topology": "triangle-list" },
|
|
675
|
+
"multisample": { "count": 4 },
|
|
676
|
+
"depthStencil": {
|
|
677
|
+
"format": "stencil8",
|
|
678
|
+
"stencilFront": { "compare": "always", "passOp": "replace" },
|
|
679
|
+
"stencilBack": { "compare": "always", "passOp": "replace" }
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
const drawPipeline = device.createRenderPipeline({
|
|
683
|
+
"layout": "auto",
|
|
684
|
+
"vertex": { "module": shader, "entryPoint": "vsFull" },
|
|
685
|
+
"fragment": { "module": shader, "entryPoint": "fsBlue",
|
|
686
|
+
"targets": [{ "format": "rgba8unorm" }] },
|
|
687
|
+
"primitive": { "topology": "triangle-list" },
|
|
688
|
+
"multisample": { "count": 4 },
|
|
689
|
+
"depthStencil": {
|
|
690
|
+
"format": "stencil8",
|
|
691
|
+
"stencilFront": { "compare": "equal", "passOp": "keep" },
|
|
692
|
+
"stencilBack": { "compare": "equal", "passOp": "keep" }
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
const enc = device.createCommandEncoder();
|
|
696
|
+
// pass 1: マスク形状 (左半分) をステンシルに書く。色は writeMask=0 で不変
|
|
697
|
+
const p1 = enc.beginRenderPass({
|
|
698
|
+
"colorAttachments": [{
|
|
699
|
+
"view": msaaColor.createView(),
|
|
700
|
+
"loadOp": "clear", "storeOp": "store", "clearValue": [0, 0, 0, 0]
|
|
701
|
+
}],
|
|
702
|
+
"depthStencilAttachment": {
|
|
703
|
+
"view": msaaStencil.createView(),
|
|
704
|
+
"stencilLoadOp": "clear", "stencilStoreOp": "store",
|
|
705
|
+
"stencilClearValue": 0
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
p1.setPipeline(clipPipeline);
|
|
709
|
+
p1.setStencilReference(1);
|
|
710
|
+
p1.draw(6);
|
|
711
|
+
p1.end();
|
|
712
|
+
// pass 2: 全面に青を stencil==1 の場所だけ描き、resolve へ解決
|
|
713
|
+
const p2 = enc.beginRenderPass({
|
|
714
|
+
"colorAttachments": [{
|
|
715
|
+
"view": msaaColor.createView(),
|
|
716
|
+
"resolveTarget": resolve.createView(),
|
|
717
|
+
"loadOp": "load", "storeOp": "store"
|
|
718
|
+
}],
|
|
719
|
+
"depthStencilAttachment": {
|
|
720
|
+
"view": msaaStencil.createView(),
|
|
721
|
+
"stencilLoadOp": "load", "stencilStoreOp": "store"
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
p2.setPipeline(drawPipeline);
|
|
725
|
+
p2.setStencilReference(1);
|
|
726
|
+
p2.draw(3);
|
|
727
|
+
p2.end();
|
|
728
|
+
const buf = device.createBuffer({ "size": 256 * H, "usage": GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
|
|
729
|
+
enc.copyTextureToBuffer({ "texture": resolve }, { "buffer": buf, "bytesPerRow": 256 }, { "width": W, "height": H });
|
|
730
|
+
device.queue.submit([enc.finish()]);
|
|
731
|
+
await buf.mapAsync(GPUMapMode.READ);
|
|
732
|
+
const px = new Uint8Array(buf.getMappedRange().slice(0));
|
|
733
|
+
buf.unmap();
|
|
734
|
+
const at = (x, y) => y * 256 + x * 4;
|
|
735
|
+
const l = at(2, 4), r = at(6, 4);
|
|
736
|
+
assert(px[l + 2] === 255 && px[l + 3] === 255,
|
|
737
|
+
"マスク内 (左) は青: rgba=" + px[l] + "," + px[l + 1] + "," + px[l + 2] + "," + px[l + 3]);
|
|
738
|
+
assert(px[r + 3] === 0,
|
|
739
|
+
"マスク外 (右) は透明のまま: rgba=" + px[r] + "," + px[r + 1] + "," + px[r + 2] + "," + px[r + 3]);
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
await softTest("WebGPU: stencil8 パイプライン + setStencilReference (マスク経路)", async () => {
|
|
743
|
+
assert(device, "needs device");
|
|
744
|
+
const shader = device.createShaderModule({ "code": `
|
|
745
|
+
@vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
|
|
746
|
+
var p = array<vec2f, 3>(vec2f(-1,-3), vec2f(3,1), vec2f(-1,1));
|
|
747
|
+
return vec4f(p[i], 0, 1);
|
|
748
|
+
}
|
|
749
|
+
@fragment fn fs() -> @location(0) vec4f { return vec4f(0, 0, 1, 1); }
|
|
750
|
+
` });
|
|
751
|
+
const layout = device.createPipelineLayout({ "bindGroupLayouts": [] });
|
|
394
752
|
// 1st: ステンシルへ replace 書き込み (色は書かない設定でも良いが簡略化)
|
|
395
753
|
const writePipe = device.createRenderPipeline({
|
|
396
|
-
layout, vertex: { module: shader, entryPoint: "vs" },
|
|
397
|
-
fragment: { module: shader, entryPoint: "fs", targets: [{ format: "rgba8unorm" }] },
|
|
398
|
-
primitive: { topology: "triangle-list" },
|
|
399
|
-
depthStencil: {
|
|
400
|
-
format: "stencil8",
|
|
401
|
-
stencilFront: { compare: "always", failOp: "keep", depthFailOp: "keep", passOp: "replace" },
|
|
402
|
-
stencilBack: { compare: "always", failOp: "keep", depthFailOp: "keep", passOp: "replace" },
|
|
403
|
-
stencilReadMask: 0xff, stencilWriteMask: 0xff
|
|
754
|
+
layout, "vertex": { "module": shader, "entryPoint": "vs" },
|
|
755
|
+
"fragment": { "module": shader, "entryPoint": "fs", "targets": [{ "format": "rgba8unorm" }] },
|
|
756
|
+
"primitive": { "topology": "triangle-list" },
|
|
757
|
+
"depthStencil": {
|
|
758
|
+
"format": "stencil8",
|
|
759
|
+
"stencilFront": { "compare": "always", "failOp": "keep", "depthFailOp": "keep", "passOp": "replace" },
|
|
760
|
+
"stencilBack": { "compare": "always", "failOp": "keep", "depthFailOp": "keep", "passOp": "replace" },
|
|
761
|
+
"stencilReadMask": 0xff, "stencilWriteMask": 0xff
|
|
404
762
|
}
|
|
405
763
|
});
|
|
406
764
|
// 2nd: equal 比較で描画 (マスク内のみ)
|
|
407
765
|
const testPipe = device.createRenderPipeline({
|
|
408
|
-
layout, vertex: { module: shader, entryPoint: "vs" },
|
|
409
|
-
fragment: { module: shader, entryPoint: "fs", targets: [{ format: "rgba8unorm" }] },
|
|
410
|
-
primitive: { topology: "triangle-list" },
|
|
411
|
-
depthStencil: {
|
|
412
|
-
format: "stencil8",
|
|
413
|
-
stencilFront: { compare: "equal", failOp: "keep", depthFailOp: "keep", passOp: "keep" },
|
|
414
|
-
stencilBack: { compare: "equal", failOp: "keep", depthFailOp: "keep", passOp: "keep" },
|
|
415
|
-
stencilReadMask: 0xff, stencilWriteMask: 0x00
|
|
766
|
+
layout, "vertex": { "module": shader, "entryPoint": "vs" },
|
|
767
|
+
"fragment": { "module": shader, "entryPoint": "fs", "targets": [{ "format": "rgba8unorm" }] },
|
|
768
|
+
"primitive": { "topology": "triangle-list" },
|
|
769
|
+
"depthStencil": {
|
|
770
|
+
"format": "stencil8",
|
|
771
|
+
"stencilFront": { "compare": "equal", "failOp": "keep", "depthFailOp": "keep", "passOp": "keep" },
|
|
772
|
+
"stencilBack": { "compare": "equal", "failOp": "keep", "depthFailOp": "keep", "passOp": "keep" },
|
|
773
|
+
"stencilReadMask": 0xff, "stencilWriteMask": 0x00
|
|
416
774
|
}
|
|
417
775
|
});
|
|
418
776
|
const color = device.createTexture({
|
|
419
|
-
size: { width: 4, height: 4 }, format: "rgba8unorm",
|
|
420
|
-
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC
|
|
777
|
+
"size": { "width": 4, "height": 4 }, "format": "rgba8unorm",
|
|
778
|
+
"usage": GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC
|
|
421
779
|
});
|
|
422
780
|
const stencil = device.createTexture({
|
|
423
|
-
size: { width: 4, height: 4 }, format: "stencil8",
|
|
424
|
-
usage: GPUTextureUsage.RENDER_ATTACHMENT
|
|
781
|
+
"size": { "width": 4, "height": 4 }, "format": "stencil8",
|
|
782
|
+
"usage": GPUTextureUsage.RENDER_ATTACHMENT
|
|
425
783
|
});
|
|
426
784
|
const enc = device.createCommandEncoder();
|
|
427
785
|
const p1 = enc.beginRenderPass({
|
|
428
|
-
colorAttachments: [{ view: color.createView(), loadOp: "clear", storeOp: "store", clearValue: [0,0,0,0] }],
|
|
429
|
-
depthStencilAttachment: {
|
|
430
|
-
view: stencil.createView(),
|
|
431
|
-
stencilLoadOp: "clear", stencilStoreOp: "store", stencilClearValue: 0
|
|
786
|
+
"colorAttachments": [{ "view": color.createView(), "loadOp": "clear", "storeOp": "store", "clearValue": [0,0,0,0] }],
|
|
787
|
+
"depthStencilAttachment": {
|
|
788
|
+
"view": stencil.createView(),
|
|
789
|
+
"stencilLoadOp": "clear", "stencilStoreOp": "store", "stencilClearValue": 0
|
|
432
790
|
}
|
|
433
791
|
});
|
|
434
792
|
p1.setPipeline(writePipe);
|
|
@@ -436,18 +794,18 @@
|
|
|
436
794
|
p1.draw(3);
|
|
437
795
|
p1.end();
|
|
438
796
|
const p2 = enc.beginRenderPass({
|
|
439
|
-
colorAttachments: [{ view: color.createView(), loadOp: "load", storeOp: "store" }],
|
|
440
|
-
depthStencilAttachment: {
|
|
441
|
-
view: stencil.createView(),
|
|
442
|
-
stencilLoadOp: "load", stencilStoreOp: "store"
|
|
797
|
+
"colorAttachments": [{ "view": color.createView(), "loadOp": "load", "storeOp": "store" }],
|
|
798
|
+
"depthStencilAttachment": {
|
|
799
|
+
"view": stencil.createView(),
|
|
800
|
+
"stencilLoadOp": "load", "stencilStoreOp": "store"
|
|
443
801
|
}
|
|
444
802
|
});
|
|
445
803
|
p2.setPipeline(testPipe);
|
|
446
804
|
p2.setStencilReference(1); // ref==stencil(1) -> 描画される
|
|
447
805
|
p2.draw(3);
|
|
448
806
|
p2.end();
|
|
449
|
-
const buf = device.createBuffer({ size: 256 * 4, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
|
|
450
|
-
enc.copyTextureToBuffer({ texture: color }, { buffer: buf, bytesPerRow: 256 }, { width: 4, height: 4 });
|
|
807
|
+
const buf = device.createBuffer({ "size": 256 * 4, "usage": GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
|
|
808
|
+
enc.copyTextureToBuffer({ "texture": color }, { "buffer": buf, "bytesPerRow": 256 }, { "width": 4, "height": 4 });
|
|
451
809
|
device.queue.submit([enc.finish()]);
|
|
452
810
|
await buf.mapAsync(GPUMapMode.READ);
|
|
453
811
|
const px = new Uint8Array(buf.getMappedRange().slice(0));
|
|
@@ -455,6 +813,284 @@
|
|
|
455
813
|
assert(px[2] === 255, "stencil equal pass drew blue, got rgba=" + px[0] + "," + px[1] + "," + px[2]);
|
|
456
814
|
});
|
|
457
815
|
|
|
816
|
+
await softTest("WebGPU: pipeline-overridable constants (override がパイプラインへ反映)", async () => {
|
|
817
|
+
assert(device, "needs device");
|
|
818
|
+
// player は override 定数 (GRADIENT_TYPE=linear/radial, SPREAD_MODE, フィルタ種別,
|
|
819
|
+
// yFlipSign) でシェーダを特殊化する。ホストが fragment/vertex.constants を読まないと
|
|
820
|
+
// 全パイプラインが WGSL 既定値でコンパイルされ、放射状グラデが線形に潰れる等になる。
|
|
821
|
+
// ここで override が実際に効くか (既定値でなく指定値でコンパイルされるか) を検証する。
|
|
822
|
+
const shader = device.createShaderModule({ "code": `
|
|
823
|
+
override SELECT : u32 = 0u;
|
|
824
|
+
override TINT : f32 = 0.25;
|
|
825
|
+
@vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
|
|
826
|
+
var p = array<vec2f, 3>(vec2f(-1,-3), vec2f(3,1), vec2f(-1,1));
|
|
827
|
+
return vec4f(p[i], 0, 1);
|
|
828
|
+
}
|
|
829
|
+
@fragment fn fs() -> @location(0) vec4f {
|
|
830
|
+
if (SELECT == 1u) { return vec4f(0, TINT, 0, 1); }
|
|
831
|
+
return vec4f(TINT, 0, 0, 1);
|
|
832
|
+
}
|
|
833
|
+
` });
|
|
834
|
+
const mk = (constants) => device.createRenderPipeline({
|
|
835
|
+
"layout": "auto",
|
|
836
|
+
"vertex": { "module": shader, "entryPoint": "vs" },
|
|
837
|
+
"fragment": { "module": shader, "entryPoint": "fs",
|
|
838
|
+
"constants": constants,
|
|
839
|
+
"targets": [{ "format": "rgba8unorm" }] },
|
|
840
|
+
"primitive": { "topology": "triangle-list" }
|
|
841
|
+
});
|
|
842
|
+
const draw = async (pipeline) => {
|
|
843
|
+
const target = device.createTexture({
|
|
844
|
+
"size": { "width": 2, "height": 2 }, "format": "rgba8unorm",
|
|
845
|
+
"usage": GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC });
|
|
846
|
+
const enc = device.createCommandEncoder();
|
|
847
|
+
const pass = enc.beginRenderPass({ "colorAttachments": [{
|
|
848
|
+
"view": target.createView(), "loadOp": "clear", "storeOp": "store",
|
|
849
|
+
"clearValue": [0, 0, 0, 1] }] });
|
|
850
|
+
pass.setPipeline(pipeline); pass.draw(3); pass.end();
|
|
851
|
+
// 2 行分: copyTextureToBuffer は size >= bytesPerRow*(h-1)+w*4 = 264 を要求する
|
|
852
|
+
const buf = device.createBuffer({ "size": 256 * 2, "usage": GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
|
|
853
|
+
enc.copyTextureToBuffer({ "texture": target }, { "buffer": buf, "bytesPerRow": 256 }, { "width": 2, "height": 2 });
|
|
854
|
+
device.queue.submit([enc.finish()]);
|
|
855
|
+
await buf.mapAsync(GPUMapMode.READ);
|
|
856
|
+
const px = new Uint8Array(buf.getMappedRange().slice(0)); buf.unmap();
|
|
857
|
+
return [px[0], px[1], px[2]];
|
|
858
|
+
};
|
|
859
|
+
// constants が無視されると両方 SELECT=0/TINT=0.25 (既定) の赤~64 になり検証に落ちる。
|
|
860
|
+
const green = await draw(mk({ "SELECT": 1, "TINT": 0.75 }));
|
|
861
|
+
const red = await draw(mk({ "SELECT": 0, "TINT": 1.0 }));
|
|
862
|
+
assert(green[1] > 150 && green[0] < 60,
|
|
863
|
+
"override SELECT=1/TINT=0.75 -> 緑: rgb=" + green.join(","));
|
|
864
|
+
assert(red[0] > 240 && red[1] < 40,
|
|
865
|
+
"override SELECT=0/TINT=1.0 -> 赤最大: rgb=" + red.join(","));
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
await softTest("WebGPU: multisample.alphaToCoverageEnabled (ベクタ縁 AA)", async () => {
|
|
869
|
+
assert(device, "needs device");
|
|
870
|
+
// Next2D はベクタ図形の境界を塗り alpha に頼り、MSAA カバレッジで AA する。
|
|
871
|
+
// このフィールドが無視されると縁がジャギる。alpha=0.5 で部分被覆になるかを検証。
|
|
872
|
+
const shader = device.createShaderModule({ "code": `
|
|
873
|
+
@vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
|
|
874
|
+
var p = array<vec2f, 3>(vec2f(-1,-3), vec2f(3,1), vec2f(-1,1));
|
|
875
|
+
return vec4f(p[i], 0, 1);
|
|
876
|
+
}
|
|
877
|
+
@fragment fn fs() -> @location(0) vec4f { return vec4f(1, 0, 0, 0.5); }
|
|
878
|
+
` });
|
|
879
|
+
const pipeline = device.createRenderPipeline({
|
|
880
|
+
"layout": "auto",
|
|
881
|
+
"vertex": { "module": shader, "entryPoint": "vs" },
|
|
882
|
+
"fragment": { "module": shader, "entryPoint": "fs", "targets": [{ "format": "rgba8unorm" }] },
|
|
883
|
+
"primitive": { "topology": "triangle-list" },
|
|
884
|
+
"multisample": { "count": 4, "alphaToCoverageEnabled": true }
|
|
885
|
+
});
|
|
886
|
+
const msaa = device.createTexture({ "size": { "width": 4, "height": 4 }, "format": "rgba8unorm",
|
|
887
|
+
"sampleCount": 4, "usage": GPUTextureUsage.RENDER_ATTACHMENT });
|
|
888
|
+
const resolve = device.createTexture({ "size": { "width": 4, "height": 4 }, "format": "rgba8unorm",
|
|
889
|
+
"usage": GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC });
|
|
890
|
+
const enc = device.createCommandEncoder();
|
|
891
|
+
const pass = enc.beginRenderPass({ "colorAttachments": [{
|
|
892
|
+
"view": msaa.createView(), "resolveTarget": resolve.createView(),
|
|
893
|
+
"loadOp": "clear", "storeOp": "store", "clearValue": [0, 0, 0, 1] }] });
|
|
894
|
+
pass.setPipeline(pipeline); pass.draw(3); pass.end();
|
|
895
|
+
const buf = device.createBuffer({ "size": 256 * 4, "usage": GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
|
|
896
|
+
enc.copyTextureToBuffer({ "texture": resolve }, { "buffer": buf, "bytesPerRow": 256 }, { "width": 4, "height": 4 });
|
|
897
|
+
device.queue.submit([enc.finish()]);
|
|
898
|
+
await buf.mapAsync(GPUMapMode.READ);
|
|
899
|
+
const px = new Uint8Array(buf.getMappedRange().slice(0)); buf.unmap();
|
|
900
|
+
// alpha 0.5 + alphaToCoverage -> ~2/4 サンプル被覆 -> resolve は中間の赤。
|
|
901
|
+
// フィールドが無視されると全サンプル被覆で赤=255 になり検証に落ちる。
|
|
902
|
+
assert(px[0] > 20 && px[0] < 235,
|
|
903
|
+
"alpha-to-coverage で部分被覆 (中間の赤): rgba=" + px[0] + "," + px[1] + "," + px[2] + "," + px[3]);
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
await softTest("WebGPU: writeTexture 宛先 origin (アトラスのセル位置)", async () => {
|
|
907
|
+
assert(device, "needs device");
|
|
908
|
+
// player はアトラスの (node.x, node.y) へ writeTexture する。origin が無視されると
|
|
909
|
+
// 常に (0,0) に書かれてアトラスが破損する。origin(2,2) の書込みが右下に出るか検証。
|
|
910
|
+
const tex = device.createTexture({ "size": { "width": 4, "height": 4 }, "format": "rgba8unorm",
|
|
911
|
+
"usage": GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC });
|
|
912
|
+
const red = new Uint8Array(2 * 2 * 4);
|
|
913
|
+
for (let i = 0; i < red.length; i += 4) { red[i] = 255; red[i + 3] = 255; }
|
|
914
|
+
device.queue.writeTexture({ "texture": tex, "origin": { "x": 2, "y": 2, "z": 0 } },
|
|
915
|
+
red, { "bytesPerRow": 2 * 4, "rowsPerImage": 2 }, { "width": 2, "height": 2 });
|
|
916
|
+
const buf = device.createBuffer({ "size": 256 * 4, "usage": GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
|
|
917
|
+
const enc = device.createCommandEncoder();
|
|
918
|
+
enc.copyTextureToBuffer({ "texture": tex }, { "buffer": buf, "bytesPerRow": 256 }, { "width": 4, "height": 4 });
|
|
919
|
+
device.queue.submit([enc.finish()]);
|
|
920
|
+
await buf.mapAsync(GPUMapMode.READ);
|
|
921
|
+
const px = new Uint8Array(buf.getMappedRange().slice(0)); buf.unmap();
|
|
922
|
+
const at = (x, y) => y * 256 + x * 4;
|
|
923
|
+
assert(px[at(3, 3)] === 255, "origin(2,2) の赤が右下(3,3)に存在: got=" + px[at(3, 3)]);
|
|
924
|
+
assert(px[at(0, 0)] === 0, "左上(0,0)は空 (origin が (0,0) に潰れていない): got=" + px[at(0, 0)]);
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
await softTest("WebGPU: compute pipeline + storage buffer 書き戻し (dispatchWorkgroups)", async () => {
|
|
928
|
+
assert(device, "needs device");
|
|
929
|
+
// Next2D の 2D ラスタライザは compute を使わないが、WebGPU の機能を特定用途に
|
|
930
|
+
// 絞らず全面的に利用可能にするため compute 経路を検証する。storage buffer への
|
|
931
|
+
// 書込みと compute.constants (override) の反映を 1 テストで確認する。
|
|
932
|
+
const shader = device.createShaderModule({ "code": `
|
|
933
|
+
override MUL : u32 = 1u;
|
|
934
|
+
@group(0) @binding(0) var<storage, read_write> outBuf : array<u32>;
|
|
935
|
+
@compute @workgroup_size(4) fn main(@builtin(global_invocation_id) gid : vec3u) {
|
|
936
|
+
outBuf[gid.x] = gid.x * MUL;
|
|
937
|
+
}
|
|
938
|
+
` });
|
|
939
|
+
const pipeline = device.createComputePipeline({
|
|
940
|
+
"layout": "auto",
|
|
941
|
+
"compute": { "module": shader, "entryPoint": "main", "constants": { "MUL": 3 } }
|
|
942
|
+
});
|
|
943
|
+
const N = 4;
|
|
944
|
+
const storage = device.createBuffer({ "size": N * 4,
|
|
945
|
+
"usage": GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC });
|
|
946
|
+
const bindGroup = device.createBindGroup({
|
|
947
|
+
"layout": pipeline.getBindGroupLayout(0),
|
|
948
|
+
"entries": [{ "binding": 0, "resource": { "buffer": storage } }]
|
|
949
|
+
});
|
|
950
|
+
const enc = device.createCommandEncoder();
|
|
951
|
+
const pass = enc.beginComputePass();
|
|
952
|
+
pass.setPipeline(pipeline);
|
|
953
|
+
pass.setBindGroup(0, bindGroup);
|
|
954
|
+
pass.dispatchWorkgroups(1);
|
|
955
|
+
pass.end();
|
|
956
|
+
const readback = device.createBuffer({ "size": N * 4,
|
|
957
|
+
"usage": GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
|
|
958
|
+
enc.copyBufferToBuffer(storage, 0, readback, 0, N * 4);
|
|
959
|
+
device.queue.submit([enc.finish()]);
|
|
960
|
+
await readback.mapAsync(GPUMapMode.READ);
|
|
961
|
+
const out = new Uint32Array(readback.getMappedRange().slice(0)); readback.unmap();
|
|
962
|
+
// MUL=3 override が効けば [0,3,6,9]。無視 (既定 1) なら [0,1,2,3] で落ちる。
|
|
963
|
+
assert(out[0] === 0 && out[1] === 3 && out[2] === 6 && out[3] === 9,
|
|
964
|
+
"compute storage 書込み + override MUL=3: got=" + Array.from(out).join(","));
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
await softTest("WebGPU: error scope (pushErrorScope/popErrorScope)", async () => {
|
|
968
|
+
assert(device, "needs device");
|
|
969
|
+
// 正常操作 → popErrorScope は null。不正操作 → validation error を捕捉。
|
|
970
|
+
device.pushErrorScope("validation");
|
|
971
|
+
device.createBuffer({ "size": 16, "usage": GPUBufferUsage.COPY_DST });
|
|
972
|
+
const noErr = await device.popErrorScope();
|
|
973
|
+
assert(noErr === null, "正常操作では error なし: got=" + noErr);
|
|
974
|
+
device.pushErrorScope("validation");
|
|
975
|
+
// MAP_READ | MAP_WRITE の同時指定は不正 (validation error)
|
|
976
|
+
device.createBuffer({ "size": 16, "usage": GPUBufferUsage.MAP_READ | GPUBufferUsage.MAP_WRITE });
|
|
977
|
+
const err = await device.popErrorScope();
|
|
978
|
+
assert(err !== null, "不正操作で validation error を捕捉 (null でない)");
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
await softTest("WebGPU: render bundle (createRenderBundleEncoder/executeBundles)", async () => {
|
|
982
|
+
assert(device, "needs device");
|
|
983
|
+
const shader = device.createShaderModule({ "code": `
|
|
984
|
+
@vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
|
|
985
|
+
var p = array<vec2f, 3>(vec2f(-1,-3), vec2f(3,1), vec2f(-1,1));
|
|
986
|
+
return vec4f(p[i], 0, 1);
|
|
987
|
+
}
|
|
988
|
+
@fragment fn fs() -> @location(0) vec4f { return vec4f(0, 1, 0, 1); }
|
|
989
|
+
` });
|
|
990
|
+
const pipeline = device.createRenderPipeline({
|
|
991
|
+
"layout": "auto",
|
|
992
|
+
"vertex": { "module": shader, "entryPoint": "vs" },
|
|
993
|
+
"fragment": { "module": shader, "entryPoint": "fs", "targets": [{ "format": "rgba8unorm" }] },
|
|
994
|
+
"primitive": { "topology": "triangle-list" }
|
|
995
|
+
});
|
|
996
|
+
const bundleEnc = device.createRenderBundleEncoder({ "colorFormats": ["rgba8unorm"] });
|
|
997
|
+
bundleEnc.setPipeline(pipeline); bundleEnc.draw(3);
|
|
998
|
+
const bundle = bundleEnc.finish();
|
|
999
|
+
const target = device.createTexture({ "size": { "width": 2, "height": 2 }, "format": "rgba8unorm",
|
|
1000
|
+
"usage": GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC });
|
|
1001
|
+
const enc = device.createCommandEncoder();
|
|
1002
|
+
const pass = enc.beginRenderPass({ "colorAttachments": [{
|
|
1003
|
+
"view": target.createView(), "loadOp": "clear", "storeOp": "store", "clearValue": [0, 0, 0, 1] }] });
|
|
1004
|
+
pass.executeBundles([bundle]); pass.end();
|
|
1005
|
+
const buf = device.createBuffer({ "size": 256 * 2, "usage": GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
|
|
1006
|
+
enc.copyTextureToBuffer({ "texture": target }, { "buffer": buf, "bytesPerRow": 256 }, { "width": 2, "height": 2 });
|
|
1007
|
+
device.queue.submit([enc.finish()]);
|
|
1008
|
+
await buf.mapAsync(GPUMapMode.READ);
|
|
1009
|
+
const px = new Uint8Array(buf.getMappedRange().slice(0)); buf.unmap();
|
|
1010
|
+
// bundle 経由の draw が実行されないと clearValue の黒のまま。
|
|
1011
|
+
assert(px[1] > 240 && px[0] < 40, "render bundle 経由の緑描画: rgb=" + px[0] + "," + px[1] + "," + px[2]);
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
await softTest("WebGPU: occlusion query (createQuerySet/beginOcclusionQuery/resolveQuerySet)", async () => {
|
|
1015
|
+
assert(device, "needs device");
|
|
1016
|
+
const shader = device.createShaderModule({ "code": `
|
|
1017
|
+
@vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
|
|
1018
|
+
var p = array<vec2f, 3>(vec2f(-1,-3), vec2f(3,1), vec2f(-1,1));
|
|
1019
|
+
return vec4f(p[i], 0, 1);
|
|
1020
|
+
}
|
|
1021
|
+
@fragment fn fs() -> @location(0) vec4f { return vec4f(1, 0, 0, 1); }
|
|
1022
|
+
` });
|
|
1023
|
+
const pipeline = device.createRenderPipeline({
|
|
1024
|
+
"layout": "auto",
|
|
1025
|
+
"vertex": { "module": shader, "entryPoint": "vs" },
|
|
1026
|
+
"fragment": { "module": shader, "entryPoint": "fs", "targets": [{ "format": "rgba8unorm" }] },
|
|
1027
|
+
"primitive": { "topology": "triangle-list" }
|
|
1028
|
+
});
|
|
1029
|
+
const querySet = device.createQuerySet({ "type": "occlusion", "count": 1 });
|
|
1030
|
+
assert(querySet.count === 1 && querySet.type === "occlusion",
|
|
1031
|
+
"querySet メタ: count=" + querySet.count + " type=" + querySet.type);
|
|
1032
|
+
const target = device.createTexture({ "size": { "width": 2, "height": 2 }, "format": "rgba8unorm",
|
|
1033
|
+
"usage": GPUTextureUsage.RENDER_ATTACHMENT });
|
|
1034
|
+
const enc = device.createCommandEncoder();
|
|
1035
|
+
const pass = enc.beginRenderPass({ "colorAttachments": [{
|
|
1036
|
+
"view": target.createView(), "loadOp": "clear", "storeOp": "store", "clearValue": [0, 0, 0, 1] }],
|
|
1037
|
+
"occlusionQuerySet": querySet });
|
|
1038
|
+
pass.setPipeline(pipeline);
|
|
1039
|
+
pass.beginOcclusionQuery(0); pass.draw(3); pass.endOcclusionQuery();
|
|
1040
|
+
pass.end();
|
|
1041
|
+
const resolveBuf = device.createBuffer({ "size": 8, "usage": GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC });
|
|
1042
|
+
enc.resolveQuerySet(querySet, 0, 1, resolveBuf, 0);
|
|
1043
|
+
const readBuf = device.createBuffer({ "size": 8, "usage": GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
|
|
1044
|
+
enc.copyBufferToBuffer(resolveBuf, 0, readBuf, 0, 8);
|
|
1045
|
+
device.queue.submit([enc.finish()]);
|
|
1046
|
+
await readBuf.mapAsync(GPUMapMode.READ);
|
|
1047
|
+
const counts = new Uint32Array(readBuf.getMappedRange().slice(0)); readBuf.unmap();
|
|
1048
|
+
// 全画面三角形を描いたので被覆サンプル数 > 0 (query 未対応なら 0 のまま)。
|
|
1049
|
+
assert(counts[0] > 0, "occlusion query 被覆サンプル数 > 0: got=" + counts[0]);
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
await softTest("WebGPU: async pipeline + onSubmittedWorkDone", async () => {
|
|
1053
|
+
assert(device, "needs device");
|
|
1054
|
+
const shader = device.createShaderModule({ "code": `
|
|
1055
|
+
@vertex fn vs() -> @builtin(position) vec4f { return vec4f(0, 0, 0, 1); }
|
|
1056
|
+
@fragment fn fs() -> @location(0) vec4f { return vec4f(1, 0, 0, 1); }
|
|
1057
|
+
` });
|
|
1058
|
+
const rp = await device.createRenderPipelineAsync({
|
|
1059
|
+
"layout": "auto",
|
|
1060
|
+
"vertex": { "module": shader, "entryPoint": "vs" },
|
|
1061
|
+
"fragment": { "module": shader, "entryPoint": "fs", "targets": [{ "format": "rgba8unorm" }] },
|
|
1062
|
+
"primitive": { "topology": "point-list" }
|
|
1063
|
+
});
|
|
1064
|
+
assert(rp && typeof rp.getBindGroupLayout === "function", "createRenderPipelineAsync が pipeline を返す");
|
|
1065
|
+
const cs = device.createShaderModule({ "code": `
|
|
1066
|
+
@group(0) @binding(0) var<storage, read_write> o : array<u32>;
|
|
1067
|
+
@compute @workgroup_size(1) fn main() { o[0] = 1u; }
|
|
1068
|
+
` });
|
|
1069
|
+
const cp = await device.createComputePipelineAsync({
|
|
1070
|
+
"layout": "auto", "compute": { "module": cs, "entryPoint": "main" } });
|
|
1071
|
+
assert(cp && typeof cp.getBindGroupLayout === "function", "createComputePipelineAsync が pipeline を返す");
|
|
1072
|
+
device.queue.submit([]);
|
|
1073
|
+
await device.queue.onSubmittedWorkDone(); // resolve すれば成功
|
|
1074
|
+
assert(true, "onSubmittedWorkDone が resolve");
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
await softTest("WebGPU: debug marker / device.lost / device.destroy", async () => {
|
|
1078
|
+
assert(device, "needs device");
|
|
1079
|
+
// debug marker は結果に影響しないが呼べること (no-throw) を確認する。
|
|
1080
|
+
const enc = device.createCommandEncoder();
|
|
1081
|
+
enc.pushDebugGroup("group"); enc.insertDebugMarker("marker"); enc.popDebugGroup();
|
|
1082
|
+
const target = device.createTexture({ "size": { "width": 2, "height": 2 }, "format": "rgba8unorm",
|
|
1083
|
+
"usage": GPUTextureUsage.RENDER_ATTACHMENT });
|
|
1084
|
+
const pass = enc.beginRenderPass({ "colorAttachments": [{
|
|
1085
|
+
"view": target.createView(), "loadOp": "clear", "storeOp": "store", "clearValue": [0, 0, 0, 1] }] });
|
|
1086
|
+
pass.pushDebugGroup("pass"); pass.insertDebugMarker("pm"); pass.popDebugGroup();
|
|
1087
|
+
pass.end();
|
|
1088
|
+
device.queue.submit([enc.finish()]);
|
|
1089
|
+
// device.lost は Promise、destroy は関数として存在すること (device 共有中なので実際には呼ばない)。
|
|
1090
|
+
assert(device.lost && typeof device.lost.then === "function", "device.lost は Promise");
|
|
1091
|
+
assert(typeof device.destroy === "function", "device.destroy が存在");
|
|
1092
|
+
});
|
|
1093
|
+
|
|
458
1094
|
// ==================== Audio ====================
|
|
459
1095
|
let audioCtx = null;
|
|
460
1096
|
await test("AudioContext: decodeAudioData (Media Foundation)", async () => {
|
|
@@ -465,6 +1101,20 @@
|
|
|
465
1101
|
globalThis.__selftestAudioBuffer = buffer;
|
|
466
1102
|
});
|
|
467
1103
|
|
|
1104
|
+
await test("Audio 要素 (new Audio / play / pause)", async () => {
|
|
1105
|
+
// ゲームコードが効果音再生に使う HTMLAudioElement 最小実装
|
|
1106
|
+
const a = new Audio(); // URL なし (ボイス無しでも API は成立する)
|
|
1107
|
+
a.volume = 0;
|
|
1108
|
+
a.loop = false;
|
|
1109
|
+
a.preload = "auto";
|
|
1110
|
+
a.currentTime = 0;
|
|
1111
|
+
assert(a.paused === true, "initial paused");
|
|
1112
|
+
await a.play();
|
|
1113
|
+
assert(a.paused === false, "playing");
|
|
1114
|
+
a.pause();
|
|
1115
|
+
assert(a.paused === true, "paused after pause()");
|
|
1116
|
+
});
|
|
1117
|
+
|
|
468
1118
|
await softTest("Audio: gain 接続 + 再生 + ended 発火", async () => {
|
|
469
1119
|
assert(audioCtx && globalThis.__selftestAudioBuffer, "needs decoded buffer");
|
|
470
1120
|
const gainNode = audioCtx.createGain();
|
|
@@ -481,8 +1131,8 @@
|
|
|
481
1131
|
// ==================== 入力 / その他 ====================
|
|
482
1132
|
await test("Gamepad: navigator.getGamepads()", () => {
|
|
483
1133
|
const pads = navigator.getGamepads();
|
|
484
|
-
assert(Array.isArray(pads) ||
|
|
485
|
-
|
|
1134
|
+
assert(Array.isArray(pads) || pads && typeof pads.length === "number",
|
|
1135
|
+
"returns array-like");
|
|
486
1136
|
});
|
|
487
1137
|
|
|
488
1138
|
await softTest("clipboard: writeText / readText 往復", async () => {
|