# GeekCade Cabinet — Full Architecture Reference This document is the complete spec for the GeekCade cabinet project. It covers what each component does, how they interact, the design decisions behind them, and every command/file/gotcha discovered along the way. If you're a human picking up this project after a year, or an AI assistant joining a new conversation, **read this top to bottom** before changing anything. For a short user-facing intro, see [README.md](README.md). For AI assistant operating context, see [CLAUDE.md](CLAUDE.md). --- ## Table of contents 1. [What this project is](#what-this-project-is) 2. [System architecture](#system-architecture) 3. [Boot flow](#boot-flow) 4. [File and directory layout](#file-and-directory-layout) 5. [The four scripts](#the-four-scripts) 6. [USB asset sync](#usb-asset-sync) 7. [AttractMode Plus configuration](#attractmode-plus-configuration) 8. [MAME configuration](#mame-configuration) 9. [Hard-won lessons (read before "fixing" things)](#hard-won-lessons) 10. [Build history](#build-history) 11. [Useful commands](#useful-commands) 12. [Future work](#future-work) --- ## What this project is A reproducible install/maintenance system that turns a fresh Ubuntu installation into a dedicated arcade cabinet PC. The cabinet is connected to a monitor via HDMI/DP and to physical input (joystick encoder presenting as USB keyboard, or a real USB keyboard). **Design goals:** - Bare-metal: no X11, no Wayland, no display manager, no desktop environment. AM+ runs directly on KMS/DRM. - Reproducible: a fresh OS install becomes a working cabinet in 5-10 minutes via a single bootstrap command. - Self-contained: all hosted assets at one URL pattern. No external dependencies that could disappear. - Self-documenting: scripts are heavily commented; this readme captures the why behind the what. - Headless-serviceable: SSH in from another machine to inspect/edit. Cabinet keyboard is for AM+ navigation only. - Idempotent: every script can be re-run safely. Reset → install → asset sync should always end up at the same state. **Non-goals:** - Not a kiosk lockdown — `geekcade` user has sudo. The cabinet is meant for the owner, not random arcade visitors. - Not multi-user. Just `geekcade`. - Not generic — opinionated paths and conventions throughout. If you fork this, expect to rename things. --- ## System architecture ``` ┌─────────────────────────────────────────────────────────────────────┐ │ CABINET PC │ │ │ │ Ubuntu 26.04 server install (no DE) │ │ │ │ │ ├─ systemd │ │ │ ├─ getty@tty1 (auto-login as geekcade) │ │ │ ├─ disable-tty-blank.service (no console blanking) │ │ │ └─ media-GEEKCADE.automount (USB auto-mount) │ │ │ │ │ ├─ MAME (apt package) │ │ │ └─ ~/mame/ (homepath, all paths absolute in mame.ini) │ │ │ │ │ └─ AttractMode Plus (compiled from GitHub) │ │ └─ ~/attractplus/ (config dir via --config flag) │ │ │ │ Inputs: USB keyboard or joystick encoder (HID keyboard mode) │ │ Output: HDMI/DP direct to monitor │ │ Network: LAN-connected, SSH accessible │ └─────────────────────────────────────────────────────────────────────┘ │ │ wget on first install ▼ ┌─────────────────────────────────────────────────────────────────────┐ │ DEPLOY SERVER (install.geekcade.com) │ │ │ │ HTTP-only static server hosting: │ │ /deploy/geekcade_install.sh │ │ /deploy/geekcade_menu.sh │ │ /deploy/geekcade_assets.sh │ │ /deploy/geekcade_reset.sh │ │ /deploy/dkong.zip (sample ROM) │ │ /deploy/geekcade_wallpaper.jpg │ │ /deploy/geekcade_readme.md │ └─────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────┐ │ USB ASSET DRIVE (labeled "GEEKCADE", exFAT) │ │ │ │ /extras/ Pleasuredome MAME EXTRAs │ │ ├─ <20 .zip files> bezels, flyers, marquees, etc. │ │ ├─ cheat.7z MAME cheat archive │ │ └─ artwork/, dats/, folders/, history/, samples/ (subfolders) │ │ /multimedia/ │ │ ├─ soundtrack/ in-game music (mp3 per game) │ │ └─ videosnaps/ attract preview videos (mp4) │ │ /roms/ flat .zip files (one per game) │ │ /support/ AntoPISA's MAME_SupportFiles │ │ ├─ bestgames/bestgames.ini │ │ ├─ catver/catver.ini + UI_files/ │ │ ├─ command/dats/command.dat + folders/command.ini │ │ └─ gameinit/dats/gameinit.dat + folders/gameinit.ini │ │ /installer/ optional offline-bootstrap copy │ └─────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────┐ │ DEV / SOURCE MACHINE (your Mac or edgedev Linux box) │ │ │ │ - GitHub repo: edgespresso/geekcade │ │ - Local clone for editing scripts │ │ - Push to GitHub, then deploy to web server │ └─────────────────────────────────────────────────────────────────────┘ ``` --- ## Boot flow ``` Power on │ ▼ BIOS/UEFI → systemd boot │ ▼ Reach multi-user.target │ ▼ getty@tty1 service starts │ ▼ /etc/systemd/system/getty@tty1.service.d/autologin.conf │ forces auto-login as geekcade │ ▼ geekcade login → ~/.bash_profile sources │ ▼ Conditional autostart block in .bash_profile: │ if [ "$(tty)" = "/dev/tty1" ] && [ -x "$HOME/geekcade_menu.sh" ]; then │ "$HOME/geekcade_menu.sh" │ fi │ ▼ ~/geekcade_menu.sh runs │ ├─→ Check /tmp/.geekcade_skip_gate ? │ │ │ ├─ Exists (from prior AM+ exit) → drop straight to admin menu │ │ │ └─ Doesn't exist → 5-second boot gate │ │ │ ├─ Key pressed → admin menu │ │ │ └─ Timeout → ~/attractplus.sh launches AM+ │ │ │ ▼ │ AM+ runs, user plays games │ │ │ ▼ │ User exits AM+ │ │ │ ▼ │ Menu touches /tmp/.geekcade_skip_gate │ │ │ ▼ │ Menu calls: sudo systemctl restart getty@tty1 │ │ (this is the ONLY known fix for the SDL2 │ │ KMSDRM keyboard freeze - see lessons) │ │ │ ▼ │ getty respawns → fresh tty1 → auto-login │ │ │ ▼ │ Back to top of boot flow ``` The skip-gate flag pattern is critical: AM+ exits return the user to the admin menu (not the boot gate again), but a fresh boot gives them the gate. This is what users intuitively want. SSH sessions get a normal shell — the autostart block specifically checks `tty == /dev/tty1`, so remote logins land at a normal bash prompt. --- ## File and directory layout ### On the cabinet (`/home/geekcade/`) ``` ~/ User home ├── geekcade_install.sh Installer (fetched on bootstrap) ├── geekcade_menu.sh Boot gate + admin menu (fetched by installer) ├── geekcade_assets.sh USB asset sync (fetched by installer) ├── geekcade_reset.sh Reset script (manually placed) ├── mame.sh MAME launcher (sets SDL env vars) ├── attractplus.sh AM+ launcher (--config flag) ├── .bash_profile Has tty1 autostart block (marker-bracketed) ├── .bash_aliases Has mame/attractplus aliases (marker-bracketed) ├── .geekcade_install_success Created when installer completes cleanly │ ├── attractplus/ AM+ config-dir (passed via --config) │ ├── config/ │ │ └── attract.cfg Display config + filters │ ├── emulators/ │ │ └── mame.cfg MAME emulator entry + artwork paths │ ├── romlists/ │ │ └── mame.txt Generated by --build-romlist │ ├── cache/ AM+ runtime cache (rebuilt on launch) │ ├── stats/ Per-game play counts │ ├── flyers/ marquees/ snaps/ wheels/ AM+ default artwork folders │ │ (user can drop custom art here; │ │ takes precedence over USB-synced │ │ ~/mame/ folders) │ └── (plus various other AM+ subfolders: layouts, plugins, sounds, etc.) │ ├── attractplus-source/ AM+ build directory (compile flow only) │ ├── mame/ MAME homepath │ ├── mame.ini All paths absolute │ ├── roms/ ROM zips │ │ │ │ --- Synced from USB extras (zips extracted into folders) --- │ ├── artpreview/ artwork/ │ ├── bosses/ cabinets/ │ ├── cpanel/ devices/ │ ├── ends/ flyers/ │ ├── gameover/ howto/ │ ├── icons/ logo/ │ ├── manuals/ marquees/ │ ├── pcb/ scores/ │ ├── select/ snap/ │ ├── titles/ versus/ │ ├── warning/ cheat.7z (kept as 7z, not extracted) │ │ │ │ --- Synced from USB extras (already-extracted folders) --- │ ├── dats/ history.dat, mameinfo.dat, command.dat │ ├── folders/ catver.ini, nplayers.ini, etc. │ ├── history/ history.xml │ ├── samples/ per-game .zip samples │ │ │ │ --- Synced from USB multimedia --- │ ├── soundtrack/ in-game music mp3s │ ├── videosnaps/ attract-mode preview mp4s │ │ │ │ --- MAME runtime state (regenerated, removed by soft reset) --- │ ├── cfg/ nvram/ inp/ sta/ diff/ comments/ hi/ │ │ │ │ --- Idempotency markers --- │ └── /.geekcade_synced_size per-folder size marker for skip-on-resync │ ├── logs/ Installer log files ├── wallpaper/ Cabinet desktop wallpaper └── configs/ Convenience symlinks to common configs ``` ### System files installed by installer ``` /etc/systemd/system/ ├── getty@tty1.service.d/autologin.conf Auto-login override ├── disable-tty-blank.service No console blanking ├── media-GEEKCADE.mount USB mount unit └── media-GEEKCADE.automount USB lazy automount /etc/sudoers.d/ ├── geekcade-getty Passwordless systemctl restart getty@tty1 └── geekcade-poweroff Passwordless reboot/shutdown /media/GEEKCADE/ USB mount point (created at install) ``` ### On the deploy server (`https://install.geekcade.com/deploy/`) ``` geekcade_install.sh geekcade_menu.sh geekcade_assets.sh geekcade_reset.sh (optional but useful) geekcade_readme.md (this doc, optional) dkong.zip sample ROM for first-run testing geekcade_wallpaper.jpg cabinet wallpaper image ``` ### On the USB asset drive (labeled `GEEKCADE`, exFAT) ``` GEEKCADE/ USB root ├── extras/ │ ├── artpreview.zip bosses.zip cabinets.zip cheat.7z │ ├── cpanel.zip devices.zip ends.zip flyers.zip │ ├── gameover.zip howto.zip icons.zip logo.zip │ ├── manuals.zip marquees.zip pcb.zip scores.zip │ ├── select.zip snap.zip titles.zip versus.zip │ ├── warning.zip │ ├── artwork/ (already-extracted folder) │ ├── dats/ (already-extracted folder) │ ├── folders/ (already-extracted folder) │ ├── history/ (already-extracted folder) │ └── samples/ (already-extracted folder) ├── multimedia/ │ ├── soundtrack/ │ └── videosnaps/ ├── roms/ flat .zip files ├── support/ AntoPISA MAME_SupportFiles │ ├── bestgames/ │ │ └── bestgames.ini (+ readmes, ignored) │ ├── catver/ │ │ ├── catver.ini │ │ └── UI_files/ │ │ ├── catlist.ini genre.ini │ │ ├── genre_ows.ini mature.ini │ │ └── not_mature.ini │ ├── command/ │ │ ├── dats/command.dat │ │ └── folders/command.ini │ └── gameinit/ │ ├── dats/gameinit.dat │ └── folders/gameinit.ini └── installer/ optional offline copy of scripts ├── geekcade_install.sh ├── geekcade_menu.sh ├── geekcade_assets.sh ├── geekcade_reset.sh └── geekcade_readme.md ``` --- ## The four scripts ### `geekcade_install.sh` Runs once on a fresh Ubuntu install. Idempotent — re-running on an existing install upgrades and reconfigures without breaking. Build 34+. **Flags:** ``` --compile Build AM+ from latest GitHub source (DEFAULT) --release Install official Ubuntu .deb release (only works on 22.04/24.04) --skip-attract Skip AM+ install entirely --debug Print shell trace --brief Hide command output (still logs to file) ``` **What it does (high level):** 1. Run as `geekcade` user (not root). Asks for sudo password once at start. 2. Installs apt packages (mame, build deps, audio stack, rsync, udisks2, etc.). 3. Adds `geekcade` to `input`, `video`, `render`, `kvm` groups (KMSDRM access). 4. Installs PipeWire-Pulse shim (AM+ uses miniaudio which speaks Pulse protocol). 5. Enables user lingering (`loginctl enable-linger geekcade`) so user services run without login session. 6. Configures systemd auto-mount for the GEEKCADE USB label. 7. Configures auto-login on tty1. 8. Disables tty1 console blanking via systemd service. 9. Either compiles AM+ from source (`--compile`, default) or installs the .deb (`--release`). 10. Creates launcher scripts: `mame.sh` (with KMSDRM env vars), `attractplus.sh` (with --config flag). 11. Writes `~/mame/mame.ini` from `mame -cc`, with absolute paths via sed. 12. Creates `~/attractplus/config/attract.cfg` (display config) and `~/attractplus/emulators/mame.cfg` (emulator config) if they don't exist (preserves existing). 13. Adds bash aliases (`mame`, `attractplus`, `attract` → `.sh` launchers). 14. Adds tty1 autostart block to `~/.bash_profile`. 15. Sets up `/etc/sudoers.d/geekcade-getty` for passwordless `systemctl restart getty@tty1`. 16. Fetches the latest `geekcade_menu.sh` and `geekcade_assets.sh` from deploy server. 17. Runs `geekcade_assets.sh` if a GEEKCADE USB is detected (auto-sync on fresh install). **Why `--compile` is default:** Ubuntu 26.04 has no matching AM+ .deb at the time of writing. The release path falls back to compile anyway — defaulting straight to compile saves a step. ### `geekcade_menu.sh` Runs on every tty1 login via `.bash_profile` autostart. Displays the boot gate and admin menu. **Boot gate:** 5-second countdown. Any key drops to the admin menu, otherwise launches `~/attractplus.sh`. The skip-gate flag (`/tmp/.geekcade_skip_gate`) is touched by the menu when AM+ exits, causing the next login to skip the gate and land directly at the menu. **Menu options (build 38):** ``` 1) Launch Attract-Mode — runs ~/attractplus.sh 2) Sync Assets from USB — runs ~/geekcade_assets.sh 3) Rebuild Attract-Mode MAME Romlist 4) View Latest Installer Log 5) Check for System Updates (apt) 6) Download Latest GeekCade Installer 7) Download Latest GeekCade Menu 8) Run Installer - COMPILE Attract-Mode Plus 9) Run Installer - RELEASE Attract-Mode Plus 10) Run Installer - SKIP Attract-Mode Plus 11) Reboot System 12) Shutdown System q) Exit to Shell ``` The "Download Latest" options use `_download_to` helper that fetches from `${GEEKCADE_BASE_URL}` (default `https://install.geekcade.com/deploy/`). ### `geekcade_assets.sh` Syncs Pleasuredome assets from USB to cabinet. Build 34. **Flags:** ``` --source PATH Use specific path as USB source (overrides auto-detect) --check Only check if USB is detected, exit 0/1 silently --help Show help ``` **Auto-detection:** Searches `/media/GEEKCADE`, `/media/geekcade/GEEKCADE`, `/run/media/...`, `/mnt/...` paths. Fast-paths via `/dev/disk/by-label/GEEKCADE` check to avoid triggering systemd automount when no USB is present. **Sync sections (in order):** 1. **Extras zips** → `~/mame//` (extracted from each `extras/.zip`) 2. **Extras subfolders** → `~/mame//` (rsync merge of `extras/{artwork,dats,folders,history,samples}`) 3. **Multimedia** → `~/mame/{soundtrack,videosnaps}/` (rsync merge) 4. **Roms** → `~/mame/roms/` (rsync merge), then rebuild AM+ romlist + ensure display filter 5. **Support files** → `~/mame/{folders,dats}/` (file-by-file `cp` of AntoPISA's MAME_SupportFiles), then rebuild romlist again (catver categories now available) **Order is intentional:** support runs LAST so AntoPISA's versions of files (newer/more authoritative) win on filename collisions with Pleasuredome's `extras/dats/`. **Idempotency strategies:** - Extras zips track extraction via `~/mame//.geekcade_synced_size` marker file containing the source zip's byte size. Re-run skips already-extracted zips. - rsync uses `--size-only` (NOT `--checksum`, NOT mtime-based) to handle exFAT mtime drift across macOS↔Linux. - mame.ini and attract.cfg edits use marker patterns so they're rewritten correctly on re-run, not duplicated. **AM+ display filter (added during rom sync):** ``` filter GeekCade Working Parents rule Status not_equals preliminary rule CloneOf equals rule Category not_contains Mahjong rule Category not_contains Casino rule Category not_contains Casino / rule Category not_contains Computer rule Category not_contains Console rule Category not_contains Handheld rule Category not_contains Tabletop rule Category not_contains Mechanical rule Category not_contains Slot Machine rule Category not_contains Calculator rule Category not_contains Electromechanical rule Category not_contains Misc. rule Category not_contains Utilities rule Category not_contains System rule Category not_contains BIOS ``` **This is a display-time filter, not a sync-time filter.** All ROMs (including clones) stay on disk so MAME's parent/clone dependencies work. AM+ just hides them from the user's menu. ### `geekcade_reset.sh` Undoes most of what the installer did. Two modes. **Default (soft reset):** - Removes config files: `mame.ini`, `attract.cfg`, `mame.cfg`, `romlists/mame.txt` - Removes per-game state: `cfg/`, `nvram/`, `inp/`, `sta/`, `diff/`, `comments/`, `hi/`, etc. - Removes AM+ cache - Removes launcher scripts, bash aliases block, autostart block, sudoers, systemd units - **Preserves** all USB-synced content: roms, extracted asset folders, dats, folders, history, samples, videosnaps, soundtrack, cheat.7z, and the `.geekcade_synced_size` markers After soft reset + reinstall, the next asset sync is fast — rsync's size-only check skips files that are already there. **`--hard` mode:** Same as default plus `rm -rf ~/mame ~/attractplus`. Use when you really want a clean slate (e.g., to re-test from scratch, or because something is so corrupted that surgical removal isn't enough). --- ## USB asset sync ### Source layout (Pleasuredome convention) The USB at `/media/GEEKCADE/` should follow this layout: ``` extras/ Pleasuredome MAME EXTRAs (https://pleasuredome.miraheze.org/wiki/MAME_EXTRAs) multimedia/ Pleasuredome MAME Multimedia roms/ Flat .zip ROM files (your MAME version's matching set) support/ AntoPISA's MAME_SupportFiles (https://github.com/AntoPISA/MAME_SupportFiles) ``` ### Why we extract zips instead of letting MAME read them directly MAME natively reads `flyers.zip` as if it were a folder (zip-as-folder mode). We could keep zips and use the Pleasuredome convention `flyers_directory flyers;` and MAME would find them. **But AttractMode Plus does NOT read into zips.** AM+'s artwork paths point at folders and look for `.png` etc. inside. So we extract once. After that: - MAME reads the folder (faster than in-archive decompression) - AM+ reads the folder (works at all) - mame.ini paths are intuitive: `flyers_directory /home/geekcade/mame/flyers` `cheat.7z` is the exception — it stays as a 7z archive because MAME reads it natively and AM+ doesn't use cheats. ### Why we use `--size-only` for rsync exFAT (the USB filesystem) stores modification times in **local time with no timezone info**. Linux interprets exFAT mtimes as **UTC**. macOS displays them in local time but sets them via UTC. Result: a file with mtime "2:00 PM" written from Mac, read on Linux, looks like it was modified at "2:00 PM UTC" — which in your local timezone is hours different from when you actually wrote it. rsync's default behavior (compare size + mtime) would see this as "different file" and recopy every single time, even though the file is identical. **`--size-only` solves this by comparing only file sizes.** Safe for our use case because Pleasuredome zips are versioned (different content always means different size). ### Why we always rebuild romlist after support sync AM+'s romlist contains a snapshot of game metadata at the time of build — including the `Category` field which comes from `catver.ini`. If you build the romlist BEFORE catver is in place (e.g., when roms sync runs first), every entry's Category field is empty. Then filters like `Category not_contains Mahjong` silently match every entry (because empty-string contains nothing). Build 33+ ALWAYS rebuilds the romlist after support sync, even if rom sync already triggered one. This guarantees catver data is in the romlist before the next AM+ launch. ### USB auto-mount on headless systems `udisks2` handles auto-mounting when there's a graphical session — but headless Ubuntu has no graphical session, so udisks2 sits idle and never mounts USBs. **Solution:** A `systemd.mount` + `systemd.automount` unit pair watches for `/dev/disk/by-label/GEEKCADE` and mounts it to `/media/GEEKCADE` lazily (on first access) without requiring any session. ``` /etc/systemd/system/media-GEEKCADE.mount [Unit] Description=GeekCade USB Drive (GEEKCADE label) ConditionPathExists=/dev/disk/by-label/GEEKCADE [Mount] What=/dev/disk/by-label/GEEKCADE Where=/media/GEEKCADE Type=auto Options=uid=1000,gid=1000,nofail,noatime,x-systemd.idle-timeout=60 [Install] WantedBy=multi-user.target ``` ``` /etc/systemd/system/media-GEEKCADE.automount [Unit] Description=Automount for GeekCade USB Drive [Automount] Where=/media/GEEKCADE TimeoutIdleSec=60 [Install] WantedBy=multi-user.target ``` The `idle-timeout` unmounts the USB after 60 seconds of inactivity, allowing safe physical removal. **Critical lesson:** The USB detection function MUST first check `/dev/disk/by-label/GEEKCADE` (instant, kernel-level) BEFORE trying any `[ -d /media/GEEKCADE ]` checks. The automount unit triggers a mount on directory access, which hangs the script for ~30 seconds when no USB is present. Build 26 fixed this with a fast-path check. --- ## AttractMode Plus configuration ### Why we pass `--config` everywhere AM+ defaults to writing config to `~/.attract/`. We override this with `--config /home/geekcade/attractplus` so config lives in a predictable place. But importantly, **even with --config**, AM+ creates a `config/` SUBFOLDER inside the config dir. So the actual config file is at: ``` ~/attractplus/config/attract.cfg <-- not ~/attractplus/attract.cfg ``` This is the path our installer writes to and the asset script reads from. Build 34 fixed this — earlier builds had the wrong path and silently failed to add display filters. ### attract.cfg structure ``` general ... display Arcade layout Attrac-Man romlist mame in_cycle yes in_menu yes filter GeekCade Working Parents rule Status not_equals preliminary rule CloneOf equals ... sound ... ``` The display block is what users see. The filter block trims it down at display time without modifying the romlist. ### mame.cfg structure This is the AM+ **emulator** config (one per emulator, not per display). ``` executable /home/geekcade/mame.sh args [name] rompath /home/geekcade/mame/roms romext .zip artwork flyer /home/geekcade/attractplus/flyers;/home/geekcade/mame/flyers artwork marquee /home/geekcade/attractplus/marquees;/home/geekcade/mame/marquees artwork snap /home/geekcade/attractplus/snaps;/home/geekcade/mame/snap;/home/geekcade/mame/videosnaps artwork cabinet /home/geekcade/mame/cabinets;/home/geekcade/mame/devices artwork title /home/geekcade/mame/titles artwork pcb /home/geekcade/mame/pcb artwork bosses /home/geekcade/mame/bosses artwork scores /home/geekcade/mame/scores artwork select /home/geekcade/mame/select artwork versus /home/geekcade/mame/versus artwork gameover /home/geekcade/mame/gameover artwork howto /home/geekcade/mame/howto artwork video /home/geekcade/mame/videosnaps artwork music /home/geekcade/mame/soundtrack ``` AM+ supports multi-path artwork via semicolon separator. The first path that contains `.png` (or appropriate extension) wins. We put `~/attractplus/` paths FIRST so user customizations override the USB-synced default. The script only adds artwork paths for folders that actually exist on the cabinet. Missing folders → no path added → no broken reference. Self-healing if the user later syncs more content. ### Audio: PipeWire-Pulse shim AM+ uses miniaudio internally, which on Linux speaks the PulseAudio protocol. Modern Ubuntu uses PipeWire by default, so we install `pipewire-pulse` to provide the Pulse protocol while keeping the rest of PipeWire intact: ``` sudo apt install -y pipewire pipewire-pulse pipewire-audio wireplumber pulseaudio-utils systemctl --user enable --now pipewire pipewire-pulse wireplumber loginctl enable-linger geekcade ``` The `enable-linger` is necessary because user services normally only run while the user is logged in. Lingering keeps PipeWire alive across login state changes. --- ## MAME configuration ### `~/mame.sh` launcher ```bash #!/usr/bin/env bash export SDL_VIDEODRIVER=kmsdrm export SDL_AUDIODRIVER=alsa export SDL_VIDEO_KMSDRM_CRTC=0 exec /usr/games/mame -homepath /home/geekcade/mame "$@" ``` **Why these env vars:** - `SDL_VIDEODRIVER=kmsdrm` — direct DRM/KMS rendering, no X required - `SDL_AUDIODRIVER=alsa` — bypass PulseAudio for MAME (avoids latency issues; MAME does its own audio buffering) - `SDL_VIDEO_KMSDRM_CRTC=0` — pin to first CRTC (display output). Otherwise SDL2 might pick the wrong one on multi-output systems. ### `mame.ini` paths (all absolute) `mame -cc` generates a default mame.ini with relative paths like `flyers_directory flyers`. We sed-edit them to absolute paths: ``` sed -i 's#^cfg_directory[[:space:]].*#cfg_directory /home/geekcade/mame/cfg#' mame.ini ``` Note the `[[:space:]]` — modern MAME's mame.ini uses tabs, not spaces. Without `[[:space:]]` the regex misses the tab and silently fails. ### Why absolute paths matter MAME resolves relative paths against the **current working directory**, not against `homepath`. So if you launch MAME from `/some/random/path`, relative `flyers` becomes `/some/random/path/flyers` — not `/home/geekcade/mame/flyers`. This silently broke ROM loading and asset display for hours of debugging. Absolute paths everywhere. No exceptions. ### Bash aliases route bare commands ``` alias mame='/home/geekcade/mame.sh' alias attractplus='/home/geekcade/attractplus.sh' alias attract='/home/geekcade/attractplus.sh' ``` So typing `mame dkong` runs `mame.sh dkong` which sets the SDL env vars and calls `/usr/games/mame -homepath /home/geekcade/mame dkong`. To bypass the alias (for diagnostics like `mame --version`), use `command mame --version` or call `/usr/games/mame --version` directly. --- ## Hard-won lessons ### The SDL2 KMSDRM keyboard freeze **Symptom:** AM+ exits cleanly back to the shell, but the keyboard is dead. Nothing types. Ctrl+C does nothing. tty1 is permanently borked until reboot. **Root cause:** SDL2's KMSDRM video driver grabs the input device at startup and doesn't release it cleanly on exit in some versions. The kernel input subsystem still thinks SDL has the keyboard. **Things that DON'T work (we tested all of them):** - `kbd_mode -a` to reset keyboard mode - `chvt 2; chvt 1` to switch consoles and back - Sleep/wait for SDL to "release" the device - Sending various signals to the SDL process before exit - Modifying SDL2 build flags **The ONLY thing that works:** `sudo systemctl restart getty@tty1` This kills the stuck shell entirely, respawns getty, gives a fresh login session that has clean input handles. The `geekcade-getty` sudoers entry makes this passwordless. In the menu: when AM+ exits, the menu touches `/tmp/.geekcade_skip_gate` (so the next login skips straight to admin menu instead of boot gate again), then runs the systemctl restart. DO NOT add a respawn loop, sleep, or "graceful retry" to the AM+ launcher. The only fix is the getty restart. We learned this through 22 build iterations. ### The "two config worlds" problem Early on, MAME and AM+ launched without env-var tweaks would create config dirs at `~/.mame/` and `~/.attractplus/` (default locations). Meanwhile our installer was setting up `~/mame/` and `~/attractplus/` (custom locations). Result: parallel config worlds. Edits to one didn't affect the other. **Fix:** Bash aliases route ALL invocations of `mame` and `attractplus` through the .sh launchers. The launchers explicitly pass `-homepath` and `--config` so config goes to the right place. If you `which mame` you should see the alias. If `mame` isn't aliased (e.g., in a script context where aliases don't expand), use `~/mame.sh` directly. ### AM+ creates a `config/` subfolder AM+'s `--config ` doesn't put config files in `` — it puts them in `/config/`. So we have to create `~/attractplus/config/` and write `attract.cfg` there. Build 34 fixed this — earlier builds wrote to `~/attractplus/attract.cfg` which AM+ ignored. ### Auto-mount on headless requires systemd, not udisks2 `udisks2` is installed and running, but it depends on a logged-in graphical session to actually do auto-mount. tty1 auto-login doesn't count as a graphical session. So plug-in events get logged but no mount happens. **Solution:** Custom `systemd.mount` + `systemd.automount` units watching for the GEEKCADE label. ### USB detection must short-circuit before checking mount paths `[ -d /media/GEEKCADE ]` triggers the systemd automount unit, which tries to mount the device. If there's no USB, the mount fails after a 30-second timeout, hanging the script. **Fix:** Check `/dev/disk/by-label/GEEKCADE` first (instant kernel check). If no labeled device, skip the `/media/` paths entirely. ### exFAT timestamps drift across OSes exFAT stores mtimes without timezone info. macOS sets them in UTC; Linux interprets them in UTC. Result: same file looks different to rsync's mtime check. **Fix:** rsync `--size-only`. Don't compare timestamps, just sizes. ### Romlist must be built AFTER catver If catver.ini isn't in `~/mame/folders/` when AM+ builds the romlist, the Category field in the romlist is empty. Display filters that key off Category silently match everything. **Fix:** Always rebuild romlist after support sync, even if rom sync already triggered one. ### MAME audio needs both alsa-utils AND `sound=sdl` in mame.ini Without `alsa-utils` apt package, MAME can't even open `/dev/snd/*`. With it but `sound=auto` or `sound=pa`, MAME picks PulseAudio which fights with our PipeWire setup. Setting `sound=sdl` in mame.ini routes MAME's audio through SDL2 which uses ALSA (per our `SDL_AUDIODRIVER=alsa` env var). ### `geekcade` user needs input/video/render groups KMSDRM access requires `video` and `render` groups. Evdev keyboard access requires `input`. Standard Ubuntu doesn't add these by default. The installer adds them. After group changes, the user must log out + log back in for them to take effect. The installer reminds the user to reboot after install. --- ## Build history Full chronological notes (concise version — the long story is in chat history): ``` 1 Original v2.0 input - working baseline 2 KMSDRM env vars + groups + audio + sed-tab fix + TTY blanking 3 alsa-utils added 4 PipeWire-Pulse shim for AM+ 5 Bash aliases for mame/attractplus 6 Absolute paths in mame.ini 7-12 Misc fixes consolidated 13 Always-latest AM+ from GitHub, auto-login, boot splash 14 Passthrough for --version flags 15 Removed AM+ launcher respawn loop 16 Actual readable splash screen 17 Stripped to minimum (no shutdown logic) 18 kbd_mode trap (REVERTED — doesn't work) 19 getty@tty1 restart on AM+ exit (this is the fix) 20 Menu integration with skip-gate flag 21 GEEKCADE_BASE_URL pattern, fetch menu from server 22 --compile is default 23 systemd USB automount 24 Removed lib stub, added assets fetch 25 USB auto-sync in installer if USB present 26 detect_usb fast-path avoids automount trigger 27 Extras zip rsync (later changed in 29) 28 Pleasuredome-canonical mame.ini syntax (later changed in 29) 29 Extract zips into folders, AM+ artwork wired up (this is what stuck) 30 Section 3: extras subfolders 31 Section 4: multimedia + refactor rsync_one_folder helper 32 Section 5: roms + romlist rebuild + display filter 33 Section 6: support files (AntoPISA) 34 Fix attract.cfg path (config/ subfolder), fix dedup flag, stronger filter rules, reset cleanup of ~/.attract, soft reset preserves USB content 35 Attempted live progress display via awk (broken — rsync uses \r not \n) 36 Live progress fixed via stdbuf + tr '\r' '\n' + bash while-read pipe 37 Domain migration prep — switched scripts to https://geekcade.live/deploy (canceled, refund processed) 38 Domain finalized as install.geekcade.com (subdomain of pre-owned geekcade.com). All scripts + bootstrap + homepage point at the new URL. ``` --- ## Useful commands ### Refresh scripts from deploy server ```bash # Add to ~/.bash_aliases for convenience alias gc-refresh='for f in geekcade_install.sh geekcade_menu.sh geekcade_assets.sh geekcade_reset.sh; do wget -qO ~/$f https://install.geekcade.com/deploy/$f && chmod +x ~/$f && echo "✓ $f"; done' ``` Then just `gc-refresh` to pull fresh versions. ### Standard iteration loop ```bash gc-refresh # pull fresh scripts ./geekcade_reset.sh # soft reset (preserves USB-synced content) ./geekcade_install.sh # reinstall # Optional: ~/geekcade_assets.sh # if USB wasn't auto-detected sudo reboot ``` ### Diagnostics ```bash # What got synced from USB ls -d ~/mame/*/ 2>/dev/null find ~/mame -name '.geekcade_synced_size' -exec dirname {} \; # Verify mame.ini paths grep -E "_directory|path[[:space:]]" ~/mame/mame.ini # Verify AM+ filter grep -A 16 "GeekCade Working Parents" ~/attractplus/config/attract.cfg # Verify AM+ artwork wiring grep "^artwork" ~/attractplus/emulators/mame.cfg # Romlist size wc -l ~/attractplus/romlists/mame.txt # What apt packages are installed dpkg -l | grep -E "mame|attract" # Check audio groups for geekcade user groups geekcade # Check sudoers sudo cat /etc/sudoers.d/geekcade-* # Check systemd units systemctl status disable-tty-blank systemctl status media-GEEKCADE.automount systemctl status getty@tty1 # Check USB detection ls /dev/disk/by-label/ ~/geekcade_assets.sh --check && echo "USB found" || echo "no USB" ``` ### Manual operations ```bash # Force USB mount (if automount is being weird) sudo systemctl start media-GEEKCADE.mount # Force USB unmount before unplugging sudo umount /media/GEEKCADE # Manually rebuild AM+ romlist ~/attractplus.sh --build-romlist mame # Launch a specific game from CLI ~/mame.sh dkong # Verify AM+ binary version command attractplus --version # bypass alias if needed # Free up tty1 if SDL2 froze it sudo systemctl restart getty@tty1 ``` --- ## Future work Listed in rough priority order. None of these are blocking — the cabinet works without them. 1. **HTTPS for the deploy server.** Currently HTTP-only. Free with Let's Encrypt. Low priority because the cabinet is on a trusted home LAN. 2. **GitHub Actions auto-deploy.** Push to `main` → server pulls. Removes the manual "scp scripts to web server" step. Medium priority, nice-to-have. 3. **LAN-based asset transfer.** Skip USB entirely; push assets directly from dev machine to cabinet over LAN. Faster than USB for incremental updates. Major architectural shift, low priority since USB works. 4. **Per-game custom layouts.** AM+ supports per-game layout overrides (e.g., a vertical layout for vertical games). Currently we just use one Attrac-Man layout for everything. 5. **More emulators.** AM+ template directory has configs for ~25 other emulators (PCSX2, Stella, Snes9x, etc.). Adding any of them is mostly: install emulator, drop config in `~/attractplus/emulators/`, add ROMs, build romlist. Could write a `geekcade_assets_.sh` per emulator. 6. **MAME optimization.** `mame -cc` produces a generous default. Tuning for cabinet-specific use (no menu bar, no debug, specific resolution lock) could improve startup time and visual polish. 7. **Cabinet artwork on bootup.** Currently the boot gate is plain text. A splash image during the 5-second countdown would be more cabinet-like. 8. **Volume control hotkey.** AM+ has a built-in volume control. Currently relies on USB keyboard up/down. Worth verifying the joystick encoder mappings. 9. **Save-state hotkeys.** MAME supports save states. Currently uses default keybinds. May need adjustment for cabinet-specific buttons. 10. **CI test for the install scripts.** Set up a fresh Ubuntu Docker container and run the installer in it. Catches regressions. The cabinet IS the current test environment, which is fragile. --- ## Glossary - **Cabinet PC**: the headless box running Ubuntu, MAME, and AttractMode Plus. Always-on, locally connected to the monitor and joystick. - **AM+** / **AttractMode Plus**: the frontend that displays game art and lets you pick games. Forked from AttractMode with Pillarpix's video and visual improvements. - **MAME**: the actual emulator. Reads `.zip` files and emulates the hardware. - **MAME EXTRAs**: Pleasuredome torrent of artwork (flyers, marquees, etc.) and metadata (history, dats). - **AntoPISA SupportFiles**: separate project that maintains current `catver.ini`, `bestgames.ini`, etc., aligned with current MAME versions. - **catver.ini**: maps each game name to a category like "Maze / Driving" or "Computer / Atari 400". - **history.dat / history.xml**: per-game blurbs (release year, manufacturer, gameplay, trivia). - **Boot gate**: 5-second window at tty1 login where any keypress drops to the admin menu instead of launching AM+. - **Skip-gate flag**: `/tmp/.geekcade_skip_gate` file the menu touches when AM+ exits, causing the next tty1 login to skip the gate. - **KMSDRM**: kernel modesetting / direct rendering manager. The Linux subsystem that lets userspace draw directly to a screen without X11. - **Pleasuredome**: a long-running MAME asset preservation community. Hosts the canonical EXTRAs torrent. - **Pleasuredome convention**: their recommended mame.ini syntax, where `flyers_directory flyers;` tells MAME to find `flyers.zip` (or `flyers/`) inside ``. We don't use this — we use explicit absolute paths instead. - **Self-routing structure**: AntoPISA's support folder layout where each category has its own `dats/` and `folders/` subfolders, mirroring MAME's expected destinations. --- *Last updated: build 38, 2026-05-17.*