A minimal, appliance-style Linux for virtual pinball cabinets.
Boots fast, brings up a GPU-accelerated display, and launches
Visual Pinball and the
vpinfe frontend — and nothing else.
Warning
VPinOS is in beta. It works end to end (boot, launch, install), but it is under active development: expect rough edges, breaking changes between releases, and features that are still placeholders (the console menu, for one). Don't rely on it for anything you can't reinstall, keep backups of your tables and settings, and please report problems.
Note
3 sample tables are bundled directly in the ISO (Fair Fight, Mars
Trek, Halley Comet) — no network needed to see them, live or installed.
A 4th table (Cyclopes) is available but not bundled; fetching it
requires re-enabling vpinos-fetch-tables.service, which is off by
default. See Sample tables for details.
VPinOS is a custom Debian 13 (trixie) image, built with live-build, that ships as a single hybrid ISO. It is not a general-purpose desktop. The whole point is a cabinet that goes from power-on to pinball with as little OS in the way as possible:
- Base: Debian trixie with trixie-backportsenabled. The Linux 7.1 kernel, a current Mesa (26.1) and current AMD GPU firmware come from backports so recent GPUs work out of the box (see Requirements); everything else is stock trixie.
- Display: Hyprland as the compositor, with the launched program as its only Wayland client.
- Graphics: open-source Mesa Vulkan drivers (AMD, Intel, NVIDIA via NVK) on the Linux 7.1 kernel, so VPinball's BGFX renderer runs with a real GPU.
- Apps: vpinballandvpinfe(a cabinet frontend/launcher), plus Google Chrome for vpinfe's local UI.
- Account: one hardcoded appliance user, vpinos(passwordvpinos), in thevideo,input,audio,render,dialout,plugdevandsudogroups. The apps run as this user, not as root.
The same image runs two ways. Everything is identical except where it lives and whether changes survive a reboot.
The live session is the way to try VPinOS on a machine without touching its disks. To make it permanent, pick Install VPinOS from the menu and follow the prompts (language, keyboard, partitioning, summary).
The menu launches automatically on login — a plain numbered shell prompt
("Quit to shell" drops to a normal shell, e.g. for debugging — running
vpinos-menu by hand brings it back). The top level differs slightly
between the live session and an installed system:
live session installed system
-------------------------------- --------------------------------
1) Setup 1) Setup
2) Testing 2) VPinFE
3) Install VPinOS 3) Testing
4) System Info (Debug) 4) System Info (Debug)
q) Quit to shell q) Quit to shell
s) Shutdown s) Shutdown
"Setup" groups everything that configures the cabinet rather than runs it:
1) GPU Driver: default
2) VPinOS Configuration
3) Network Settings
4) VPXConfig (Advanced VPinball Configuration)
5) VPinFE Map Controls
6) Boot on startup: menu (installed systems only)
"VPinOS Configuration" (vpinos-config.py — was "Monitor Detection" until it
grew past just monitors) starts a local web server and opens it in a
windowed Chrome, same pattern as "VPXConfig" below (127.0.0.1:1112, this
machine only, stopped as soon as you close the browser): identify which
output name (DP-2, HDMI-A-1, ...) is which physical screen, assign each
one a role (Table/Backglass/DMD) and refresh rate, set VPinball
Mode/Rendering Options, and save straight into
hyprland.conf/VPinballX.ini.
"Network Settings" runs nmtui, NetworkManager's own text UI — edit or
activate Ethernet/Wi-Fi connections, and set the system hostname. Not
Wi-Fi-specific despite the common association.
"VPXConfig" starts a configuration tool with a web interface: the menu
starts its local server (127.0.0.1:1111, this machine only), opens it in a
windowed Chrome, and stops the server as soon as you close the browser (or
click its own Quit button).
"VPinFE Map Controls" runs vpinfe --gamepadtest, vpinfe's own
controller-mapping mode.
"Boot on startup" (installed systems only — not shown on the live image) lets you pick a program to launch automatically on boot instead of this menu, e.g. VPinFE for a cabinet that should go straight to the frontend. Quitting that program (or it exiting for any other reason) always falls back to this menu, never a dead end.
"Testing" groups things you run to try the cabinet out, rather than configure it:
1) Launch VPinball Example Table
2) VPinFE (live session only)
VPinFE itself — the cabinet's table-browsing frontend — is tucked into Testing on the live session (there's nowhere else to reach it from), but promoted to its own top-level option on an installed system: that's the one a cabinet builder reaches for constantly once it's actually built, so it doesn't sit a menu level down from day-to-day use.
"GPU Driver" switches between the open-source driver this image uses by default (Mesa/NVK on NVIDIA hardware, RADV on AMD, Intel's own driver) and the NVIDIA proprietary driver, precompiled into the image at build time. Takes effect on the very next launch — no reboot needed, live or installed. Off by default: Hyprland/Wayland compatibility with the proprietary driver hasn't been independently verified on real hardware by this project.
"Shutdown" asks for confirmation, then powers the machine off.
The menu is a deliberate placeholder to prove out the launch path; booting straight into the frontend is still to come.
Prebuilt ISOs are published on the Releases page.
- A release is created only when a version tag (v1.2.3, matching the version stamped into the image) is pushed. TheBuild VPinOS ISOGitHub Actions workflow builds the image from a clean checkout and attaches it.
- Each release contains:
- live-image-amd64.hybrid.iso— the image (roughly 2 GB)
- live-image-amd64.packages— every package and version in the image
- live-image-amd64.contents/- .files— the full file listing
- Pushes to mainand manual runs build the image and upload it as a short-lived workflow artifact for verification, but do not create a release.
- The version shown on the boot splash, in the installer, on the login
banner and in /etc/os-releaseall come from that one file:config/includes.chroot/etc/os-release.
- Download live-image-amd64.hybrid.isofrom the latest release.
- Write it to a USB stick (this erases the stick), for example:
or use a tool such as balenaEtcher. Double-checksudo dd if=live-image-amd64.hybrid.iso of=/dev/sdX bs=4M status=progress conv=fsync /dev/sdXfirst.
- Boot the target machine from the stick. The image carries both BIOS (syslinux) and UEFI (GRUB) boot files.
- Run vpinos-menu. To install, choose "Install VPinOS".
- x86-64 PC with a Vulkan-capable GPU. The image ships the Linux 7.1
kernel, Mesa 26.1 and current AMD GPU firmware from Debian's
trixie-backports (trixie's own 6.12 kernel and Mesa 25.0 are too old for
the newest cards). VPinOS uses only the open-source Mesa drivers
— no proprietary AMD or NVIDIA drivers — and they are all
included, so no driver install is needed:
- AMD — Mesa RADV
- Intel — Mesa Intel Vulkan
- NVIDIA — the open-source NVK driver (Mesa) with NVIDIA's GPU
firmware, for Turing (RTX 16/20-series) and newer cards. Not yet tested
on real NVIDIA hardware. Older NVIDIA cards (Kepler, Maxwell, Pascal
— e.g. Quadro K-series, GTX 900/10-series) are not known to have a
working Vulkan driver here (untested with the newer Mesa): the kernel's
nouveaudriver still runs the display, but Vulkan may fall back to software (see below). Whether OpenGL on those cards is usable for vpinball is untested.
- How to tell if your GPU is being used: run vulkaninfo --summary. If the device isllvmpipe(orlavapipe), Vulkan is falling back to software rendering on the CPU — the GPU isn't supported, and it will be far too slow for pinball.
- The installer sets up a UEFI boot (GRUB EFI). Installing onto a BIOS-only machine has not been tested.
- A network connection is needed to update, and for the 4th sample table (not bundled, see Sample tables below), but not otherwise to run.
- A network connection is required to complete installation via
Calamares. The image ships without apt package indices (see the
lb configcommand's--apt-indices falsefurther down) to keep the ISO smaller, so the installer refreshes them from the network (apt-get update, viaupdate_db: trueinconfig/includes.chroot/etc/calamares/modules/packages.conf) before it can remove the live-only packages during install. No connection at that point means installation fails with a "Package Manager error". Connect via "Network Settings" (nmtui, see below) from the live session before launching the installer.
Besides the open-source NVK driver above (the default, no install needed),
VPinOS can also precompile NVIDIA's proprietary driver at build time and
let you switch to it at runtime from the console menu (vpinos-menu →
"GPU Driver" → "NVIDIA proprietary"). As of the current image, this is
NVIDIA's 615.71.09 driver from NVIDIA's own apt repository, built against
the open-source nvidia-kernel-open-dkms kernel module (NVIDIA no longer
ships the closed kernel module on this driver branch). That open kernel
module supports Turing and newer GPUs:
Older cards (Kepler, Maxwell, Pascal — e.g. GTX 900/10-series and
earlier) aren't supported by this proprietary path at all; they fall back to
the default NVK/nouveau path above. Also note: the proprietary driver has
not yet been independently verified by this project on real NVIDIA
hardware with Hyprland/Wayland — see notes/vpinos.md and
notes/nvidia-proprietary.md for the ongoing investigation.
~/tables ships pre-populated with 3 of the 4 tables from
superhac/vpinos-test-tables
— Fair Fight, Mars Trek and Halley Comet (143.2 MB) —
fetched at build time, not committed into this repo. The 4th table,
Cyclopes (103.3 MB), is left out: bundling all 4 (246.5 MB)
would push the ISO over GitHub's 2 GB release-asset limit.
A systemd service, vpinos-fetch-tables.service, can instead download
the full current set from the network on boot — not enabled by
default for now, since the bundled tables already populate ~/tables
and its own idempotency check would just no-op against that. Re-enable
it (systemctl enable vpinos-fetch-tables.service in
0200-enable-kiosk.hook.chroot) to go back to download-on-boot instead
of bundling, e.g. if the upstream table set changes enough that the
bundled copies go stale.
vpinball, vpinfe and vpxconfig are not compiled into the image. They come from a
signed apt repository, vpinos/deb-repo,
whose packages are built in
vpinos/deb-package-builder.
The repository's source and public key are part of the image, so on an
installed system:
sudo apt update && sudo apt upgradepicks up new vpinball / vpinfe / vpxconfig releases and Debian security updates. A newly built ISO always contains whatever is currently published there.
The image (live and installed) has trixie-backports enabled alongside
trixie, trixie-updates and trixie-security, all with the
main contrib non-free non-free-firmware areas. An apt preference
(/etc/apt/preferences.d/vpinos-backports.pref) makes the kernel, Mesa and
GPU firmware follow backports, so sudo apt upgrade on an installed system
brings newer versions of those as backports publishes them. Notes:
- Reboot after a kernel update. The previous kernel stays installed and can be chosen from the GRUB menu if the new one misbehaves.
- Everything else stays on trixie. Backports are opt-in in Debian, so to take
any other package from there, ask for it explicitly:
sudo apt install -t trixie-backports <package>.
- Because those packages track backports, kernel/Mesa updates can change GPU behavior between releases — if something regresses after an upgrade, boot the older kernel from GRUB and report it.
VPinOS is a single-purpose cabinet image and ships with a known default
password (vpinos / vpinos) and an SSH server installed for remote
maintenance. On any cabinet that is reachable from a network, change the
password after installing (passwd), and don't expose it to the internet.
You need Docker; everything else happens inside the vpinos-builder
container defined by the Dockerfile.
docker build -t vpinos-builder .
docker run --rm --privileged --ulimit nofile=65536:65536 -v "$PWD:/work" -w /work vpinos-builder lb clean
docker run --rm --ulimit nofile=65536:65536 -v "$PWD:/work" -w /work vpinos-builder \
lb config \
--distribution trixie \
--architectures amd64 \
--binary-images iso-hybrid \
--archive-areas "main contrib non-free non-free-firmware" \
--bootappend-live "boot=live components username=vpinos" \
--apt-indices false
# NOT --backports true -- trixie-backports comes from
# config/archives/vpinos-backports.list instead, pinned to a frozen
# snapshot.debian.org timestamp (the live mirror's kernel packaging
# proved unreliable). See notes/vpinos.md step 2 for the full story,
# including why config/archives/vpinos-backports.pref pins by
# `origin snapshot.debian.org`, not just by release/suite name.
#
# --apt-indices false -- drops /var/lib/apt/lists (the downloaded
# Packages/Release/Translation indices for every configured repo,
# ~168M) from the shipped image after live-build's own final
# `Apt chroot update` re-sync against the real mirrors. A
# chroot-stage hook can't do this: that re-sync runs in the later
# binary stage, after every chroot hook, and silently repopulates
# whatever a hook deletes. Nothing at runtime needs it --
# network-manager + systemd-timesyncd are already present, so
# `apt-get update` works normally the first time anyone installs
# something on a real cabinet. See notes/vpinos.md for the full story.
# Fix ownership of the generated config only -- never `chown -R` the whole
# project: it corrupts cache/bootstrap and the built image ends up with its
# base system owned by the wrong user.
docker run --rm -v "$PWD:/work" -w /work vpinos-builder sh -c \
'for d in config auto local .build; do [ -e "$d" ] && chown -R '"$(id -u):$(id -g)"' "$d"; done; true'
docker run --rm --privileged --ulimit nofile=65536:65536 -v "$PWD:/work" -w /work vpinos-builder lb buildThe ISO lands in the project root as live-image-amd64.hybrid.iso. Always
run the full lb clean → lb config → lb build cycle after
editing anything under config/; a bare lb build silently reuses stale
stages. The GitHub Actions workflow runs
this same sequence.
chroot/, binary/, cache/, .build/ and dist/ are build output and
are not committed.
Beta. Early and actively developed. The build, live boot, launch path (Hyprland → vpinball / vpinfe) and installer are working end to end. Booting straight into the frontend on an installed cabinet is now opt-in ("Boot on startup"). Still ahead: persistence for the live medium, and narrowing GPU/firmware support once the target hardware is settled.