@next2d/builder 0.1.7 → 0.1.10

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 (75) hide show
  1. package/dist/cli.d.ts +59 -0
  2. package/dist/cli.js +141 -0
  3. package/dist/constants.d.ts +53 -0
  4. package/dist/constants.js +64 -0
  5. package/dist/context.d.ts +29 -0
  6. package/dist/context.js +19 -0
  7. package/dist/electron.d.ts +10 -0
  8. package/dist/electron.js +142 -0
  9. package/dist/index.js +18 -754
  10. package/dist/native.d.ts +27 -0
  11. package/dist/native.js +123 -0
  12. package/dist/utils.d.ts +27 -0
  13. package/dist/utils.js +40 -0
  14. package/dist/web.d.ts +18 -0
  15. package/dist/web.js +70 -0
  16. package/dist/xbox.d.ts +62 -0
  17. package/dist/xbox.js +426 -0
  18. package/package.json +12 -9
  19. package/templates/xbox/CMakeLists.txt +116 -11
  20. package/templates/xbox/README.md +34 -8
  21. package/templates/xbox/cmake/FetchDawn.cmake +47 -6
  22. package/templates/xbox/cmake/FindV8.cmake +15 -0
  23. package/templates/xbox/js/bootstrap.js +151 -28
  24. package/templates/xbox/js/selftest.js +740 -90
  25. package/templates/xbox/src/AssetLoader.cpp +115 -12
  26. package/templates/xbox/src/AssetLoader.h +4 -0
  27. package/templates/xbox/src/EmbeddedAssets.cpp +149 -0
  28. package/templates/xbox/src/EmbeddedAssets.h +60 -0
  29. package/templates/xbox/src/EventLoop.cpp +13 -1
  30. package/templates/xbox/src/EventLoop.h +12 -0
  31. package/templates/xbox/src/bindings/Audio.cpp +211 -31
  32. package/templates/xbox/src/bindings/Bindings.h +10 -0
  33. package/templates/xbox/src/bindings/Canvas.cpp +77 -2
  34. package/templates/xbox/src/bindings/Canvas2D.cpp +496 -43
  35. package/templates/xbox/src/bindings/Console.cpp +6 -1
  36. package/templates/xbox/src/bindings/DomShims.cpp +143 -0
  37. package/templates/xbox/src/bindings/EventTarget.cpp +11 -0
  38. package/templates/xbox/src/bindings/Fetch.cpp +2 -8
  39. package/templates/xbox/src/bindings/Image.cpp +243 -15
  40. package/templates/xbox/src/bindings/Polyfills.cpp +1226 -0
  41. package/templates/xbox/src/bindings/RasterCore.h +105 -1
  42. package/templates/xbox/src/bindings/Video.cpp +153 -10
  43. package/templates/xbox/src/bindings/webgpu/WebGPU.cpp +844 -18
  44. package/templates/xbox/src/bindings/webgpu/WebGPUCommon.h +7 -9
  45. package/templates/xbox/src/bindings/webgpu/WebGPUEnums.h +32 -0
  46. package/templates/xbox/src/gpu/DawnContext.cpp +245 -7
  47. package/templates/xbox/src/gpu/DawnContext.h +18 -0
  48. package/templates/xbox/src/main.cpp +459 -8
  49. package/templates/xbox/src/platform/AudioDecoder.cpp +121 -0
  50. package/templates/xbox/src/platform/AudioDecoder.h +18 -0
  51. package/templates/xbox/src/platform/AudioEngine.cpp +37 -6
  52. package/templates/xbox/src/platform/DecodeQueue.cpp +136 -0
  53. package/templates/xbox/src/platform/DecodeQueue.h +27 -0
  54. package/templates/xbox/src/platform/ImageDecoder.cpp +44 -0
  55. package/templates/xbox/src/platform/ImageDecoder.h +18 -0
  56. package/templates/xbox/src/platform/StbTextRasterizer.cpp +324 -0
  57. package/templates/xbox/src/platform/StbTextRasterizer.h +37 -0
  58. package/templates/xbox/src/platform/TextRasterizer.cpp +32 -0
  59. package/templates/xbox/src/platform/WicDecoder.cpp +19 -0
  60. package/templates/xbox/src/v8/CodeCache.h +223 -0
  61. package/templates/xbox/src/v8/V8Runtime.cpp +76 -11
  62. package/templates/xbox/src/v8/V8Util.h +79 -0
  63. package/templates/xbox/src/v8/WeakHandle.h +45 -0
  64. package/templates/xbox/src/worker/WorkerRuntime.cpp +432 -48
  65. package/templates/xbox/src/worker/WorkerRuntime.h +43 -13
  66. package/templates/xbox/tests/check_local.sh +7 -2
  67. package/templates/xbox/tests/gamecore_api_probe.cpp +172 -0
  68. package/templates/xbox/tests/raster_test.cpp +50 -0
  69. package/templates/xbox/tests/windows_platform_test.cpp +75 -3
  70. package/templates/xbox/third_party/LICENSES.md +14 -0
  71. package/templates/xbox/third_party/dr_mp3.h +5412 -0
  72. package/templates/xbox/third_party/dr_wav.h +9131 -0
  73. package/templates/xbox/third_party/stb_image.h +7988 -0
  74. package/templates/xbox/third_party/stb_truetype.h +5079 -0
  75. package/templates/xbox/third_party/stb_vorbis.c +5584 -0
