@next2d/builder 0.1.8 → 0.1.11

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 (67) 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 -790
  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 +11 -9
  19. package/templates/xbox/CMakeLists.txt +47 -11
  20. package/templates/xbox/README.md +18 -0
  21. package/templates/xbox/js/bootstrap.js +120 -0
  22. package/templates/xbox/src/AssetLoader.cpp +115 -12
  23. package/templates/xbox/src/AssetLoader.h +4 -0
  24. package/templates/xbox/src/EmbeddedAssets.cpp +149 -0
  25. package/templates/xbox/src/EmbeddedAssets.h +60 -0
  26. package/templates/xbox/src/EventLoop.cpp +0 -15
  27. package/templates/xbox/src/bindings/Audio.cpp +118 -22
  28. package/templates/xbox/src/bindings/Canvas.cpp +32 -2
  29. package/templates/xbox/src/bindings/Canvas2D.cpp +495 -39
  30. package/templates/xbox/src/bindings/Console.cpp +6 -1
  31. package/templates/xbox/src/bindings/DomShims.cpp +18 -2
  32. package/templates/xbox/src/bindings/EventTarget.cpp +9 -0
  33. package/templates/xbox/src/bindings/Image.cpp +83 -57
  34. package/templates/xbox/src/bindings/Polyfills.cpp +671 -236
  35. package/templates/xbox/src/bindings/RasterCore.h +105 -1
  36. package/templates/xbox/src/bindings/Video.cpp +57 -3
  37. package/templates/xbox/src/bindings/webgpu/WebGPU.cpp +29 -3
  38. package/templates/xbox/src/bindings/webgpu/WebGPUCommon.h +4 -1
  39. package/templates/xbox/src/bindings/webgpu/WebGPUEnums.h +32 -0
  40. package/templates/xbox/src/gpu/DawnContext.cpp +167 -16
  41. package/templates/xbox/src/main.cpp +385 -16
  42. package/templates/xbox/src/platform/AudioDecoder.cpp +121 -0
  43. package/templates/xbox/src/platform/AudioDecoder.h +18 -0
  44. package/templates/xbox/src/platform/AudioEngine.cpp +28 -3
  45. package/templates/xbox/src/platform/DecodeQueue.cpp +136 -0
  46. package/templates/xbox/src/platform/DecodeQueue.h +27 -0
  47. package/templates/xbox/src/platform/ImageDecoder.cpp +44 -0
  48. package/templates/xbox/src/platform/ImageDecoder.h +18 -0
  49. package/templates/xbox/src/platform/StbTextRasterizer.cpp +324 -0
  50. package/templates/xbox/src/platform/StbTextRasterizer.h +37 -0
  51. package/templates/xbox/src/platform/TextRasterizer.cpp +32 -0
  52. package/templates/xbox/src/platform/WicDecoder.cpp +19 -0
  53. package/templates/xbox/src/v8/CodeCache.h +223 -0
  54. package/templates/xbox/src/v8/V8Runtime.cpp +34 -33
  55. package/templates/xbox/src/v8/V8Util.h +54 -4
  56. package/templates/xbox/src/worker/WorkerRuntime.cpp +165 -65
  57. package/templates/xbox/src/worker/WorkerRuntime.h +23 -3
  58. package/templates/xbox/tests/check_local.sh +5 -1
  59. package/templates/xbox/tests/gamecore_api_probe.cpp +172 -0
  60. package/templates/xbox/tests/raster_test.cpp +50 -0
  61. package/templates/xbox/tests/windows_platform_test.cpp +75 -3
  62. package/templates/xbox/third_party/LICENSES.md +14 -0
  63. package/templates/xbox/third_party/dr_mp3.h +5412 -0
  64. package/templates/xbox/third_party/dr_wav.h +9131 -0
  65. package/templates/xbox/third_party/stb_image.h +7988 -0
  66. package/templates/xbox/third_party/stb_truetype.h +5079 -0
  67. package/templates/xbox/third_party/stb_vorbis.c +5584 -0
@@ -4,28 +4,47 @@
4
4
  // bootstrap.js とアプリ本体(ESM)の読み込み -> ゲームループ、という流れ。
