A fork of Visual Pinball (VPinballX) that adds the Ball History feature — a system for recording, replaying, and training with ball state during gameplay. Useful for analysing what happened on a given shot, recreating tricky situations, and drilling specific shots over and over to improve pinball skill.
The fork is maintained at garybrowndev/vpinball. All three Windows x64 rendering backends — DirectX, OpenGL, and BGFX — are regularly built and tested in both Debug and Release for Ball History development. Non-Windows platforms compile (the feature is #ifdef-guarded out) but are not actively exercised.
Looking for the upstream Visual Pinball README? Jump to the original ↓
AI-assisted development. This fork is built with heavy use of AI tooling — primarily Claude Code — for everything from Ball History feature work to upstream-merge conflict resolution, debugging, and documentation (including this README). Contributors are encouraged to use Claude Code (or equivalent) during active development. The repo ships with a detailed
CLAUDE.md(build recipes, upstream-API change tracking, debugging lessons, trainer phase timeline) that is consumed by Claude Code automatically as project context — keep it up to date when you change conventions, and it pays for itself on the next session.
Ball History runs as an in-game overlay on top of any VPX table. While playing, press V to open the Ball History menu. The feature has two operational modes:
- Continuously snapshots every ball's full physics state (position, velocity, angular momentum, orientation) into timestamped
BallHistoryRecords. - Press
Rduring play to recall a previous ball state — useful for rewinding to "right before that drain" without restarting the table. - Take manual control of one or more balls: pause the simulation, scrub through history, drag balls to new positions on the playfield, and resume play from the new state.
- Auto-control vertices: place waypoints on the playfield and have the system steer a ball through them — handy for exercising specific shot lines repeatably.
- Define a start location (where the ball spawns, with optional X/Y/Z offset and initial velocity), a pass corridor (geometry the ball must travel through to count as a pass), and a fail zone (region that ends the run as a fail).
- Configure runs per session, countdown delay between runs, ball-hold during countdown, physics variance per run (so you're not drilling against a deterministic target), gameplay difficulty (separate from variance), and a per-run timeout.
- The trainer plays a result sound (pass / fail / timeout) and holds the result on screen for a configurable result-display window before starting the next countdown.
- Per-run results are tracked and shown in a status panel (Previous / Current / Remaining) with overall pass/fail tallies for the session.
- Trainer configurations are saved per-table as
.inifiles and reload automatically when you re-open the same table.
- The menu, status panel, and per-run text are rendered via ImGui overlay in
LiveUI. - Physical visualisations on the playfield (fake balls, the corridor walls, the fail-zone intersection circle, vertex labels) reuse VPinball's own parts system —
CComObject<Ball>,CComObject<Light>,CComObject<Rubber>— so they shade and rotate correctly with whatever camera/cabinet view the table is using.
- Grab the latest build from
garybrowndev/vpinball(or build it yourself — see below). - Launch any VPX table and press
F5to start play. - Press
Vto open the Ball History menu. - Read the menus / descriptions and figure it out — Normal mode is at the top, Trainer mode is in its own submenu.
- Press
Rduring play to recall the most-recent history snapshot. (Other keys — flippers, plunger, etc. — pass through to ImGui as normal while the menu is open.)
DM @garybrowndev with feedback or issues.
- Sync the code (
git pull, etc.). - Download precompiled third-party deps from upstream GitHub Actions — find a successful
vpinballworkflow run on master, download both:VPinballX-<VERSION>-dev-third-party-windows-x64-Debug.zipVPinballX-<VERSION>-dev-third-party-windows-x64-Release.zip
- Extract Debug first, then Release, both into
./third-party/, overwriting existing files. - Revert any git changes caused by the overwrite:
git checkout -- third-party/. - Run
make/create_vs_solution.bat, choose2022. (Re-run this any time you switch betweenmasterandintegration/development, or after merging upstream — differentvpx-core.vcxitemstemplates are used per branch.) - Open
.build/vsproject/VisualPinball.slnin Visual Studio 2022. - Set
vpxas the Startup Project. - Build for Debug or Release / x64.
gh run download <RUN_ID> --repo vpinball/vpinball -n "VPinballX-...-dev-third-party-windows-x64-Debug.zip" --dir /tmp/vpx-deps
gh run download <RUN_ID> --repo vpinball/vpinball -n "VPinballX-...-dev-third-party-windows-x64-Release.zip" --dir /tmp/vpx-deps-release
cp -rf /tmp/vpx-deps/* third-party/
cp -rf /tmp/vpx-deps-release/* third-party/
git checkout -- third-party/- After upstream merges, always re-run
make/create_vs_solution.bat. - When changing widely-included headers (
def.h,stdafx.h,ballhistory.h), force a full rebuild by deleting all.objfiles:rm -rf .build/obj/vpx/Debug-x64/*.obj. To force a relink, also delete the exe. - Build only
vpx(skip plugin projects): add-p:BuildProjectReferences=falseto MSBuild. - MSBuild from CLI:
"/c/Program Files/Microsoft Visual Studio/2022/Community/MSBuild/Current/Bin/MSBuild.exe" \ ".build/vsproject/vpx.vcxproj" -p:Configuration=Debug -p:Platform=x64 -m
platforms/windows-x64/external.sh
cp make/CMakeLists_bgfx-windows-x64.txt CMakeLists.txt
cmake -G "Visual Studio 17 2022" -A x64 -B build
cmake --build build --config Releasestart .build/bin/vpx/Debug-x64/VPinballX64.exe "C:\path\to\Example.vpx"Press F5 to play, ESC for editor, Q to quit play mode. Always launch with a table loaded when iterating on Ball History — starting empty wastes time navigating menus.
Build and run the vpx-test project from the VS solution (.build/vsproject/vpx-test.vcxproj). Test sources are in tests/.
master— tracks upstreamvpinball/vpinballplus one cherry-picked patch (the B2S compat stub — see below).integration—mastermerged with Ball History changes. Build / test branch.development— active Ball History work. PRs land here first, then ship tointegrationafter local verification.- Remotes:
origin=garybrowndev/vpinball,upstream=vpinball/vpinball.
origin/master intentionally carries the B2S compat stub (Restore B2SBackglassServer discovery under modern -Play path) cherry-picked on top of upstream. This is the one patch required for any stock (non-Ball-History) build to work with PinUp-Popper + B2SBackglassServer on a cabinet — upstream rejected the fix (PR #2529) but the cabinet still needs to boot tables.
Consequence: git merge --ff-only upstream/master will fail on master after a fetch. Instead:
# Rebase (preferred — clean single-patch history)
git fetch upstream
git checkout master
git rebase upstream/master
git push --force-with-lease origin masterIf the patch ever lands upstream or is replaced, drop the cherry-pick and restore git merge --ff-only.
git fetch upstream- Checkout
master, rebase ontoupstream/master, force-push to origin. - Checkout
integration, mergemaster— resolve upstream-vs-Ball-History conflicts here. - Download fresh third-party deps if upstream added new plugins or libraries.
- Re-run
make/create_vs_solution.bat. - Build and test integration.
- Merge
integrationintodevelopment.
When upstream changes conflict with Ball History integration points, the convention is: accept upstream for all conflicts in the merge commit, then re-apply Ball History changes in separate (reviewable) commits. Files that always need re-integration: player.h/cpp, LiveUI.cpp, InputManager.cpp, build files.
- VPinball settings:
C:\Users\<user>\AppData\Roaming\VPinballX\10.8\VPinballX.ini - Ball History settings:
<exe folder>/BallHistory/(per-table.inifiles) - In-game settings UI: press F12 while playing (replaces the old Video Options dialog)
- Ball History debug log:
<exe folder>/BallHistory/logs/ballhistory_debug.log(Debug builds only)
In a cabinet VPinballX.ini [TableOverride] section, ViewCabRotation = 90.000000 is the universal cabinet-orientation fix on a landscape cabinet. Empirically verified across a 500-table audit — produces correct landscape orientation for both rotation regimes seen in the wild (legacy-mode tables with ROTF=270 baked in, and window-mode tables with the ROTF=0 author bug). Works because window mode auto-adds 270° before applying the user value, and legacy mode applies it through a Y/Z-flipped coordinate system; both paths converge on the same visual orientation.
Per-table sidecar .ini files (<table>.ini next to <table>.vpx) are only needed for genuine outliers, not the routine ROTF=0 author bug.
| Directory | Purpose |
|---|---|
src/core/ |
Application core — main entry, player loop, settings, pin table, undo, Ball History |
src/physics/ |
Physics engine, collision, quad trees, ball/flipper/plunger hit objects |
src/renderer/ |
Rendering abstraction (DirectX / OpenGL / bgfx), shaders, render targets, VR |
src/parts/ |
Pinball table elements (Ball, Bumper, Flipper, Gate, Kicker, Light, Ramp, Rubber, Spinner, Surface, Trigger, …) |
src/ui/ |
Editor UI, debugger, live UI, dialog properties |
src/input/ |
InputManager (SDL-based, replaces old PinInput / DirectInput) |
src/audio/ |
Audio playback (miniaudio, replaces old SDL_mixer) |
src/math/, src/utils/, src/assets/, src/shaders/, src/plugins/ |
Math, utilities, bundled assets, shaders, plugin host |
make/ |
VS solution / project templates, CMake files per platform, build scripts |
platforms/ |
Per-platform external dependency build scripts |
plugins/ |
Runtime plugin implementations (PinMAME, DMD, DOF, B2S, …) |
standalone/ |
Standalone builds (iOS, Android, macOS — not Ball History targets) |
third-party/ |
Precompiled external dependencies (gitignored — must be downloaded) |
The primary area of active development (~10,800 lines combined). Key types:
BallHistory— central controller integrated into thePlayergame loop. OwnsNormalOptionsandTrainerOptions.BallHistoryState— snapshot of one ball's physics state.BallHistoryRecord— timestamped collection ofBallHistoryStatefor all tracked balls.BHLog— Debug-build logger; writes to<exe>/BallHistory/logs/ballhistory_debug.logviaBHLOG(fmt, ...). Release builds get a no-op stub.EnumAssignKeys— defined inballhistory.h(was previously inpininput.h, which upstream removed). Ball History is the sole consumer.- Platform guard — the full implementation is wrapped in
#ifdef __BALLHISTORY_WIN32__; non-Windows platforms get a no-op stub so cross-platform builds still link.
Player(src/core/player.h) — owns theBallHistoryinstance; constructor inits, destructor uninits,ApplyPlayingStateresets trainer timing.LiveUI(src/ui/live/LiveUI.cpp) — callsBallHistory::Process/ProcessKeys/ProcessMouseeach frame duringRenderUI.InputManager(src/input/InputManager.cpp) — registersV(menu) andR(recall) actions that dispatch toBallHistory::ProcessKeys.HitBall(src/physics/hitball.h) — hasfriend struct BallHistoryform_oldposaccess.PinTable(src/parts/pintable.h) — provides part management (AddPart/RemovePart) and element access viaGetParts().
Creating a visual (fake ball, line, intersection circle):
CComObject<T>::CreateInstance(&obj); obj->AddRef();obj->Init(...);obj->m_wzName = L"uniqueName";(required byAddPart)m_ptable->AddPart(obj);(setsm_ptable, callsAddRef)obj->RenderSetup(renderDevice);(must be afterAddPart)
Destroying:
obj->RenderRelease();(must be beforeRemovePart)m_ptable->RemovePart(obj);(callsRelease)- Clear from the tracking map.
After UnInit, call m_renderer->m_renderDevice->SubmitRenderFrame() to flush pending GPU resources.
- Crash handler produces resolved x64 stack traces in
crash.txt(the upstream stack code was patched to useIMAGE_FILE_MACHINE_AMD64and a rewrittenWriteCallStack). Always checkcrash.txtfirst. BHLogwrites to<exe>/BallHistory/logs/ballhistory_debug.login Debug builds. EachBHLOG(...)call auto-flushes. Canonical log filenames arestatic constexprmembers onBHLog— add new ones there rather than hardcoding paths.- Visual Studio attach — useful when
crash.txtis missing (the SEH crash handler doesn't catch_purecall,abort(), or CRT assertion dialogs). From Git Bash:The double slash oncmd //c start "" "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\devenv.exe" //DebugExe "C:\path\to\VPinballX64.exe"
//DebugExeis required — Git Bash MSYS rewrites single-slash args to Unix paths and breaks the launch. - Conditional breakpoints for teardown bugs — many UAFs only fire in
BallHistory::UnInit, which is called only fromPlayer::~Player(). Breakpoint atballhistory.cppUnInitto fire exclusively during teardown rather than every frame. - Refcount inspection — watch
m_dwRefon COM objects;0xDDDDDDDDis MSVC Debug CRT's dead-land fill and confirms a delete-then-use bug.
- RenderDevice pending buffer assertion — Debug-only assertion on exit after Ball History use (
m_pendingSharedIndexBuffers.empty()). Mitigated bySubmitRenderFrame()in destructor; may still fire in some exit paths. - Trainer key collisions with table scripts — the menu key was moved from
CtoVafterExample.vpx's VBScript "Manual Ball Control" was found to bind toC(keycode 46) and silently freeze the ball when the menu opened. If a new collision is suspected on another table, signal to look for: a specific ball velocity literal being written every physics step. Grep the table script for that constant first — it's almost certainly the table, not the physics engine.
.clang-formatis present: WebKit base, Allman braces, 3-space indent, 190-column limit, tabs = 3 spaces.- Member variables use
m_prefix (e.g.,m_Position,m_BallHistoryRecords). - Enums use
TypeName_Valuenaming (e.g.,ModeStateType_Config,MenuStateType_Root_SelectMode). - Static constants defined as
static constclass members. - COM objects used for rendered elements (
CComObject<Ball>,CComObject<Light>,CComObject<Rubber>).
See CLAUDE.md for the full developer guide — upstream API change tracking, a trainer-phase timeline reference, and a catalogue of debugging lessons learned the expensive way (one symptom value pointing at a VBScript collision rather than the physics engine, sync-vs-async sound stalls in Win32 PlaySound, etc.).
Original readme for forked repo below...
An open source pinball table editor and simulator.
This project was started by Randy Davis, open sourced in 2010 and continued by the Visual Pinball development team. This is the official repository.
- Simulates pinball table physics and renders the table with DirectX, OpenGL or bgfx
- Simple editor to (re-)create any kind of pinball table
- Live editing of most content within the rendered viewport
- Table logic (and game rules) can be controlled via Visual Basic Script
- Over 1050 real/unique pinball machines from ~100 manufacturers, plus over 550 original creations were rebuilt/designed using the Visual Pinball X editor (over 3000 if one counts all released tables, incl. MODs and different variants), and even more when including its predecessor versions (Visual Pinball 9.X)
- Emulation of real pinball machines via PinMAME is possible via Visual Basic Script (Visual PinMAME), or via the libPinMAME-API/plugin
- Supports configurable camera views (e.g. to allow for correct display in virtual pinball cabinets)
- Support for Tablet/Touch input, Joypads, or specialized pinball controllers
- Support for Stereo3D output
- Support for Head tracking via BAM
- Support for VR/XR HMD rendering (including PUP, B2S backglass and DMD output support)
- Support for WCG/HDR rendering (for now only via the BGFX (D3D11/12) build)
- Support for Windows (x86), Linux (x86/Arm, incl. RaspberryPi and RK3588), macOS, iOS/tvOS, Android (the latter builds are also available via the respective app stores for free)
- Plugin system to drive/fuel all kinds of displays (DMD, backglass, etc), add custom/dynamically-changed content (PUP, Serum, etc), direct output framework (DOF), sensors, and much more (WIP)
All releases are available on the releases page.
Documentation is currently sparse. Check the docs directory for various guides and references.
Build instructions are available in the make directory README.