@@ -6,23 +6,45 @@
6
6
  #include <windowsx.h>
7
7
  #include <XGameRuntime.h>
8
8
 
9
+ // winmm (timeBeginPeriod) と DbgHelp (StackWalk64) はコンソール (Game Core OS) に
10
+ // 存在しないデスクトップ専用 API。コンソールは全画面 VSync 固定でペーシング不要、
11
+ // クラッシュ解析はプラットフォーム側の仕組みに任せる。
12
+ #if !NEXT2D_XBOX_CONSOLE
13
+ #include <timeapi.h>
14
+ #include <DbgHelp.h>
15
+ #pragma comment(lib, "winmm.lib")
16
+ #endif
17
+
9
18
  #include "HostContext.h"
10
19
  #include "AssetLoader.h"
20
+ #include "EmbeddedAssets.h"
11
21
  #include "EventLoop.h"
12
22
  #include "gpu/DawnContext.h"
23
+ #include "platform/DecodeQueue.h"
13
24
  #include "platform/GamepadManager.h"
14
25
  #include "platform/AudioEngine.h"
26
+ #include "platform/StbTextRasterizer.h"
15
27
  #include "worker/WorkerRuntime.h"
16
28
  #include "bindings/Bindings.h"
17
29
  #include "bindings/EventTarget.h"
18
30
  #include "v8/V8Runtime.h"
19
31
 
32
+ #include "v8/V8Util.h"
33
+
34
+ #include <v8-profiler.h>
35
+
36
+ #include <algorithm>
20
37
  #include <filesystem>
21
38
  #include <fstream>
22
39
  #include <functional>
23
40
  #include <iostream>
41
+ #include <map>
24
42
  #include <sstream>
25
43
  #include <string>
44
+ #include <utility>
45
+ #include <vector>
46
+ #include <cstdio>
47
+ #include <cstring>
26
48
 
27
49
  namespace fs = std::filesystem;
28
50
  using namespace next2d;
