Scaffold vpinball-wasm: Emscripten port of Visual Pinball
CI / Build wasm engine (push) Failing after 3m30s
CI / Publish to npm registry (push) Skipped

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:
2026-08-22 14:30:14 +02:00
commit 0a63835689
18 changed files with 1151 additions and 0 deletions
+59
View File
@@ -0,0 +1,59 @@
import type { LoadPinballOptions, PinballInstance } from './types.js';
export type { LoadPinballOptions, PinballInstance } from './types.js';
/**
* Instantiates the WebAssembly build of Visual Pinball against the given
* canvas.
*
* NOTE (current milestone): this wrapper boots the Emscripten module and
* exposes lifecycle control (start/stop/dispose). Runtime table loading
* (`loadTable`) mounts the given bytes into the module's virtual filesystem,
* but wiring the native engine to actually pick up a runtime-loaded table
* (as opposed to the table baked in at build time) is tracked as a follow-up
* milestone in the README's roadmap - see "Browser file-loading UX".
*/
export async function loadPinball(options: LoadPinballOptions): Promise<PinballInstance> {
const baseUrl = options.baseUrl ?? '.';
// dist/vpinball.js is built with -sMODULARIZE=1 -sEXPORT_NAME=VPinballModule,
// so importing it yields a factory function, not a module with side effects.
const factory = (await import(/* @vite-ignore */ `${baseUrl}/vpinball.js`)).default as (
moduleArgs: Record<string, unknown>
) => Promise<EmscriptenModule>;
const module = await factory({
canvas: options.canvas,
locateFile: (path: string) => `${baseUrl}/${path}`,
});
if (options.tableData) {
mountTable(module, options.tableData);
}
return {
loadTable(vpxBytes: ArrayBuffer) {
mountTable(module, vpxBytes);
},
start() {
module.ccall?.('vpinball_wasm_start', null, [], []);
},
stop() {
module.ccall?.('vpinball_wasm_stop', null, [], []);
},
dispose() {
module.ccall?.('vpinball_wasm_dispose', null, [], []);
},
};
}
function mountTable(module: EmscriptenModule, vpxBytes: ArrayBuffer): void {
module.FS.writeFile('/table.vpx', new Uint8Array(vpxBytes));
}
interface EmscriptenModule {
FS: {
writeFile(path: string, data: Uint8Array): void;
};
ccall?: (name: string, returnType: string | null, argTypes: string[], args: unknown[]) => unknown;
}