5
5
  #include <Windows.h>
6
6
  #include <windowsx.h>
7
- #include <timeapi.h>
8
7
  #include <XGameRuntime.h>
9
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>
10
15
  #pragma comment(lib, "winmm.lib")
16
+ #endif
11
17
 
12
18
  #include "HostContext.h"
13
19
  #include "AssetLoader.h"
20
+ #include "EmbeddedAssets.h"
14
21
  #include "EventLoop.h"
15
22
  #include "gpu/DawnContext.h"
23
+ #include "platform/DecodeQueue.h"
16
24
  #include "platform/GamepadManager.h"
17
25
  #include "platform/AudioEngine.h"
26
+ #include "platform/StbTextRasterizer.h"
18
27
  #include "worker/WorkerRuntime.h"
19
28
  #include "bindings/Bindings.h"
20
29
  #include "bindings/EventTarget.h"
21
30
  #include "v8/V8Runtime.h"
22
31
 
32
+ #include "v8/V8Util.h"
33
+
34
+ #include <v8-profiler.h>
35
+
36
+ #include <algorithm>
23
37
  #include <filesystem>
24
38
  #include <fstream>
25
39
  #include <functional>
26
40
  #include <iostream>
41
+ #include <map>
27
42
  #include <sstream>
28
43
  #include <string>
44
+ #include <utility>
45
+ #include <vector>
46
+ #include <cstdio>
47
+ #include <cstring>
29
48
 
30
49
  namespace fs = std::filesystem;
31
50
  using namespace next2d;
@@ -33,6 +52,137 @@ using namespace next2d;
33
52
  namespace {
34
53
 
35
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
+ }
36
186
  DawnContext* g_dawn = nullptr;
37
187
  HostContext* g_host = nullptr;
38
188
  V8Runtime* g_runtime = nullptr;
@@ -196,7 +346,9 @@ HWND CreateHostWindow(HINSTANCE instance, int width, int height)
196
346
  // ウィンドウが画面(作業領域)より大きいと画面外にはみ出し、提示が
197
347
  // 見切れる/非等倍でスケールされて歪む (CI の低解像度ディスプレイで
198
348
  // 1920x1080 ウィンドウ → 描画が縦長になっていた)。作業領域に収まるよう
199
- // クランプする。実機コンソールでは作業領域=全画面のため 1920x1080 のまま。
349
+ // クランプする。コンソールは常に全画面のためクランプ不要
350
+ // (SystemParametersInfo は Game Core に無い)。
351
+ #if !NEXT2D_XBOX_CONSOLE
200
352
  RECT wa = {};
201
353
  if (SystemParametersInfoW(SPI_GETWORKAREA, 0, &wa, 0)) {
202
354
  const int maxW = wa.right - wa.left;
@@ -204,6 +356,7 @@ HWND CreateHostWindow(HINSTANCE instance, int width, int height)
204
356
  if (maxW > 0 && width > maxW) { width = maxW; }
205
357
  if (maxH > 0 && height > maxH) { height = maxH; }
206
358
  }
359
+ #endif
207
360
 
208
361
  // コンソールでは全画面相当。PC(GDK) 検証ではウィンドウ。
209
362
  const DWORD style = WS_OVERLAPPEDWINDOW | WS_VISIBLE;
@@ -215,16 +368,120 @@ HWND CreateHostWindow(HINSTANCE instance, int width, int height)
215
368
 
216
369
  } // namespace
217
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
+
218
459
  int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR cmd_line, int)