@@ -30,6 +52,137 @@ using namespace next2d;
30
52
  namespace {
31
53
 
32
54
  bool g_running = true;
55
+
56
+ // --- --perf: フレーム統計 ---------------------------------------------------
57
+ // メインループの各区間の所要時間 (ms) を集計し、300 フレーム毎に 1 行で出力する。
58
+ // avg でボトルネックの区間を、max でヒッチ (スパイク) の在り処を特定する。
59
+ // GUI 実行では stderr が見えないため next2d-perf.log にも追記する。
60
+ struct PerfSection {
61
+ double sum = 0.0;
62
+ double max = 0.0;
63
+ void Add(double ms)
64
+ {
65
+ sum += ms;
66
+ if (ms > max) {
67
+ max = ms;
68
+ }
69
+ }
70
+ };
71
+
72
+ struct PerfStats {
73
+ static constexpr int kWindow = 300; // 60fps で約 5 秒
74
+ PerfSection tasks; // 入力/音声/タイマー/マイクロタスク
75
+ PerfSection js; // メイン rAF (アプリロジック/描画コマンド生成)
76
+ PerfSection worker; // レンダラ worker (WebGPU コマンド発行 + submit)
77
+ PerfSection tick; // V8 プラットフォームタスク + Dawn Tick
78
+ PerfSection present; // Present (GPU/VSync 待ちを含む)
79
+ PerfSection busy; // 上記合計 (ペーシング Sleep を除く実働)
80
+ PerfSection frame; // フレーム間隔 (実効フレームレートの逆数)
81
+ int frames = 0;
82
+
83
+ void Flush()
84
+ {
85
+ if (frames == 0) {
86
+ return;
87
+ }
88
+ const auto avg = [this](const PerfSection& s) { return s.sum / frames; };
89
+ char line[320];
90
+ std::snprintf(line, sizeof(line),
91
+ "[perf] frame %.1fms (max %.1f) busy %.1fms | "
92
+ "tasks %.2f/%.1f js %.2f/%.1f worker %.2f/%.1f "
93
+ "tick %.2f/%.1f present %.2f/%.1f (avg/max, %d frames)",
94
+ avg(frame), frame.max, avg(busy),
95
+ avg(tasks), tasks.max, avg(js), js.max, avg(worker), worker.max,
96
+ avg(tick), tick.max, avg(present), present.max, frames);
97
+ std::cerr << line << std::endl;
98
+ std::ofstream ofs("next2d-perf.log", std::ios::app);
99
+ if (ofs) {
100
+ ofs << line << std::endl;
101
+ }
102
+ *this = PerfStats{};
103
+ }
104
+ };
105
+
106
+ // --profile: CPU プロファイル結果を self サンプル数の降順で next2d-cpuprofile.log へ。
107
+ // ノードツリーを再帰集計し「関数 (script:line)」毎の self 時間を出す。
108
+ constexpr unsigned kProfileIntervalUs = 500;
109
+
110
+ void DumpCpuProfileNode(const v8::CpuProfileNode* node,
111
+ std::map<std::string, unsigned>& self_hits)
112
+ {
113
+ const char* name = node->GetFunctionNameStr();
114
+ const char* script = node->GetScriptResourceNameStr();
115
+ std::string key = (name && *name) ? name : "(anonymous)";
116
+ if (script && *script) {
117
+ // pak 内キーの末尾だけで十分読める
118
+ std::string s(script);
119
+ const auto slash = s.find_last_of('/');
120
+ if (slash != std::string::npos) {
121
+ s = s.substr(slash + 1);
122
+ }
123
+ key += " (" + s + ":" + std::to_string(node->GetLineNumber()) + ")";
124
+ }
125
+ self_hits[key] += node->GetHitCount();
126
+ for (int i = 0; i < node->GetChildrenCount(); ++i) {
127
+ DumpCpuProfileNode(node->GetChild(i), self_hits);
128
+ }
129
+ }
130
+
131
+ void WriteCpuProfile(v8::CpuProfile* profile, unsigned interval_us, const char* reason)
132
+ {
133
+ std::map<std::string, unsigned> self_hits;
134
+ DumpCpuProfileNode(profile->GetTopDownRoot(), self_hits);
135
+
136
+ unsigned total = 0;
137
+ for (const auto& [key, hits] : self_hits) {
138
+ total += hits;
139
+ }
140
+ std::vector<std::pair<std::string, unsigned>> sorted(self_hits.begin(), self_hits.end());
141
+ std::sort(sorted.begin(), sorted.end(),
142
+ [](const auto& a, const auto& b) { return a.second > b.second; });
143
+
144
+ std::ofstream ofs("next2d-cpuprofile.log", std::ios::app);
145
+ const double dur_ms =
146
+ static_cast<double>(profile->GetEndTime() - profile->GetStartTime()) / 1000.0;
147
+ char head[224];
148
+ std::snprintf(head, sizeof(head),
149
+ "[cpuprofile] %s: window %.1fs, %u samples (interval %uus), top self-time:",
150
+ reason, dur_ms / 1000.0, total, interval_us);
151
+ std::cerr << head << std::endl;
152
+ if (ofs) {
153
+ ofs << head << std::endl;
154
+ }
155
+ int rank = 0;
156
+ for (const auto& [key, hits] : sorted) {
157
+ if (hits == 0 || ++rank > 50) {
158
+ break;
159
+ }
160
+ char line[512];
161
+ std::snprintf(line, sizeof(line), " %5.1f%% %8.1fms %s",
162
+ total ? 100.0 * hits / total : 0.0,
163
+ hits * (interval_us / 1000.0), key.c_str());
164
+ std::cerr << line << std::endl;
165
+ if (ofs) {
166
+ ofs << line << std::endl;
167
+ }
168
+ }
169
+ }
170
+
171
+ // 現在のプロファイル窓を書き出して新しい窓を開始する。
172
+ // 遷移スパイク (単発の長い rAF) 直後に呼ぶことで、スパイクを含む窓と含まない窓の
173
+ // 比較から犯人関数を分離する (セッション全体の集計では定常コストに埋もれるため)。
174
+ void DumpAndRestartProfile(v8::CpuProfiler* profiler, v8::Isolate* isolate,
175
+ const char* reason)
176
+ {
177
+ v8::HandleScope hs(isolate);
178
+ v8::CpuProfile* p = profiler->StopProfiling(
179
+ v8::String::NewFromUtf8Literal(isolate, "next2d"));
180
+ if (p) {
181
+ WriteCpuProfile(p, kProfileIntervalUs, reason);
182
+ p->Delete();
183
+ }
184
+ profiler->StartProfiling(v8::String::NewFromUtf8Literal(isolate, "next2d"));
185
+ }
33
186
  DawnContext* g_dawn = nullptr;
34
187
  HostContext* g_host = nullptr;
35
188
  V8Runtime* g_runtime = nullptr;
@@ -190,6 +343,21 @@ HWND CreateHostWindow(HINSTANCE instance, int width, int height)
190
343
  wc.lpszClassName = L"Next2DXboxHostWindow";
191
344
  RegisterClassExW(&wc);
192
345
 
346
+ // ウィンドウが画面(作業領域)より大きいと画面外にはみ出し、提示が
347
+ // 見切れる/非等倍でスケールされて歪む (CI の低解像度ディスプレイで
348
+ // 1920x1080 ウィンドウ → 描画が縦長になっていた)。作業領域に収まるよう
349
+ // クランプする。コンソールは常に全画面のためクランプ不要
350
+ // (SystemParametersInfo は Game Core に無い)。
351
+ #if !NEXT2D_XBOX_CONSOLE
352
+ RECT wa = {};
353
+ if (SystemParametersInfoW(SPI_GETWORKAREA, 0, &wa, 0)) {
354
+ const int maxW = wa.right - wa.left;
355
+ const int maxH = wa.bottom - wa.top;
356
+ if (maxW > 0 && width > maxW) { width = maxW; }
357
+ if (maxH > 0 && height > maxH) { height = maxH; }
358
+ }
359
+ #endif
360
+
193
361
  // コンソールでは全画面相当。PC(GDK) 検証ではウィンドウ。
194
362
  const DWORD style = WS_OVERLAPPEDWINDOW | WS_VISIBLE;
195
363
  return CreateWindowExW(
@@ -200,16 +368,133 @@ HWND CreateHostWindow(HINSTANCE instance, int width, int height)
200
368
 
201
369
  } // namespace
202
370
 
371
+ // 未処理例外(segfault/AV 等)発生時にコールスタックを next2d-error.log へ書き出す。
372
+ // GUI 実行では stderr が見えないため、C++ クラッシュ地点を掴む唯一の手段。
373
+ // DbgHelp の StackWalk64 で例外コンテキストからフレームを辿り、可能なら
374
+ // シンボル名 (関数+行) とモジュールを添える (PDB があれば関数/行まで解決)。
375
+ // コンソールには DbgHelp が無いためデスクトップ専用 (クラッシュ解析は実機ツール側)。
376
+ #if !NEXT2D_XBOX_CONSOLE
377
+ static LONG WINAPI CrashHandler(EXCEPTION_POINTERS* ep)
378
+ {
379
+ std::ofstream ofs("next2d-error.log", std::ios::app);
380
+ if (!ofs || !ep || !ep->ExceptionRecord || !ep->ContextRecord) {
381
+ return EXCEPTION_EXECUTE_HANDLER;
382
+ }
383
+
384
+ ofs << "\n[CRASH] code=0x" << std::hex
385
+ << ep->ExceptionRecord->ExceptionCode
386
+ << " addr=" << ep->ExceptionRecord->ExceptionAddress
387
+ << std::dec << std::endl;
388
+
389
+ HANDLE process = GetCurrentProcess();
390
+ HANDLE thread = GetCurrentThread();
391
+ SymSetOptions(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS | SYMOPT_LOAD_LINES);
392
+ SymInitialize(process, nullptr, TRUE);
393
+
394
+ CONTEXT ctx = *ep->ContextRecord;
395
+ STACKFRAME64 frame = {};
396
+ DWORD machine = 0;
397
+ #if defined(_M_X64)
398
+ machine = IMAGE_FILE_MACHINE_AMD64;
399
+ frame.AddrPC.Offset = ctx.Rip;
400
+ frame.AddrFrame.Offset = ctx.Rbp;
401
+ frame.AddrStack.Offset = ctx.Rsp;
402
+ #elif defined(_M_ARM64)
403
+ machine = IMAGE_FILE_MACHINE_ARM64;
404
+ frame.AddrPC.Offset = ctx.Pc;
405
+ frame.AddrFrame.Offset = ctx.Fp;
406
+ frame.AddrStack.Offset = ctx.Sp;
407
+ #endif
408
+ frame.AddrPC.Mode = AddrModeFlat;
409
+ frame.AddrFrame.Mode = AddrModeFlat;
410
+ frame.AddrStack.Mode = AddrModeFlat;
411
+
412
+ char symbuf[sizeof(SYMBOL_INFO) + 256] = {};
413
+ SYMBOL_INFO* sym = reinterpret_cast<SYMBOL_INFO*>(symbuf);
414
+ sym->SizeOfStruct = sizeof(SYMBOL_INFO);
415
+ sym->MaxNameLen = 255;
416
+
417
+ for (int i = 0; i < 48; ++i) {
418
+ if (!StackWalk64(machine, process, thread, &frame, &ctx,
419
+ nullptr, SymFunctionTableAccess64, SymGetModuleBase64, nullptr)) {
420
+ break;
421
+ }
422
+ const DWORD64 pc = frame.AddrPC.Offset;
423
+ if (!pc) {
424
+ break;
425
+ }
426
+
427
+ ofs << " #" << i << " 0x" << std::hex << pc << std::dec;
428
+
429
+ DWORD64 disp = 0;
430
+ if (SymFromAddr(process, pc, &disp, sym)) {
431
+ ofs << " " << sym->Name << "+0x" << std::hex << disp << std::dec;
432
+ }
433
+
434
+ const DWORD64 modbase = SymGetModuleBase64(process, pc);
435
+ if (modbase) {
436
+ char modname[MAX_PATH] = {};
437
+ if (GetModuleFileNameA(reinterpret_cast<HMODULE>(modbase), modname, MAX_PATH)) {
438
+ const char* base = std::strrchr(modname, '\\');
439
+ ofs << " [" << (base ? base + 1 : modname) << "]";
440
+ }
441
+ }
442
+
443
+ IMAGEHLP_LINE64 line = {};
444
+ line.SizeOfStruct = sizeof(IMAGEHLP_LINE64);
445
+ DWORD line_disp = 0;
446
+ if (SymGetLineFromAddr64(process, pc, &line_disp, &line) && line.FileName) {
447
+ const char* fbase = std::strrchr(line.FileName, '\\');
448
+ ofs << " (" << (fbase ? fbase + 1 : line.FileName) << ":" << line.LineNumber << ")";
449
+ }
450
+ ofs << std::endl;
451
+ }
452
+
453
+ ofs.flush();
454
+ SymCleanup(process);
455
+ return EXCEPTION_EXECUTE_HANDLER; // プロセスを終了させる (無限再入回避)
456
+ }
457
+ #endif // !NEXT2D_XBOX_CONSOLE
458
+
203
459
  int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR cmd_line, int)
