Skip to content

Commit 86c4db5

Browse files
committed
#75 wip: xboxビルドテスト v68
1 parent 1ebec3a commit 86c4db5

6 files changed

Lines changed: 329 additions & 89 deletions

File tree

templates/xbox/src/bindings/Audio.cpp

Lines changed: 78 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "AssetLoader.h"
55
#include "EventTarget.h"
66
#include "platform/AudioEngine.h"
7+
#include "platform/DecodeQueue.h"
78
#include "v8/V8Util.h"
89
#include "v8/WeakHandle.h"
910

@@ -284,30 +285,49 @@ void DecodeAudioData(const v8::FunctionCallbackInfo<v8::Value>& args)
284285
static_cast<uint8_t*>(store->Data()) + store->ByteLength());
285286
}
286287

288+
// Media Foundation デコードはバックグラウンドスレッドへ (Promise なので元々
289+
// 非同期契約)。BGM の同期デコードは画面遷移時の数百 ms ヒッチになっていた。
290+
auto resolver_ref =
291+
std::make_shared<v8::Global<v8::Promise::Resolver>>(isolate, resolver);
287292
auto pcm = std::make_shared<PcmBuffer>();
288-
const bool decoded = !input.empty() && AudioEngine::Decode(input, *pcm);
289-
290-
// 重要: デコード失敗でも Reject しない。@next2d/media の SoundDecodeService は
291-
// decodeAudioData の reject を catch して先頭バイトをスキップし再帰リトライするが、
292-
// そのループ条件 (`idx > byteLength`) が常に false のため同一バッファで無限再帰し、
293-
// マイクロタスクが枯渇してフレームワークのローディング画面ごとフリーズする。
294-
// ここで無音の空バッファを resolve すれば、そのサウンドは無音になるだけでアプリは
295-
// 進行できる (graceful degradation)。実データが壊れているか MF 非対応かは上のログで判別。
296-
if (!decoded) {
297-
pcm->samples.clear(); // 0 サンプル = 無音
298-
pcm->channels = pcm->channels ? pcm->channels : 2;
299-
pcm->sample_rate = pcm->sample_rate ? pcm->sample_rate : 48000;
300-
}
293+
auto decoded = std::make_shared<bool>(false);
301294

302-
v8::Local<v8::Object> buffer = InternalTemplate(isolate)->NewInstance(ctx).ToLocalChecked();
303-
Attach<PcmBuffer>(isolate, buffer, pcm);
304-
v8util::SetValue(isolate, buffer, "sampleRate", v8::Number::New(isolate, pcm->sample_rate));
305-
v8util::SetValue(isolate, buffer, "numberOfChannels", v8::Integer::New(isolate, pcm->channels));
306-
v8util::SetValue(isolate, buffer, "duration",
307-
v8::Number::New(isolate, pcm->channels
308-
? static_cast<double>(pcm->samples.size()) / pcm->channels / pcm->sample_rate
309-
: 0.0));
310-
resolver->Resolve(ctx, buffer).Check();
295+
decodequeue::Submit(
296+
[input = std::move(input), pcm, decoded]() {
297+
*decoded = !input.empty() && AudioEngine::Decode(input, *pcm);
298+
},
299+
[isolate, resolver_ref, pcm, decoded]() {
300+
v8::HandleScope hs(isolate);
301+
v8::Local<v8::Promise::Resolver> r = resolver_ref->Get(isolate);
302+
v8::Local<v8::Context> c = r->GetCreationContextChecked();
303+
v8::Context::Scope cs(c);
304+
305+
// 重要: デコード失敗でも Reject しない。@next2d/media の SoundDecodeService は
306+
// decodeAudioData の reject を catch して先頭バイトをスキップし再帰リトライするが、
307+
// そのループ条件 (`idx > byteLength`) が常に false のため同一バッファで無限再帰し、
308+
// マイクロタスクが枯渇してフレームワークのローディング画面ごとフリーズする。
309+
// ここで無音の空バッファを resolve すれば、そのサウンドは無音になるだけでアプリは
310+
// 進行できる (graceful degradation)。実データが壊れているか MF 非対応かは上のログで判別。
311+
if (!*decoded) {
312+
pcm->samples.clear(); // 0 サンプル = 無音
313+
pcm->channels = pcm->channels ? pcm->channels : 2;
314+
pcm->sample_rate = pcm->sample_rate ? pcm->sample_rate : 48000;
315+
}
316+
317+
v8::Local<v8::Object> buffer =
318+
InternalTemplate(isolate)->NewInstance(c).ToLocalChecked();
319+
Attach<PcmBuffer>(isolate, buffer, pcm);
320+
v8util::SetValue(isolate, buffer, "sampleRate",
321+
v8::Number::New(isolate, pcm->sample_rate));
322+
v8util::SetValue(isolate, buffer, "numberOfChannels",
323+
v8::Integer::New(isolate, pcm->channels));
324+
v8util::SetValue(isolate, buffer, "duration",
325+
v8::Number::New(isolate, pcm->channels
326+
? static_cast<double>(pcm->samples.size()) / pcm->channels / pcm->sample_rate
327+
: 0.0));
328+
r->Resolve(c, buffer).Check();
329+
resolver_ref->Reset();
330+
});
311331
}
312332

