@next2d/builder 0.1.6 → 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.
Files changed (34) hide show
  1. package/README.md +5 -1
  2. package/dist/index.js +140 -36
  3. package/package.json +3 -2
  4. package/templates/xbox/CMakeLists.txt +74 -2
  5. package/templates/xbox/README.md +73 -12
  6. package/templates/xbox/cmake/FetchDawn.cmake +47 -6
  7. package/templates/xbox/cmake/FindV8.cmake +22 -1
  8. package/templates/xbox/js/bootstrap.js +31 -28
  9. package/templates/xbox/js/selftest.js +740 -90
  10. package/templates/xbox/src/EventLoop.cpp +28 -1
  11. package/templates/xbox/src/EventLoop.h +12 -0
  12. package/templates/xbox/src/bindings/Audio.cpp +99 -15
  13. package/templates/xbox/src/bindings/Bindings.h +10 -0
  14. package/templates/xbox/src/bindings/Canvas.cpp +45 -0
  15. package/templates/xbox/src/bindings/Canvas2D.cpp +2 -5
  16. package/templates/xbox/src/bindings/DomShims.cpp +127 -0
  17. package/templates/xbox/src/bindings/EventTarget.cpp +2 -0
  18. package/templates/xbox/src/bindings/Fetch.cpp +2 -8
  19. package/templates/xbox/src/bindings/Image.cpp +212 -10
  20. package/templates/xbox/src/bindings/Polyfills.cpp +791 -0
  21. package/templates/xbox/src/bindings/Video.cpp +96 -7
  22. package/templates/xbox/src/bindings/webgpu/WebGPU.cpp +815 -15
  23. package/templates/xbox/src/bindings/webgpu/WebGPUCommon.h +3 -8
  24. package/templates/xbox/src/gpu/DawnContext.cpp +90 -3
  25. package/templates/xbox/src/gpu/DawnContext.h +18 -0
  26. package/templates/xbox/src/main.cpp +88 -6
  27. package/templates/xbox/src/platform/AudioEngine.cpp +9 -3
  28. package/templates/xbox/src/v8/V8Runtime.cpp +69 -4
  29. package/templates/xbox/src/v8/V8Util.h +29 -0
  30. package/templates/xbox/src/v8/WeakHandle.h +45 -0
  31. package/templates/xbox/src/worker/WorkerRuntime.cpp +325 -41
  32. package/templates/xbox/src/worker/WorkerRuntime.h +20 -10
  33. package/templates/xbox/tests/check_local.sh +3 -2
  34. package/templates/xbox/tests/raster_test.bin +0 -0
@@ -15,6 +15,7 @@
15
15
  #include <v8.h>
16
16
 
17
17
  #include "v8/V8Util.h"
18
+ #include "v8/WeakHandle.h"
18
19
 
19
20
  #include <memory>
20
21
  #include <string>
@@ -27,13 +28,8 @@ struct Holder {
27
28
  T handle;
28
29
  };
29
30
 
30
- template <typename T>
31
- void ReleaseHolder(const v8::WeakCallbackInfo<Holder<T>>& info)
32
- {
33
- delete info.GetParameter();
34
- }
35
-
36
31
  // wgpu ハンドル T を、指定 ObjectTemplate から生成したオブジェクトにラップする。
32
+ // GC 時の解放は AttachWeak (第一パスで Global を Reset する規約に準拠) に委ねる。
37
33
  template <typename T>