204
460
  {
461
+ #if !NEXT2D_XBOX_CONSOLE
462
+ // 最初にクラッシュハンドラを設置し、以降の segfault 等でスタックを記録する。
463
+ SetUnhandledExceptionFilter(CrashHandler);
464
+
465
+ // Sleep() の分解能を 1ms に上げる (フレームペーシング用。既定 15.6ms では
466
+ // 60Hz を刻めない)。コンソールは全画面 VSync 提示でペーシングされるため不要。
467
+ timeBeginPeriod(1);
468
+ #endif
469
+
205
470
  // --selftest: アプリの代わりに js/selftest.js を実行し、全バインディングを
206
471
  // 実機/PC(GDK) 上で検証して終了する (テスト完了で自動終了)。
207
472
  const bool selftest = cmd_line && wcsstr(cmd_line, L"--selftest") != nullptr;
208
473
 
209
- // 1. GDK ランタイム初期化
210
- if (FAILED(XGameRuntimeInitialize())) {
474
+ // --perf: フレーム統計を 300 フレーム (約 5 秒) 毎に stderr と next2d-perf.log
475
+ // へ出力する。どの区間 (JS / レンダラ worker / GPU / Present) にフレーム時間を
476
+ // 使っているかを数値化し、以降の最適化 (worker 並列化等) の要否判断に使う。
477
+ const bool perf = cmd_line && wcsstr(cmd_line, L"--perf") != nullptr;
478
+
479
+ // --profile: V8 の sampling CPU プロファイラを起動から終了まで回し、
480
+ // 終了時に self 時間の上位関数を next2d-cpuprofile.log へ出力する。
481
+ // jitless でも動作し、GC は "(garbage collector)" として現れる。
482
+ // --perf で見つかった js 区間の遷移スパイク (~1.2s) の中身を特定する用途。
483
+ const bool profile = cmd_line && wcsstr(cmd_line, L"--profile") != nullptr;
484
+
485
+ // 1. GDK ランタイム初期化。
486
+ // デスクトップでは Gaming Services 未導入の環境 (CI ランナー等) でも
487
+ // 起動できるよう、失敗を警告に留めて続行する (本ホストは XUser 等を未使用)。
488
+ // コンソールでは必須のため失敗したら終了する。
489
+ bool xgame_initialized = SUCCEEDED(XGameRuntimeInitialize());
490
+ if (!xgame_initialized) {
491
+ #if NEXT2D_XBOX_CONSOLE
211
492
  std::cerr << "XGameRuntimeInitialize failed" << std::endl;
212
493
  return 1;
494
+ #else
495
+ std::cerr << "warning: XGameRuntimeInitialize failed (Gaming Services not installed?)"
496
+ << " - continuing on desktop" << std::endl;
497
+ #endif
213
498
  }
214
499
 
215
500
  const fs::path exe_dir = ExeDir();
@@ -217,6 +502,51 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR cmd_line, int)
217
502
  const fs::path bootstrap_js = exe_dir / "js" / "bootstrap.js";
218
503
  const fs::path selftest_js = exe_dir / "js" / "selftest.js";
219
504
 
505
+ // 埋め込みアセット (exe 内 RCDATA "N2DASSETS") を初期化する。存在すれば
506
+ // assets/app 一式と js/bootstrap.js を exe 内から読み、隣接平文 JS は不要になる。
507
+ // 無ければ全読み込みはファイルへフォールバックする (開発ビルド)。
508
+ if (InitEmbeddedAssets()) {
509
+ SetEmbeddedAssetsRoot(assets_app.string());
510
+ std::cerr << "[Assets] using embedded pak" << std::endl;
511
+ }
512
+
513
+ // フォント登録 (stb_truetype 用)。コンソールにはシステムフォント/DirectWrite が
514
+ // 無いため、資材内の TTF/OTF を stbtext レジストリへ登録しておく
515
+ // (デスクトップは DirectWrite が主経路のため未使用でも無害)。
516
+ // 対象: 埋め込み pak 内の *.ttf/*.otf + exe 隣接 fonts/ (開発ビルド用)。
517
+ {
518
+ const auto register_font = [](const std::string& name,
519
+ std::vector<uint8_t> bytes) {
520
+ if (stbtext::RegisterFont(name, std::move(bytes))) {
521
+ std::cerr << "[Font] registered: " << name << std::endl;
522
+ }
523
+ };
524
+ const auto is_font_key = [](const std::string& key) {
525
+ const auto dot = key.find_last_of('.');
526
+ if (dot == std::string::npos) return false;
527
+ const std::string ext = key.substr(dot + 1);
528
+ return ext == "ttf" || ext == "otf" || ext == "TTF" || ext == "OTF";
529
+ };
530
+ ForEachEmbeddedAsset(
531
+ [&](const std::string& key, const std::vector<uint8_t>& data) {
532
+ if (is_font_key(key)) {
533
+ register_font(fs::path(key).stem().string(), data);
534
+ }
535
+ });
536
+ std::error_code fonts_ec;
537
+ for (const auto& entry :
538
+ fs::directory_iterator(exe_dir / "fonts", fonts_ec)) {
539
+ const std::string p = entry.path().string();
540
+ if (is_font_key(p)) {
541
+ std::ifstream ifs(entry.path(), std::ios::binary);
542
+ std::vector<uint8_t> bytes(
543
+ (std::istreambuf_iterator<char>(ifs)),
544
+ std::istreambuf_iterator<char>());
545
+ register_font(entry.path().stem().string(), std::move(bytes));
546
+ }
547
+ }
548
+ }
549
+
220
550
  // 2. HostContext と論理ビューポート
221
551
  HostContext host;
222
552
  host.viewport_width = 1920;
@@ -230,6 +560,22 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR cmd_line, int)
230
560
  return 1;
