Skip to content

Latest commit

 

History

519 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

vpin

Crates.io Docs.rs npm

Rust library for working with Visual Pinball VPX files. Also available on npm as a WASM package: @francisdb/vpin-wasm - see wasm-readme.md for the JavaScript/TypeScript API.

Join #vpxtool on "Virtual Pinball Chat" discord for support and questions.

Documentation

https://docs.rs/vpin

Features

The library provides several optional features that can be enabled:

  • parallel (default): Enables parallel processing using rayon for better performance
  • wasm: Enables WebAssembly bindings for browser/Node.js usage
  • script-audit: Adds script-level checks to vpx::audit (missing Option Explicit, duplicate procedures, unused variables, Execute usage, VPinMAME setup). Pulls in the vbscript parser, so it is off by default; the wasm feature includes it

To use only VPX functionality without parallel support:

[dependencies]
vpin = { version = "0.29", default-features = false }

To enable specific features:

[dependencies]
vpin = { version = "0.29", default-features = false, features = ["wasm"] }

Example code

Check the examples folder

Expanded VPX Format

The library supports extracting VPX files to an expanded directory format for easier editing and version control.

Primitive mesh data is written as Wavefront OBJ. For glTF/GLB, use the whole table exporter in vpin::vpx::export::gltf_export.

Derived Mesh Generation

When extracting VPX files, the library can optionally generate mesh files for game items that don't store explicit mesh data but are defined by drag points (walls, ramps, rubbers, flashers). Use ExpandOptions to enable this:

use vpin::vpx::expanded::ExpandOptions;

let options = ExpandOptions::new().generate_derived_meshes(true);

Writing Part of a Table

ExpandOptions::filter takes a predicate over each file's path relative to the expanded directory. Rejected files are not written and the work to produce them (decoding bitmaps, decompressing meshes) is skipped, which makes extracting only the JSON files of a large table fast. Index files such as images.json still list every entry, so a partial directory documents what was left out; it cannot be read back as a table.

use std::path::Path;
use vpin::vpx::expanded::ExpandOptions;

// everything except the image and sound data
let options = ExpandOptions::new().filter(|path: &Path| {
    !path.starts_with("images") && !path.starts_with("sounds")
});

VPZ Table Pack

vpin::vpz reads and writes the VPZ table pack of Visual Pinball X, a table as JSON documents and assets in their native formats, stored as a directory or as a .vpz zip archive. The format is still marked preliminary upstream, see VPZ File Format.

let pack = vpin::vpz::read("table.vpz")?;
println!("{} parts", pack.parts.len());
vpin::vpz::write(&pack, "table-folder")?;

The manifest and the asset sidecars are typed; the table and scene documents are ordered JSON properties.

vpin::vpz::from_vpx gives the pack vpinball saves from a table, byte for byte for a table vpinball last saved itself. vpinball upgrades older tables on load (defaults of newer fields, legacy fields converted, invalid references cleared) before saving them; the conversion keeps the fields of the file, and carries the legacy fields vpinball converts on load.

vpin::vpz::to_vpx gives the table vpinball saves after loading a pack. Meshes go through meters and back, so their vertices may move by a float step, as they do in vpinball.

let vpx = vpin::vpx::read(std::path::Path::new("table.vpx"))?;
let pack = vpin::vpz::from_vpx(&vpx, "Mon Oct  5 12:00:00 2026")?;
vpin::vpz::write(&pack, "table.vpz")?;
let table = vpin::vpz::to_vpx(&vpin::vpz::read("table.vpz")?, "Mon Oct  5 12:05:00 2026")?;

Whole-Table Export (OBJ / glTF)

The library can export a complete table - generated meshes for every part type, materials, and textures - for use in Blender and other 3D tools:

  • glTF/GLB (export::gltf_export::export_gltf) - PBR materials, embedded textures, the three VPinball view cameras, and KHR_lights_punctual lights. Output follows the glTF conventions (Y-up right-handed, meters) so it opens correctly with default import settings.
  • OBJ + MTL (export::obj_export::export_obj) - with texture extraction, MTL dedup, and configurable output.

Both exporters take options for the output units (ExportUnits: VPU, mm, cm, m) and the OBJ exporter also for the axis convention (AxisConvention):

use vpin::vpx::export::obj_export::{export_obj, AxisConvention, ExportUnits, ObjExportOptions};

let options = ObjExportOptions {
    units: ExportUnits::M,
    // Y-up right-handed opens upright in Blender with default import
    // settings; the default (ZDownRightHanded) matches vpinball's own
    // OBJ export convention.
    axes: AxisConvention::YUpRightHanded,
    ..ObjExportOptions::default()
};

Note that the glTF specification defines meters and Y-up as the only conforming conventions - the unit option exists for pipelines that expect a different scale.