38
34
  v8::Local<v8::Object> Wrap(v8::Isolate* isolate,
39
35
  v8::Local<v8::ObjectTemplate> tmpl,
@@ -43,8 +39,7 @@ v8::Local<v8::Object> Wrap(v8::Isolate* isolate,
43
39
  v8::Local<v8::Object> obj = tmpl->NewInstance(ctx).ToLocalChecked();
44
40
  auto* holder = new Holder<T>{std::move(handle)};
45
41
  obj->SetInternalField(0, v8::External::New(isolate, holder));
46
- auto* global = new v8::Global<v8::Object>(isolate, obj);
47
- global->SetWeak(holder, ReleaseHolder<T>, v8::WeakCallbackType::kParameter);
42
+ v8util::AttachWeak(isolate, obj, holder);
48
43
  return obj;
49
44
  }
50
45
 
@@ -1,7 +1,6 @@
1
1
  #include "DawnContext.h"
2
2
 
3
3
  #include <Windows.h>
4
- #include <dawn/native/DawnNative.h>
5
4
  #include <iostream>
6
5
 
7
6
  namespace next2d {
@@ -114,12 +113,13 @@ bool DawnContext::AcquireDevice()
114
113
  return false;
115
114
  }
116
115
 
117
- // surface の優先フォーマットを取得
116
+ // surface の優先フォーマットと CopySrc 可否 (デバッグ読み戻し用) を取得
118
117
  wgpu::SurfaceCapabilities caps = {};
119
118
  surface_.GetCapabilities(adapter_, &caps);
120
119
  if (caps.formatCount > 0) {
121
120
  format_ = caps.formats[0];
122
121
  }
122
+ can_copy_src_ = (caps.usages & wgpu::TextureUsage::CopySrc) == wgpu::TextureUsage::CopySrc;
123
123
 
124
124
  Configure(width_, height_);
125
125
  return true;
@@ -138,26 +138,113 @@ void DawnContext::Configure(uint32_t width, uint32_t height)
138
138
  config.device = device_;
139
139
  config.format = format_;
140
140
  config.usage = wgpu::TextureUsage::RenderAttachment;
141
+ if (can_copy_src_) {
142
+ // 黒画面デバッグ用の surface 読み戻し (DebugProbeSurface) を可能にする
143
+ config.usage |= wgpu::TextureUsage::CopySrc;
144
+ }
141
145
  config.width = width_;
142
146
  config.height = height_;
143
147
  config.presentMode = wgpu::PresentMode::Fifo;
144
148
  config.alphaMode = wgpu::CompositeAlphaMode::Opaque;
145
149
 
146
150
  surface_.Configure(&config);
151
+ configured_ = true;
147
152
  }
148
153
 
149
154
  wgpu::Texture DawnContext::GetCurrentTexture()
150
155
  {
156
+ if (!configured_) {
157
+ return nullptr;
158
+ }
151
159
  wgpu::SurfaceTexture surface_texture = {};
152
160
  surface_.GetCurrentTexture(&surface_texture);
161
+ frame_texture_acquired_ = true;
162
+ current_texture_ = surface_texture.texture;
153
163
  return surface_texture.texture;
154
164
  }
155
165
 