231
561
  }
232
562
 
563
+ // ウィンドウは要求サイズより小さく作られ得る (作業領域クランプ / DPI /
564
+ // CI の低解像度ディスプレイ)。surface・canvas・screen は全て viewport_* から
565
+ // 派生するため、ここで「実クライアント矩形」を採用して一致させる。これを
566
+ // 怠ると surface(1920x1080) を実ウィンドウ(例 1024x768)へ非等倍提示して
567
+ // 画面が縦長に歪む。V8 バインディング設置(screen/innerWidth/dpr)と
568
+ // Dawn 初期化の前に確定させる必要がある。
569
+ {
570
+ RECT rc = {};
571
+ if (GetClientRect(hwnd, &rc) && rc.right > 0 && rc.bottom > 0) {
572
+ host.viewport_width = rc.right;
573
+ host.viewport_height = rc.bottom;
574
+ }
575
+ std::cerr << "[Win] client rect: " << host.viewport_width
576
+ << "x" << host.viewport_height << std::endl;
577
+ }
578
+
233
579
  // 4. V8 プロセス初期化 + Isolate/Context + バインディング
234
580
  V8Runtime::InitializeProcess(nullptr);
235
581
  V8Runtime runtime;
@@ -255,7 +601,11 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR cmd_line, int)
255
601
  host.workers = &workers;