Which items an export includes is an ItemFilter on the options (export::item_filter). Both exporters default to ItemFilter::everything(), every item type with geometry; ObjExportOptions::vpinball_strict() narrows it to what vpinball's own File -> Export -> OBJ Mesh writes (ItemFilter::vpinball_obj_export(), declared in export::vpinball_rules). Either exporter takes either preset, narrowed or widened by type, name, editor layer visibility or a predicate:

use vpin::vpx::export::gltf_export::GltfExportOptions;
use vpin::vpx::export::item_filter::{ItemFilter, ItemType};

// A GLB with only what vpinball's OBJ export would carry, plus the lights
let options = GltfExportOptions::glb()
    .with_filter(ItemFilter::vpinball_obj_export().with_type(ItemType::Light));

// Everything except the items on hidden editor layers
let filter = ItemFilter::everything().skip_editor_hidden(true);

See the examples folder for complete export examples, and wasm-readme.md for the same functionality from JavaScript (export_glb, export_obj).

VPinball Coordinate System

VPinball uses a left-handed coordinate system:

    (0,0)───────────────→ +X (right)
      │   ┌───────────┐
      │   │           │
      │   │ Playfield │
      │   │           │
      ↓   └───────────┘
     +Y (towards player)

    +Z points up (towards the cabinet top glass)
  • Origin: Top-left corner of the playfield (near the back of the cabinet)
  • X-axis: Positive to the right (across the playfield)
  • Y-axis: Positive towards the player (down the playfield)
  • Z-axis: Positive upward (towards the glass)

Polygon Winding Order

Due to the Y-axis pointing down, winding order appears reversed compared to standard mathematical conventions:

  • A polygon whose vertices go clockwise on screen has a positive orientation determinant
  • A polygon whose vertices go counter-clockwise on screen has a negative orientation determinant

VPinball's triangulation algorithm (used for flashers, walls, etc.) normalizes all polygons to counter-clockwise order before processing. This library matches that behavior.

Projects using vpin

https://github.com/francisdb/vpxtool https://github.com/jsm174/vpx-editor

Other links

Running the integration tests

We expect a folder ~/vpinball/tables to exist that contains a lot of vpx files. The tests will recursively search for these files and run the tests on them.

cargo test --release -- --ignored --nocapture

The corpus tests run tables in parallel across all cores. Every in-flight table holds several copies of itself in memory, so on a machine with many cores this can use a lot of RAM. Set RAYON_NUM_THREADS to reduce the number of worker threads if needed, for example RAYON_NUM_THREADS=8.

WASM tests for server-side WASM (wasmtime)

# Install the target and wasmtime (do this only once)
rustup target add wasm32-wasip1

# Recommended, installs a prebuilt binary in seconds.
# See https://docs.wasmtime.dev/cli-install.html for other options like your package manager
curl https://wasmtime.dev/install.sh -sSf | bash

# Alternative, builds wasmtime from source which takes over 5 minutes
cargo install wasmtime-cli

# Run tests
cargo test --target wasm32-wasip1 --features wasm

WASM tests for browser (wasm-bindgen-test)

# Install the target and wasm-bindgen-test (do this only once)
rustup target add wasm32-unknown-unknown
cargo install wasm-bindgen-cli

# Run tests
cargo test --target wasm32-unknown-unknown

Fuzzing

The fuzz/ directory contains cargo-fuzz targets that feed random bytes into the parsers to find inputs that crash the library. Parse errors on garbage input are expected; panics are bugs.

There are three targets:

  • vpx_from_bytes - the full VPX read path, including the compound file container
  • gamedata - the BIFF gamedata parser, bypassing the container so the fuzzer spends its time in this library's own code (much higher throughput)
  • gameitem - the game item parser, covering all item types
# Install the tooling (once). Fuzzing requires the nightly toolchain because
# cargo-fuzz relies on unstable compiler flags; the library itself stays on stable.
rustup toolchain install nightly
cargo install cargo-fuzz

# Run a target (stop with ctrl-c, or bound it with e.g. -- -max_total_time=600)
cargo +nightly fuzz run gamedata

# The full-file target works best when seeded with a real table
mkdir -p fuzz/corpus/vpx_from_bytes
cp testdata/completely_blank_table_10_7_4.vpx fuzz/corpus/vpx_from_bytes/
cargo +nightly fuzz run vpx_from_bytes

When a crashing input is found it is written to fuzz/artifacts/<target>/. Re-run the target with that file as an argument to reproduce the crash, and use cargo +nightly fuzz tmin <target> <file> to minimize it to the smallest input that still crashes. The corpus under fuzz/corpus/ grows as the fuzzer discovers new code paths and is worth keeping between runs.

Making a release

We use https://github.com/release-plz/release-plz which creates a release pr on every commit to main

About

Rust library for working with Visual Pinball VPX files

Topics

Resources

Stars

10 stars

Watchers

3 watching

Forks

Releases

Packages

Used by

Contributors

Languages