166
+ // 提示直前の surface 中央行を読み戻し、非黒ピクセル数をログする (黒画面調査用)。
167
+ // CI では目視できないため、これが「画面に何が出ているか」の唯一の証拠になる。
168
+ void DawnContext::DebugProbeSurface()
169
+ {
170
+ if (!can_copy_src_ || !current_texture_ || width_ == 0) {
171
+ std::cerr << "[GPU] surface probe unavailable (CopySrc="
172
+ << (can_copy_src_ ? "yes" : "no") << ")" << std::endl;
173
+ return;
174
+ }
175
+
176
+ const uint32_t row_bytes = width_ * 4;
177
+ wgpu::BufferDescriptor buf_desc = {};
178
+ buf_desc.size = (row_bytes + 3) & ~3u;
179
+ buf_desc.usage = wgpu::BufferUsage::CopyDst | wgpu::BufferUsage::MapRead;
180
+ wgpu::Buffer buffer = device_.CreateBuffer(&buf_desc);
181
+
182
+ wgpu::TexelCopyTextureInfo src = {};
183
+ src.texture = current_texture_;
184
+ src.origin = { 0, height_ / 2, 0 };
185
+
186
+ wgpu::TexelCopyBufferInfo dst = {};
187
+ dst.buffer = buffer;
188
+ dst.layout.offset = 0;
189
+ dst.layout.bytesPerRow = wgpu::kCopyStrideUndefined; // 1 行コピーでは不要
190
+
191
+ wgpu::Extent3D extent = { width_, 1, 1 };
192
+
193
+ wgpu::CommandEncoder encoder = device_.CreateCommandEncoder();
194
+ encoder.CopyTextureToBuffer(&src, &dst, &extent);
195
+ wgpu::CommandBuffer commands = encoder.Finish();
196
+ device_.GetQueue().Submit(1, &commands);
197
+
198
+ bool done = false;
199
+ wgpu::Future future = buffer.MapAsync(wgpu::MapMode::Read, 0, buf_desc.size,
200
+ wgpu::CallbackMode::AllowProcessEvents,
201
+ [&done](wgpu::MapAsyncStatus, wgpu::StringView) { done = true; });
202
+ // GPU 完了待ちは TimedWaitAny で行う (ProcessEvents の空スピンでは
203
+ // GPU がフレーム描画中のとき完了前にループ上限へ達する)
204
+ instance_.WaitAny(future, 2'000'000'000); // 2s
205
+ if (!done) {
206
+ std::cerr << "[GPU] surface probe: map timeout" << std::endl;
207
+ return;
208
+ }
209
+
210
+ const auto* px = static_cast<const uint8_t*>(buffer.GetConstMappedRange(0, buf_desc.size));
211
+ uint32_t non_black = 0;
212
+ for (uint32_t x = 0; px && x < width_; ++x) {
213
+ const uint8_t* p = px + static_cast<size_t>(x) * 4;
214
+ if (p[0] || p[1] || p[2]) {
215
+ ++non_black;
216
+ }
217
+ }
218
+ std::cerr << "[GPU] surface probe (present #" << present_count_
219
+ << "): non-black " << non_black << "/" << width_ << " px";
220
+ if (px && width_ > 1) {
221
+ const uint8_t* c = px + static_cast<size_t>(width_ / 2) * 4;
222
+ std::cerr << ", center=(" << +c[0] << "," << +c[1] << "," << +c[2] << "," << +c[3] << ")";
223
+ }
224
+ std::cerr << std::endl;
225
+ buffer.Unmap();
226
+ }
227
+
156
228
  void DawnContext::Present()
157
229
  {
158
- if (surface_) {
230
+ // 提示するのは「未構成でない」かつ「今フレーム GetCurrentTexture 済み」のときのみ。
231
+ // 取得なしで Present すると Dawn の検証エラーになる (描画しないフレームは提示しない)。
232
+ if (surface_ && configured_ && frame_texture_acquired_) {
233
+ ++present_count_;
234
+ if (present_count_ <= 3 || present_count_ == 60 || present_count_ % 300 == 0) {
235
+ DebugProbeSurface();
236
+ }
159
237
  surface_.Present();
238
+ } else {
239
+ ++present_skipped_;
240
+ if (present_skipped_ <= 3 || present_skipped_ % 300 == 0) {
241
+ std::cerr << "[GPU] present skipped #" << present_skipped_
242
+ << " (configured=" << configured_
243
+ << " acquired=" << frame_texture_acquired_ << ")" << std::endl;
244
+ }
160
245
  }
246
+ frame_texture_acquired_ = false;
247
+ current_texture_ = nullptr;
161
248
  }
162
249
 
163
250
  void DawnContext::Tick()
@@ -36,6 +36,9 @@ public:
36
36
  // 提示する。
37
37
  void Present();
38
38
 
39
+ // 提示直前の surface 中央行を読み戻して非黒ピクセル数をログする (黒画面調査用)。
40
+ void DebugProbeSurface();
41
+
39
42
  // Dawn の非同期イベント(コールバック)を進める。毎フレーム呼ぶ。
40
43
  void Tick();
41
44
 
@@ -59,6 +62,21 @@ private:
59
62
  HWND hwnd_ = nullptr;
60
63
  uint32_t width_ = 0;
61
64
  uint32_t height_ = 0;
65
+
66
+ // surface が Configure 済みか。未構成のまま Present すると Dawn の
67
+ // 検証エラーが毎フレーム出るため、Present/GetCurrentTexture をゲートする。
68
+ bool configured_ = false;
69
+
70
+ // 今フレーム GetCurrentTexture が呼ばれたか。呼ばれていないフレームで
71
+ // Present すると Dawn の検証エラーになるため、Present をゲートする
72
+ // (アプリが描画しないフレームは提示しない)。
73
+ bool frame_texture_acquired_ = false;
74
+
75
+ // デバッグ用: surface の CopySrc 可否 / 今フレームのテクスチャ / 提示統計
76
+ bool can_copy_src_ = false;
77
+ wgpu::Texture current_texture_ = nullptr;
78
+ uint64_t present_count_ = 0;
79
+ uint64_t present_skipped_ = 0;
62
80
  };
63
81
 
64
82
  } // namespace next2d