256
602
  g_dawn = &dawn;
257
603
 
258
- dawn.Initialize(hwnd, host.viewport_width, host.viewport_height);
604
+ // GPU 無し環境 (CI 等) では Dawn 初期化に失敗し得るが、CPU 側の機能と
605
+ // selftest は動かせるため続行する (Present 等は内部でゲートされる)
606
+ if (!dawn.Initialize(hwnd, host.viewport_width, host.viewport_height)) {
607
+ std::cerr << "warning: Dawn initialization failed - continuing without GPU" << std::endl;
608
+ }
259
609
  gamepad.Initialize();
260
610
  audio.Initialize();
261
611
 
@@ -270,9 +620,29 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR cmd_line, int)
270
620
  InstallWorker(runtime.isolate(), ctx->Global(), &host);
271
621
  }
272
622
 
623
+ // --profile: 起動直後からサンプリング開始 (0.5ms 間隔)
624
+ v8::CpuProfiler* cpu_profiler = nullptr;
625
+ if (profile) {
626
+ v8::Isolate::Scope iso_scope(runtime.isolate());
627
+ v8::HandleScope hs(runtime.isolate());
628
+ cpu_profiler = v8::CpuProfiler::New(runtime.isolate());
629
+ cpu_profiler->SetSamplingInterval(kProfileIntervalUs);
630
+ cpu_profiler->StartProfiling(
631
+ v8::String::NewFromUtf8Literal(runtime.isolate(), "next2d"));
632
+ std::cerr << "[cpuprofile] profiling started" << std::endl;
633
+ }
634
+
273
635
  // 6. bootstrap.js -> アプリ本体(ESM) の順で読み込み