219
460
  {
461
+ #if !NEXT2D_XBOX_CONSOLE
462
+ // 最初にクラッシュハンドラを設置し、以降の segfault 等でスタックを記録する。
463
+ SetUnhandledExceptionFilter(CrashHandler);
464
+
220
465
  // Sleep() の分解能を 1ms に上げる (フレームペーシング用。既定 15.6ms では
221
- // 60Hz を刻めない)
466
+ // 60Hz を刻めない)。コンソールは全画面 VSync 提示でペーシングされるため不要。
222
467
  timeBeginPeriod(1);
468
+ #endif
223
469
 
224
470
  // --selftest: アプリの代わりに js/selftest.js を実行し、全バインディングを
225
471
  // 実機/PC(GDK) 上で検証して終了する (テスト完了で自動終了)。
226
472
  const bool selftest = cmd_line && wcsstr(cmd_line, L"--selftest") != nullptr;
227
473
 
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
+
228
485
  // 1. GDK ランタイム初期化。
229
486
  // デスクトップでは Gaming Services 未導入の環境 (CI ランナー等) でも
230
487
  // 起動できるよう、失敗を警告に留めて続行する (本ホストは XUser 等を未使用)。
@@ -245,6 +502,51 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR cmd_line, int)
245
502
  const fs::path bootstrap_js = exe_dir / "js" / "bootstrap.js";
246
503
  const fs::path selftest_js = exe_dir / "js" / "selftest.js";
247
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
+
248
550
  // 2. HostContext と論理ビューポート
249
551
  HostContext host;
250
552
  host.viewport_width = 1920;
@@ -318,9 +620,29 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR cmd_line, int)
318
620
  InstallWorker(runtime.isolate(), ctx->Global(), &host);
319
621
  }
320
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
+
321
635
  // 6. bootstrap.js -> アプリ本体(ESM) の順で読み込み
322
636
  {
323
- 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);
324
646
  if (boot.empty()) {
325
647
  std::cerr << "bootstrap.js not found: " << bootstrap_js << std::endl;
326
648
  } else if (!runtime.RunScript(boot, "js/bootstrap.js")) {
@@ -328,7 +650,7 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR cmd_line, int)
328
650
  }
329
651
 
330
652
  if (selftest) {
331
- const std::string script = ReadFile(selftest_js);
653
+ const std::string script = load_host_script("js/selftest.js", selftest_js);
332
654
  if (script.empty()) {
333
655
  std::cerr << "selftest.js not found: " << selftest_js << std::endl;
334
656
  return 1;
@@ -350,6 +672,8 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR cmd_line, int)
350
672
  // 7. ゲームループ
351
673
  MSG msg = {};
352
674
  int exit_code = 0;
675
+ PerfStats perf_stats;
676
+ double perf_prev_frame = 0.0;
353
677
  // Isolate::Scope はループ内に限定する。Enter されたままの isolate を
354
678
  // Dispose すると V8 の fatal (Disposing the isolate that is entered) になる。
355
679
  {
@@ -369,6 +693,17 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR cmd_line, int)
369
693
 
370
694
  const double now = event_loop.Now();
371
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
+
372
707
  v8::HandleScope handle_scope(runtime.isolate());
373
708
  v8::Local<v8::Context> ctx = runtime.context();
374
709
  v8::Context::Scope context_scope(ctx);
@@ -379,24 +714,54 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR cmd_line, int)
379
714
  // 音声: 再生終了した source へ "ended" を配送する
380
715
  PumpAudioEvents(runtime.isolate());
381
716
 
717
+ // バックグラウンドデコード完了の反映 (画像 load イベント / 音声 Promise)
718
+ decodequeue::Pump();
719
+
382
720
  // タイマー -> マイクロタスク
383
721
  event_loop.PumpTimers();
384
722
  runtime.PumpMicrotasks();
723
+ perf_mark(perf_stats.tasks);
385
724
 
386
725
  // メインスレッドの requestAnimationFrame (アプリのロジック/描画コマンド生成)
726
+ const double profile_js_start = cpu_profiler ? event_loop.Now() : 0.0;
387
727
  event_loop.RunAnimationFrame(now);
388
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
+ }
389
740
 
390
741
  // Worker (レンダラ等) のメッセージ配送 + rAF。
391
742
  // レンダラ worker はここで WebGPU コマンドを発行し submit する。
392
743
  workers.Pump(now);
744
+ perf_mark(perf_stats.worker);
393
745
 
394
746
  // V8 プラットフォームタスク + Dawn の非同期処理
395
747
  runtime.PumpPlatformTasks();
396
748
  dawn.Tick();
749
+ perf_mark(perf_stats.tick);
397
750
 
398
751
  // 提示
399
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
+ }
400
765
 
