11 Commits
Author SHA1 Message Date
valknar 10f32789e6 Trigger CI to verify simplified embedded-cache config
CI / Build wasm engine (push) Successful in 8m48s
2026-08-24 12:01:25 +02:00
valknar bf818bdd29 Trigger CI to verify gitea/runner migration + cache config fix
CI / Build wasm engine (push) Canceled after 4m37s
2026-08-24 11:56:29 +02:00
valknarandClaude Sonnet 5 4946dd2033 Trigger CI again after force-recreating the stale runner daemon
CI / Build wasm engine (push) Canceled after 1m16s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 11:43:24 +02:00
valknarandClaude Sonnet 5 7e5793bb17 Trigger CI to test the dedicated cache-server setup
CI / Build wasm engine (push) Canceled after 4m13s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 11:39:56 +02:00
valknarandClaude Sonnet 5 f52fa0cb79 Trigger CI with debug logging to capture the exact cache URL
CI / Build wasm engine (push) Successful in 11m17s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 10:54:35 +02:00
valknarandClaude Sonnet 5 9c8afb7541 Trigger CI again to inspect the live job container's network
CI / Build wasm engine (push) Canceled after 5m3s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 10:49:51 +02:00
valknarandClaude Sonnet 5 02f17165f2 Trigger CI to verify Gitea Actions cache fix (act_runner network/host config)
CI / Build wasm engine (push) Successful in 11m23s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 10:33:08 +02:00
valknarandClaude Sonnet 5 0f945ed68e Bump version to 0.3.3 for the dispose() hang fix
CI / Build wasm engine (push) Successful in 11m16s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 10:12:51 +02:00
valknarandClaude Sonnet 5 43216a408e Fix PinMAME busy-wait freeze on Controller.Stop() (dispose() hang)
Controller::Stop() (real desktop vpinball code, unmodified until now)
busy-waits for PinmameIsRunning() to clear after calling PinmameStop(),
in a sleep loop meant for a real OS thread to notice the quit flag and
finish stopping on its own. Under Emscripten there is no such thread -
PinmameStop() there only sets the quit flag - so the loop spun forever
on the single available thread, with nothing left to ever clear it.

This fires from Player::~Player()'s GameEvents_Exit script event
(controller.vbs's default Exit handler calls Controller.Stop), which
runs synchronously inside dispose() - hanging any consumer's teardown
for a ROM-based table, most visibly React StrictMode/unmount cleanup
calling stop() then dispose() shortly after.

Fixed by driving one more (now-instant, since the quit flag is already
set) PinmameEmscriptenStep() call directly under __EMSCRIPTEN__ instead
of busy-waiting - it runs cpu_post_run() and OnStateChange(0)
synchronously right there.

Also adds a "Stop & Dispose" button to the basic example, exercising
the same stop()-then-wait-two-frames-then-dispose() sequence consumers
use, to make this kind of regression visible without a separate app.

Confirmed fixed by hands-on testing: dispose() on a running ROM-based
table now returns immediately instead of hanging the tab.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 10:12:25 +02:00
valknarandClaude Sonnet 5 6ff116ba02 Bump version to 0.3.2 for the MsgBox fix
CI / Build wasm engine (push) Successful in 10m55s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 19:31:05 +02:00
valknarandClaude Sonnet 5 1d7c4e7b7d Implement real MsgBox() for VBScript table scripts (browser confirm/alert)
libwinevbs's __LIBWINEVBS__ build of Global_MsgBox() only logged the
prompt text and never set a return value, since it has no host UI to
show a dialog through. Any table script that branches on
MsgBox(...) = vbYes (a common pattern - e.g. core.vbs's own trough
ball-count dialog) always took the "no" branch, since the result
stayed at its zero-initialized default.

Adds a msgbox callback to libwinevbs_callbacks_t, wired on the
vpinball-wasm side to a real window.confirm()/alert() call. Both are
synchronous browser APIs that block JS execution and return a value
immediately, matching what VBScript's MsgBox() needs (its result is
used by the calling statement right away) - the only way to answer it
asynchronously would require Asyncify or a busy-wait poll loop, which
this project has deliberately avoided elsewhere.

Confirmed fixed by hands-on testing: a table's ball-count confirm
dialog now correctly returns Yes/No based on the user's click, instead
of always silently taking the "no" branch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 19:30:25 +02:00
6 changed files with 182 additions and 3 deletions
+30
View File
@@ -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 &amp; 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>
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "@valknar/vpinball-wasm", "name": "@valknar/vpinball-wasm",
"version": "0.3.1", "version": "0.3.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@valknar/vpinball-wasm", "name": "@valknar/vpinball-wasm",
"version": "0.3.1", "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
View File
@@ -1,6 +1,6 @@
{ {
"name": "@valknar/vpinball-wasm", "name": "@valknar/vpinball-wasm",
"version": "0.3.1", "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
@@ -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);
}