313333
// --- Audio (HTMLAudioElement 最小実装) -------------------------------------
@@ -362,17 +382,49 @@ void AudioElementConstructor(const v8::FunctionCallbackInfo<v8::Value>& args)
362382
v8::Local<v8::Object> self = args.This();
363383
Attach<AudioVoice>(isolate, self, nullptr);
364384

365-
// URL 指定があれば同期でロード+デコードして voice を作る (失敗時は無音の no-op)
385+
// URL 指定があればロードし、デコードはバックグラウンドスレッドへ (失敗時は
386+
// 無音の no-op)。デコード完了前に play() が呼ばれていた場合 (paused=false) は
387+
// 完了時に再生を開始する (同期デコード時代の「即時再生」挙動を維持)。
366388
if (args.Length() > 0 && args[0]->IsString()) {
367389
const std::string src = ToStdString(isolate, args[0]);
368390
v8util::SetValue(isolate, self, "src", args[0]);
369391
HostContext* host = HostContext::From(isolate);
370392
auto bytes = host->assets->ReadBinary(src);
371393
if (bytes) {
394+
auto self_ref = std::make_shared<v8::Global<v8::Object>>(isolate, self);
372395
auto pcm = std::make_shared<PcmBuffer>();
373-
if (AudioEngine::Decode(*bytes, *pcm)) {
374-
Attach<AudioVoice>(isolate, self, host->audio->CreateVoice(pcm));
375-
}
396+
auto decoded = std::make_shared<bool>(false);
397+
decodequeue::Submit(
398+
[input = std::move(*bytes), pcm, decoded]() {
399+
*decoded = AudioEngine::Decode(input, *pcm);
400+
},
401+
[isolate, self_ref, pcm, decoded, host]() {
402+
v8::HandleScope hs(isolate);
403+
v8::Local<v8::Object> el = self_ref->Get(isolate);
404+
v8::Local<v8::Context> c = el->GetCreationContextChecked();
405+
v8::Context::Scope cs(c);
406+
if (*decoded) {
407+
auto voice = host->audio->CreateVoice(pcm);
408+
Attach<AudioVoice>(isolate, el, voice);
409+
// デコード中に play() 済みなら開始する
410+
v8::Local<v8::Value> paused_v, loop_v, vol_v;
411+
const bool paused =
412+
!el->Get(c, Str(isolate, "paused")).ToLocal(&paused_v) ||
413+
paused_v->BooleanValue(isolate);
414+
if (!paused && voice) {
415+
if (el->Get(c, Str(isolate, "volume")).ToLocal(&vol_v) &&
416+
vol_v->IsNumber()) {
417+
voice->SetVolume(static_cast<float>(
418+
vol_v.As<v8::Number>()->Value()));
419+
}
420+
const bool loop =
421+
el->Get(c, Str(isolate, "loop")).ToLocal(&loop_v) &&
422+
loop_v->BooleanValue(isolate);
423+
voice->Start(loop);
424+
}
425+
}
426+
self_ref->Reset();
427+
});
376428
}
377429
}
378430

