@next2d/builder 0.1.5 → 0.1.6

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.
Files changed (55) hide show
  1. package/README.md +11 -0
  2. package/dist/index.js +247 -3
  3. package/package.json +10 -8
  4. package/templates/xbox/CMakeLists.txt +121 -0
  5. package/templates/xbox/MicrosoftGame.config +36 -0
  6. package/templates/xbox/README.md +319 -0
  7. package/templates/xbox/cmake/FetchDawn.cmake +42 -0
  8. package/templates/xbox/cmake/FindV8.cmake +62 -0
  9. package/templates/xbox/js/bootstrap.js +143 -0
  10. package/templates/xbox/js/selftest.js +515 -0
  11. package/templates/xbox/src/AssetLoader.cpp +106 -0
  12. package/templates/xbox/src/AssetLoader.h +35 -0
  13. package/templates/xbox/src/EventLoop.cpp +162 -0
  14. package/templates/xbox/src/EventLoop.h +73 -0
  15. package/templates/xbox/src/HostContext.h +58 -0
  16. package/templates/xbox/src/bindings/Audio.cpp +320 -0
  17. package/templates/xbox/src/bindings/Bindings.h +75 -0
  18. package/templates/xbox/src/bindings/Canvas.cpp +171 -0
  19. package/templates/xbox/src/bindings/Canvas2D.cpp +462 -0
  20. package/templates/xbox/src/bindings/Console.cpp +52 -0
  21. package/templates/xbox/src/bindings/DomShims.cpp +221 -0
  22. package/templates/xbox/src/bindings/EventTarget.cpp +127 -0
  23. package/templates/xbox/src/bindings/EventTarget.h +17 -0
  24. package/templates/xbox/src/bindings/Fetch.cpp +149 -0
  25. package/templates/xbox/src/bindings/Gamepad.cpp +79 -0
  26. package/templates/xbox/src/bindings/Image.cpp +160 -0
  27. package/templates/xbox/src/bindings/ImageSource.h +26 -0
  28. package/templates/xbox/src/bindings/Network.cpp +303 -0
  29. package/templates/xbox/src/bindings/RasterCore.h +265 -0
  30. package/templates/xbox/src/bindings/Timers.cpp +87 -0
  31. package/templates/xbox/src/bindings/Video.cpp +235 -0
  32. package/templates/xbox/src/bindings/webgpu/WebGPU.cpp +1318 -0
  33. package/templates/xbox/src/bindings/webgpu/WebGPUCommon.h +119 -0
  34. package/templates/xbox/src/bindings/webgpu/WebGPUEnums.h +245 -0
  35. package/templates/xbox/src/gpu/DawnContext.cpp +170 -0
  36. package/templates/xbox/src/gpu/DawnContext.h +64 -0
  37. package/templates/xbox/src/main.cpp +368 -0
  38. package/templates/xbox/src/platform/AudioEngine.cpp +184 -0
  39. package/templates/xbox/src/platform/AudioEngine.h +65 -0
  40. package/templates/xbox/src/platform/GamepadManager.cpp +83 -0
  41. package/templates/xbox/src/platform/GamepadManager.h +40 -0
  42. package/templates/xbox/src/platform/ImageTypes.h +17 -0
  43. package/templates/xbox/src/platform/TextRasterizer.cpp +183 -0
  44. package/templates/xbox/src/platform/TextRasterizer.h +35 -0
  45. package/templates/xbox/src/platform/WicDecoder.cpp +63 -0
  46. package/templates/xbox/src/platform/WicDecoder.h +16 -0
  47. package/templates/xbox/src/v8/V8Runtime.cpp +284 -0
  48. package/templates/xbox/src/v8/V8Runtime.h +68 -0
  49. package/templates/xbox/src/v8/V8Util.h +56 -0
  50. package/templates/xbox/src/worker/WorkerRuntime.cpp +453 -0
  51. package/templates/xbox/src/worker/WorkerRuntime.h +108 -0
  52. package/templates/xbox/tests/check_local.sh +56 -0
  53. package/templates/xbox/tests/raster_test.bin +0 -0
  54. package/templates/xbox/tests/raster_test.cpp +279 -0
  55. package/templates/xbox/tests/windows_platform_test.cpp +187 -0