274
636
  {
275
- const std::string boot = ReadFile(bootstrap_js);
637
+ // 埋め込み pak を優先し、無ければ隣接ファイルから読む host スクリプトローダ。
638
+ const auto load_host_script = [&](const std::string& key, const fs::path& file) {
639
+ if (const auto* embedded = GetEmbeddedAsset(key)) {
640
+ return std::string(embedded->begin(), embedded->end());
641
+ }
642
+ return ReadFile(file);
643
+ };
644
+
645
+ const std::string boot = load_host_script("js/bootstrap.js", bootstrap_js);
276
646
  if (boot.empty()) {
277
647
  std::cerr << "bootstrap.js not found: " << bootstrap_js << std::endl;
278
648
  } else if (!runtime.RunScript(boot, "js/bootstrap.js")) {
@@ -280,7 +650,7 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR cmd_line, int)
280
650
  }
281
651
 
282
652
  if (selftest) {
283
- const std::string script = ReadFile(selftest_js);
653
+ const std::string script = load_host_script("js/selftest.js", selftest_js);
284
654
  if (script.empty()) {
285
655
  std::cerr << "selftest.js not found: " << selftest_js << std::endl;
286
656
  return 1;
@@ -300,9 +670,14 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR cmd_line, int)
300
670
  }
301
671
 
302
672
  // 7. ゲームループ
303
- v8::Isolate::Scope isolate_scope(runtime.isolate());
304
673
  MSG msg = {};
305
674
  int exit_code = 0;
675
+ PerfStats perf_stats;
676
+ double perf_prev_frame = 0.0;
677
+ // Isolate::Scope はループ内に限定する。Enter されたままの isolate を
678
+ // Dispose すると V8 の fatal (Disposing the isolate that is entered) になる。
679
+ {
680
+ v8::Isolate::Scope isolate_scope(runtime.isolate());
306
681
  while (g_running) {
307
682
  // Win32 メッセージ
308
683
  while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
@@ -318,6 +693,17 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR cmd_line, int)
318
693
 
319
694
  const double now = event_loop.Now();
320
695
 
696
+ // --perf: 直前の計測点からの経過を各区間へ記録する軽量マーカー
697
+ double perf_cursor = now;
698
+ const auto perf_mark = [&](PerfSection& section) {
699
+ if (!perf) {
700
+ return;
701
+ }
702
+ const double t = event_loop.Now();
703
+ section.Add(t - perf_cursor);
704
+ perf_cursor = t;
705
+ };
706
+
321
707
  v8::HandleScope handle_scope(runtime.isolate());
322
708
  v8::Local<v8::Context> ctx = runtime.context();
323
709
  v8::Context::Scope context_scope(ctx);
@@ -328,24 +714,64 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR cmd_line, int)
328
714
  // 音声: 再生終了した source へ "ended" を配送する
329
715
  PumpAudioEvents(runtime.isolate());
330
716
 
717
+ // バックグラウンドデコード完了の反映 (画像 load イベント / 音声 Promise)
718
+ decodequeue::Pump();
719
+
331
720
  // タイマー -> マイクロタスク
