Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f945ed68e | ||
|
|
43216a408e | ||
|
|
6ff116ba02 | ||
|
|
1d7c4e7b7d | ||
|
|
c7f7cb6afa | ||
|
|
99ae5b9eda |
@@ -39,6 +39,13 @@
|
|||||||
background: rgba(255,255,255,0.15); color: #eee; cursor: pointer;
|
background: rgba(255,255,255,0.15); color: #eee; cursor: pointer;
|
||||||
}
|
}
|
||||||
#fullscreen-button.hidden { display: none; }
|
#fullscreen-button.hidden { display: none; }
|
||||||
|
|
||||||
|
#dispose-button {
|
||||||
|
position: absolute; top: 12px; left: 12px; z-index: 10;
|
||||||
|
padding: 8px 14px; border-radius: 6px; border: none;
|
||||||
|
background: rgba(220,38,38,0.85); color: #fff; cursor: pointer;
|
||||||
|
}
|
||||||
|
#dispose-button.hidden { display: none; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -54,6 +61,7 @@
|
|||||||
<button id="start-button" disabled>Loading...</button>
|
<button id="start-button" disabled>Loading...</button>
|
||||||
</div>
|
</div>
|
||||||
<button id="fullscreen-button" class="hidden">Fullscreen</button>
|
<button id="fullscreen-button" class="hidden">Fullscreen</button>
|
||||||
|
<button id="dispose-button" class="hidden">Stop & Dispose</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
@@ -66,6 +74,7 @@
|
|||||||
const fileInput = document.getElementById('table-file');
|
const fileInput = document.getElementById('table-file');
|
||||||
const romInput = document.getElementById('rom-file');
|
const romInput = document.getElementById('rom-file');
|
||||||
const fullscreenButton = document.getElementById('fullscreen-button');
|
const fullscreenButton = document.getElementById('fullscreen-button');
|
||||||
|
const disposeButton = document.getElementById('dispose-button');
|
||||||
|
|
||||||
let uploadedTableData;
|
let uploadedTableData;
|
||||||
fileInput.addEventListener('change', async () => {
|
fileInput.addEventListener('change', async () => {
|
||||||
@@ -104,6 +113,7 @@
|
|||||||
pinball.start();
|
pinball.start();
|
||||||
overlay.classList.add('hidden');
|
overlay.classList.add('hidden');
|
||||||
fullscreenButton.classList.remove('hidden');
|
fullscreenButton.classList.remove('hidden');
|
||||||
|
disposeButton.classList.remove('hidden');
|
||||||
|
|
||||||
// Touch controls are additive UI for touch-capable devices - not
|
// Touch controls are additive UI for touch-capable devices - not
|
||||||
// required for desktop mouse+keyboard play.
|
// required for desktop mouse+keyboard play.
|
||||||
@@ -118,6 +128,26 @@
|
|||||||
// browser denies it) - nothing to recover from here.
|
// browser denies it) - nothing to recover from here.
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Mirrors the stop()-then-wait-a-couple-frames-then-dispose() pattern
|
||||||
|
// consumers need: stop() only takes effect on the engine's next
|
||||||
|
// internal step, so disposing synchronously right after can race it.
|
||||||
|
disposeButton.addEventListener('click', () => {
|
||||||
|
console.log('[dispose test] calling stop()...');
|
||||||
|
pinball.stop();
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
console.log('[dispose test] calling dispose()...');
|
||||||
|
pinball.dispose();
|
||||||
|
console.log('[dispose test] dispose() returned - no hang');
|
||||||
|
disposeButton.classList.add('hidden');
|
||||||
|
fullscreenButton.classList.add('hidden');
|
||||||
|
overlay.classList.remove('hidden');
|
||||||
|
startButton.textContent = 'Disposed (reload page to restart)';
|
||||||
|
startButton.disabled = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@valknar/vpinball-wasm",
|
"name": "@valknar/vpinball-wasm",
|
||||||
"version": "0.2.0",
|
"version": "0.3.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@valknar/vpinball-wasm",
|
"name": "@valknar/vpinball-wasm",
|
||||||
"version": "0.2.0",
|
"version": "0.3.3",
|
||||||
"license": "SEE LICENSE IN LICENSE",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.6.0"
|
"typescript": "^5.6.0"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@valknar/vpinball-wasm",
|
"name": "@valknar/vpinball-wasm",
|
||||||
"version": "0.3.0",
|
"version": "0.3.3",
|
||||||
"description": "Visual Pinball's engine compiled to WebAssembly - real .vpx tables, real VBScript, WebGL2 rendering, in the browser",
|
"description": "Visual Pinball's engine compiled to WebAssembly - real .vpx tables, real VBScript, WebGL2 rendering, in the browser",
|
||||||
"license": "SEE LICENSE IN LICENSE",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
diff --git a/include/libwinevbs.h b/include/libwinevbs.h
|
||||||
|
index 3ba4df9..6cac7df 100644
|
||||||
|
--- a/include/libwinevbs.h
|
||||||
|
+++ b/include/libwinevbs.h
|
||||||
|
@@ -52,6 +52,14 @@ LIBWINEVBS_API const char* libwinevbs_hresult_name(HRESULT hr);
|
||||||
|
typedef struct {
|
||||||
|
void (*log)(libwinevbs_log_level_t level, const char* format, va_list args);
|
||||||
|
HRESULT (*create_object)(const WCHAR* progid, IClassFactory* cf, IUnknown** obj);
|
||||||
|
+ /* Answers a VBScript MsgBox() call. Must return synchronously (its result
|
||||||
|
+ is used by the calling script statement immediately) - e.g. a real,
|
||||||
|
+ blocking confirm()/alert() call under Emscripten. type is the raw
|
||||||
|
+ VBScript "buttons" argument (MB_OK=0, MB_OKCANCEL=1, MB_YESNO=4, ...);
|
||||||
|
+ title may be NULL. Return the matching button id (IDOK=1, IDCANCEL=2,
|
||||||
|
+ IDABORT=3, IDRETRY=4, IDIGNORE=5, IDYES=6, IDNO=7), matching vbOK..vbNo.
|
||||||
|
+ If unset, MsgBox always answers IDOK/vbOK without asking anything. */
|
||||||
|
+ int (*msgbox)(const char* prompt, int type, const char* title);
|
||||||
|
} libwinevbs_callbacks_t;
|
||||||
|
|
||||||
|
LIBWINEVBS_API void libwinevbs_init(const libwinevbs_callbacks_t* callbacks);
|
||||||
|
diff --git a/src/libwinevbs.c b/src/libwinevbs.c
|
||||||
|
index 7b3d6f0..91091e4 100644
|
||||||
|
--- a/src/libwinevbs.c
|
||||||
|
+++ b/src/libwinevbs.c
|
||||||
|
@@ -32,6 +32,14 @@ HRESULT libwinevbs_create_object(const WCHAR* progid, IClassFactory* cf, IUnknow
|
||||||
|
return CLASS_E_CLASSNOTAVAILABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
+int libwinevbs_msgbox(const char* prompt, int type, const char* title)
|
||||||
|
+{
|
||||||
|
+ if (g_callbacks.msgbox)
|
||||||
|
+ return g_callbacks.msgbox(prompt, type, title);
|
||||||
|
+
|
||||||
|
+ return 1; /* IDOK/vbOK - no host callback registered, so just proceed */
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
const char* libwinevbs_hresult_name(HRESULT hr)
|
||||||
|
{
|
||||||
|
switch (hr) {
|
||||||
|
diff --git a/wine/dlls/vbscript/global.c b/wine/dlls/vbscript/global.c
|
||||||
|
index 0e0e144..351f967 100644
|
||||||
|
--- a/wine/dlls/vbscript/global.c
|
||||||
|
+++ b/wine/dlls/vbscript/global.c
|
||||||
|
@@ -33,6 +33,7 @@
|
||||||
|
#include <locale.h>
|
||||||
|
#include "scrrun_private.h"
|
||||||
|
HRESULT libwinevbs_create_object(const WCHAR *progid, IClassFactory* cf, IUnknown** obj);
|
||||||
|
+int libwinevbs_msgbox(const char *prompt, int type, const char *title);
|
||||||
|
extern HRESULT WINAPI WshShellFactory_CreateInstance(IClassFactory*,IUnknown*,REFIID,void**);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
@@ -2834,9 +2835,12 @@ static HRESULT Global_MsgBox(BuiltinDisp *This, VARIANT *args, unsigned args_cnt
|
||||||
|
hres = show_msgbox(This->ctx, prompt, type, title, res);
|
||||||
|
#else
|
||||||
|
if(SUCCEEDED(hres)) {
|
||||||
|
- char buf[2048];
|
||||||
|
+ char buf[2048], title_buf[256] = {0};
|
||||||
|
WideCharToMultiByte(CP_ACP, 0, prompt, -1, buf, sizeof(buf) - 1, NULL, NULL);
|
||||||
|
+ if (title)
|
||||||
|
+ WideCharToMultiByte(CP_ACP, 0, title, -1, title_buf, sizeof(title_buf) - 1, NULL, NULL);
|
||||||
|
libwinevbs_log(LIBWINEVBS_LOG_INFO, "vbscript: MsgBox prompt=%s", buf);
|
||||||
|
+ hres = return_short(res, libwinevbs_msgbox(buf, type, title ? title_buf : NULL));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
diff --git a/src/cpuexec.c b/src/cpuexec.c
|
diff --git a/src/cpuexec.c b/src/cpuexec.c
|
||||||
index 07db6a1..0bfcbe7 100644
|
index 07db6a1..19da31a 100644
|
||||||
--- a/src/cpuexec.c
|
--- a/src/cpuexec.c
|
||||||
+++ b/src/cpuexec.c
|
+++ b/src/cpuexec.c
|
||||||
@@ -466,6 +466,65 @@ void cpu_run(void)
|
@@ -466,6 +466,78 @@ void cpu_run(void)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,6 +29,19 @@ index 07db6a1..0bfcbe7 100644
|
|||||||
+ once it has fully quit (cpu_post_run() has already been invoked). */
|
+ once it has fully quit (cpu_post_run() has already been invoked). */
|
||||||
+int cpu_run_emscripten_step(void)
|
+int cpu_run_emscripten_step(void)
|
||||||
+{
|
+{
|
||||||
|
+ /* usrintrf.c's per-frame video update contains a "while (g_fPause) {
|
||||||
|
+ ...; draw_screen(); ... }" busy-wait (see the VPINMAME/LIBPINMAME
|
||||||
|
+ branch around updatescreen()) that assumes a real OS thread will
|
||||||
|
+ later flip g_fPause back to 0 from outside it. We're single-threaded
|
||||||
|
+ here, so entering that loop at all would spin forever burning 100%
|
||||||
|
+ CPU with no way out - nothing else can ever run to clear the flag.
|
||||||
|
+ Skip cpu_timeslice() (and therefore that loop) entirely while paused;
|
||||||
|
+ the emulation simply doesn't advance until Controller.Pause is set
|
||||||
|
+ back to False and this function is called again next frame. */
|
||||||
|
+ extern int g_fPause;
|
||||||
|
+ if (g_fPause)
|
||||||
|
+ return 1;
|
||||||
|
+
|
||||||
+ const double frame_target = timer_get_time() + (1.0 / 60.0);
|
+ const double frame_target = timer_get_time() + (1.0 / 60.0);
|
||||||
+
|
+
|
||||||
+ while (!time_to_quit && !time_to_reset && timer_get_time() < frame_target)
|
+ while (!time_to_quit && !time_to_reset && timer_get_time() < frame_target)
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
diff --git a/src/core/VPApp.cpp b/src/core/VPApp.cpp
|
||||||
|
index 7175dd2..f05a324 100644
|
||||||
|
--- a/src/core/VPApp.cpp
|
||||||
|
+++ b/src/core/VPApp.cpp
|
||||||
|
@@ -36,6 +36,10 @@
|
||||||
|
#include <libwinevbs/libwinevbs.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
+#ifdef __EMSCRIPTEN__
|
||||||
|
+#include <emscripten.h>
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
#include "parts/ball.h"
|
||||||
|
#include "parts/timer.h"
|
||||||
|
#include "parts/flipper.h"
|
||||||
|
@@ -273,6 +277,34 @@ int VPApp::GetLogicalNumberOfProcessors() const
|
||||||
|
return m_logicalNumberOfProcessors;
|
||||||
|
}
|
||||||
|
|
||||||
|
+#ifdef __EMSCRIPTEN__
|
||||||
|
+// VBScript's MsgBox() must return synchronously (its VARIANT result is used
|
||||||
|
+// right after the call, in the same script statement) - window.confirm()/
|
||||||
|
+// alert() are the only browser APIs that block JS execution and return a
|
||||||
|
+// value synchronously, so they're used here directly rather than routing
|
||||||
|
+// through any async/JS-callback mechanism the host page might otherwise want
|
||||||
|
+// to customize the look of (which would require Asyncify to suspend the
|
||||||
|
+// call, since single-threaded builds have no other way to block for it).
|
||||||
|
+// type is the raw VBScript "buttons" argument (MB_OK=0, MB_OKCANCEL=1,
|
||||||
|
+// MB_ABORTRETRYIGNORE=2, MB_YESNOCANCEL=3, MB_YESNO=4, MB_RETRYCANCEL=5).
|
||||||
|
+// The returned id matches vbOK(1)/vbCancel(2)/vbYes(6)/vbNo(7).
|
||||||
|
+static int EmscriptenMsgBox(const char* prompt, int type, const char* title)
|
||||||
|
+{
|
||||||
|
+ const string message = (title && title[0]) ? (string(title) + "\n\n" + prompt) : string(prompt);
|
||||||
|
+ switch (type & 0x0F)
|
||||||
|
+ {
|
||||||
|
+ case 1: // MB_OKCANCEL
|
||||||
|
+ return EM_ASM_INT({ return confirm(UTF8ToString($0)) ? 1 : 0; }, message.c_str()) ? 1 /* IDOK */ : 2 /* IDCANCEL */;
|
||||||
|
+ case 3: // MB_YESNOCANCEL - Cancel isn't distinguishable from No via confirm(), best effort
|
||||||
|
+ case 4: // MB_YESNO
|
||||||
|
+ return EM_ASM_INT({ return confirm(UTF8ToString($0)) ? 1 : 0; }, message.c_str()) ? 6 /* IDYES */ : 7 /* IDNO */;
|
||||||
|
+ default:
|
||||||
|
+ EM_ASM_({ alert(UTF8ToString($0)); }, message.c_str());
|
||||||
|
+ return 1; // IDOK
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
void VPApp::InitInstance()
|
||||||
|
{
|
||||||
|
std::filesystem::path iniFileName = m_commandLineCustomSettingsFileName;
|
||||||
|
@@ -325,6 +357,9 @@ void VPApp::InitInstance()
|
||||||
|
delete[] buffer;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
+#ifdef __EMSCRIPTEN__
|
||||||
|
+ callbacks.msgbox = &EmscriptenMsgBox;
|
||||||
|
+#endif
|
||||||
|
libwinevbs_init(&callbacks);
|
||||||
|
#endif
|
||||||
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
diff --git a/plugins/pinmame/Controller.cpp b/plugins/pinmame/Controller.cpp
|
||||||
|
index 29c2405..a498fee 100644
|
||||||
|
--- a/plugins/pinmame/Controller.cpp
|
||||||
|
+++ b/plugins/pinmame/Controller.cpp
|
||||||
|
@@ -265,8 +265,20 @@ void Controller::Stop()
|
||||||
|
if (PinmameIsRunning())
|
||||||
|
{
|
||||||
|
PinmameStop();
|
||||||
|
+#ifdef __EMSCRIPTEN__
|
||||||
|
+ // PinmameStop() only sets a "please quit" flag here (see its own
|
||||||
|
+ // Emscripten branch) - there's no separate OS thread that will ever
|
||||||
|
+ // notice it and finish stopping on its own, so busy-waiting for
|
||||||
|
+ // PinmameIsRunning() to clear on this single thread would spin
|
||||||
|
+ // forever. Drive one more step directly instead: with the quit flag
|
||||||
|
+ // already set, it's a bounded, instant call that completes the
|
||||||
|
+ // pending teardown (calls cpu_post_run() and OnStateChange(0))
|
||||||
|
+ // synchronously right here.
|
||||||
|
+ PinmameEmscriptenStep();
|
||||||
|
+#else
|
||||||
|
while (PinmameIsRunning() != 0) // Wait until the machine is stopped
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(75));
|
||||||
|
+#endif
|
||||||
|
if (m_onGameEndHandler)
|
||||||
|
m_onGameEndHandler(this);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user