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,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;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Public TypeScript surface for @valknar/vpinball-wasm.
|
||||
*
|
||||
* This wraps the raw Emscripten-generated module (dist/vpinball.js) so
|
||||
* consumers don't need to know about ccall/cwrap/FS internals directly.
|
||||
*/
|
||||
|
||||
export interface LoadPinballOptions {
|
||||
/** Canvas the engine renders into via SDL3's WebGL2 backend. */
|
||||
canvas: HTMLCanvasElement;
|
||||
/**
|
||||
* Raw bytes of a .vpx table file to load on startup. If omitted, the
|
||||
* bundled default table (package/assets/test000-default-table.vpx) is
|
||||
* used instead.
|
||||
*/
|
||||
tableData?: ArrayBuffer;
|
||||
/** Base URL to fetch dist/vpinball.wasm (and .data, if present) from. */
|
||||
baseUrl?: string;
|
||||
}
|
||||
|
||||
export interface PinballInstance {
|
||||
/** Load a different .vpx table at runtime, replacing the current one. */
|
||||
loadTable(vpxBytes: ArrayBuffer): void;
|
||||
/** Start (or resume) the simulation's main loop. */
|
||||
start(): void;
|
||||
/** Pause the simulation's main loop. */
|
||||
stop(): void;
|
||||
/** Tear down the instance and free its WebAssembly memory. */
|
||||
dispose(): void;
|
||||
}
|
||||
Reference in New Issue
Block a user