@@ -4,8 +4,11 @@
4
4
  // bootstrap.js とアプリ本体(ESM)の読み込み -> ゲームループ、という流れ。
5
5
  #include <Windows.h>
6
6
  #include <windowsx.h>
7
+ #include <timeapi.h>
7
8
  #include <XGameRuntime.h>
8
9
 
10
+ #pragma comment(lib, "winmm.lib")
11
+
9
12
  #include "HostContext.h"
10
13
  #include "AssetLoader.h"
11
14
  #include "EventLoop.h"
@@ -190,6 +193,18 @@ HWND CreateHostWindow(HINSTANCE instance, int width, int height)
190
193
  wc.lpszClassName = L"Next2DXboxHostWindow";
191
194
  RegisterClassExW(&wc);
192
195
 
196
+ // ウィンドウが画面(作業領域)より大きいと画面外にはみ出し、提示が
197
+ // 見切れる/非等倍でスケールされて歪む (CI の低解像度ディスプレイで
198
+ // 1920x1080 ウィンドウ → 描画が縦長になっていた)。作業領域に収まるよう
199
+ // クランプする。実機コンソールでは作業領域=全画面のため 1920x1080 のまま。
200
+ RECT wa = {};
201
+ if (SystemParametersInfoW(SPI_GETWORKAREA, 0, &wa, 0)) {
202
+ const int maxW = wa.right - wa.left;
203
+ const int maxH = wa.bottom - wa.top;
204
+ if (maxW > 0 && width > maxW) { width = maxW; }
205
+ if (maxH > 0 && height > maxH) { height = maxH; }
206
+ }
207
+
193
208
  // コンソールでは全画面相当。PC(GDK) 検証ではウィンドウ。
194
209
  const DWORD style = WS_OVERLAPPEDWINDOW | WS_VISIBLE;
195
210
  return CreateWindowExW(
@@ -202,14 +217,27 @@ HWND CreateHostWindow(HINSTANCE instance, int width, int height)
202
217
 
203
218
  int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR cmd_line, int)
204
219
  {
220
+ // Sleep() の分解能を 1ms に上げる (フレームペーシング用。既定 15.6ms では
221
+ // 60Hz を刻めない)
222
+ timeBeginPeriod(1);
223
+
205
224
  // --selftest: アプリの代わりに js/selftest.js を実行し、全バインディングを
206
225
  // 実機/PC(GDK) 上で検証して終了する (テスト完了で自動終了)。
207
226
  const bool selftest = cmd_line && wcsstr(cmd_line, L"--selftest") != nullptr;
208
227
 
209
- // 1. GDK ランタイム初期化
210
- if (FAILED(XGameRuntimeInitialize())) {
228
+ // 1. GDK ランタイム初期化。
229
+ // デスクトップでは Gaming Services 未導入の環境 (CI ランナー等) でも
230
+ // 起動できるよう、失敗を警告に留めて続行する (本ホストは XUser 等を未使用)。
231
+ // コンソールでは必須のため失敗したら終了する。
232
+ bool xgame_initialized = SUCCEEDED(XGameRuntimeInitialize());
233
+ if (!xgame_initialized) {
234
+ #if NEXT2D_XBOX_CONSOLE
211
235
  std::cerr << "XGameRuntimeInitialize failed" << std::endl;
212
236
  return 1;
237
+ #else
238
+ std::cerr << "warning: XGameRuntimeInitialize failed (Gaming Services not installed?)"
239
+ << " - continuing on desktop" << std::endl;
240
+ #endif
213
241
  }
214
242
 
215
243
  const fs::path exe_dir = ExeDir();
@@ -230,6 +258,22 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR cmd_line, int)
230
258
  return 1;
231
259
  }
232
260
 
