@next2d/builder 0.1.5 → 0.1.7
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.
- package/README.md +15 -0
- package/dist/index.js +315 -3
- package/package.json +10 -8
- package/templates/xbox/CMakeLists.txt +124 -0
- package/templates/xbox/MicrosoftGame.config +36 -0
- package/templates/xbox/README.md +372 -0
- package/templates/xbox/cmake/FetchDawn.cmake +42 -0
- package/templates/xbox/cmake/FindV8.cmake +68 -0
- package/templates/xbox/js/bootstrap.js +143 -0
- package/templates/xbox/js/selftest.js +515 -0
- package/templates/xbox/src/AssetLoader.cpp +106 -0
- package/templates/xbox/src/AssetLoader.h +35 -0
- package/templates/xbox/src/EventLoop.cpp +162 -0
- package/templates/xbox/src/EventLoop.h +73 -0
- package/templates/xbox/src/HostContext.h +58 -0
- package/templates/xbox/src/bindings/Audio.cpp +320 -0
- package/templates/xbox/src/bindings/Bindings.h +75 -0
- package/templates/xbox/src/bindings/Canvas.cpp +171 -0
- package/templates/xbox/src/bindings/Canvas2D.cpp +462 -0
- package/templates/xbox/src/bindings/Console.cpp +52 -0
- package/templates/xbox/src/bindings/DomShims.cpp +221 -0
- package/templates/xbox/src/bindings/EventTarget.cpp +127 -0
- package/templates/xbox/src/bindings/EventTarget.h +17 -0
- package/templates/xbox/src/bindings/Fetch.cpp +149 -0
- package/templates/xbox/src/bindings/Gamepad.cpp +79 -0
- package/templates/xbox/src/bindings/Image.cpp +160 -0
- package/templates/xbox/src/bindings/ImageSource.h +26 -0
- package/templates/xbox/src/bindings/Network.cpp +303 -0
- package/templates/xbox/src/bindings/RasterCore.h +265 -0
- package/templates/xbox/src/bindings/Timers.cpp +87 -0
- package/templates/xbox/src/bindings/Video.cpp +235 -0
- package/templates/xbox/src/bindings/webgpu/WebGPU.cpp +1318 -0
- package/templates/xbox/src/bindings/webgpu/WebGPUCommon.h +119 -0
- package/templates/xbox/src/bindings/webgpu/WebGPUEnums.h +245 -0
- package/templates/xbox/src/gpu/DawnContext.cpp +170 -0
- package/templates/xbox/src/gpu/DawnContext.h +64 -0
- package/templates/xbox/src/main.cpp +368 -0
- package/templates/xbox/src/platform/AudioEngine.cpp +184 -0
- package/templates/xbox/src/platform/AudioEngine.h +65 -0
- package/templates/xbox/src/platform/GamepadManager.cpp +83 -0
- package/templates/xbox/src/platform/GamepadManager.h +40 -0
- package/templates/xbox/src/platform/ImageTypes.h +17 -0
- package/templates/xbox/src/platform/TextRasterizer.cpp +183 -0
- package/templates/xbox/src/platform/TextRasterizer.h +35 -0
- package/templates/xbox/src/platform/WicDecoder.cpp +63 -0
- package/templates/xbox/src/platform/WicDecoder.h +16 -0
- package/templates/xbox/src/v8/V8Runtime.cpp +285 -0
- package/templates/xbox/src/v8/V8Runtime.h +68 -0
- package/templates/xbox/src/v8/V8Util.h +56 -0
- package/templates/xbox/src/worker/WorkerRuntime.cpp +453 -0
- package/templates/xbox/src/worker/WorkerRuntime.h +108 -0
- package/templates/xbox/tests/check_local.sh +56 -0
- package/templates/xbox/tests/raster_test.cpp +279 -0
- package/templates/xbox/tests/windows_platform_test.cpp +187 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
// Next2D Xbox (GDK native) host エントリポイント。
|
|
2
|
+
//
|
|
3
|
+
// GDK ランタイム初期化 -> ウィンドウ生成 -> V8/Dawn/入力/音声のブートストラップ ->
|
|
4
|
+
// bootstrap.js とアプリ本体(ESM)の読み込み -> ゲームループ、という流れ。
|
|
5
|
+
#include <Windows.h>
|
|
6
|
+
#include <windowsx.h>
|
|
7
|
+
#include <XGameRuntime.h>
|
|
8
|
+
|
|
9
|
+
#include "HostContext.h"
|
|
10
|
+
#include "AssetLoader.h"
|
|
11
|
+
#include "EventLoop.h"
|
|
12
|
+
#include "gpu/DawnContext.h"
|
|
13
|
+
#include "platform/GamepadManager.h"
|
|
14
|
+
#include "platform/AudioEngine.h"
|
|
15
|
+
#include "worker/WorkerRuntime.h"
|
|
16
|
+
#include "bindings/Bindings.h"
|
|
17
|
+
#include "bindings/EventTarget.h"
|
|
18
|
+
#include "v8/V8Runtime.h"
|
|
19
|
+
|
|
20
|
+
#include <filesystem>
|
|
21
|
+
#include <fstream>
|
|
22
|
+
#include <functional>
|
|
23
|
+
#include <iostream>
|
|
24
|
+
#include <sstream>
|
|
25
|
+
#include <string>
|
|
26
|
+
|
|
27
|
+
namespace fs = std::filesystem;
|
|
28
|
+
using namespace next2d;
|
|
29
|
+
|
|
30
|
+
namespace {
|
|
31
|
+
|
|
32
|
+
bool g_running = true;
|
|
33
|
+
DawnContext* g_dawn = nullptr;
|
|
34
|
+
HostContext* g_host = nullptr;
|
|
35
|
+
V8Runtime* g_runtime = nullptr;
|
|
36
|
+
|
|
37
|
+
// window(global) / document / 主要 canvas へイベントを配送する。
|
|
38
|
+
// window へのキーボード、canvas へのポインタ/ホイールは player の登録先に合わせている。
|
|
39
|
+
void FireEvent(const char* type, const std::function<void(v8::Isolate*, v8::Local<v8::Object>)>& fill)
|
|
40
|
+
{
|
|
41
|
+
if (!g_runtime || !g_host) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
v8::Isolate* isolate = g_runtime->isolate();
|
|
45
|
+
v8::HandleScope hs(isolate);
|
|
46
|
+
v8::Local<v8::Context> ctx = g_runtime->context();
|
|
47
|
+
v8::Context::Scope cs(ctx);
|
|
48
|
+
|
|
49
|
+
v8::Local<v8::Object> event = v8::Object::New(isolate);
|
|
50
|
+
event->Set(ctx, v8::String::NewFromUtf8(isolate, "type").ToLocalChecked(),
|
|
51
|
+
v8::String::NewFromUtf8(isolate, type).ToLocalChecked()).Check();
|
|
52
|
+
auto noop = [](const v8::FunctionCallbackInfo<v8::Value>&) {};
|
|
53
|
+
event->Set(ctx, v8::String::NewFromUtf8(isolate, "preventDefault").ToLocalChecked(),
|
|
54
|
+
v8::Function::New(ctx, noop).ToLocalChecked()).Check();
|
|
55
|
+
event->Set(ctx, v8::String::NewFromUtf8(isolate, "stopPropagation").ToLocalChecked(),
|
|
56
|
+
v8::Function::New(ctx, noop).ToLocalChecked()).Check();
|
|
57
|
+
fill(isolate, event);
|
|
58
|
+
|
|
59
|
+
// window(global) と document、主要 canvas へ配送
|
|
60
|
+
v8::Local<v8::Object> global = ctx->Global();
|
|
61
|
+
DispatchEvent(isolate, global, event);
|
|
62
|
+
v8::Local<v8::Value> doc;
|
|
63
|
+
if (global->Get(ctx, v8::String::NewFromUtf8(isolate, "document").ToLocalChecked()).ToLocal(&doc) &&
|
|
64
|
+
doc->IsObject()) {
|
|
65
|
+
DispatchEvent(isolate, doc.As<v8::Object>(), event);
|
|
66
|
+
}
|
|
67
|
+
if (!g_host->main_canvas.IsEmpty()) {
|
|
68
|
+
DispatchEvent(isolate, g_host->main_canvas.Get(isolate), event);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static void SetNum(v8::Isolate* iso, v8::Local<v8::Object> o, const char* k, double v)
|
|
73
|
+
{
|
|
74
|
+
v8::Local<v8::Context> c = iso->GetCurrentContext();
|
|
75
|
+
o->Set(c, v8::String::NewFromUtf8(iso, k).ToLocalChecked(), v8::Number::New(iso, v)).Check();
|
|
76
|
+
}
|
|
77
|
+
static void SetBoolProp(v8::Isolate* iso, v8::Local<v8::Object> o, const char* k, bool v)
|
|
78
|
+
{
|
|
79
|
+
v8::Local<v8::Context> c = iso->GetCurrentContext();
|
|
80
|
+
o->Set(c, v8::String::NewFromUtf8(iso, k).ToLocalChecked(), v8::Boolean::New(iso, v)).Check();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
|
|
84
|
+
{
|
|
85
|
+
switch (msg) {
|
|
86
|
+
case WM_CLOSE:
|
|
87
|
+
case WM_DESTROY:
|
|
88
|
+
g_running = false;
|
|
89
|
+
PostQuitMessage(0);
|
|
90
|
+
return 0;
|
|
91
|
+
|
|
92
|
+
// ---- ポインタ (マウス) ----
|
|
93
|
+
case WM_LBUTTONDOWN: case WM_RBUTTONDOWN: case WM_MBUTTONDOWN:
|
|
94
|
+
case WM_LBUTTONUP: case WM_RBUTTONUP: case WM_MBUTTONUP:
|
|
95
|
+
case WM_MOUSEMOVE: {
|
|
96
|
+
const double px = GET_X_LPARAM(lparam);
|
|
97
|
+
const double py = GET_Y_LPARAM(lparam);
|
|
98
|
+
const int button = (msg == WM_RBUTTONDOWN || msg == WM_RBUTTONUP) ? 2
|
|
99
|
+
: (msg == WM_MBUTTONDOWN || msg == WM_MBUTTONUP) ? 1 : 0;
|
|
100
|
+
const char* type =
|
|
101
|
+
(msg == WM_MOUSEMOVE) ? "pointermove" :
|
|
102
|
+
(msg == WM_LBUTTONUP || msg == WM_RBUTTONUP || msg == WM_MBUTTONUP) ? "pointerup" : "pointerdown";
|
|
103
|
+
FireEvent(type, [=](v8::Isolate* iso, v8::Local<v8::Object> e) {
|
|
104
|
+
SetNum(iso, e, "clientX", px); SetNum(iso, e, "clientY", py);
|
|
105
|
+
SetNum(iso, e, "offsetX", px); SetNum(iso, e, "offsetY", py);
|
|
106
|
+
SetNum(iso, e, "pageX", px); SetNum(iso, e, "pageY", py);
|
|
107
|
+
SetNum(iso, e, "button", button);
|
|
108
|
+
SetNum(iso, e, "pointerId", 1);
|
|
109
|
+
v8::Local<v8::Context> c = iso->GetCurrentContext();
|
|
110
|
+
e->Set(c, v8::String::NewFromUtf8(iso, "pointerType").ToLocalChecked(),
|
|
111
|
+
v8::String::NewFromUtf8(iso, "mouse").ToLocalChecked()).Check();
|
|
112
|
+
});
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
case WM_MOUSEWHEEL: {
|
|
116
|
+
const double delta = -static_cast<double>(GET_WHEEL_DELTA_WPARAM(wparam));
|
|
117
|
+
FireEvent("wheel", [=](v8::Isolate* iso, v8::Local<v8::Object> e) {
|
|
118
|
+
SetNum(iso, e, "deltaY", delta); SetNum(iso, e, "deltaX", 0);
|
|
119
|
+
});
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---- キーボード ----
|
|
124
|
+
case WM_KEYDOWN: case WM_KEYUP: {
|
|
125
|
+
const int vk = static_cast<int>(wparam);
|
|
126
|
+
const bool down = (msg == WM_KEYDOWN);
|
|
127
|
+
const bool repeat = down && (lparam & (1 << 30)) != 0;
|
|
128
|
+
FireEvent(down ? "keydown" : "keyup", [=](v8::Isolate* iso, v8::Local<v8::Object> e) {
|
|
129
|
+
SetNum(iso, e, "keyCode", vk);
|
|
130
|
+
SetNum(iso, e, "which", vk);
|
|
131
|
+
SetBoolProp(iso, e, "repeat", repeat);
|
|
132
|
+
SetBoolProp(iso, e, "shiftKey", (GetKeyState(VK_SHIFT) & 0x8000) != 0);
|
|
133
|
+
SetBoolProp(iso, e, "ctrlKey", (GetKeyState(VK_CONTROL) & 0x8000) != 0);
|
|
134
|
+
SetBoolProp(iso, e, "altKey", (GetKeyState(VK_MENU) & 0x8000) != 0);
|
|
135
|
+
// key: A-Z/0-9 のみ簡易マッピング (それ以外は空)
|
|
136
|
+
char key[2] = {0, 0};
|
|
137
|
+
if ((vk >= 'A' && vk <= 'Z') || (vk >= '0' && vk <= '9')) key[0] = static_cast<char>(vk);
|
|
138
|
+
v8::Local<v8::Context> c = iso->GetCurrentContext();
|
|
139
|
+
e->Set(c, v8::String::NewFromUtf8(iso, "key").ToLocalChecked(),
|
|
140
|
+
v8::String::NewFromUtf8(iso, key).ToLocalChecked()).Check();
|
|
141
|
+
});
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
case WM_SIZE:
|
|
146
|
+
if (g_dawn && g_host && wparam != SIZE_MINIMIZED) {
|
|
147
|
+
const uint32_t w = LOWORD(lparam);
|
|
148
|
+
const uint32_t h = HIWORD(lparam);
|
|
149
|
+
if (w > 0 && h > 0) {
|
|
150
|
+
g_host->viewport_width = static_cast<int>(w);
|
|
151
|
+
g_host->viewport_height = static_cast<int>(h);
|
|
152
|
+
if (g_dawn->ready()) {
|
|
153
|
+
g_dawn->Configure(w, h);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return 0;
|
|
158
|
+
default:
|
|
159
|
+
return DefWindowProc(hwnd, msg, wparam, lparam);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
std::string ReadFile(const fs::path& path)
|
|
164
|
+
{
|
|
165
|
+
std::ifstream ifs(path, std::ios::binary);
|
|
166
|
+
if (!ifs) {
|
|
167
|
+
return {};
|
|
168
|
+
}
|
|
169
|
+
std::ostringstream oss;
|
|
170
|
+
oss << ifs.rdbuf();
|
|
171
|
+
return oss.str();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 実行ファイル隣接ディレクトリを基準に絶対パスを得る。
|
|
175
|
+
fs::path ExeDir()
|
|
176
|
+
{
|
|
177
|
+
wchar_t buffer[MAX_PATH] = {};
|
|
178
|
+
GetModuleFileNameW(nullptr, buffer, MAX_PATH);
|
|
179
|
+
return fs::path(buffer).parent_path();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
HWND CreateHostWindow(HINSTANCE instance, int width, int height)
|
|
183
|
+
{
|
|
184
|
+
WNDCLASSEXW wc = {};
|
|
185
|
+
wc.cbSize = sizeof(wc);
|
|
186
|
+
wc.style = CS_HREDRAW | CS_VREDRAW;
|
|
187
|
+
wc.lpfnWndProc = WndProc;
|
|
188
|
+
wc.hInstance = instance;
|
|
189
|
+
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
|
190
|
+
wc.lpszClassName = L"Next2DXboxHostWindow";
|
|
191
|
+
RegisterClassExW(&wc);
|
|
192
|
+
|
|
193
|
+
// コンソールでは全画面相当。PC(GDK) 検証ではウィンドウ。
|
|
194
|
+
const DWORD style = WS_OVERLAPPEDWINDOW | WS_VISIBLE;
|
|
195
|
+
return CreateWindowExW(
|
|
196
|
+
0, wc.lpszClassName, L"Next2D",
|
|
197
|
+
style, CW_USEDEFAULT, CW_USEDEFAULT, width, height,
|
|
198
|
+
nullptr, nullptr, instance, nullptr);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
} // namespace
|
|
202
|
+
|
|
203
|
+
int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR cmd_line, int)
|
|
204
|
+
{
|
|
205
|
+
// --selftest: アプリの代わりに js/selftest.js を実行し、全バインディングを
|
|
206
|
+
// 実機/PC(GDK) 上で検証して終了する (テスト完了で自動終了)。
|
|
207
|
+
const bool selftest = cmd_line && wcsstr(cmd_line, L"--selftest") != nullptr;
|
|
208
|
+
|
|
209
|
+
// 1. GDK ランタイム初期化
|
|
210
|
+
if (FAILED(XGameRuntimeInitialize())) {
|
|
211
|
+
std::cerr << "XGameRuntimeInitialize failed" << std::endl;
|
|
212
|
+
return 1;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const fs::path exe_dir = ExeDir();
|
|
216
|
+
const fs::path assets_app = exe_dir / "assets" / "app";
|
|
217
|
+
const fs::path bootstrap_js = exe_dir / "js" / "bootstrap.js";
|
|
218
|
+
const fs::path selftest_js = exe_dir / "js" / "selftest.js";
|
|
219
|
+
|
|
220
|
+
// 2. HostContext と論理ビューポート
|
|
221
|
+
HostContext host;
|
|
222
|
+
host.viewport_width = 1920;
|
|
223
|
+
host.viewport_height = 1080;
|
|
224
|
+
g_host = &host;
|
|
225
|
+
|
|
226
|
+
// 3. ウィンドウ
|
|
227
|
+
HWND hwnd = CreateHostWindow(instance, host.viewport_width, host.viewport_height);
|
|
228
|
+
if (!hwnd) {
|
|
229
|
+
std::cerr << "CreateWindow failed" << std::endl;
|
|
230
|
+
return 1;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 4. V8 プロセス初期化 + Isolate/Context + バインディング
|
|
234
|
+
V8Runtime::InitializeProcess(nullptr);
|
|
235
|
+
V8Runtime runtime;
|
|
236
|
+
if (!runtime.Initialize(&host)) {
|
|
237
|
+
std::cerr << "V8 init failed" << std::endl;
|
|
238
|
+
return 1;
|
|
239
|
+
}
|
|
240
|
+
g_runtime = &runtime;
|
|
241
|
+
|
|
242
|
+
// 5. サブシステム生成と HostContext への接続
|
|
243
|
+
EventLoop event_loop(runtime.isolate());
|
|
244
|
+
DawnContext dawn;
|
|
245
|
+
AssetLoader assets(assets_app.string());
|
|
246
|
+
GamepadManager gamepad;
|
|
247
|
+
AudioEngine audio;
|
|
248
|
+
WorkerRuntime workers(runtime.isolate(), &host);
|
|
249
|
+
|
|
250
|
+
host.event_loop = &event_loop;
|
|
251
|
+
host.gpu = &dawn;
|
|
252
|
+
host.assets = &assets;
|
|
253
|
+
host.gamepad = &gamepad;
|
|
254
|
+
host.audio = &audio;
|
|
255
|
+
host.workers = &workers;
|
|
256
|
+
g_dawn = &dawn;
|
|
257
|
+
|
|
258
|
+
dawn.Initialize(hwnd, host.viewport_width, host.viewport_height);
|
|
259
|
+
gamepad.Initialize();
|
|
260
|
+
audio.Initialize();
|
|
261
|
+
|
|
262
|
+
// main Context に EventLoop を紐付け、Worker コンストラクタを設置する。
|
|
263
|
+
// (V8Runtime::Initialize 時点では EventLoop/WorkerRuntime 未生成のためここで行う)
|
|
264
|
+
{
|
|
265
|
+
v8::Isolate::Scope iso_scope(runtime.isolate());
|
|
266
|
+
v8::HandleScope hs(runtime.isolate());
|
|
267
|
+
v8::Local<v8::Context> ctx = runtime.context();
|
|
268
|
+
v8::Context::Scope cs(ctx);
|
|
269
|
+
ctx->SetAlignedPointerInEmbedderData(kEventLoopEmbedderSlot, &event_loop);
|
|
270
|
+
InstallWorker(runtime.isolate(), ctx->Global(), &host);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 6. bootstrap.js -> アプリ本体(ESM) の順で読み込み
|
|
274
|
+
{
|
|
275
|
+
const std::string boot = ReadFile(bootstrap_js);
|
|
276
|
+
if (boot.empty()) {
|
|
277
|
+
std::cerr << "bootstrap.js not found: " << bootstrap_js << std::endl;
|
|
278
|
+
} else if (!runtime.RunScript(boot, "js/bootstrap.js")) {
|
|
279
|
+
std::cerr << "bootstrap.js execution failed" << std::endl;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (selftest) {
|
|
283
|
+
const std::string script = ReadFile(selftest_js);
|
|
284
|
+
if (script.empty()) {
|
|
285
|
+
std::cerr << "selftest.js not found: " << selftest_js << std::endl;
|
|
286
|
+
return 1;
|
|
287
|
+
}
|
|
288
|
+
if (!runtime.RunScript(script, "js/selftest.js")) {
|
|
289
|
+
std::cerr << "selftest.js execution failed" << std::endl;
|
|
290
|
+
return 1;
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
auto entry = assets.ResolveEntryModule();
|
|
294
|
+
if (!entry) {
|
|
295
|
+
std::cerr << "Application entry module not found under assets/app" << std::endl;
|
|
296
|
+
} else if (!runtime.RunModule("", *entry)) {
|
|
297
|
+
std::cerr << "Application module execution failed" << std::endl;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// 7. ゲームループ
|
|
303
|
+
v8::Isolate::Scope isolate_scope(runtime.isolate());
|
|
304
|
+
MSG msg = {};
|
|
305
|
+
int exit_code = 0;
|
|
306
|
+
while (g_running) {
|
|
307
|
+
// Win32 メッセージ
|
|
308
|
+
while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
|
|
309
|
+
if (msg.message == WM_QUIT) {
|
|
310
|
+
g_running = false;
|
|
311
|
+
}
|
|
312
|
+
TranslateMessage(&msg);
|
|
313
|
+
DispatchMessage(&msg);
|
|
314
|
+
}
|
|
315
|
+
if (!g_running) {
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const double now = event_loop.Now();
|
|
320
|
+
|
|
321
|
+
v8::HandleScope handle_scope(runtime.isolate());
|
|
322
|
+
v8::Local<v8::Context> ctx = runtime.context();
|
|
323
|
+
v8::Context::Scope context_scope(ctx);
|
|
324
|
+
|
|
325
|
+
// 入力
|
|
326
|
+
gamepad.Poll(now);
|
|
327
|
+
|
|
328
|
+
// 音声: 再生終了した source へ "ended" を配送する
|
|
329
|
+
PumpAudioEvents(runtime.isolate());
|
|
330
|
+
|
|
331
|
+
// タイマー -> マイクロタスク
|
|
332
|
+
event_loop.PumpTimers();
|
|
333
|
+
runtime.PumpMicrotasks();
|
|
334
|
+
|
|
335
|
+
// メインスレッドの requestAnimationFrame (アプリのロジック/描画コマンド生成)
|
|
336
|
+
event_loop.RunAnimationFrame(now);
|
|
337
|
+
runtime.PumpMicrotasks();
|
|
338
|
+
|
|
339
|
+
// Worker (レンダラ等) のメッセージ配送 + rAF。
|
|
340
|
+
// レンダラ worker はここで WebGPU コマンドを発行し submit する。
|
|
341
|
+
workers.Pump(now);
|
|
342
|
+
|
|
343
|
+
// V8 プラットフォームタスク + Dawn の非同期処理
|
|
344
|
+
runtime.PumpPlatformTasks();
|
|
345
|
+
dawn.Tick();
|
|
346
|
+
|
|
347
|
+
// 提示
|
|
348
|
+
dawn.Present();
|
|
349
|
+
|
|
350
|
+
// selftest 完了検知: selftest.js が globalThis.__selftestExitCode を設定したら終了
|
|
351
|
+
if (selftest) {
|
|
352
|
+
v8::Local<v8::Value> code;
|
|
353
|
+
if (ctx->Global()->Get(ctx, v8::String::NewFromUtf8Literal(
|
|
354
|
+
runtime.isolate(), "__selftestExitCode")).ToLocal(&code) &&
|
|
355
|
+
code->IsNumber()) {
|
|
356
|
+
exit_code = static_cast<int>(code.As<v8::Number>()->Value());
|
|
357
|
+
g_running = false;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// 8. 後始末
|
|
363
|
+
host.main_canvas.Reset();
|
|
364
|
+
runtime.Dispose();
|
|
365
|
+
V8Runtime::ShutdownProcess();
|
|
366
|
+
XGameRuntimeUninitialize();
|
|
367
|
+
return exit_code;
|
|
368
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#include "AudioEngine.h"
|
|
2
|
+
|
|
3
|
+
#include <xaudio2.h>
|
|
4
|
+
#include <mfapi.h>
|
|
5
|
+
#include <mfidl.h>
|
|
6
|
+
#include <mfreadwrite.h>
|
|
7
|
+
#include <shlwapi.h>
|
|
8
|
+
#include <wrl/client.h>
|
|
9
|
+
#include <iostream>
|
|
10
|
+
|
|
11
|
+
#pragma comment(lib, "mfplat.lib")
|
|
12
|
+
#pragma comment(lib, "mfreadwrite.lib")
|
|
13
|
+
#pragma comment(lib, "mfuuid.lib")
|
|
14
|
+
#pragma comment(lib, "shlwapi.lib")
|
|
15
|
+
|
|
16
|
+
using Microsoft::WRL::ComPtr;
|
|
17
|
+
|
|
18
|
+
namespace next2d {
|
|
19
|
+
|
|
20
|
+
AudioVoice::AudioVoice(IXAudio2* xaudio, std::shared_ptr<PcmBuffer> buffer)
|
|
21
|
+
: buffer_(std::move(buffer))
|
|
22
|
+
{
|
|
23
|
+
WAVEFORMATEX wfx = {};
|
|
24
|
+
wfx.wFormatTag = WAVE_FORMAT_IEEE_FLOAT;
|
|
25
|
+
wfx.nChannels = static_cast<WORD>(buffer_->channels);
|
|
26
|
+
wfx.nSamplesPerSec = buffer_->sample_rate;
|
|
27
|
+
wfx.wBitsPerSample = 32;
|
|
28
|
+
wfx.nBlockAlign = wfx.nChannels * wfx.wBitsPerSample / 8;
|
|
29
|
+
wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign;
|
|
30
|
+
|
|
31
|
+
xaudio->CreateSourceVoice(&voice_, &wfx);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
AudioVoice::~AudioVoice()
|
|
35
|
+
{
|
|
36
|
+
if (voice_) {
|
|
37
|
+
voice_->Stop();
|
|
38
|
+
voice_->DestroyVoice();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
void AudioVoice::Start(bool loop)
|
|
43
|
+
{
|
|
44
|
+
if (!voice_) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
XAUDIO2_BUFFER buf = {};
|
|
48
|
+
buf.AudioBytes = static_cast<UINT32>(buffer_->samples.size() * sizeof(float));
|
|
49
|
+
buf.pAudioData = reinterpret_cast<const BYTE*>(buffer_->samples.data());
|
|
50
|
+
buf.Flags = XAUDIO2_END_OF_STREAM;
|
|
51
|
+
buf.LoopCount = loop ? XAUDIO2_LOOP_INFINITE : 0;
|
|
52
|
+
voice_->SubmitSourceBuffer(&buf);
|
|
53
|
+
voice_->Start(0);
|
|
54
|
+
started_ = true;
|
|
55
|
+
looping_ = loop;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
void AudioVoice::Stop()
|
|
59
|
+
{
|
|
60
|
+
if (voice_) {
|
|
61
|
+
voice_->Stop();
|
|
62
|
+
voice_->FlushSourceBuffers();
|
|
63
|
+
}
|
|
64
|
+
started_ = false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
bool AudioVoice::IsFinished() const
|
|
68
|
+
{
|
|
69
|
+
if (!voice_ || !started_ || looping_) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
XAUDIO2_VOICE_STATE state = {};
|
|
73
|
+
voice_->GetState(&state);
|
|
74
|
+
return state.BuffersQueued == 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
void AudioVoice::SetVolume(float v)
|
|
78
|
+
{
|
|
79
|
+
if (voice_) {
|
|
80
|
+
voice_->SetVolume(v);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
AudioEngine::AudioEngine() = default;
|
|
85
|
+
|
|
86
|
+
AudioEngine::~AudioEngine()
|
|
87
|
+
{
|
|
88
|
+
if (master_) {
|
|
89
|
+
master_->DestroyVoice();
|
|
90
|
+
}
|
|
91
|
+
if (xaudio_) {
|
|
92
|
+
xaudio_->Release();
|
|
93
|
+
}
|
|
94
|
+
MFShutdown();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
bool AudioEngine::Initialize()
|
|
98
|
+
{
|
|
99
|
+
if (FAILED(MFStartup(MF_VERSION))) {
|
|
100
|
+
std::cerr << "[Audio] MFStartup failed" << std::endl;
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
if (FAILED(XAudio2Create(&xaudio_, 0, XAUDIO2_DEFAULT_PROCESSOR))) {
|
|
104
|
+
std::cerr << "[Audio] XAudio2Create failed" << std::endl;
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
if (FAILED(xaudio_->CreateMasteringVoice(&master_))) {
|
|
108
|
+
std::cerr << "[Audio] CreateMasteringVoice failed" << std::endl;
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
bool AudioEngine::Decode(const std::vector<uint8_t>& input, PcmBuffer& out)
|
|
115
|
+
{
|
|
116
|
+
// メモリストリームから Media Foundation の SourceReader を作り、
|
|
117
|
+
// 32bit float PCM へ変換して読み出す。
|
|
118
|
+
ComPtr<IStream> stream =
|
|
119
|
+
SHCreateMemStream(input.data(), static_cast<UINT>(input.size()));
|
|
120
|
+
if (!stream) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
ComPtr<IMFByteStream> byte_stream;
|
|
125
|
+
if (FAILED(MFCreateMFByteStreamOnStream(stream.Get(), &byte_stream))) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
ComPtr<IMFSourceReader> reader;
|
|
130
|
+
if (FAILED(MFCreateSourceReaderFromByteStream(byte_stream.Get(), nullptr, &reader))) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 出力を float PCM に指定
|
|
135
|
+
ComPtr<IMFMediaType> pcm_type;
|
|
136
|
+
MFCreateMediaType(&pcm_type);
|
|
137
|
+
pcm_type->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio);
|
|
138
|
+
pcm_type->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_Float);
|
|
139
|
+
if (FAILED(reader->SetCurrentMediaType(
|
|
140
|
+
MF_SOURCE_READER_FIRST_AUDIO_STREAM, nullptr, pcm_type.Get()))) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
ComPtr<IMFMediaType> actual;
|
|
145
|
+
reader->GetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, &actual);
|
|
146
|
+
UINT32 channels = 2, sample_rate = 48000;
|
|
147
|
+
actual->GetUINT32(MF_MT_AUDIO_NUM_CHANNELS, &channels);
|
|
148
|
+
actual->GetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, &sample_rate);
|
|
149
|
+
out.channels = channels;
|
|
150
|
+
out.sample_rate = sample_rate;
|
|
151
|
+
|
|
152
|
+
for (;;) {
|
|
153
|
+
DWORD flags = 0;
|
|
154
|
+
ComPtr<IMFSample> sample;
|
|
155
|
+
if (FAILED(reader->ReadSample(MF_SOURCE_READER_FIRST_AUDIO_STREAM, 0,
|
|
156
|
+
nullptr, &flags, nullptr, &sample))) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
if (flags & MF_SOURCE_READERF_ENDOFSTREAM) {
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
if (!sample) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
ComPtr<IMFMediaBuffer> media_buffer;
|
|
167
|
+
sample->ConvertToContiguousBuffer(&media_buffer);
|
|
168
|
+
BYTE* data = nullptr;
|
|
169
|
+
DWORD length = 0;
|
|
170
|
+
media_buffer->Lock(&data, nullptr, &length);
|
|
171
|
+
const float* f = reinterpret_cast<const float*>(data);
|
|
172
|
+
out.samples.insert(out.samples.end(), f, f + length / sizeof(float));
|
|
173
|
+
media_buffer->Unlock();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return !out.samples.empty();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
std::shared_ptr<AudioVoice> AudioEngine::CreateVoice(std::shared_ptr<PcmBuffer> buffer)
|
|
180
|
+
{
|
|
181
|
+
return std::make_shared<AudioVoice>(xaudio_, std::move(buffer));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
} // namespace next2d
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// AudioEngine: XAudio2 のマスタリングボイスを保持し、PCM バッファを再生する。
|
|
2
|
+
// WebAudio の最小サブセット (decode 済み PCM の play/stop/volume/loop) を支える。
|
|
3
|
+
//
|
|
4
|
+
// NOTE: 完全な WebAudio グラフ (AnalyserNode, フィルタ, パンナー等) は未対応。
|
|
5
|
+
// Next2D の効果音/BGM 再生に必要な範囲を実装する。拡張は README 参照。
|
|
6
|
+
#pragma once
|
|
7
|
+
|
|
8
|
+
#include <cstdint>
|
|
9
|
+
#include <memory>
|
|
10
|
+
#include <vector>
|
|
11
|
+
|
|
12
|
+
struct IXAudio2;
|
|
13
|
+
struct IXAudio2MasteringVoice;
|
|
14
|
+
struct IXAudio2SourceVoice;
|
|
15
|
+
|
|
16
|
+
namespace next2d {
|
|
17
|
+
|
|
18
|
+
// 32bit float インターリーブ PCM
|
|
19
|
+
struct PcmBuffer {
|
|
20
|
+
uint32_t sample_rate = 48000;
|
|
21
|
+
uint32_t channels = 2;
|
|
22
|
+
std::vector<float> samples; // interleaved
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// 単一の再生ボイス。start/stop/volume を提供する。実装は AudioEngine.cpp。
|
|
26
|
+
class AudioVoice {
|
|
27
|
+
public:
|
|
28
|
+
AudioVoice(IXAudio2* xaudio, std::shared_ptr<PcmBuffer> buffer);
|
|
29
|
+
~AudioVoice();
|
|
30
|
+
|
|
31
|
+
void Start(bool loop);
|
|
32
|
+
void Stop();
|
|
33
|
+
void SetVolume(float v);
|
|
34
|
+
|
|
35
|
+
// 非ループ再生でキューが空になった (= 再生終了) かどうか。"ended" 発火判定に使う。
|
|
36
|
+
bool IsFinished() const;
|
|
37
|
+
|
|
38
|
+
private:
|
|
39
|
+
std::shared_ptr<PcmBuffer> buffer_;
|
|
40
|
+
IXAudio2SourceVoice* voice_ = nullptr;
|
|
41
|
+
bool started_ = false;
|
|
42
|
+
bool looping_ = false;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
class AudioEngine {
|
|
46
|
+
public:
|
|
47
|
+
AudioEngine();
|
|
48
|
+
~AudioEngine();
|
|
49
|
+
|
|
50
|
+
bool Initialize();
|
|
51
|
+
|
|
52
|
+
// Media Foundation で圧縮音声(mp3/aac/wav 等)を PCM float へデコードする。
|
|
53
|
+
static bool Decode(const std::vector<uint8_t>& input, PcmBuffer& out);
|
|
54
|
+
|
|
55
|
+
// 再生ボイスを生成する。呼び出し側が寿命を管理する。
|
|
56
|
+
std::shared_ptr<AudioVoice> CreateVoice(std::shared_ptr<PcmBuffer> buffer);
|
|
57
|
+
|
|
58
|
+
IXAudio2* xaudio() const { return xaudio_; }
|
|
59
|
+
|
|
60
|
+
private:
|
|
61
|
+
IXAudio2* xaudio_ = nullptr;
|
|
62
|
+
IXAudio2MasteringVoice* master_ = nullptr;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
} // namespace next2d
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#include "GamepadManager.h"
|
|
2
|
+
|
|
3
|
+
#include <GameInput.h>
|
|
4
|
+
#include <iostream>
|
|
5
|
+
|
|
6
|
+
namespace next2d {
|
|
7
|
+
|
|
8
|
+
GamepadManager::GamepadManager() = default;
|
|
9
|
+
|
|
10
|
+
GamepadManager::~GamepadManager()
|
|
11
|
+
{
|
|
12
|
+
if (game_input_) {
|
|
13
|
+
game_input_->Release();
|
|
14
|
+
game_input_ = nullptr;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
bool GamepadManager::Initialize()
|
|
19
|
+
{
|
|
20
|
+
if (FAILED(GameInputCreate(&game_input_))) {
|
|
21
|
+
std::cerr << "[Gamepad] GameInputCreate failed" << std::endl;
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
snapshots_.resize(4);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
void GamepadManager::Poll(double now_ms)
|
|
29
|
+
{
|
|
30
|
+
for (auto& snap : snapshots_) {
|
|
31
|
+
snap.connected = false;
|
|
32
|
+
}
|
|
33
|
+
if (!game_input_) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
IGameInputReading* reading = nullptr;
|
|
38
|
+
if (FAILED(game_input_->GetCurrentReading(GameInputKindGamepad, nullptr, &reading))) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
GameInputGamepadState state = {};
|
|
43
|
+
if (reading->GetGamepadState(&state)) {
|
|
44
|
+
GamepadSnapshot& snap = snapshots_[0];
|
|
45
|
+
snap.connected = true;
|
|
46
|
+
snap.timestamp = now_ms;
|
|
47
|
+
|
|
48
|
+
// 軸 (Y は W3C 仕様に合わせ反転)
|
|
49
|
+
snap.axes[0] = state.leftThumbstickX;
|
|
50
|
+
snap.axes[1] = -state.leftThumbstickY;
|
|
51
|
+
snap.axes[2] = state.rightThumbstickX;
|
|
52
|
+
snap.axes[3] = -state.rightThumbstickY;
|
|
53
|
+
|
|
54
|
+
auto set = [&](int index, bool down, float value) {
|
|
55
|
+
snap.pressed[index] = down;
|
|
56
|
+
snap.buttons[index] = value;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const auto btn = state.buttons;
|
|
60
|
+
// standard mapping index 準拠
|
|
61
|
+
set(0, btn & GameInputGamepadA, (btn & GameInputGamepadA) ? 1.f : 0.f);
|
|
62
|
+
set(1, btn & GameInputGamepadB, (btn & GameInputGamepadB) ? 1.f : 0.f);
|
|
63
|
+
set(2, btn & GameInputGamepadX, (btn & GameInputGamepadX) ? 1.f : 0.f);
|
|
64
|
+
set(3, btn & GameInputGamepadY, (btn & GameInputGamepadY) ? 1.f : 0.f);
|
|
65
|
+
set(4, btn & GameInputGamepadLeftShoulder, (btn & GameInputGamepadLeftShoulder) ? 1.f : 0.f);
|
|
66
|
+
set(5, btn & GameInputGamepadRightShoulder, (btn & GameInputGamepadRightShoulder) ? 1.f : 0.f);
|
|
67
|
+
set(6, state.leftTrigger > 0.f, state.leftTrigger);
|
|
68
|
+
set(7, state.rightTrigger > 0.f, state.rightTrigger);
|
|
69
|
+
set(8, btn & GameInputGamepadView, (btn & GameInputGamepadView) ? 1.f : 0.f);
|
|
70
|
+
set(9, btn & GameInputGamepadMenu, (btn & GameInputGamepadMenu) ? 1.f : 0.f);
|
|
71
|
+
set(10, btn & GameInputGamepadLeftThumbstick, (btn & GameInputGamepadLeftThumbstick) ? 1.f : 0.f);
|
|
72
|
+
set(11, btn & GameInputGamepadRightThumbstick, (btn & GameInputGamepadRightThumbstick) ? 1.f : 0.f);
|
|
73
|
+
set(12, btn & GameInputGamepadDPadUp, (btn & GameInputGamepadDPadUp) ? 1.f : 0.f);
|
|
74
|
+
set(13, btn & GameInputGamepadDPadDown, (btn & GameInputGamepadDPadDown) ? 1.f : 0.f);
|
|
75
|
+
set(14, btn & GameInputGamepadDPadLeft, (btn & GameInputGamepadDPadLeft) ? 1.f : 0.f);
|
|
76
|
+
set(15, btn & GameInputGamepadDPadRight, (btn & GameInputGamepadDPadRight) ? 1.f : 0.f);
|
|
77
|
+
set(16, false, 0.f); // guide ボタンは GameInput では非公開
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
reading->Release();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
} // namespace next2d
|