Scaffold vpinball-wasm: Emscripten port of Visual Pinball
Build scripts, CMake/source patches, npm packaging, and a Gitea CI workflow to compile the real Visual Pinball engine (SDL3 + WebGL2 + libwinevbs for real VBScript) to WebAssembly. The patches are validated end-to-end: the patched engine boots in a real browser, loads a real .vpx table, compiles its real GLSL shaders, computes environment map radiance, initializes physics, and starts the VBScript engine, before hanging on the one deliberately-deferred piece of work (game loop rewrite around emscripten_set_main_loop), documented in the README's roadmap.
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
diff --git a/wine/include/winnt.h b/wine/include/winnt.h
|
||||
index 477bd87..d9357fd 100644
|
||||
--- a/wine/include/winnt.h
|
||||
+++ b/wine/include/winnt.h
|
||||
@@ -2043,6 +2043,29 @@ typedef ARM64_NT_CONTEXT CONTEXT, *PCONTEXT;
|
||||
|
||||
#endif /* __aarch64__ */
|
||||
|
||||
+#if defined(__LIBWINEVBS__) && defined(__EMSCRIPTEN__)
|
||||
+/* libwinevbs-only: wasm32 has no real CPU context / SEH unwinding to describe.
|
||||
+ * This library never performs structured exception handling or stack
|
||||
+ * unwinding (no real threads, no SEH usage) - these types only need to exist
|
||||
+ * so the unconditional NTSYSAPI declarations below type-check. */
|
||||
+#define CONTEXT_FULL 0x1
|
||||
+
|
||||
+typedef struct _RUNTIME_FUNCTION
|
||||
+{
|
||||
+ DWORD dummy;
|
||||
+} RUNTIME_FUNCTION, *PRUNTIME_FUNCTION;
|
||||
+
|
||||
+typedef struct _CONTEXT
|
||||
+{
|
||||
+ DWORD ContextFlags;
|
||||
+} CONTEXT, *PCONTEXT;
|
||||
+
|
||||
+typedef struct _KNONVOLATILE_CONTEXT_POINTERS
|
||||
+{
|
||||
+ DWORD dummy;
|
||||
+} KNONVOLATILE_CONTEXT_POINTERS, *PKNONVOLATILE_CONTEXT_POINTERS;
|
||||
+#endif /* __LIBWINEVBS__ && __EMSCRIPTEN__ */
|
||||
+
|
||||
#if !defined(CONTEXT_FULL) && !defined(RC_INVOKED)
|
||||
#error You need to define a CONTEXT for your CPU
|
||||
#endif
|
||||
@@ -0,0 +1,271 @@
|
||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||
index 1fcf242..f9338b2 100644
|
||||
--- a/CMakeLists.txt
|
||||
+++ b/CMakeLists.txt
|
||||
@@ -50,6 +50,7 @@ set(_vpx_valid_combos
|
||||
GL-windows
|
||||
GL-macos
|
||||
GL-linux
|
||||
+ GL-emscripten
|
||||
DX9-windows)
|
||||
if(NOT "${RENDERER}-${PLATFORM}" IN_LIST _vpx_valid_combos)
|
||||
string(REPLACE ";" "\n " _vpx_available "${_vpx_valid_combos}")
|
||||
@@ -784,6 +785,123 @@ elseif(PLATFORM STREQUAL "linux")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
+elseif(PLATFORM STREQUAL "emscripten")
|
||||
+ # Static wasm32 dependency libs/headers staged by vpinball-wasm's
|
||||
+ # scripts/build-deps.sh (SDL3/SDL3_image/SDL3_ttf/FreeImage/libwinevbs
|
||||
+ # built via emcmake). No RPATH concept applies to a wasm binary.
|
||||
+ if(NOT DEFINED VPINBALL_WASM_DEPS_DIR)
|
||||
+ set(VPINBALL_WASM_DEPS_DIR "${CMAKE_SOURCE_DIR}/../build/deps-wasm32")
|
||||
+ endif()
|
||||
+
|
||||
+ if(RENDERER STREQUAL "GL")
|
||||
+ add_library(glad
|
||||
+ third-party/include/glad/src/gles2.c
|
||||
+ )
|
||||
+ set_target_properties(glad PROPERTIES LINKER_LANGUAGE C)
|
||||
+ target_include_directories(glad PUBLIC
|
||||
+ third-party/include/glad/include
|
||||
+ )
|
||||
+ endif()
|
||||
+
|
||||
+ # No raw HID device access in a browser sandbox (real-hardware nudge/plunger
|
||||
+ # boards) - hidapi-hidraw is not linked for this platform, so exclude its
|
||||
+ # one consumer.
|
||||
+ list(REMOVE_ITEM VPX_STANDALONE_SOURCES
|
||||
+ src/input/OpenPinDevHandler.cpp
|
||||
+ )
|
||||
+
|
||||
+ add_executable(vpinball
|
||||
+ ${VPX_STANDALONE_SOURCES}
|
||||
+ )
|
||||
+
|
||||
+ target_include_directories(vpinball PUBLIC
|
||||
+ ${CMAKE_SOURCE_DIR}/third-party/include
|
||||
+ ${CMAKE_SOURCE_DIR}
|
||||
+ ${VPINBALL_WASM_DEPS_DIR}/include
|
||||
+ ${VPINBALL_WASM_DEPS_DIR}/include/libwinevbs/atl/include
|
||||
+ ${VPINBALL_WASM_DEPS_DIR}/include/libwinevbs/atlmfc/include
|
||||
+ ${VPINBALL_WASM_DEPS_DIR}/include/libwinevbs/wine/include
|
||||
+ src
|
||||
+ plugins
|
||||
+ )
|
||||
+
|
||||
+ target_compile_definitions(vpinball PRIVATE
|
||||
+ __STANDALONE__
|
||||
+
|
||||
+ __LIBWINEVBS__
|
||||
+ __WINESRC__
|
||||
+ WINE_UNICODE_NATIVE
|
||||
+
|
||||
+ "__forceinline=__attribute__((always_inline)) inline"
|
||||
+ )
|
||||
+
|
||||
+ target_compile_definitions(vpinball PRIVATE
|
||||
+ ENABLE_OPENGL
|
||||
+ __OPENGLES__
|
||||
+ )
|
||||
+
|
||||
+ target_compile_options(vpinball PUBLIC
|
||||
+ -fdenormal-fp-math=preserve-sign
|
||||
+ -freciprocal-math
|
||||
+ -fassociative-math
|
||||
+ -fapprox-func
|
||||
+ -fno-math-errno
|
||||
+ -fno-trapping-math
|
||||
+ -ffp-contract=off
|
||||
+ -Wno-shorten-64-to-32
|
||||
+ )
|
||||
+
|
||||
+ target_link_directories(vpinball PUBLIC
|
||||
+ "${VPINBALL_WASM_DEPS_DIR}/lib"
|
||||
+ )
|
||||
+
|
||||
+ target_link_libraries(vpinball PUBLIC
|
||||
+ SDL3
|
||||
+ SDL3_image
|
||||
+ SDL3_ttf
|
||||
+ freetype
|
||||
+ freeimage
|
||||
+ winevbs
|
||||
+ glad
|
||||
+ )
|
||||
+
|
||||
+ set_target_properties(vpinball PROPERTIES
|
||||
+ RUNTIME_OUTPUT_NAME "vpinball"
|
||||
+ SUFFIX ".js"
|
||||
+ )
|
||||
+
|
||||
+ set(RESOURCES_DIR "$<TARGET_FILE_DIR:vpinball>")
|
||||
+ set(SHADER_DIR_NAME "shaders-${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_REV}")
|
||||
+ set(SHADER_DIR "${RESOURCES_DIR}/${SHADER_DIR_NAME}")
|
||||
+
|
||||
+ # PRE_LINK (not POST_BUILD): the --preload-file linker flags below run
|
||||
+ # emscripten's file_packager as part of the link step itself, so the
|
||||
+ # staged directories must exist *before* linking, not after.
|
||||
+ add_custom_command(TARGET vpinball PRE_LINK
|
||||
+ COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_SOURCE_DIR}/src/assets" "${RESOURCES_DIR}/assets"
|
||||
+ COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_SOURCE_DIR}/scripts" "${RESOURCES_DIR}/scripts"
|
||||
+ COMMAND "${CMAKE_COMMAND}" -E make_directory "${RESOURCES_DIR}/tables"
|
||||
+ COMMAND "${CMAKE_COMMAND}" -E copy "${CMAKE_SOURCE_DIR}/tests/assets/test000-default-table.vpx" "${RESOURCES_DIR}/tables/default.vpx"
|
||||
+ COMMAND "${CMAKE_COMMAND}" -E make_directory "${SHADER_DIR}"
|
||||
+ )
|
||||
+ foreach(_shader IN LISTS VPX_GL_SHADERS)
|
||||
+ add_custom_command(TARGET vpinball PRE_LINK
|
||||
+ COMMAND "${CMAKE_COMMAND}" -E copy "${CMAKE_SOURCE_DIR}/src/shaders/hlsl_glsl/${_shader}" "${SHADER_DIR}"
|
||||
+ )
|
||||
+ endforeach()
|
||||
+
|
||||
+ # Embed the staged assets/scripts/shaders/default table into the wasm
|
||||
+ # build's virtual filesystem so the engine can open them with ordinary
|
||||
+ # fopen()/std::ifstream calls, unmodified, exactly as it does natively.
|
||||
+ # Mount points are relative (matching how the native build locates these
|
||||
+ # directories next to the executable, i.e. relative to cwd "/" in MEMFS).
|
||||
+ target_link_options(vpinball PRIVATE
|
||||
+ "SHELL:--preload-file ${RESOURCES_DIR}/assets@assets"
|
||||
+ "SHELL:--preload-file ${RESOURCES_DIR}/scripts@scripts"
|
||||
+ "SHELL:--preload-file ${RESOURCES_DIR}/tables@tables"
|
||||
+ "SHELL:--preload-file ${SHADER_DIR}@${SHADER_DIR_NAME}"
|
||||
+ )
|
||||
+
|
||||
# iOS and Android libvpinball build
|
||||
|
||||
elseif(PLATFORM STREQUAL "ios" OR PLATFORM STREQUAL "ios-simulator" OR PLATFORM STREQUAL "android")
|
||||
@@ -957,4 +1075,9 @@ elseif(PLATFORM STREQUAL "ios" OR PLATFORM STREQUAL "ios-simulator" OR PLATFORM
|
||||
|
||||
endif()
|
||||
|
||||
-include("${CMAKE_SOURCE_DIR}/make/CMakeLists_plugins.txt")
|
||||
+# vpinball-wasm: plugins are all optional/native-hardware-integration-scoped
|
||||
+# (PinMAME, DOF, B2S, FlexDMD, Inspector, FFmpeg-dependent, etc.) and don't
|
||||
+# map to a browser sandbox for this project's v1 - see README's roadmap.
|
||||
+if(NOT PLATFORM STREQUAL "emscripten")
|
||||
+ include("${CMAKE_SOURCE_DIR}/make/CMakeLists_plugins.txt")
|
||||
+endif()
|
||||
diff --git a/src/core/def.h b/src/core/def.h
|
||||
index 7335929..eb2c0b4 100644
|
||||
--- a/src/core/def.h
|
||||
+++ b/src/core/def.h
|
||||
@@ -337,6 +337,9 @@ static const string platform_bits[2] = { "32"s, "64"s };
|
||||
#elif defined(__ANDROID__) // leave here, as it also defines linux
|
||||
#define GET_PLATFORM_OS_ENUM 1
|
||||
#define GET_PLATFORM_OS "android"
|
||||
+#elif defined(__EMSCRIPTEN__) // leave here, as it also defines linux on some toolchains
|
||||
+ #define GET_PLATFORM_OS_ENUM 6
|
||||
+ #define GET_PLATFORM_OS "emscripten"
|
||||
#elif (defined(__linux) || defined(__linux__))
|
||||
#define GET_PLATFORM_OS_ENUM 2
|
||||
#define GET_PLATFORM_OS "linux"
|
||||
@@ -352,7 +355,7 @@ static const string platform_bits[2] = { "32"s, "64"s };
|
||||
#define GET_PLATFORM_OS "macos"
|
||||
#endif
|
||||
#endif
|
||||
-static const string platform_os[6] = { "windows"s, "android"s, "linux"s, "ios"s, "tvos"s, "macos"s };
|
||||
+static const string platform_os[7] = { "windows"s, "android"s, "linux"s, "ios"s, "tvos"s, "macos"s, "emscripten"s };
|
||||
|
||||
#if defined(ENABLE_BGFX)
|
||||
#define GET_PLATFORM_RENDERER_ENUM 2
|
||||
diff --git a/src/core/main.cpp b/src/core/main.cpp
|
||||
index dafe6ee..0015268 100644
|
||||
--- a/src/core/main.cpp
|
||||
+++ b/src/core/main.cpp
|
||||
@@ -228,13 +228,14 @@ extern "C" int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/,
|
||||
return retval;
|
||||
}
|
||||
|
||||
-#if defined(__STANDALONE__) && ((defined(__linux__) && !defined(__ANDROID__)) || defined(__MINGW32__))
|
||||
+#if defined(__STANDALONE__) && ((defined(__linux__) && !defined(__ANDROID__)) || defined(__MINGW32__) || defined(__EMSCRIPTEN__))
|
||||
extern int g_argc;
|
||||
extern const char **g_argv;
|
||||
int main(int argc, const char** argv) {
|
||||
#ifdef __MINGW32__
|
||||
signal(SIGINT, OnSignalHandler);
|
||||
-#else
|
||||
+#elif !defined(__EMSCRIPTEN__)
|
||||
+ // No meaningful SIGINT/process-signal equivalent in a browser sandbox.
|
||||
struct sigaction sigIntHandler;
|
||||
sigIntHandler.sa_handler = OnSignalHandler;
|
||||
sigemptyset(&sigIntHandler.sa_mask);
|
||||
diff --git a/src/input/InputManager.cpp b/src/input/InputManager.cpp
|
||||
index 540b822..1af6bd8 100644
|
||||
--- a/src/input/InputManager.cpp
|
||||
+++ b/src/input/InputManager.cpp
|
||||
@@ -20,7 +20,8 @@
|
||||
|
||||
#include "input/SDLInputHandler.h"
|
||||
|
||||
-#ifndef __LIBVPINBALL__
|
||||
+#if !defined(__LIBVPINBALL__) && !defined(__EMSCRIPTEN__)
|
||||
+ // No raw HID device access in a browser sandbox.
|
||||
#include "input/OpenPinDevHandler.h"
|
||||
#endif
|
||||
|
||||
@@ -82,7 +83,7 @@ InputManager::InputManager(Player* player)
|
||||
// Initialize device handlers
|
||||
m_inputHandlers.push_back(std::make_unique<SDLInputHandler>(*this));
|
||||
m_sdlHandler = static_cast<SDLInputHandler*>(m_inputHandlers.back().get());
|
||||
- #ifndef __LIBVPINBALL__
|
||||
+ #if !defined(__LIBVPINBALL__) && !defined(__EMSCRIPTEN__)
|
||||
m_inputHandlers.push_back(std::make_unique<OpenPinDevHandler>(*this));
|
||||
#endif
|
||||
|
||||
diff --git a/src/physics/kdtree.cpp b/src/physics/kdtree.cpp
|
||||
index 3f262a9..10ac63e 100644
|
||||
--- a/src/physics/kdtree.cpp
|
||||
+++ b/src/physics/kdtree.cpp
|
||||
@@ -421,7 +421,7 @@ void HitKDNode::HitTestBall(const HitKD* hitoct, const HitBall* const pball, Col
|
||||
if (pball->m_hitBBox.right >= vcenter)
|
||||
m_children[1].HitTestBall(hitoct, pball, coll);
|
||||
}
|
||||
- else f (axis == 1)
|
||||
+ else if (axis == 1)
|
||||
{
|
||||
const float vcenter = (m_rectbounds.top+m_rectbounds.bottom)*0.5f;
|
||||
if (pball->m_hitBBox.top <= vcenter)
|
||||
diff --git a/standalone/vpinball_standalone_i.h b/standalone/vpinball_standalone_i.h
|
||||
index 29898aa..3bac681 100644
|
||||
--- a/standalone/vpinball_standalone_i.h
|
||||
+++ b/standalone/vpinball_standalone_i.h
|
||||
@@ -3351,8 +3351,19 @@ ITableGlobal : public IDispatch
|
||||
virtual HRESULT STDMETHODCALLTYPE put_ShowCursor(
|
||||
VARIANT_BOOL show) = 0;
|
||||
|
||||
+ // vpinball-wasm: mirrors the #ifdef _WIN64 split already used by
|
||||
+ // ScriptGlobalTable's own declaration/implementation of this method -
|
||||
+ // this generated interface header previously hardcoded the 64-bit
|
||||
+ // (SIZE_T) overload unconditionally, which made ScriptGlobalTable
|
||||
+ // abstract (unable to override this pure virtual) on 32-bit targets
|
||||
+ // like wasm32.
|
||||
+#ifdef _WIN64
|
||||
virtual HRESULT STDMETHODCALLTYPE get_GetPlayerHWnd(
|
||||
SIZE_T *pVal) = 0;
|
||||
+#else
|
||||
+ virtual HRESULT STDMETHODCALLTYPE get_GetPlayerHWnd(
|
||||
+ LONG *pVal) = 0;
|
||||
+#endif
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE StopSound(
|
||||
BSTR Sound) = 0;
|
||||
diff --git a/standalone/vpinball_standalone_i_proxy.cpp b/standalone/vpinball_standalone_i_proxy.cpp
|
||||
index 72a2d2e..2fe1b73 100644
|
||||
--- a/standalone/vpinball_standalone_i_proxy.cpp
|
||||
+++ b/standalone/vpinball_standalone_i_proxy.cpp
|
||||
@@ -3043,7 +3043,15 @@ STDMETHODIMP ScriptGlobalTable::Invoke(DISPID dispIdMember, REFIID /*riid*/, LCI
|
||||
if (wFlags & DISPATCH_PROPERTYGET) {
|
||||
// line 724: [propget, id(14), helpstring("property GetPlayerHWnd")] HRESULT GetPlayerHWnd([out, retval] SIZE_T *pVal);
|
||||
V_VT(&res) = VT_UI4;
|
||||
+ // vpinball-wasm: this generated proxy hardcodes the _WIN64 (SIZE_T)
|
||||
+ // overload of get_GetPlayerHWnd; on a genuine 32-bit target (wasm32)
|
||||
+ // the interface instead declares the LONG overload (see
|
||||
+ // ScriptGlobalTable.h's #ifdef _WIN64), so match it here too.
|
||||
+#ifdef _WIN64
|
||||
hres = get_GetPlayerHWnd((SIZE_T*)&V_UI4(&res));
|
||||
+#else
|
||||
+ hres = get_GetPlayerHWnd((LONG*)&V_UI4(&res));
|
||||
+#endif
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
--- a/third-party/include/ThreadPool.h
|
||||
+++ b/third-party/include/ThreadPool.h
|
||||
@@ -110,9 +110,20 @@
|
||||
: pool_size(threads)
|
||||
, in_flight(0)
|
||||
{
|
||||
+#ifndef __EMSCRIPTEN__
|
||||
const std::unique_lock<std::mutex> lock(this->queue_mutex);
|
||||
for (std::size_t i = 0; i != threads; ++i)
|
||||
start_worker(i, lock);
|
||||
+#else
|
||||
+ // vpinball-wasm: a single-threaded Emscripten build (no -pthread /
|
||||
+ // SharedArrayBuffer for v1) cannot construct real std::thread workers -
|
||||
+ // std::thread's constructor throws at runtime otherwise ("thread
|
||||
+ // constructor failed"). Run zero real workers; enqueue() below executes
|
||||
+ // tasks synchronously and inline instead, matching the porting plan's
|
||||
+ // "run ThreadPool work serially" decision for one-off parallel work
|
||||
+ // (parallel .vpx item deserialization at load, env-map baking, etc.).
|
||||
+ (void)pool_size;
|
||||
+#endif
|
||||
}
|
||||
|
||||
// add new work item to the pool
|
||||
@@ -139,6 +150,15 @@
|
||||
|
||||
std::future<return_type> res = task->get_future();
|
||||
|
||||
+#ifdef __EMSCRIPTEN__
|
||||
+ // vpinball-wasm: no real worker threads exist (see the constructor) -
|
||||
+ // run the task synchronously inline so callers still get a valid,
|
||||
+ // already-ready future, exactly as if it had run on a worker.
|
||||
+ (void)std::atomic_fetch_add_explicit(&in_flight, std::size_t(1), std::memory_order_relaxed);
|
||||
+ (*task)();
|
||||
+ (void)std::atomic_fetch_sub_explicit(&in_flight, std::size_t(1), std::memory_order_acq_rel);
|
||||
+ return res;
|
||||
+#else
|
||||
std::unique_lock<std::mutex> lock(queue_mutex);
|
||||
if (tasks.size () >= max_queue_size)
|
||||
// wait for the queue to empty or be stopped
|
||||
@@ -160,6 +180,7 @@
|
||||
condition_consumers.notify_one();
|
||||
|
||||
return res;
|
||||
+#endif
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user