261
+ // ウィンドウは要求サイズより小さく作られ得る (作業領域クランプ / DPI /
262
+ // CI の低解像度ディスプレイ)。surface・canvas・screen は全て viewport_* から
263
+ // 派生するため、ここで「実クライアント矩形」を採用して一致させる。これを
264
+ // 怠ると surface(1920x1080) を実ウィンドウ(例 1024x768)へ非等倍提示して
265
+ // 画面が縦長に歪む。V8 バインディング設置(screen/innerWidth/dpr)と
266
+ // Dawn 初期化の前に確定させる必要がある。
267
+ {
268
+ RECT rc = {};
269
+ if (GetClientRect(hwnd, &rc) && rc.right > 0 && rc.bottom > 0) {
270
+ host.viewport_width = rc.right;
271
+ host.viewport_height = rc.bottom;
272
+ }
273
+ std::cerr << "[Win] client rect: " << host.viewport_width
274
+ << "x" << host.viewport_height << std::endl;
275
+ }
276
+
233
277
  // 4. V8 プロセス初期化 + Isolate/Context + バインディング
234
278
  V8Runtime::InitializeProcess(nullptr);
235
279
  V8Runtime runtime;
@@ -255,7 +299,11 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR cmd_line, int)
255
299
  host.workers = &workers;
256
300
  g_dawn = &dawn;
257
301
 
258
- dawn.Initialize(hwnd, host.viewport_width, host.viewport_height);
302
+ // GPU 無し環境 (CI 等) では Dawn 初期化に失敗し得るが、CPU 側の機能と
303
+ // selftest は動かせるため続行する (Present 等は内部でゲートされる)
304
+ if (!dawn.Initialize(hwnd, host.viewport_width, host.viewport_height)) {
305
+ std::cerr << "warning: Dawn initialization failed - continuing without GPU" << std::endl;
306
+ }
259
307
  gamepad.Initialize();
260
308
  audio.Initialize();
261
309
 
@@ -300,9 +348,12 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR cmd_line, int)
300
348
  }
301
349
 
302
350
  // 7. ゲームループ
303
- v8::Isolate::Scope isolate_scope(runtime.isolate());
304
351
  MSG msg = {};
305
352
  int exit_code = 0;
353
+ // Isolate::Scope はループ内に限定する。Enter されたままの isolate を
354
+ // Dispose すると V8 の fatal (Disposing the isolate that is entered) になる。
355
+ {
356
+ v8::Isolate::Scope isolate_scope(runtime.isolate());
306
357
  while (g_running) {
307
358
  // Win32 メッセージ
308
359
  while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
@@ -347,6 +398,27 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR cmd_line, int)
347
398
  // 提示
348
399
  dawn.Present();
349
400
 
401
+ // フレームペーシング: ブラウザの rAF 同様 ~60Hz に揃える。
402
+ // Present はスキップ時にブロックしないため、これが無いとループが
403
+ // 数万回転/秒で空回りし、rAF 前提のゲーム/Tween のタイミングが崩れる。
404
+ {
405
+ const double frame_ms = event_loop.Now() - now;
406
+ if (frame_ms < 15.0) {
407
+ Sleep(static_cast<DWORD>(15.0 - frame_ms));
408
+ }
409
+ }
410
+
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
+
350
422
  // selftest 完了検知: selftest.js が globalThis.__selftestExitCode を設定したら終了
351
423
  if (selftest) {
352
424
  v8::Local<v8::Value> code;
@@ -359,10 +431,20 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR cmd_line, int)
359
431
  }
360
432
  }
361
433
 
362
- // 8. 後始末
434
+ }
435
+
436
+ // 8. 後始末: v8::Global を保持するものは Isolate 破棄前に必ず明示解放する。
437
+ // (スタック変数はスコープ終了 = runtime.Dispose() の後に巻き戻されるため、
438
+ // デストラクタ任せにすると破棄済み Isolate への Global::Reset で fail-fast する)
439
+ workers.Shutdown(); // WorkerInstance の Global<Context> / EventLoop
440
+ event_loop.Shutdown(); // main の setTimeout/rAF コールバック (Global<Function>)
441
+ ShutdownAudioEvents();
442
+ ShutdownWebGPU();
363
443
  host.main_canvas.Reset();
364
444
  runtime.Dispose();
