One RIOJoy-<ver>.zip now deploys on both OS generations (~75 MB with the offline XP prerequisites): - Layout: RIOJoy\app (net48 x64) + RIOJoy\app-xp (net40 x86) + RIOJoy\vendor (ViGEmBus; vendor\xp: .NET 4.0 offline installer + KB2468871, both signature-verified; RioGamepadXP driver slots in when 8B lands - build warns/skips until then). - RIOJoy\install-core.bat: shared OS-detecting install logic (ver -> 5.1 = XP); pure cmd.exe on the XP path, delegates to install-rio.ps1 on 10/11. Exports RIOJOY_APPDIR for shortcut creation. - postinstall.bat (TeslaConsole, unattended): elevates, runs the core, then deletes install.bat + README.txt so C:\games stays clean. - install.bat (standalone computers): same core + Start Menu/desktop shortcuts via make-shortcut.vbs (XP-safe); keeps the README. - README.txt at the zip root explains which entry point to run. - deploy *.ps1 re-encoded UTF-8-with-BOM: Windows PowerShell read the BOM-less files as ANSI, where an em-dash byte parses as a curly quote and breaks string parsing (bit install-rio.ps1 in dry-run testing). Verified: zip layout correct; extracted package dispatch-tested on Win10 non-elevated (OS detect -> modern route -> admin guard, system untouched). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
26 KiB
RIOJoy — modernization plan
Modernize the cockpit RIO interface app for Windows 10/11, removing the vJoy dependency and replacing it with a custom virtual HID device, rewritten in C#/.NET as a background tray app with per-game profiles.
The legacy app is preserved under legacy/ as the behavioral
reference. The RIO wire format and input map are documented in
PROTOCOL.md.
Purpose
The cockpits run two native games (Firestorm, Red Planet) that talk to the RIO hardware directly and never use this app. RIOJoy exists to broaden which other games can run in the cockpits: arbitrary games don't know about the cockpit's extra hardware (5 analog axes, 96 lighted buttons, the plasma/VFD display, the labeled wallpaper), so RIOJoy bridges the RIO to whatever input those games do understand — joystick, keyboard, and mouse — and drives the cockpit's outputs on their behalf.
Target architecture
┌────────────────── C# / .NET Framework 4.8 tray app (x64) ─────────────────┐
│ Serial (RIO protocol) → Input mapper (profile) → Output router │
│ COM port, 9600 8N1 72 inputs + keypads ├─ Keyboard/Mouse: SendInput (P/Invoke)
│ packet parse + ACK/NAK decode iRIO bitfield ├─ Joystick: HID report → DeviceIoControl ↓
│ analog poll + recovery axis calibration └─ Lamps: LampRequest back over serial
│ Plasma/VFD on 2nd COM · Profiles + auto-switch + tray UI + logging │
└───────────────────────────────────────────────┬───────────────────────────┘
│ IOCTL (input report bytes)
┌─────────────────────────────────────────────────▼──────────────────────────┐
│ RioGamepad.sys — KMDF + VHF (vhf.sys) virtual HID device │
│ Report descriptor: X,Y,Z,Rx,Ry,Rz (16-bit) · 1 hat · 96 buttons │
│ Control device + custom IOCTL → VhfReadReportSubmit() → Windows sees │
│ a HID gamepad │
└─────────────────────────────────────────────────────────────────────────────┘
Decisions (confirmed)
- Virtual joystick: custom VHF/UMDF HID driver (full fidelity: 6 axes, 96 buttons, 1 hat — exactly the legacy vJoy layout). Not ViGEm (can't hold 96 buttons).
- Stack: C# / .NET Framework 4.8, x64 — in-box on Windows 10/11, so
deployed builds are framework-dependent with no runtime to install. (Modern C#
language features that net48 lacks are supplied by the PolySharp source
generator + a few NuGet shims; see
src/RioJoy.Core/Compat/.) Driver is C (WDK), separate toolchain. - Form: background tray app (NotifyIcon); start/stop managed by the TeslaConsole launcher (no logon auto-start).
- Targets: Windows 10/11 x64 only. The legacy x86 / WinXP targets are dropped.
What ports over vs. what's new
| Concern | Legacy | Modern |
|---|---|---|
| Serial + RIO protocol | overlapped I/O + watch thread | SerialPort + async loop; faithful port of the packet state machine |
| Input decode | iRIO[] bitfield + Press_V2 |
same semantics, ported to C# |
| Keyboard/mouse | SendInput scancode |
SendInput via P/Invoke (≈verbatim) |
| Joystick | vJoy SetAxis/SetBtn/SetDiscPov |
HID report → IOCTL → RioGamepad.sys |
| Lamps | LampRequest over serial |
same |
| Axis calibration | UpdateJoystick/Throttle/Padal |
ported math |
| Plasma display | CPlasma (COM2) |
ported; content per-profile |
| Config | SimpleIni, single file, hard-coded COM1 | profile library (JSON), configurable ports; importer for legacy RIO.ini |
Profiles (the core abstraction)
A profile fully describes how the cockpit behaves for one non-native game. Everything is per-profile, not global:
- Button/keypad mapping (the decoded
iRIOtable for this game) - Axis calibration, curves, and invert flags
- Lamp behavior
- Plasma/VFD content (or "off")
- Cockpit overlay labels + the generated wallpaper (Phase 7)
- Display/resolution targets
Profiles never describe the native games — those are the "hands-off" case.
Serial-port yield & the auto-switch state machine
Because the native games own the RIO's COM port directly, RIOJoy must yield it. A process/window watcher drives three states:
| Detected running app | RIOJoy behavior |
|---|---|
| Native game (Firestorm, Red Planet) | Release COM port, go fully dormant — no serial, no HID, no overlay |
| Supported non-native game | Acquire port, load that game's profile, drive HID / keyboard / mouse / lamps / plasma / wallpaper |
| Nothing / desktop | Idle — port released or a configurable neutral default |
Config therefore holds: the per-game profile library, a list of native games to yield to, and the executable→profile match rules. Manual override from the tray menu is always available.
Phases
Phase 0 — Repo & scaffold ✅ (this commit)
git init, remotehttps://gitea.mysticmachines.com/VWE/RIOJoy.git.- Legacy C++ moved to
legacy/; cockpit art todocs/reference/. - Solution
RioJoy.slnwithsrc/RioJoy.Core(lib) +src/RioJoy.Tray(tray app);driver/placeholder for the WDK project. - This plan + PROTOCOL.md.
Phase 1 — Virtual HID driver — test-signed, installed, verified ✅
Implemented in driver/RioGamepad/; builds to
RioGamepad.sys against the EWDK (KMDF 1.15 + VHF, x64, warnings-as-errors).
- KMDF + VHF virtual HID gamepad: report descriptor = 6×16-bit axes, 1 hat,
96 buttons (
ReportDescriptor.h). - Device interface + custom
IOCTL_RIO_SUBMIT_REPORT→VhfReadReportSubmit; the driver is a thin relay, with the 25-byte report layout pinned inPublic.h. - C# side of the contract:
RioJoy.Core.Hid.RioHidReportpacks axes/hat/buttons into that exact report (unit-tested). Replaces the throwaway test harness idea. - Test-signed +
pnputil-installed on the cabinet; INF declaresvhfas a lower filter (LowerFiltersAddReg) — without itVhfCreatefails and the device shows Code 31. The EWDK's in-build catalog/sign tasks are bypassed; the.catis made withinf2cat/signtoolat install time (driver/*.ps1,driver/README.md). - End-to-end verified: the real
HidFeederJoystickSinkopens the device and submits reports; axes (min/mid/max), buttons, and the POV hat all read back correctly throughwinmm joyGetPosEx/joy.cpl. The controller'sjoy.cplname is set via the DirectInputOEMNameregistry value at install (VHF can't supply a HID product string). - ⏳ Remaining: none for the driver itself; redistribution off owned cabinets would need attestation signing (Phase 6).
Phase 2 — Serial + RIO protocol core (RioJoy.Core) — code-complete ✅
Implemented in src/RioJoy.Core/Protocol + Serial, covered by
tests/RioJoy.Core.Tests (xUnit, 54 tests):
- Packet parser/builder: command/length table, 7-bit checksum, control chars,
framing resync on a high-bit byte mid-packet (
PacketParser,PacketBuilder). - Typed RIO→PC decodes:
AnalogReport(14-bit sign-extend),VersionInfo,CheckStatus; lamp-state composition (RioLampState). RioSerialLink: async receive loop with ACK/NAK policy (legacy force-accept vs. opt-inVerifyInboundChecksum), analog poll timer + >5 s reset-recovery.IRioTransportabstraction with aSerialPort-backed implementation (9600 8N1, DTR reset pulse); clean COM-port acquire/release = create/dispose the transport (foundation for serial yield). The read loop is tested against an in-memory fake transport.- ⏳ Remaining: verify against real hardware (version reply, check reply, analog stream) — needs a cabinet; can't be done off-device.
Phase 3 — Input mapping + output routing — code-complete ✅
Implemented in src/RioJoy.Core/Mapping + Output, covered by the
Mapping tests (xUnit, 84 tests total):
RioMapEntrydecodes the 16-bitiRIOword (flags + value) and resolves the routingKindby the legacy precedence (joy+hat+mouse ⇒ RIO command; none ⇒ keyboard; else joy → hat → mouse).RioAddress(button + keypad→address offsets) andRioInputMap(112-entry per-profile table) replace the hard-codediRIO[].InputRouterportsPress_V2/Release_V2: modifier ordering, scancode keys, joystick buttons, POV hat, mouse move/click, RIO-command dispatch, and lamp feedback (bright on press / dim on release; RIO commands carry none).- Output is split behind sink interfaces (
IInputSink,IJoystickSink,ILampSink,IRioCommandSink) so routing is pure and unit-tested;SendInputSinkis the realSendInputkeyboard/mouse adapter. - The joystick sink's real adapter — the HID feeder → RioGamepad driver via
DeviceIoControl(Output/HidFeederJoystickSink) — is implemented, wired in (RioCoordinatorselects it when the driver is present, elseNullJoystickSink), and verified end-to-end against the installed driver (Phase 1). - ⏳ Remaining: the legacy default map /
RIO.inibecomes an importable profile (Phase 5/7).
Phase 4 — Axis calibration + plasma display — code-complete ✅
Implemented in src/RioJoy.Core/Calibration + Plasma (105 xUnit tests total):
AxisCalibratorportsUpdateThrottle/UpdatePadal/UpdateJoystick: throttle deadzone + ratchet field, pedal deadzones, X/Y auto-ranging from observed min/max, rudder mixing (enableZR), and all per-axis invert flags. Stateful (start positions, last outputs) like the legacy globals, with the RIOcmd axis resets. Outputs clamp to the documented0..32766range.IJoystickSinkgainsSetAxis(JoyAxis, value)so calibrated axes reach the HID feeder;AxisOutputscarries the six values.PlasmaCommandsports theCPlasmaESC command set (clear/cursor/font/attr/box draw+fill/text) +GetFontSize+ thePlasmaPosTextauto-fit/centering;PlasmaDisplaywrites them over the secondary COM transport.- ⏳ Remaining: hardware verification of axis feel + plasma output; the
game-specific
PlasmaScoreDrawlayout is profile content (Phase 5/7).
Phase 5 — Tray app + profiles — code-complete ✅
Core logic in src/RioJoy.Core/Profiles + RioRuntime; UI/OS in src/RioJoy.Tray
(241 xUnit tests total across the suite):
RioProfile+AppConfigmodel;ConfigStoreJSON persistence (round-trip tested);RioIniImporterports the legacyRIO.ini(buttons/inverts/greeting).AutoSwitchResolver+AutoSwitchWatcher: the three-state decision (Yield native / Activate profile / Idle) from the foreground executable, native always winning; raises only on change. Pure + tested.RioRuntimeassembles a profile's live pipeline: serial button/keypad packets →InputRouter; analog replies →AxisCalibrator→ the six joystick axes; RIO commands → calibration resets + serial requests + lamp re-init. End-to-end tested over the fake transport.- Tray:
NotifyIconmenu mirroring the legacy console menu (axis resets, version/status, diagnostic toggles, quit) + profile selection (auto vs. manual);RioCoordinatorowns the serial acquire/release tied to the watcher (native-game COM-port yield); OS adapters (ForegroundProcessProvider). The app's start/stop lifecycle is owned by the TeslaConsole launcher (no logon auto-start). - Joystick output now uses the real
HidFeederJoystickSinkwhen the driver is present (verified end-to-end);NullJoystickSinkremains only as the no-driver fallback. - ⏳ Remaining: full on-cabinet verification of the auto-switch + acquire/release lifecycle against real RIO hardware.
Phase 6 — Packaging / signing / deploy — done ✅
- Driver test-signing +
pnputilinstall scripted (driver/sign.ps1,driver/install.ps1,driver/uninstall.ps1,driver/README.md); proven on the cabinet. - Deployment package (
deploy/):build-package.ps1producesdist/RIOJoy-<version>.zip(framework-dependent net48 app +postinstall.bat/install-rio.ps1/pre-uninstall.bat/uninstall-rio.ps1, all idempotent) with the cabinet docREADME-DEPLOY.txt. Deployed builds use the signed ViGEmBus virtual controller (Xbox 360 layout, 11 buttons) so no test signing / Secure Boot change / reboot is needed; the custom RioGamepad driver remains the full-fidelity option for owned cabinets. App lifecycle is owned by the TeslaConsole launcher (no auto-start). ⏳ Verify remaining: first real deploy on a cabinet via TeslaConsole.
Phase 7 — Profile/mapping editor + cockpit overlay generator — in progress
Replaces the legacy Google-Sheet → .data → GIMP → Script-Fu pipeline
(see docs/reference/customBackground/).
- Overlay generator — done ✅, verified on real assets.
RioJoy.Core/Overlayis the pure, unit-tested layout engine:FontFitteris a faithful port of thecalc-fontsizeauto-fit search (validated against a brute-force oracle),OverlayLayoutEngineportscreate-data-layer's fit + horizontal/vertical justification, andOverlayTemplate/OverlayRegion(aregions.jsonviaOverlayTemplateStore) hold the cell geometry/color. Label text is per-profile (RioProfile.OverlayLabels);GoobieDataImporterreads the legacy.datasheet into label rows. The rasterizer lives insrc/RioJoy.Overlay(SkiaSharp):SkiaTextMeasurer(shared with the engine so measured layout == drawn output) +SkiaOverlayRenderer(draw labels → PNG). The full chain is exercised end-to-end on the real cockpit art (regions.json+riojoy.png+TEST.data) byOverlayRenderIntegrationTests.RioJoy.Tray/WallpaperApplierapplies the result viaSystemParametersInfo. - Region authoring — done ✅.
tools/XcfRegionExtractparses the GIMP source (riojoy.xcf) and writes the 119-celldocs/reference/customBackground/regions.json(per-layer offsets/size/font/color,BaseImagePath→ exportedriojoy.png), anchored byCockpitRegionsTests. The in-app box editor lives in the wallpaper maker ("Edit cell boxes"): the selected cell grows resize handles — drag to move/resize (plain clicks still cycle stacked cells), or type exact X/Y/W/H — with the move/resize math inRioJoy.Core/Overlay/OverlayRegionEdit(unit-tested, min-size clamped). "Save template" persists the shared geometry back to the regions.json; "Reload template" discards unsaved box edits. - Runtime wiring — done ✅ (opt-in).
RioJoy.Overlay/ProfileWallpaperGeneratorrenders a profile's labels onto the template base image;RioCoordinatorgenerates + applies the wallpaper on profile activation whenAppConfig.OverlayTemplatePathis set (best-effort, off by default, never breaks activation). The liveSystemParametersInfoapply changes a user setting, so it is gated behind config and not exercised by tests. ⏳ Optional: restore the prior wallpaper when going dormant. - Wallpaper maker — done ✅.
RioJoy.Tray/Editor/WallpaperMakerForm(tray → "Wallpaper maker") is the interactive replacement for the Sheet → GIMP pipeline: it renders the profile's wallpaper live on the template base image, outlines all 119 cells, and lets the user click any cell — including the heading/banner cells the button editor can't reach — to edit its text with debounced re-render. Clicking a stacked cell cycles through the regions at that pixel (RioJoy.Core/Overlay/OverlayHitTester, unit-tested — stacking is legitimate: the image feeds six chroma-split displays). Imports a legacy.datasheet row (game picker for multi-row sheets), exports the PNG, and can apply the desktop wallpaper immediately (same per-profile path the runtime uses). Prompts for and remembersAppConfig.OverlayTemplatePathon first use. - Mapping editor — done ✅ (cockpit control-panel layout).
RioJoy.Tray/Editor/ProfileEditorFormshows the cockpit as a clickable control panel matching the original Win32 RIO design (docs/Win32RIO/, by FASA/Michel Lowrance): five MFD clusters, four board columns (Throttle/Secondary/Screen/ Joystick-Hat), an encoder-gauge strip, and the two later-added 4×4 keypads (rendered without lamps). The layout (RioJoy.Core.Editing.CockpitPanel, which places every address 0x00–0x47 / 0x50–0x6F exactly once, unit-tested) deliberately follows the design, not the wallpaper positions (those are a VGA chroma-split display artifact). Lamp buttons shade Off/assigned; keypads are neutral. Clicking a button edits its label and action/modifiers/lamp; theiRIOword goes throughButtonBinding↔RioMapEntry.Create— no hex. The value field is a context-sensitive picker (keyboard key by name viaKeyCatalog, joystick Button N, hat direction, mouse/RIO-command enum); modifiers enable only for keyboard. Opened from the tray ("Edit profile…"); Save persists the profile. The encoder gauges are live:RioRuntime.AxesUpdatedstreams anAxisReadout(the six virtual-joystick outputs plus the calibrated pre-mix pedal positions) into the strip at the analog poll rate — Z and the L/R pedals fill bottom-up, Rz deflects from its center tick, and the X/Y box tracks the stick as a dot (pure fraction math inRioJoy.Core.Editing.AxisGauges, unit-tested). L/R read the pedals from before the ZR mix, since the mix pins Rx/Ry to center. ⏳ Still to refine: showing each button's assigned key as a caption, grouping a button's two bank addresses, and clone-from-existing. - The unified profile JSON supersedes both
RIO.iniand the Google Sheet, with importers for each. - To confirm at Phase 7: target wallpaper resolution(s); static wallpaper vs. live overlay (e.g. lit-button highlighting mirroring lamp state).
Phase 8 — Windows XP compatibility (dual-target) — in progress
(8A/8C/8D done ✅; remaining: 8B driver + 8E XP-VM/cabinet verification) Bring RIOJoy back to the original XP-era cabinets (x86, XP SP3) without regressing Windows 10/11. Strategy: one codebase, two flavors, one universal deployment archive. The mapping editor ships everywhere (decided); only wallpaper generation (SkiaSharp) stays modern-only — XP consumes pre-rendered wallpapers.
| Windows 10/11 (unchanged) | Windows XP SP3 | |
|---|---|---|
| TFM / arch | net48, x64 | net40, x86 (.NET 4.0 is XP's ceiling; 4.5+ needs Vista) |
| Virtual joystick | ViGEm → RioGamepad → none (unchanged) | RioGamepadXP.sys — our own thin WDM HID minidriver (same feeder contract) — full 6-axis/96-button fidelity |
| Overlay render | SkiaSharp (generate + apply) | consume pre-rendered wallpaper only (PNG→BMP — XP's SystemParametersInfo takes BMP only) |
| Editor | full (incl. wallpaper maker) | mapping editor included (decided; pure WinForms/GDI+); wallpaper maker gated (Skia) |
| JSON | System.Text.Json → Newtonsoft 13 | Newtonsoft 13 (STJ needs net461+; one serializer for both flavors) |
| Install scripts | PowerShell | .bat only (XP has no in-box PowerShell) |
- 8A — Core retarget — done ✅ (
net48;net40multi-target): de-Span the protocol/serial layer (≈20 uses / 11 files →byte[]/ArraySegment; System.Memory doesn't go below net45, and at 9600 baud Span buys nothing); swap System.Text.Json → Newtonsoft inConfigStore/OverlayTemplateStore(both TFMs, so the config format can't drift); async on net40 via Microsoft.Bcl.Async + aCompat/TaskCompatshim (6 call sites: Task.Run/Delay/WhenAny/WhenAll → TaskEx; XP prereq: KB2468871, bundle it); shim the oneHashCode.Combine;#if-gate the net48-only sinks (ViGEmJoystickSink,HidFeederJoystickSink,Hid/). Risk fallback: if Bcl.Async misbehaves on real XP, the receive loop reverts to a dedicated thread (the legacyCommWatchProcshape) for net40. - 8B — RioGamepadXP.sys, the XP flavor of our own driver. Third-party
virtual-joystick drivers are ruled out (decided: no vJoy — the project
is unmaintained; PPJoy likewise). Instead, rebuild the thin driver side of
our existing split for XP: a WDM HID minidriver in the shape of the
DDK
vhidminisample (HidRegisterMinidriver, x86), exposing the samePublic.hcontract as the modern driver — identicalIOCTL_RIO_SUBMIT_REPORT, identical 25-byte report, same descriptor (6×16-bit axes, hat, 96 buttons) — so the existingHidFeederJoystickSinkdrives it unchanged (only the device path differs). Precedent: the original FASAtasgame.syswas exactly an XP HID minidriver (docs/Win32RIO/, analyzed); ours stays thin with serial in user mode. Toolchain: WDK 7.1.0 (last XP-capable kit) underdriver/RioGamepadXP/; XP x86 enforces no kernel signing, so install is just the INF — none of the Phase 1 test-signing/Secure Boot friction exists there. Acquisition order (decided): the Xbox 360 pad (ViGEm) stays preferred on 10/11 — net48: ViGEm → RioGamepad → Null (unchanged); net40: RioGamepadXP → Null. Staging: the XP app is useful before the driver lands — milestone 1 ships keyboard/mouse + lamps + plasma (joystick = Null sink), the driver follows as milestone 2. - 8C — Tray on net40/x86 — done ✅: multi-target
RioJoy.Tray(net40 drops the ViGEm + RioJoy.Overlay references); gateWallpaperMakerForm+ overlay generation;WallpaperApplierconverts PNG→BMP via GDI+ beforeSystemParametersInfo(harmless on 10/11, required on XP); profile editor stays (Segoe UI falls back to Tahoma). XP cabinets consume wallpapers pre-rendered on a modern machine (RioProfile.WallpaperPathtravels with the config). - 8D — Packaging — done ✅ (one universal archive, two install entry
points).
build-package.ps1produces a singleRIOJoy-<ver>.zip(~75 MB with the offline XP redistributables) that deploys on both XP and 10/11; the RioGamepadXP driver files bundle automatically once 8B lands (warn/skip until then). The shared OS-detecting install logic lives inRIOJoy\install-core.bat(pure cmd on the XP path); shortcuts viamake-shortcut.vbs(works on XP and 10/11 alike):- Layout:
RIOJoy\app\(net48 x64) +RIOJoy\app-xp\(net40 x86) +RIOJoy\vendor\(ViGEmBus installer for 10/11; RioGamepadXP.sys + INF for XP; .NET 4.0 Full + KB2468871 redistributables so an offline XP cabinet needs nothing else — adds ~70 MB, XP can't download anymore) +VERSION.txt+ README-DEPLOY (payload-internal detail doc). README.txt(zip root): written for a person installing on a standalone computer — which Windows versions are supported, what the two .bat entry points are and which one to run (install.batfor a standalone machine;postinstall.batis the cabinet launcher's hook), that all prerequisites are bundled for offline install, and where the app lands. Plain ASCII so XP-era Notepad renders it cleanly.postinstall.bat(zip root, unattended): the TeslaConsole/launcher entry point, as today — detects the OS (ver→ 5.1 = XP, 10.x = modern), installs the matching prereqs (ViGEmBus silently via the existing PowerShell on 10/11; .NET 4.0 + KB2468871 + driver INF on XP), and wires the matching app flavor. No prompts, idempotent. Its final step deletesinstall.batand the rootREADME.txt— on cabinet deploys the zip extracts intoC:\games, and only launcher-managed files may stay at that root (deltolerating already-missing files, so re-runs stay idempotent).install.bat(zip root, freestanding computers): same OS detection and prereq install, plus what a machine without the launcher needs — Start Menu/desktop shortcut to the right flavor's exe (still no logon auto-start; starting RIOJoy stays deliberate). Pure cmd.exe on the XP path; may call PowerShell only on the 10/11 path. Leaves the README in place (it's the standalone machine's documentation).pre-uninstall.bat/ uninstall mirror both scenarios. Release the one zip to Gitea per the established process.
- Layout:
- 8E — Verification: full suite stays net48-hosted (xUnit needs net452+; shared sources are what's tested) + a tiny net40 console self-test for the shims, run on XP. Ladder: net40/x86 binary boots on Win10 → XP VM with vRIO over a virtual COM pair (app milestone; the driver needs real/virtualized XP too — vhidmini-class drivers run fine in a VM) → real cabinet (joy.cpl shows 6 axes + 96 buttons, SendInput into a game, lamps, plasma, auto-switch yield, BMP wallpaper).
- All open decisions resolved:
- The Xbox 360 pad (ViGEm) remains the preferred controller on Windows 10/11 whenever ViGEmBus is present.
- No third-party virtual joystick drivers (vJoy/PPJoy are unmaintained) — XP gets our own RioGamepadXP.sys.
- The mapping editor ships in all instances, XP included.
- Both install scenarios in one archive:
postinstall.bat(TeslaConsole/launcher, unattended) andinstall.bat(freestanding computers, adds shortcuts); the single dist zip carries everything needed for both XP and 10/11, including offline redistributables.
Open items / risks
- Driver signing is the main friction point. Test-signing is fine for owned cabinets; redistribution needs attestation signing (EV cert + Partner Center).
- Input injection vs. session:
SendInputtargets the interactive session — fine for a tray app, which is why a Windows service was rejected. - Legacy quirks to decide on (see PROTOCOL.md ⚠️ notes): disabled inbound checksum verification; odd mouse-move deltas.