401
766
  // フレームペーシング: ブラウザの rAF 同様 ~60Hz に揃える。
402
767
  // Present はスキップ時にブロックしないため、これが無いとループが
@@ -408,17 +773,6 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR cmd_line, int)
408
773
  }
409
774
  }
410
775
 
411
- // 診断: ループ統計 (5 秒毎)
412
- {
413
- static uint64_t iteration = 0;
414
- ++iteration;
415
- if (iteration % 300 == 0) {
416
- std::cerr << "[Loop] it=" << iteration
417
- << " rAF=" << event_loop.PendingAnimationFrameCount()
418
- << " timers=" << event_loop.PendingTimerCount() << std::endl;
419
- }
420
- }
421
-
422
776
  // selftest 完了検知: selftest.js が globalThis.__selftestExitCode を設定したら終了
423
777
  if (selftest) {
424
778
  v8::Local<v8::Value> code;
@@ -433,9 +787,24 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR cmd_line, int)
433
787
 
434
788
  }
435
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
+
436
804
  // 8. 後始末: v8::Global を保持するものは Isolate 破棄前に必ず明示解放する。
437
805
  // (スタック変数はスコープ終了 = runtime.Dispose() の後に巻き戻されるため、
438
806
  // デストラクタ任せにすると破棄済み Isolate への Global::Reset で fail-fast する)
807
+ decodequeue::Shutdown(); // デコードスレッド join + 保留 complete (v8::Global) の破棄
439
808
  workers.Shutdown(); // WorkerInstance の Global<Context> / EventLoop
440
809
  event_loop.Shutdown(); // main の setTimeout/rAF コールバック (Global<Function>)
441
810
  ShutdownAudioEvents();