365
445
  V8Runtime::ShutdownProcess();
366
- XGameRuntimeUninitialize();
446
+ if (xgame_initialized) {
447
+ XGameRuntimeUninitialize();
448
+ }
367
449
  return exit_code;
368
450
  }
@@ -41,6 +41,10 @@ AudioVoice::~AudioVoice()
41
41
 
42
42
  void AudioVoice::Start(bool loop)
43
43
  {
44
+ // 音声デバイスが無い環境 (CI 等) でも「再生開始→即終了」として扱えるよう、
45
+ // voice_ の有無に関わらず開始状態は記録する (IsFinished が ended 発火を導く)。
46
+ started_ = true;
47
+ looping_ = loop;
44
48
  if (!voice_) {
45
49
  return;
46
50
  }
@@ -51,8 +55,6 @@ void AudioVoice::Start(bool loop)
51
55
  buf.LoopCount = loop ? XAUDIO2_LOOP_INFINITE : 0;
52
56
  voice_->SubmitSourceBuffer(&buf);
53
57
  voice_->Start(0);
54
- started_ = true;
55
- looping_ = loop;
56
58
  }
57
59
 
58
60
  void AudioVoice::Stop()
@@ -66,9 +68,13 @@ void AudioVoice::Stop()
66
68
 
67
69
  bool AudioVoice::IsFinished() const