templates/xbox/src/bindings/Image.cpp

Lines changed: 82 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "AssetLoader.h"
55
#include "EventTarget.h"
66
#include "ImageSource.h"
7+
#include "platform/DecodeQueue.h"
78
#include "platform/WicDecoder.h"
89
#include "v8/V8Util.h"
910
#include "v8/WeakHandle.h"
@@ -12,6 +13,8 @@
1213

1314
#include <cstring>
1415
#include <iostream>
16+
#include <memory>
17+
#include <utility>
1518
#include <vector>
1619

1720
namespace next2d {
@@ -49,34 +52,11 @@ bool ImageDecodeBase64(const std::string& in, std::vector<uint8_t>* out)
4952
return true;
5053
}
5154

52-
// 遅延発火する load/error イベント (img.src = url; img.onload = fn の順でも拾えるよう
53-
// microtask で発火する。メインループの PerformMicrotaskCheckpoint が実行する)
54-
struct PendingImageEvent {
55-
v8::Isolate* isolate;
56-
v8::Global<v8::Object> image;
57-
bool ok;
58-
};
59-
60-
void FireImageEvent(void* data)
61-
{
62-
auto* p = static_cast<PendingImageEvent*>(data);
63-
v8::Isolate* isolate = p->isolate;
64-
v8::HandleScope hs(isolate);
65-
v8::Local<v8::Object> img = p->image.Get(isolate);
66-
v8::Local<v8::Context> ctx = img->GetCreationContextChecked();
67-
v8::Context::Scope cs(ctx);
68-
69-
v8::Local<v8::Object> ev = v8::Object::New(isolate);
70-
v8util::SetValue(isolate, ev, "type", Str(isolate, p->ok ? "load" : "error"));
71-
v8util::SetValue(isolate, ev, "target", img);
72-
DispatchEvent(isolate, img, ev);
73-
74-
p->image.Reset();
75-
delete p;
76-
}
77-
78-
// src に指定された URL (assets 相対 / data:) を読み込み WIC でデコードする。
79-
// 成否イベントは microtask で発火する。
55+
// src に指定された URL (assets 相対 / data:) を読み込み、WIC デコードを
56+
// バックグラウンドスレッド (DecodeQueue) へ逃がす。完了はメインループの
57+
// decodequeue::Pump() から反映される (ブラウザ同様、src 設定後に非同期で
58+
// load イベントが届く。img.src = url; img.onload = fn の順でも拾える)。
59+
// 同期デコードは画面遷移時に 1 秒超のフレームヒッチになる (--perf で計測済み)。
8060
void LoadImageFromSrc(v8::Isolate* isolate, v8::Local<v8::Object> self, const std::string& url)
8161
{
8262
std::vector<uint8_t> input;
@@ -90,45 +70,63 @@ void LoadImageFromSrc(v8::Isolate* isolate, v8::Local<v8::Object> self, const st
9070
}
9171
}
9272
} else {
73+
// pak (メモリ) 読みなので同期で問題ない。重いのはデコードのみ。
9374
AssetLoader* assets = HostContext::From(isolate)->assets;
9475
auto bytes = assets->ReadBinary(url);
9576
if (bytes) {
9677
input = std::move(*bytes);
9778
}
9879
}
9980