@@ -0,0 +1,515 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Next2D Xbox host セルフテスト。
5
+ *
6
+ * 実行方法: Next2DXboxHost.exe --selftest
7
+ * (bootstrap.js の後にクラシックスクリプトとして評価され、アプリの代わりに走る)
8
+ *
9
+ * C++ バインディング (WebGPU / Canvas2D / Worker / XHR / Audio / 入力 / 画像) を
10
+ * 実機・PC(GDK) 上で網羅的に実行し、TAP 風の結果を console に出す。
11
+ * 完了時に globalThis.__selftestExitCode を設定するとホストが自動終了する。
12
+ *
13
+ * Windows PC / devkit を入手したら最初にこれを流すことで、
14
+ * macOS 上では検証できなかった実挙動を一括確認できる。
15
+ */
16
+ (function () {
17
+
18
+ const results = { pass: 0, fail: 0, skip: 0 };
19
+ const failures = [];
20
+
21
+ function tap(status, name, detail) {
22
+ const line = status === "ok"
23
+ ? "ok - " + name
24
+ : status === "skip"
25
+ ? "ok - " + name + " # SKIP " + (detail || "")
26
+ : "not ok - " + name + (detail ? " # " + detail : "");
27
+ console.log("[selftest] " + line);
28
+ }
29
+
30
+ function pass(name) { results.pass++; tap("ok", name); }
31
+ function fail(name, e) {
32
+ results.fail++;
33
+ const msg = e && e.message ? e.message : String(e);
34
+ failures.push(name + ": " + msg);
35
+ tap("fail", name, msg);
36
+ }
37
+ function skip(name, why) { results.skip++; tap("skip", name, why); }
38
+
39
+ function assert(cond, msg) { if (!cond) throw new Error(msg || "assertion failed"); }
40
+
41
+ function withTimeout(promise, ms, what) {
42
+ return Promise.race([
43
+ promise,
44
+ new Promise((_, rej) => setTimeout(
45
+ () => rej(new Error("timeout (" + ms + "ms): " + (what || ""))), ms))
46
+ ]);
47
+ }
48
+
49
+ async function test(name, fn, timeoutMs) {
50
+ try {
51
+ await withTimeout(Promise.resolve().then(fn), timeoutMs || 5000, name);
52
+ pass(name);
53
+ } catch (e) {
54
+ fail(name, e);
55
+ }
56
+ }
57
+
58
+ // 環境依存 (音声デバイス無し等) で失敗し得るテスト: 失敗を skip として扱う
59
+ async function softTest(name, fn, timeoutMs) {
60
+ try {
61
+ await withTimeout(Promise.resolve().then(fn), timeoutMs || 5000, name);
62
+ pass(name);
63
+ } catch (e) {
64
+ skip(name, e && e.message ? e.message : String(e));
65
+ }
66
+ }
67
+
68
+ // --- テスト用の埋め込みアセット -----------------------------------------
69
+ // 2x2 PNG: (0,0)=赤 (1,0)=緑 (0,1)=青 (1,1)=半透明白
70
+ const TEST_PNG = new Uint8Array([
71
+ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
72
+ 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02,
73
+ 0x08, 0x06, 0x00, 0x00, 0x00, 0x72, 0xb6, 0x0d, 0x24, 0x00, 0x00, 0x00,
74
+ 0x13, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0xf8, 0xcf, 0xc0, 0xf0,
75
+ 0x1f, 0x0c, 0x81, 0x34, 0x08, 0x34, 0x00, 0x00, 0x49, 0x49, 0x09, 0x78,
76
+ 0x28, 0xa0, 0xdb, 0x77, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44,
77
+ 0xae, 0x42, 0x60, 0x82
78
+ ]);
79
+
80
+ // 48kHz mono 16bit PCM WAV (矩形波 48 サンプル = 1ms)
81
+ const TEST_WAV = new Uint8Array([
82
+ 0x52, 0x49, 0x46, 0x46, 0x84, 0x00, 0x00, 0x00, 0x57, 0x41, 0x56, 0x45,
83
+ 0x66, 0x6d, 0x74, 0x20, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
84
+ 0x80, 0xbb, 0x00, 0x00, 0x00, 0x77, 0x01, 0x00, 0x02, 0x00, 0x10, 0x00,
85
+ 0x64, 0x61, 0x74, 0x61, 0x60, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x40,
86
+ 0x00, 0x40, 0x00, 0x40, 0x00, 0xc0, 0x00, 0xc0, 0x00, 0xc0, 0x00, 0xc0,
87
+ 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xc0, 0x00, 0xc0,
88
+ 0x00, 0xc0, 0x00, 0xc0, 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0x40,
89
+ 0x00, 0xc0, 0x00, 0xc0, 0x00, 0xc0, 0x00, 0xc0, 0x00, 0x40, 0x00, 0x40,
90
+ 0x00, 0x40, 0x00, 0x40, 0x00, 0xc0, 0x00, 0xc0, 0x00, 0xc0, 0x00, 0xc0,
91
+ 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0xc0, 0x00, 0xc0,
92
+ 0x00, 0xc0, 0x00, 0xc0, 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0x40,
93
+ 0x00, 0xc0, 0x00, 0xc0, 0x00, 0xc0, 0x00, 0xc0
94
+ ]);
95
+
96
+ async function main() {
97
+ console.log("[selftest] === Next2D Xbox host selftest start ===");
98
+
99
+ // ==================== 基盤 ====================
100
+ await test("globals: window/document/navigator/screen", () => {
101
+ assert(typeof window === "object", "window");
102
+ assert(typeof document === "object", "document");
103
+ assert(typeof navigator === "object", "navigator");
104
+ assert(typeof screen === "object", "screen");
105
+ assert(window === globalThis && self === globalThis, "window === globalThis");
106
+ });
107
+
108
+ await test("timers: setTimeout / setInterval / clearInterval", async () => {
109
+ const order = [];
110
+ await new Promise((resolve) => {
111
+ setTimeout(() => order.push("t10"), 10);
112
+ setTimeout(() => { order.push("t30"); resolve(); }, 30);
113
+ Promise.resolve().then(() => order.push("micro"));
114
+ order.push("sync");
115
+ });
116
+ assert(order[0] === "sync" && order[1] === "micro",
117
+ "microtask order: " + order.join(","));
118
+ assert(order.indexOf("t10") < order.indexOf("t30"), "timer order");
119
+
120
+ let ticks = 0;
121
+ await new Promise((resolve) => {
122
+ const id = setInterval(() => {
123
+ if (++ticks >= 2) { clearInterval(id); resolve(); }
124
+ }, 5);
125
+ });
126
+ assert(ticks === 2, "interval stopped after clearInterval");
127
+ });
128
+
129
+ await test("requestAnimationFrame: フレームが進む", async () => {
130
+ const t0 = performance.now();
131
+ await new Promise((resolve) => requestAnimationFrame(resolve));
132
+ await new Promise((resolve) => requestAnimationFrame(resolve));
133
+ assert(performance.now() >= t0, "performance.now monotonic");
134
+ });
135
+
136
+ await test("EventTarget: addEventListener / dispatchEvent", () => {
137
+ let got = null;
138
+ const handler = (e) => { got = e; };
139
+ window.addEventListener("selftest-event", handler);
140
+ window.dispatchEvent({ type: "selftest-event", value: 42 });
141
+ assert(got && got.value === 42, "listener called");
142
+ window.removeEventListener("selftest-event", handler);
143
+ got = null;
144
+ window.dispatchEvent({ type: "selftest-event" });
145
+ assert(got === null, "listener removed");
146
+ });
147
+
148
+ // ==================== 画像 ====================
149
+ let bitmap = null;
150
+ await test("createImageBitmap: PNG デコード (WIC)", async () => {
151
+ bitmap = await createImageBitmap(TEST_PNG.buffer);
152
+ assert(bitmap.width === 2 && bitmap.height === 2,
153
+ "2x2 png -> " + bitmap.width + "x" + bitmap.height);
154
+ });
155
+
156
+ // ==================== Canvas2D ====================
157
+ await test("Canvas2D: fill / getImageData", () => {
158
+ const c = new OffscreenCanvas(16, 16);
159
+ const ctx = c.getContext("2d");
160
+ ctx.fillStyle = "#ff0000";
161
+ ctx.beginPath();
162
+ ctx.rect(2, 2, 8, 8);
163
+ ctx.fill();
164
+ const d = ctx.getImageData(0, 0, 16, 16).data;
165
+ const at = (x, y) => (y * 16 + x) * 4;
166
+ assert(d[at(5, 5)] === 255 && d[at(5, 5) + 3] === 255, "inside filled");
167
+ assert(d[at(12, 12) + 3] === 0, "outside untouched");
168
+ });
169
+
170
+ await test("Canvas2D: clip がはみ出しを抑える (TextField 枠)", () => {
171
+ const c = new OffscreenCanvas(20, 20);
172
+ const ctx = c.getContext("2d");
173
+ ctx.save();
174
+ ctx.beginPath();
175
+ ctx.moveTo(5, 5); ctx.lineTo(15, 5); ctx.lineTo(15, 15); ctx.lineTo(5, 15);
176
+ ctx.clip();
177
+ ctx.fillStyle = "#00ff00";
178
+ ctx.fillRect(0, 0, 20, 20);
179
+ ctx.restore();
180
+ const d = ctx.getImageData(0, 0, 20, 20).data;
181
+ const at = (x, y) => (y * 20 + x) * 4;
182
+ assert(d[at(10, 10) + 1] === 255, "inside clip filled");
183
+ assert(d[at(2, 2) + 3] === 0, "outside clip untouched");
184
+ // restore 後はクリップ解除
185
+ ctx.fillRect(0, 0, 1, 1);
186
+ const d2 = ctx.getImageData(0, 0, 1, 1).data;
187
+ assert(d2[3] !== 0, "clip released after restore");
188
+ });
189
+
190
+ await test("Canvas2D: isPointInPath (ヒット判定)", () => {
191
+ const c = new OffscreenCanvas(4, 4);
192
+ const ctx = c.getContext("2d");
193
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
194
+ ctx.beginPath();
195
+ ctx.moveTo(10, 10); ctx.lineTo(30, 10); ctx.lineTo(30, 30); ctx.lineTo(10, 30);
196
+ assert(ctx.isPointInPath(20, 20) === true, "inside");
197
+ assert(ctx.isPointInPath(40, 40) === false, "outside");
198
+ });
199
+
200
+ await test("Canvas2D: fillText / measureText (DirectWrite)", () => {
201
+ const c = new OffscreenCanvas(64, 32);
202
+ const ctx = c.getContext("2d");
203
+ ctx.font = "24px sans-serif";
204
+ const m = ctx.measureText("Hello");
205
+ assert(m.width > 10, "measureText width=" + m.width);
206
+ ctx.fillStyle = "#ffffff";
207
+ ctx.fillText("A", 4, 24);
208
+ const d = ctx.getImageData(0, 0, 64, 32).data;
209
+ let opaque = 0;
210
+ for (let i = 3; i < d.length; i += 4) if (d[i] > 128) opaque++;
211
+ assert(opaque > 5, "glyph pixels=" + opaque);
212
+ });
213
+
214
+ await test("Canvas2D: drawImage (ImageBitmap 合成)", () => {
215
+ assert(bitmap, "needs decoded bitmap");
216
+ const c = new OffscreenCanvas(4, 4);
217
+ const ctx = c.getContext("2d");
218
+ ctx.drawImage(bitmap, 0, 0);
219
+ const d = ctx.getImageData(0, 0, 2, 2).data;
220
+ assert(d[0] === 255 && d[1] === 0, "(0,0) red");
221
+ assert(d[4] === 0 && d[5] === 255, "(1,0) green");
222
+ });
223
+
224
+ // ==================== ネットワーク (Blob / URL / XHR) ====================
225
+ let blobUrl = null;
226
+ await test("Blob / URL.createObjectURL / revokeObjectURL", () => {
227
+ const blob = new Blob(["self.__marker = 1;"], { type: "text/javascript" });
228
+ blobUrl = URL.createObjectURL(blob);
229
+ assert(typeof blobUrl === "string" && blobUrl.indexOf("blob:") === 0,
230
+ "url=" + blobUrl);
231
+ });
232
+
233
+ await softTest("XMLHttpRequest: アセット読込 (assets/app)", async () => {
234
+ await new Promise((resolve, reject) => {
235
+ const xhr = new XMLHttpRequest();
236
+ xhr.open("GET", "index.html", true);
237
+ xhr.onload = () => xhr.status === 200 ? resolve() : reject(new Error("status " + xhr.status));
238
+ xhr.onerror = () => reject(new Error("xhr error (assets/app 未配置?)"));
239
+ xhr.send();
240
+ });
241
+ });
242
+
243
+ // ==================== Worker ====================
244
+ await test("Worker: blob URL + postMessage echo (協調実行)", async () => {
245
+ const src = "self.onmessage = function(e) { " +
246
+ " self.postMessage({ echo: e.data.value * 2, arr: e.data.arr }); " +
247
+ "};";
248
+ const url = URL.createObjectURL(new Blob([src], { type: "text/javascript" }));
249
+ const worker = new Worker(url);
250
+ const reply = await new Promise((resolve, reject) => {
251
+ worker.onmessage = (e) => resolve(e.data);
252
+ worker.onerror = (e) => reject(new Error("worker error: " + (e && e.message)));
253
+ worker.postMessage({ value: 21, arr: new Float32Array([1.5, 2.5]) });
254
+ });
255
+ assert(reply.echo === 42, "echo=" + reply.echo);
256
+ assert(reply.arr instanceof Float32Array && reply.arr[1] === 2.5,
257
+ "structured clone TypedArray");
258
+ if (worker.terminate) worker.terminate();
259
+ });
260
+
261
+ await test("OffscreenCanvas: transferControlToOffscreen + Worker 転送", async () => {
262
+ const canvas = document.createElement("canvas");
263
+ canvas.width = 8; canvas.height = 8;
264
+ const off = canvas.transferControlToOffscreen();
265
+ assert(off && typeof off === "object", "offscreen created");
266
+ const src = "self.onmessage = function(e) { " +
267
+ " var c = e.data.canvas; " +
268
+ " self.postMessage({ w: c ? c.width : -1 }); " +
269
+ "};";
270
+ const url = URL.createObjectURL(new Blob([src], { type: "text/javascript" }));
271
+ const worker = new Worker(url);
272
+ const reply = await new Promise((resolve, reject) => {
273
+ worker.onmessage = (e) => resolve(e.data);
274
+ worker.onerror = (e) => reject(new Error("worker error"));
275
+ worker.postMessage({ canvas: off }, [off]);
276
+ });
277
+ assert(reply.w === 8, "transferred canvas width=" + reply.w);
278
+ if (worker.terminate) worker.terminate();
279
+ });
280
+
281
+ // ==================== WebGPU ====================
282
+ let device = null;
283
+ await test("WebGPU: requestAdapter / requestDevice / limits", async () => {
284
+ assert(navigator.gpu, "navigator.gpu");
285
+ const format = navigator.gpu.getPreferredCanvasFormat();
286
+ assert(typeof format === "string" && format.length > 0, "preferred format=" + format);
287
+ const adapter = await navigator.gpu.requestAdapter();
288
+ assert(adapter, "adapter");
289
+ device = await adapter.requestDevice();
290
+ assert(device, "device");
291
+ assert(device.limits && device.limits.maxTextureDimension2D >= 4096,
292
+ "maxTextureDimension2D=" + (device.limits && device.limits.maxTextureDimension2D));
293
+ });
294
+
295
+ await test("WebGPU: writeBuffer -> copyBufferToBuffer -> mapAsync 読み戻し", async () => {
296
+ 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 });
299
+ device.queue.writeBuffer(src, 0, new Uint32Array([1, 2, 3, 4]));
300
+ const enc = device.createCommandEncoder();
301
+ enc.copyBufferToBuffer(src, 0, dst, 0, 16);
302
+ device.queue.submit([enc.finish()]);
303
+ await dst.mapAsync(GPUMapMode.READ);
304
+ const view = new Uint32Array(dst.getMappedRange().slice(0));
305
+ dst.unmap();
306
+ assert(view[0] === 1 && view[3] === 4, "readback=" + Array.from(view).join(","));
307
+ });
308
+
309
+ await test("WebGPU: copyExternalImageToTexture -> copyTextureToBuffer 読み戻し", async () => {
310
+ assert(device && bitmap, "needs device+bitmap");
311
+ 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
315
+ });
316
+ device.queue.copyExternalImageToTexture(
317
+ { source: bitmap }, { texture: tex }, { width: 2, height: 2 });
318
+ // bytesPerRow は 256 アライン必須
319
+ const buf = device.createBuffer({ size: 256 * 2, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
320
+ const enc = device.createCommandEncoder();
321
+ enc.copyTextureToBuffer(
322
+ { texture: tex }, { buffer: buf, bytesPerRow: 256 }, { width: 2, height: 2 });
323
+ device.queue.submit([enc.finish()]);
324
+ await buf.mapAsync(GPUMapMode.READ);
325
+ const px = new Uint8Array(buf.getMappedRange().slice(0));
326
+ buf.unmap();
327
+ assert(px[0] === 255 && px[1] === 0 && px[2] === 0, "(0,0) red: " + px[0] + "," + px[1] + "," + px[2]);
328
+ assert(px[4] === 0 && px[5] === 255, "(1,0) green");
329
+ });
330
+
331
+ await test("WebGPU: dynamic offset 付き uniform で描画 (塗りの中核経路)", async () => {
332
+ assert(device, "needs device");
333
+ const shader = device.createShaderModule({ code: `
334
+ struct U { color: vec4f };
335
+ @group(0) @binding(0) var<uniform> u: U;
336
+ @vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
337
+ var p = array<vec2f, 3>(vec2f(-1,-3), vec2f(3,1), vec2f(-1,1));
338
+ return vec4f(p[i], 0, 1);
339
+ }
340
+ @fragment fn fs() -> @location(0) vec4f { return u.color; }
341
+ ` });
342
+ const bgl = device.createBindGroupLayout({ entries: [{
343
+ binding: 0, visibility: GPUShaderStage.FRAGMENT,
344
+ buffer: { type: "uniform", hasDynamicOffset: true }
345
+ }] });
346
+ const pipeline = device.createRenderPipeline({
347
+ layout: device.createPipelineLayout({ bindGroupLayouts: [bgl] }),
348
+ vertex: { module: shader, entryPoint: "vs" },
349
+ fragment: { module: shader, entryPoint: "fs",
350
+ targets: [{ format: "rgba8unorm" }] },
351
+ primitive: { topology: "triangle-list" }
352
+ });
353
+ // offset 0 = 赤 / offset 256 = 緑。dynamic offset 256 で緑が描かれるべき。
354
+ const ubo = device.createBuffer({ size: 512, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
355
+ device.queue.writeBuffer(ubo, 0, new Float32Array([1, 0, 0, 1]));
356
+ 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 }
359
+ }] });
360
+ const target = device.createTexture({
361
+ size: { width: 4, height: 4 }, format: "rgba8unorm",
362
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC
363
+ });
364
+ const enc = device.createCommandEncoder();
365
+ const pass = enc.beginRenderPass({ colorAttachments: [{
366
+ view: target.createView(), loadOp: "clear", storeOp: "store",
367
+ clearValue: [0, 0, 0, 0]
368
+ }] });
369
+ pass.setPipeline(pipeline);
370
+ pass.setBindGroup(0, bindGroup, [256]); // ← dynamic offset
371
+ pass.draw(3);
372
+ pass.end();
373
+ const buf = device.createBuffer({ size: 256 * 4, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
374
+ const enc2 = device.createCommandEncoder();
375
+ enc2.copyTextureToBuffer({ texture: target }, { buffer: buf, bytesPerRow: 256 }, { width: 4, height: 4 });
376
+ device.queue.submit([enc.finish(), enc2.finish()]);
377
+ await buf.mapAsync(GPUMapMode.READ);
378
+ const px = new Uint8Array(buf.getMappedRange().slice(0));
379
+ buf.unmap();
380
+ assert(px[1] === 255 && px[0] === 0,
381
+ "dynamic offset 256 -> green, got rgba=" + px[0] + "," + px[1] + "," + px[2] + "," + px[3]);
382
+ });
383
+
384
+ await test("WebGPU: stencil8 パイプライン + setStencilReference (マスク経路)", async () => {
385
+ assert(device, "needs device");
386
+ const shader = device.createShaderModule({ code: `
387
+ @vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
388
+ var p = array<vec2f, 3>(vec2f(-1,-3), vec2f(3,1), vec2f(-1,1));
389
+ return vec4f(p[i], 0, 1);
390
+ }
391
+ @fragment fn fs() -> @location(0) vec4f { return vec4f(0, 0, 1, 1); }
392
+ ` });
393
+ const layout = device.createPipelineLayout({ bindGroupLayouts: [] });
394
+ // 1st: ステンシルへ replace 書き込み (色は書かない設定でも良いが簡略化)
395
+ 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
404
+ }
405
+ });
406
+ // 2nd: equal 比較で描画 (マスク内のみ)
407
+ 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
416
+ }
417
+ });
418
+ const color = device.createTexture({
419
+ size: { width: 4, height: 4 }, format: "rgba8unorm",
420
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC
421
+ });
422
+ const stencil = device.createTexture({
423
+ size: { width: 4, height: 4 }, format: "stencil8",
424
+ usage: GPUTextureUsage.RENDER_ATTACHMENT
425
+ });
426
+ const enc = device.createCommandEncoder();
427
+ 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
432
+ }
433
+ });
434
+ p1.setPipeline(writePipe);
435
+ p1.setStencilReference(1);
436
+ p1.draw(3);
437
+ p1.end();
438
+ const p2 = enc.beginRenderPass({
439
+ colorAttachments: [{ view: color.createView(), loadOp: "load", storeOp: "store" }],
440
+ depthStencilAttachment: {
441
+ view: stencil.createView(),
442
+ stencilLoadOp: "load", stencilStoreOp: "store"
443
+ }
444
+ });
445
+ p2.setPipeline(testPipe);
446
+ p2.setStencilReference(1); // ref==stencil(1) -> 描画される
447
+ p2.draw(3);
448
+ 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 });
451
+ device.queue.submit([enc.finish()]);
452
+ await buf.mapAsync(GPUMapMode.READ);
453
+ const px = new Uint8Array(buf.getMappedRange().slice(0));
454
+ buf.unmap();
455
+ assert(px[2] === 255, "stencil equal pass drew blue, got rgba=" + px[0] + "," + px[1] + "," + px[2]);
456
+ });
457
+
458
+ // ==================== Audio ====================
459
+ let audioCtx = null;
460
+ await test("AudioContext: decodeAudioData (Media Foundation)", async () => {
461
+ audioCtx = new AudioContext();
462
+ const buffer = await audioCtx.decodeAudioData(TEST_WAV.buffer.slice(0));
463
+ assert(buffer.sampleRate === 48000, "sampleRate=" + buffer.sampleRate);
464
+ assert(buffer.duration > 0, "duration=" + buffer.duration);
465
+ globalThis.__selftestAudioBuffer = buffer;
466
+ });
467
+
468
+ await softTest("Audio: gain 接続 + 再生 + ended 発火", async () => {
469
+ assert(audioCtx && globalThis.__selftestAudioBuffer, "needs decoded buffer");
470
+ const gainNode = audioCtx.createGain();
471
+ gainNode.connect(audioCtx.destination);
472
+ gainNode.gain.value = 0.0; // ミュートで再生 (実機でも無音)
473
+ const source = audioCtx.createBufferSource();
474
+ source.buffer = globalThis.__selftestAudioBuffer;
475
+ source.connect(gainNode);
476
+ const ended = new Promise((resolve) => source.addEventListener("ended", resolve));
477
+ source.start(0);
478
+ await withTimeout(ended, 3000, "ended event");
479
+ });
480
+
481
+ // ==================== 入力 / その他 ====================
482
+ await test("Gamepad: navigator.getGamepads()", () => {
483
+ const pads = navigator.getGamepads();
484
+ assert(Array.isArray(pads) || (pads && typeof pads.length === "number"),
485
+ "returns array-like");
486
+ });
487
+
488
+ await softTest("clipboard: writeText / readText 往復", async () => {
489
+ await navigator.clipboard.writeText("next2d-selftest");
490
+ const text = await navigator.clipboard.readText();
491
+ assert(text === "next2d-selftest", "roundtrip=" + text);
492
+ });
493
+
494
+ await softTest("fetch: アセット取得", async () => {
495
+ const res = await fetch("index.html");
496
+ assert(res && (res.ok || res.status === 200), "status=" + (res && res.status));
497
+ });
498
+
499
+ // ==================== まとめ ====================
500
+ console.log("[selftest] === done: " +
501
+ results.pass + " passed, " +
502
+ results.fail + " failed, " +
503
+ results.skip + " skipped ===");
504
+ if (failures.length) {
505
+ console.error("[selftest] failures:\n - " + failures.join("\n - "));
506
+ }
507
+ globalThis.__selftestExitCode = results.fail > 0 ? 1 : 0;
508
+ }
509
+
510
+ main().catch((e) => {
511
+ console.error("[selftest] fatal: " + (e && e.stack ? e.stack : e));
512
+ globalThis.__selftestExitCode = 2;
513
+ });
514
+
515
+ })();
@@ -0,0 +1,106 @@
1
+ #include "AssetLoader.h"
2
+
3
+ #include <filesystem>
4
+ #include <fstream>
5
+ #include <regex>
6
+ #include <sstream>
7
+
8
+ namespace fs = std::filesystem;
9
+
10
+ namespace next2d {
11
+
12
+ AssetLoader::AssetLoader(std::string root)
13
+ : root_(std::move(root))
14
+ {
15
+ }
16
+
17
+ std::string AssetLoader::Resolve(const std::string& url) const
18
+ {
19
+ // 先頭の "/" や "./"、絶対 URL のオリジンを剥がして assets/app 基準に寄せる。
20
+ std::string path = url;
21
+
22
+ // スキーム付き (file://, http://localhost/...) はパス部だけ取り出す
23
+ const auto scheme = path.find("://");
24
+ if (scheme != std::string::npos) {
25
+ const auto slash = path.find('/', scheme + 3);
26
+ path = (slash != std::string::npos) ? path.substr(slash) : "/";
27
+ }
28
+
29
+ // クエリ/ハッシュ除去
30
+ const auto q = path.find_first_of("?#");
31
+ if (q != std::string::npos) {
32
+ path = path.substr(0, q);
33
+ }
34
+
35
+ while (!path.empty() && (path.front() == '/' || path.front() == '\\')) {
36
+ path.erase(path.begin());
37
+ }
38
+ if (path.rfind("./", 0) == 0) {
39
+ path = path.substr(2);
40
+ }
41
+
42
+ return (fs::path(root_) / path).string();
43
+ }
44
+
45
+ std::optional<std::vector<uint8_t>> AssetLoader::ReadBinary(const std::string& url) const
46
+ {
47
+ const std::string path = Resolve(url);
48
+ std::ifstream ifs(path, std::ios::binary | std::ios::ate);
49
+ if (!ifs) {
50
+ return std::nullopt;
51
+ }
52
+ const std::streamsize size = ifs.tellg();
53
+ ifs.seekg(0, std::ios::beg);
54
+
55
+ std::vector<uint8_t> buffer(static_cast<size_t>(size));
56
+ if (size > 0 && !ifs.read(reinterpret_cast<char*>(buffer.data()), size)) {
57
+ return std::nullopt;
58
+ }
59
+ return buffer;
60
+ }
61
+
62
+ std::optional<std::string> AssetLoader::ReadText(const std::string& url) const
63
+ {
64
+ const std::string path = Resolve(url);
65
+ std::ifstream ifs(path, std::ios::binary);
66
+ if (!ifs) {
67
+ return std::nullopt;
68
+ }
69
+ std::ostringstream oss;
70
+ oss << ifs.rdbuf();
71
+ return oss.str();
72
+ }
73
+
74
+ std::optional<std::string> AssetLoader::ResolveEntryModule() const
75
+ {
76
+ // vite が出力する index.html から <script type="module" src="..."> を抽出する。
77
+ const fs::path index = fs::path(root_) / "index.html";
78
+ std::ifstream ifs(index, std::ios::binary);
79
+ if (!ifs) {
80
+ return std::nullopt;
81
+ }
82
+ std::ostringstream oss;
83
+ oss << ifs.rdbuf();
84
+ const std::string html = oss.str();
85
+
86
+ // type="module" を含む <script ... src="..."> を優先して拾う。
87
+ std::regex module_re(
88
+ R"(<script[^>]*type=["']module["'][^>]*src=["']([^"']+)["'])",
89
+ std::regex::icase);
90
+ std::smatch m;
91
+ if (std::regex_search(html, m, module_re)) {
92
+ return (fs::path(root_) / Resolve(m[1].str()).substr(root_.size() + 1)).string();
93
+ }
94
+
95
+ // 逆順 (src が先) も試す
96
+ std::regex module_re2(
97
+ R"(<script[^>]*src=["']([^"']+)["'][^>]*type=["']module["'])",
98
+ std::regex::icase);
99
+ if (std::regex_search(html, m, module_re2)) {
100
+ return (fs::path(root_) / Resolve(m[1].str()).substr(root_.size() + 1)).string();
101
+ }
102
+
103
+ return std::nullopt;
104
+ }
105
+
106
+ } // namespace next2d
@@ -0,0 +1,35 @@
1
+ // AssetLoader: assets/app 以下(builder が配置した JS/HTML/画像/音声)を読み込む。
2
+ // fetch / XMLHttpRequest / Image / AudioContext のローカル読み込みの基盤。
3
+ #pragma once
4
+
5
+ #include <cstdint>
6
+ #include <optional>
7
+ #include <string>
8
+ #include <vector>
9
+
10
+ namespace next2d {
11
+
12
+ class AssetLoader {
13
+ public:
14
+ // root は実行ファイル隣の "assets/app" を指す。
15
+ explicit AssetLoader(std::string root);
16
+
17
+ // アプリのエントリ JS パスを index.html の <script type="module" src> から解決する。
18
+ std::optional<std::string> ResolveEntryModule() const;
19
+
20
+ // 相対 URL/パスを assets/app 基準の絶対パスへ解決する。
21
+ std::string Resolve(const std::string& url) const;
22
+
23
+ // バイナリ読み込み。存在しなければ nullopt。
24
+ std::optional<std::vector<uint8_t>> ReadBinary(const std::string& url) const;
25
+
26
+ // テキスト読み込み。
27
+ std::optional<std::string> ReadText(const std::string& url) const;
28
+
29
+ const std::string& root() const { return root_; }
30
+
31
+ private:
32
+ std::string root_;
33
+ };
34
+
35
+ } // namespace next2d