68
70
  {
69
- if (!voice_ || !started_ || looping_) {
71
+ if (!started_ || looping_) {
70
72
  return false;
71
73
  }
74
+ if (!voice_) {
75
+ // 実ボイスが無い (音声デバイス無し) 場合は開始と同時に終了扱い。
76
+ return true;
77
+ }
72
78
  XAUDIO2_VOICE_STATE state = {};
73
79
  voice_->GetState(&state);
74
80
  return state.BuffersQueued == 0;
@@ -43,15 +43,26 @@ V8Runtime::~V8Runtime()
43
43
  void V8Runtime::InitializeProcess(const char* exec_path)
44
44
  {
45
45
  // Xbox(GDK) / Nintendo Switch のリテール環境は動的コード生成(JIT)を禁止する。
46
- // V8 を jitless (Ignition インタプリタのみ・TurboFan/Sparkplug/RWXページ無し) で動かす。
47
- // WebAssembly も JIT を要するため無効化する。
48
- v8::V8::SetFlagsFromString("--jitless --no-expose-wasm --no-opt");
46
+ // - JS: --jitless (Ignition インタプリタのみ・RWX ページ無し)
47
+ // - WebAssembly: --wasm-jitless (DrumBrake = V8 の wasm インタープリタ。
48
+ // prebuilt V8 r2 は v8_enable_drumbrake=true でビルドされている)
49
+ v8::V8::SetFlagsFromString("--jitless --wasm-jitless");
49
50
 
50
51
  v8::V8::InitializeICUDefaultLocation(exec_path);
51
52
  v8::V8::InitializeExternalStartupData(exec_path);
52
53
  platform_ = v8::platform::NewDefaultPlatform();
53
54
  v8::V8::InitializePlatform(platform_.get());
54
55
  v8::V8::Initialize();
56
+
57
+ // WebAssembly (DrumBrake) はメモリ境界検査を guard page + trap handler で行うため、
58
+ // embedder が trap handler を有効化する必要がある (d8/Chrome も起動時に行う)。
59
+ // 未設定のまま wasm を実行するとインタープリタ初期化で fail-fast する。
60
+ // コンソール実機で VEH が使えない場合は、V8 を v8_drumbrake_bounds_checks=true
61
+ // (明示境界チェック) でビルドし直すこと。
62
+ if (!v8::V8::EnableWebAssemblyTrapHandler(true)) {
63
+ std::cerr << "warning: EnableWebAssemblyTrapHandler failed"
64
+ << " - WebAssembly may not work" << std::endl;
65
+ }
55
66
  }
56
67
 
57
68
  void V8Runtime::ShutdownProcess()
@@ -75,6 +86,35 @@ bool V8Runtime::Initialize(HostContext* host)
75
86
  // 例外時に未処理を潰さないよう、明示的に MicrotasksPolicy を制御する
76
87
  isolate_->SetMicrotasksPolicy(v8::MicrotasksPolicy::kExplicit);
77
88
 
89
+ // 未処理の Promise 拒否を stderr へ報告する。framework の boot は async のため、
90
+ // この報告が無いと例外がどこにも出ずゲームが無音で停止する (実際に発生した)。
91
+ isolate_->SetPromiseRejectCallback([](v8::PromiseRejectMessage message) {
92
+ if (message.GetEvent() != v8::kPromiseRejectWithNoHandler) {
93
+ return; // 後からハンドラが付く分などは報告しない
94
+ }
95
+ v8::Isolate* isolate = v8::Isolate::GetCurrent();
96
+ v8::HandleScope hs(isolate);
97
+ v8::Local<v8::Value> value = message.GetValue();
98
+ std::string text = "(no value)";
99
+ std::string stack;
100
+ if (!value.IsEmpty()) {
101
+ text = v8util::ToStdString(isolate, value);
102
+ v8::Local<v8::Context> ctx = isolate->GetCurrentContext();
103
+ if (!ctx.IsEmpty() && value->IsObject()) {
104
+ v8::Local<v8::Value> s;
105
+ if (value.As<v8::Object>()
106
+ ->Get(ctx, v8util::Str(isolate, "stack")).ToLocal(&s) &&
107
+ s->IsString()) {
108
+ stack = v8util::ToStdString(isolate, s);
109
+ }
110
+ }
111
+ }
112
+ std::cerr << "[V8] Unhandled promise rejection: " << text << std::endl;
113
+ if (!stack.empty() && stack != text) {
114
+ std::cerr << stack << std::endl;
115
+ }
116
+ });
117
+
78
118
  v8::Isolate::Scope isolate_scope(isolate_);
79
119
  v8::HandleScope handle_scope(isolate_);
80
120
 
@@ -148,12 +188,37 @@ v8::MaybeLocal<v8::Module> V8Runtime::LoadModule(const std::string& path,
148
188
  return cached->second.Get(isolate_);
149
189
  }
150
190
 
151
- const std::string source = ReadTextFile(abs);
191
+ std::string source = ReadTextFile(abs);
152
192
  if (source.empty() && !fs::exists(abs)) {
153
193
  v8util::ThrowTypeError(isolate_, "Module not found: " + abs);
154
194
  return v8::MaybeLocal<v8::Module>();
155
195
  }
156
196
 
197
+ // «診断» Tween(Job) の凍結調査: app.js の Job 更新コードへトレースを注入する。
198
+ // rAF が 2 本 (ticker + 1 Job) のまま描画が止まる現象の currentTime/duration の
199
+ // 実値を観測する。パターン不一致なら無変更 (ログのみ)。
200
+ if (source.find("(e.currentTime=(t-e.startTime)/1e3,") != std::string::npos) {
201
+ const auto patch = [&source](const std::string& from, const std::string& to) {
202
+ const auto pos = source.find(from);
203
+ if (pos != std::string::npos) {
204
+ source.replace(pos, from.size(), to);
205
+ } else {
206
+ std::cerr << "[V8] job trace patch not applied: "
207
+ << from.substr(0, 40) << std::endl;
208
+ }
209
+ };
210
+ patch("(e.currentTime=(t-e.startTime)/1e3,",
211
+ "(e.currentTime=(t-e.startTime)/1e3,"
212
+ "((globalThis.__jt=(globalThis.__jt||0)+1)<=60||globalThis.__jt%600===0)&&"
213
+ "console.info(\"[jobtrace] #\"+globalThis.__jt,"
214
+ "\"ct=\"+e.currentTime.toFixed(3),\"dur=\"+e.duration,"
215
+ "\"t=\"+t.toFixed(1),\"st=\"+e.startTime.toFixed(1)),");
216
+ patch("e.currentTime>=e.duration?(",
217
+ "e.currentTime>=e.duration?("
218
+ "console.info(\"[jobtrace] COMPLETE dur=\"+e.duration),");
219
+ std::cerr << "[V8] job trace patch applied to " << abs << std::endl;
220
+ }
221
+
157
222
  v8::Local<v8::String> src = v8util::Str(isolate_, source);
158
223
 
159
224
  v8::ScriptOrigin origin(
@@ -2,6 +2,7 @@
2
2
  #pragma once
3
3
 
4
4
  #include <v8.h>
5
+ #include <iostream>
5
6
  #include <string>
6
7
  #include <string_view>
7
8
 
@@ -53,4 +54,32 @@ inline void ThrowTypeError(v8::Isolate* isolate, std::string_view message)
53
54
  isolate->ThrowException(v8::Exception::TypeError(Str(isolate, message)));
54
55
  }
55
56
 
57
+ // TryCatch が例外を保持していれば stderr へ報告する。
58
+ // rAF / タイマー / onmessage / イベントリスナーのコールバックは TryCatch で
59
+ // 保護されるが、報告しないと例外が無音で消えて原因究明が不可能になる
60
+ // (レンダラ worker が毎フレーム throw していても黒画面にしか見えない)。
61
+ inline void ReportCaught(v8::Isolate* isolate, v8::TryCatch* tc, const char* where)
62
+ {
63
+ if (!tc->HasCaught()) {
64
+ return;
65
+ }
66
+ v8::HandleScope hs(isolate);
67
+ v8::Local<v8::Context> ctx = isolate->GetCurrentContext();
68
+ const std::string text = ToStdString(isolate, tc->Exception());
69
+ std::cerr << "[JS] exception in " << where << ": " << text << std::endl;
70
+ v8::Local<v8::Value> stack;
71
+ if (!ctx.IsEmpty() && tc->StackTrace(ctx).ToLocal(&stack)) {
72
+ const std::string s = ToStdString(isolate, stack);
73
+ if (!s.empty() && s != text) {
74
+ std::cerr << s << std::endl;
75
+ }
76
+ }
77
+ v8::Local<v8::Message> message = tc->Message();
78
+ if (!message.IsEmpty() && !ctx.IsEmpty()) {
79
+ std::cerr << " at "
80
+ << ToStdString(isolate, message->GetScriptOrigin().ResourceName())
81
+ << ":" << message->GetLineNumber(ctx).FromMaybe(0) << std::endl;
82
+ }
83
+ }
84
+
56
85
  } // namespace next2d::v8util
@@ -0,0 +1,45 @@
1
+ // SetWeak (GC 連動の解放) の正しい運用ヘルパー。
2
+ //
3
+ // V8 の規約では、第一パスの weak callback 内で対応する v8::Global を必ず
4
+ // Reset しなければならない。違反すると GC 時に fatal
5
+ // 「Check failed: Handle not reset in first callback.」で落ちる。
6
+ // payload と Global を束ねて所有し、GC で Reset + delete の両方を行う。
7
+ #pragma once
8
+
9
+ #include <v8.h>
10
+
11
+ namespace next2d::v8util {
12
+
13
+ template <typename T>
14
+ class WeakHandle {
15
+ public:
16
+ static void Attach(v8::Isolate* isolate, v8::Local<v8::Object> obj, T* payload)
17
+ {
18
+ auto* w = new WeakHandle<T>(payload);
19
+ w->handle_.Reset(isolate, obj);
20
+ w->handle_.SetWeak(w, &WeakHandle::OnGC, v8::WeakCallbackType::kParameter);
21
+ }
22
+
23
+ private:
24
+ explicit WeakHandle(T* payload) : payload_(payload) {}
25
+
26
+ static void OnGC(const v8::WeakCallbackInfo<WeakHandle<T>>& info)
27
+ {
28
+ WeakHandle<T>* w = info.GetParameter();
29
+ w->handle_.Reset(); // 第一パスでの Reset (V8 の必須要件)
30
+ delete w->payload_;
31
+ delete w;
32
+ }
33
+
34
+ v8::Global<v8::Object> handle_;
35
+ T* payload_;
36
+ };
37
+
38
+ // オブジェクトが GC されたときに `delete payload` する弱参照を張る。
39
+ template <typename T>
40
+ inline void AttachWeak(v8::Isolate* isolate, v8::Local<v8::Object> obj, T* payload)
41
+ {
42
+ WeakHandle<T>::Attach(isolate, obj, payload);
43
+ }
44
+
45
+ } // namespace next2d::v8util