100-
auto* img = new DecodedImage();
101-
const bool ok = !input.empty() && DecodeImageWithWIC(input, *img);
102-
103-
// «堅牢化» デコード失敗時の扱い。
104-
// player の ShapeLoadSrcUseCase は image に "load" ハンドラのみ登録し "error" を
105-
// 監視しない。そのため "error" を発火すると Event.COMPLETE が来ず、ImageShapeAtom.load()
106-
// の Promise が永久に未解決になり、ページ build (=画面遷移) がローディング画面のまま
107-
// フリーズする。デコードできない 1 枚が全体をブリックするのを防ぐため、失敗時は
108-
// 1x1 透明のプレースホルダを割り当てて "load" として扱う (該当画像だけが空になる)。
109-
if (!ok) {
110-
img->width = 1;
111-
img->height = 1;
112-
img->rgba.assign(4, 0);
113-
}
81+
auto image_ref = std::make_shared<v8::Global<v8::Object>>(isolate, self);
82+
auto decoded = std::make_shared<DecodedImage>();
83+
auto ok = std::make_shared<bool>(false);
84+
85+
decodequeue::Submit(
86+
// プールスレッド: WIC デコードのみ (V8 に触れない)
87+
[input = std::move(input), decoded, ok]() {
88+
*ok = !input.empty() && DecodeImageWithWIC(input, *decoded);
89+
},
90+
// メインスレッド: 結果を image オブジェクトへ反映し load を発火
91+
[isolate, image_ref, decoded, ok]() {
92+
v8::HandleScope hs(isolate);
93+
v8::Local<v8::Object> img_obj = image_ref->Get(isolate);
94+
v8::Local<v8::Context> ctx = img_obj->GetCreationContextChecked();
95+
v8::Context::Scope cs(ctx);
96+
97+
// «堅牢化» デコード失敗時の扱い。
98+
// player の ShapeLoadSrcUseCase は image に "load" ハンドラのみ登録し
99+
// "error" を監視しない。"error" だと Event.COMPLETE が来ず ImageShapeAtom
100+
// .load() の Promise が永久未解決になり、ページ build (=画面遷移) が
101+
// ローディングのままフリーズする。失敗時は 1x1 透明のプレースホルダを
102+
// 割り当てて "load" として扱う (該当画像だけが空になる)。
103+
auto* img = new DecodedImage(std::move(*decoded));
104+
if (!*ok) {
105+
img->width = 1;
106+
img->height = 1;
107+
img->rgba.assign(4, 0);
108+
}
114109

115-
self->SetInternalField(0, v8::External::New(isolate, img));
116-
v8util::AttachWeak(isolate, self, img);
117-
v8util::SetValue(isolate, self, "width",
118-
v8::Integer::NewFromUnsigned(isolate, img->width));
119-
v8util::SetValue(isolate, self, "height",
120-
v8::Integer::NewFromUnsigned(isolate, img->height));
121-
v8util::SetValue(isolate, self, "naturalWidth",
122-
v8::Integer::NewFromUnsigned(isolate, img->width));
123-
v8util::SetValue(isolate, self, "naturalHeight",
124-
v8::Integer::NewFromUnsigned(isolate, img->height));
125-
v8util::SetValue(isolate, self, "complete", v8::Boolean::New(isolate, true));
126-
127-
auto* pending = new PendingImageEvent();
128-
pending->isolate = isolate;
129-
pending->image.Reset(isolate, self);
130-
pending->ok = true; // 常に "load" を発火し、遷移を進行させる
131-
isolate->EnqueueMicrotask(FireImageEvent, pending);
110+
img_obj->SetInternalField(0, v8::External::New(isolate, img));
111+
v8util::AttachWeak(isolate, img_obj, img);
112+
v8util::SetValue(isolate, img_obj, "width",
113+
v8::Integer::NewFromUnsigned(isolate, img->width));
114+
v8util::SetValue(isolate, img_obj, "height",
115+
v8::Integer::NewFromUnsigned(isolate, img->height));
116+
v8util::SetValue(isolate, img_obj, "naturalWidth",
117+
v8::Integer::NewFromUnsigned(isolate, img->width));
118+
v8util::SetValue(isolate, img_obj, "naturalHeight",
119+
v8::Integer::NewFromUnsigned(isolate, img->height));
120+
v8util::SetValue(isolate, img_obj, "complete", v8::Boolean::New(isolate, true));
121+
122+
// 常に "load" を発火し、遷移を進行させる
123+
v8::Local<v8::Object> ev = v8::Object::New(isolate);
124+
v8util::SetValue(isolate, ev, "type", Str(isolate, "load"));
125+
v8util::SetValue(isolate, ev, "target", img_obj);
126+
DispatchEvent(isolate, img_obj, ev);
127+
128+
image_ref->Reset();
129+
});
132130
}
133131