332
721
  event_loop.PumpTimers();
333
722
  runtime.PumpMicrotasks();
723
+ perf_mark(perf_stats.tasks);
334
724
 
335
725
  // メインスレッドの requestAnimationFrame (アプリのロジック/描画コマンド生成)
726
+ const double profile_js_start = cpu_profiler ? event_loop.Now() : 0.0;
336
727
  event_loop.RunAnimationFrame(now);
337
728
  runtime.PumpMicrotasks();
729
+ perf_mark(perf_stats.js);
730
+
731
+ // --profile: 遷移スパイク (単発の長い rAF) を検出したら窓を切り出す
732
+ if (cpu_profiler) {
733
+ const double js_ms = event_loop.Now() - profile_js_start;
734
+ if (js_ms > 300.0) {
735
+ char reason[64];
736
+ std::snprintf(reason, sizeof(reason), "spike (js %.0fms)", js_ms);
737
+ DumpAndRestartProfile(cpu_profiler, runtime.isolate(), reason);
738
+ }
739
+ }
338
740
 
339
741
  // Worker (レンダラ等) のメッセージ配送 + rAF。
340
742
  // レンダラ worker はここで WebGPU コマンドを発行し submit する。
341
743
  workers.Pump(now);
744
+ perf_mark(perf_stats.worker);
342
745
 
343
746
  // V8 プラットフォームタスク + Dawn の非同期処理
344
747
  runtime.PumpPlatformTasks();
345
748
  dawn.Tick();
749
+ perf_mark(perf_stats.tick);
346
750
 
347
751
  // 提示
348
752
  dawn.Present();
753
+ perf_mark(perf_stats.present);
754
+
755
+ if (perf) {
756
+ perf_stats.busy.Add(perf_cursor - now);
757
+ if (perf_prev_frame > 0.0) {
758
+ perf_stats.frame.Add(now - perf_prev_frame);
759
+ }
760
+ perf_prev_frame = now;
761
+ if (++perf_stats.frames >= PerfStats::kWindow) {
762
+ perf_stats.Flush();
763
+ }
764
+ }
765
+
766
+ // フレームペーシング: ブラウザの rAF 同様 ~60Hz に揃える。
767
+ // Present はスキップ時にブロックしないため、これが無いとループが
768
+ // 数万回転/秒で空回りし、rAF 前提のゲーム/Tween のタイミングが崩れる。
769
+ {
770
+ const double frame_ms = event_loop.Now() - now;
771
+ if (frame_ms < 15.0) {
772
+ Sleep(static_cast<DWORD>(15.0 - frame_ms));
773
+ }
774
+ }
349
775
 
350
776
  // selftest 完了検知: selftest.js が globalThis.__selftestExitCode を設定したら終了
351
777
  if (selftest) {
@@ -359,10 +785,35 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR cmd_line, int)
359
785
  }
360
786
  }
361
787
 
362
- // 8. 後始末
788
+ }
789
+
790
+ // --profile: 終了時に集計して出力する (Isolate 破棄前に必ず行う)
791
+ if (cpu_profiler) {
792
+ v8::Isolate::Scope iso_scope(runtime.isolate());
793
+ v8::HandleScope hs(runtime.isolate());
794
+ v8::CpuProfile* p = cpu_profiler->StopProfiling(
795
+ v8::String::NewFromUtf8Literal(runtime.isolate(), "next2d"));
796
+ if (p) {
797
+ WriteCpuProfile(p, kProfileIntervalUs, "final");
798
+ p->Delete();
799
+ }
800
+ cpu_profiler->Dispose();
801
+ cpu_profiler = nullptr;
802
+ }
803
+
804
+ // 8. 後始末: v8::Global を保持するものは Isolate 破棄前に必ず明示解放する。
805
+ // (スタック変数はスコープ終了 = runtime.Dispose() の後に巻き戻されるため、
806
+ // デストラクタ任せにすると破棄済み Isolate への Global::Reset で fail-fast する)
807
+ decodequeue::Shutdown(); // デコードスレッド join + 保留 complete (v8::Global) の破棄
808
+ workers.Shutdown(); // WorkerInstance の Global<Context> / EventLoop
809
+ event_loop.Shutdown(); // main の setTimeout/rAF コールバック (Global<Function>)
810
+ ShutdownAudioEvents();
811
+ ShutdownWebGPU();
363
812
  host.main_canvas.Reset();
364
813
  runtime.Dispose();
365
814
  V8Runtime::ShutdownProcess();
366
- XGameRuntimeUninitialize();
815
+ if (xgame_initialized) {
816
+ XGameRuntimeUninitialize();
817
+ }
367
818
  return exit_code;
368
819
  }