@@ -0,0 +1,121 @@
1
+ #include "AudioDecoder.h"
2
+
3
+ // dr_mp3 / dr_wav / stb_vorbis: public domain のヘッダオンリーデコーダ (third_party/)。
4
+ // メモリ入力のみ使用する。実装は本 TU に集約する (ODR 一意)。
5
+ #define DR_MP3_IMPLEMENTATION
6
+ #define DR_MP3_NO_STDIO
7
+ #include "third_party/dr_mp3.h"
8
+
9
+ #define DR_WAV_IMPLEMENTATION
10
+ #define DR_WAV_NO_STDIO
11
+ #include "third_party/dr_wav.h"
12
+
13
+ // stb_vorbis は .c 配布のためここで取り込む (STB_VORBIS_HEADER_ONLY は使わない)。
14
+ #define STB_VORBIS_NO_STDIO
15
+ #define STB_VORBIS_NO_PUSHDATA_API
16
+ #include "third_party/stb_vorbis.c"
17
+
18
+ #include <cstring>
19
+
20
+ namespace next2d {
21
+
22
+ namespace {
23
+
24
+ bool DecodeMp3(const std::vector<uint8_t>& input, PcmBuffer& out)
25
+ {
26
+ drmp3_config config = {};
27
+ drmp3_uint64 total_frames = 0;
28
+ float* frames = drmp3_open_memory_and_read_pcm_frames_f32(
29
+ input.data(), input.size(), &config, &total_frames, nullptr);
30
+ if (!frames || total_frames == 0 || config.channels == 0) {
31
+ if (frames) {
32
+ drmp3_free(frames, nullptr);
33
+ }
34
+ return false;
35
+ }
36
+ out.channels = config.channels;
37
+ out.sample_rate = config.sampleRate;
38
+ out.samples.assign(frames, frames + total_frames * config.channels);
39
+ drmp3_free(frames, nullptr);
40
+ return true;
41
+ }
42
+
43
+ bool DecodeWav(const std::vector<uint8_t>& input, PcmBuffer& out)
44
+ {
45
+ unsigned int channels = 0, sample_rate = 0;
46
+ drwav_uint64 total_frames = 0;
47
+ float* frames = drwav_open_memory_and_read_pcm_frames_f32(
48
+ input.data(), input.size(), &channels, &sample_rate, &total_frames, nullptr);
49
+ if (!frames || total_frames == 0 || channels == 0) {
50
+ if (frames) {
51
+ drwav_free(frames, nullptr);
52
+ }
53
+ return false;
54
+ }
55
+ out.channels = channels;
56
+ out.sample_rate = sample_rate;
57
+ out.samples.assign(frames, frames + total_frames * channels);
58
+ drwav_free(frames, nullptr);
59
+ return true;
60
+ }
61
+
62
+ bool DecodeOgg(const std::vector<uint8_t>& input, PcmBuffer& out)
63
+ {
64
+ int err = 0;
65
+ stb_vorbis* vorbis = stb_vorbis_open_memory(
66
+ input.data(), static_cast<int>(input.size()), &err, nullptr);
67
+ if (!vorbis) {
68
+ return false;
69
+ }
70
+ const stb_vorbis_info info = stb_vorbis_get_info(vorbis);
71
+ const unsigned int total_frames = stb_vorbis_stream_length_in_samples(vorbis);
72
+ if (info.channels <= 0 || total_frames == 0) {
73
+ stb_vorbis_close(vorbis);
74
+ return false;
75
+ }
76
+ out.channels = static_cast<uint32_t>(info.channels);
77
+ out.sample_rate = info.sample_rate;
78
+ out.samples.resize(static_cast<size_t>(total_frames) * info.channels);
79
+ const int read = stb_vorbis_get_samples_float_interleaved(
80
+ vorbis, info.channels, out.samples.data(),
81
+ static_cast<int>(out.samples.size()));
82
+ stb_vorbis_close(vorbis);
83
+ if (read <= 0) {
84
+ out.samples.clear();
85
+ return false;
86
+ }
87
+ out.samples.resize(static_cast<size_t>(read) * info.channels);
88
+ return true;
89
+ }
90
+
91
+ } // namespace
92
+
93
+ bool DecodeAudio(const std::vector<uint8_t>& input, PcmBuffer& out)
94
+ {
95
+ if (input.size() < 12) {
96
+ return false;
97
+ }
98
+ const uint8_t* d = input.data();
99
+
100
+ // マジックナンバーで形式を判定して該当デコーダを先に試し、
101
+ // 外れたら残りをカスケード (誤判定・ヘッダ無し MP3 の保険)。
102
+ const bool looks_wav = std::memcmp(d, "RIFF", 4) == 0 && std::memcmp(d + 8, "WAVE", 4) == 0;
103
+ const bool looks_ogg = std::memcmp(d, "OggS", 4) == 0;
104
+ const bool looks_mp3 = std::memcmp(d, "ID3", 3) == 0
105
+ || (d[0] == 0xFF && (d[1] & 0xE0) == 0xE0);
106
+
107
+ if (looks_wav && DecodeWav(input, out)) {
108
+ return true;
109
+ }
110
+ if (looks_ogg && DecodeOgg(input, out)) {
111
+ return true;
112
+ }
113
+ if (looks_mp3 && DecodeMp3(input, out)) {
114
+ return true;
115
+ }
116
+
117
+ // 判定が外れた場合のカスケード
118
+ return DecodeWav(input, out) || DecodeOgg(input, out) || DecodeMp3(input, out);
119
+ }
120
+
121
+ } // namespace next2d
@@ -0,0 +1,18 @@
1
+ // 音声デコード (エンコード済みバイト列 -> float32 interleaved PCM)。
2
+ //
3
+ // 第一候補は dr_mp3 / dr_wav / stb_vorbis (MP3/WAV/OGG、全プラットフォーム共通、
4
+ // public domain)。コンソール (Game Core OS) にはデスクトップの Media Foundation が
5
+ // 無いため、MF 依存を第一経路から外して PC 検証と実機の挙動を一致させる。
6
+ // デスクトップのみ AAC 等の保険として AudioEngine::Decode 側に MF フォールバックが残る。
7
+ #pragma once
8
+
9
+ #include "AudioEngine.h" // PcmBuffer
10
+
11
+ #include <cstdint>
12
+ #include <vector>
13
+
14
+ namespace next2d {
15
+
16
+ bool DecodeAudio(const std::vector<uint8_t>& input, PcmBuffer& out);
17
+
18
+ } // namespace next2d