134132
// Image コンストラクタ: src セッターで読み込み+デコードし load イベントを発火する。
@@ -262,14 +260,35 @@ void CreateImageBitmap(const v8::FunctionCallbackInfo<v8::Value>& args)
262260
}
263261
}
264262

265-
auto* img = new DecodedImage();
266-
if (input.empty() || !DecodeImageWithWIC(input, *img)) {
267-
delete img;
263+
if (input.empty()) {
268264
resolver->Reject(ctx, v8::Exception::Error(Str(isolate, "Image decode failed"))).Check();
269265
return;
270266
}
271267

272-
resolver->Resolve(ctx, WrapImageBitmap(isolate, img)).Check();
268+
// WIC デコードはバックグラウンドスレッドへ (Promise なので元々非同期契約)
269+
auto resolver_ref =
270+
std::make_shared<v8::Global<v8::Promise::Resolver>>(isolate, resolver);
271+
auto decoded = std::make_shared<DecodedImage>();
272+
auto ok = std::make_shared<bool>(false);
273+
274+
decodequeue::Submit(
275+
[input = std::move(input), decoded, ok]() {
276+
*ok = DecodeImageWithWIC(input, *decoded);
277+
},
278+
[isolate, resolver_ref, decoded, ok]() {
279+
v8::HandleScope hs(isolate);
280+
v8::Local<v8::Promise::Resolver> r = resolver_ref->Get(isolate);
281+
v8::Local<v8::Context> c = r->GetCreationContextChecked();
282+
v8::Context::Scope cs(c);
283+
if (*ok) {
284+
auto* img = new DecodedImage(std::move(*decoded));
285+
r->Resolve(c, WrapImageBitmap(isolate, img)).Check();
286+
} else {
287+
r->Reject(c, v8::Exception::Error(
288+
Str(isolate, "Image decode failed"))).Check();
289+
}
290+
resolver_ref->Reset();
291+
});
273292
}
274293

275294
} // namespace

templates/xbox/src/main.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include "EmbeddedAssets.h"
1616
#include "EventLoop.h"
1717
#include "gpu/DawnContext.h"
18+
#include "platform/DecodeQueue.h"
1819
#include "platform/GamepadManager.h"
1920
#include "platform/AudioEngine.h"
2021
#include "worker/WorkerRuntime.h"
@@ -557,6 +558,9 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR cmd_line, int)
557558
// 音声: 再生終了した source へ "ended" を配送する
558559
PumpAudioEvents(runtime.isolate());
559560

561+
// バックグラウンドデコード完了の反映 (画像 load イベント / 音声 Promise)
562+
decodequeue::Pump();
563+
560564
// タイマー -> マイクロタスク
561565
event_loop.PumpTimers();
562566
runtime.PumpMicrotasks();
@@ -619,6 +623,7 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR cmd_line, int)
619623
// 8. 後始末: v8::Global を保持するものは Isolate 破棄前に必ず明示解放する。
620624
// (スタック変数はスコープ終了 = runtime.Dispose() の後に巻き戻されるため、
621625
// デストラクタ任せにすると破棄済み Isolate への Global::Reset で fail-fast する)
626+
decodequeue::Shutdown(); // デコードスレッド join + 保留 complete (v8::Global) の破棄
622627
workers.Shutdown(); // WorkerInstance の Global<Context> / EventLoop
623628
event_loop.Shutdown(); // main の setTimeout/rAF コールバック (Global<Function>)
624629
ShutdownAudioEvents();

0 commit comments

Comments
 (0)