# 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/`](../legacy/) as the behavioral reference. The RIO wire format and input map are documented in [PROTOCOL.md](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 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 8** (LTS), x64. Driver is C (WDK), separate toolchain. - **Form:** background **tray app** (NotifyIcon), auto-start with Windows. - **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 `iRIO` table 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`, remote `https://gitea.mysticmachines.com/VWE/RIOJoy.git`. - Legacy C++ moved to `legacy/`; cockpit art to `docs/reference/`. - Solution `RioJoy.sln` with `src/RioJoy.Core` (lib) + `src/RioJoy.Tray` (tray app); `driver/` placeholder for the WDK project. - This plan + [PROTOCOL.md](PROTOCOL.md). ### Phase 1 — Virtual HID driver — test-signed, installed, verified ✅ Implemented in [`driver/RioGamepad/`](../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`](../driver/RioGamepad/ReportDescriptor.h)). - Device interface + custom `IOCTL_RIO_SUBMIT_REPORT` → `VhfReadReportSubmit`; the driver is a thin relay, with the 25-byte report layout pinned in [`Public.h`](../driver/RioGamepad/Public.h). - C# side of the contract: `RioJoy.Core.Hid.RioHidReport` packs axes/hat/buttons into that exact report (unit-tested). Replaces the throwaway test harness idea. - Test-signed + `pnputil`-installed on the cabinet; INF declares `vhf` as a **lower filter** (`LowerFilters` AddReg) — without it `VhfCreate` fails and the device shows Code 31. The EWDK's in-build catalog/sign tasks are bypassed; the `.cat` is made with `inf2cat`/`signtool` at install time (`driver/*.ps1`, [`driver/README.md`](../driver/README.md)). - **End-to-end verified:** the real `HidFeederJoystickSink` opens the device and submits reports; axes (min/mid/max), buttons, and the POV hat all read back correctly through `winmm joyGetPosEx` / `joy.cpl`. The controller's `joy.cpl` name is set via the DirectInput `OEMName` registry 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-in `VerifyInboundChecksum`), analog poll timer + >5 s reset-recovery. - `IRioTransport` abstraction with a `SerialPort`-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): - `RioMapEntry` decodes the 16-bit `iRIO` word (flags + value) and resolves the routing `Kind` by the legacy precedence (joy+hat+mouse ⇒ RIO command; none ⇒ keyboard; else joy → hat → mouse). - `RioAddress` (button + keypad→address offsets) and `RioInputMap` (112-entry per-profile table) replace the hard-coded `iRIO[]`. - `InputRouter` ports `Press_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; `SendInputSink` is the real `SendInput` keyboard/mouse adapter. - The joystick sink's real adapter — the **HID feeder → RioGamepad driver via `DeviceIoControl`** (`Output/HidFeederJoystickSink`) — is implemented, wired in (`RioCoordinator` selects it when the driver is present, else `NullJoystickSink`), and verified end-to-end against the installed driver (Phase 1). - ⏳ **Remaining:** the legacy default map / `RIO.ini` becomes 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): - `AxisCalibrator` ports `UpdateThrottle`/`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 documented `0..32766` range. - `IJoystickSink` gains `SetAxis(JoyAxis, value)` so calibrated axes reach the HID feeder; `AxisOutputs` carries the six values. - `PlasmaCommands` ports the `CPlasma` ESC command set (clear/cursor/font/attr/box draw+fill/text) + `GetFontSize` + the `PlasmaPosText` auto-fit/centering; `PlasmaDisplay` writes them over the secondary COM transport. - ⏳ **Remaining:** hardware verification of axis feel + plasma output; the game-specific `PlasmaScoreDraw` layout 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` (136 xUnit tests total across the suite): - `RioProfile` + `AppConfig` model; `ConfigStore` JSON persistence (round-trip tested); `RioIniImporter` ports the legacy `RIO.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. - `RioRuntime` assembles 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: `NotifyIcon` menu mirroring the legacy console menu (axis resets, version/status, diagnostic toggles, quit) + profile selection (auto vs. manual) + "start with Windows"; `RioCoordinator` owns the serial acquire/release tied to the watcher (native-game COM-port yield); OS adapters (`ForegroundProcessProvider`, `AutoStartManager`). - Joystick output now uses the real `HidFeederJoystickSink` when the driver is present (verified end-to-end); `NullJoystickSink` remains 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 - Driver test-signing + `pnputil` install already scripted (`driver/sign.ps1`, `driver/install.ps1`, `driver/uninstall.ps1`, [`driver/README.md`](../driver/README.md)); proven on the cabinet. ⏳ Still to do: app installer, a single bundled deploy, and a cabinet deployment doc. (Production option: attestation signing.) ### Phase 7 — Profile/mapping editor + cockpit overlay generator — in progress Replaces the legacy Google-Sheet → `.data` → GIMP → Script-Fu pipeline (see [`docs/reference/customBackground/`](reference/customBackground/)). - **Overlay generator — done ✅, verified on real assets.** `RioJoy.Core/Overlay` is the pure, unit-tested layout engine: `FontFitter` is a faithful port of the `calc-fontsize` auto-fit search (validated against a brute-force oracle), `OverlayLayoutEngine` ports `create-data-layer`'s fit + horizontal/vertical justification, and `OverlayTemplate`/`OverlayRegion` (a `regions.json` via `OverlayTemplateStore`) hold the cell geometry/color. Label text is per-profile (`RioProfile.OverlayLabels`); `GoobieDataImporter` reads the legacy `.data` sheet into label rows. The rasterizer lives in `src/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`) by `OverlayRenderIntegrationTests`. `RioJoy.Tray/WallpaperApplier` applies the result via `SystemParametersInfo`. - **Region authoring — extraction done ✅.** `tools/XcfRegionExtract` parses the GIMP source (`riojoy.xcf`) and writes the 119-cell `docs/reference/customBackground/regions.json` (per-layer offsets/size/font/color, `BaseImagePath` → exported `riojoy.png`), anchored by `CockpitRegionsTests`. ⏳ Still to do: an in-app box editor for tweaks. - **Runtime wiring — done ✅ (opt-in).** `RioJoy.Overlay/ProfileWallpaperGenerator` renders a profile's labels onto the template base image; `RioCoordinator` generates + applies the wallpaper on profile activation when `AppConfig.OverlayTemplatePath` is set (best-effort, off by default, never breaks activation). The live `SystemParametersInfo` apply changes a user setting, so it is gated behind config and not exercised by tests. ⏳ Optional: restore the prior wallpaper when going dormant. - **Mapping editor — first cut done ✅.** `RioJoy.Tray/Editor/ProfileEditorForm` shows the cockpit as a scrollable, clickable grid that mirrors the legacy authoring spreadsheet's **logical** layout — parsed from the sheet's CSV export (`docs/reference/customBackground/config-sheet.csv`) by `SheetLayout`, coloured by section. (The layout deliberately follows the sheet, **not** the wallpaper region positions: the wallpaper drives 6 displays via VGA chroma-channel splitting, so its label positions stack and don't reflect the physical cockpit.) Clicking an address cell edits its label and action/modifiers/lamp; the `iRIO` word is read/written through `ButtonBinding` ↔ `RioMapEntry.Create` (round-trip tested) — no hex bit-twiddling. The value field is a **context-sensitive picker**: keyboard actions pick a key by name (`KeyCatalog` VK names), joystick a Button N, hat a direction, mouse/RIO-command the enum name; modifiers enable only for keyboard. Opened from the tray ("Edit profile…"); Save persists the profile. ⏳ Still to refine: grouping a button's two bank addresses (edit once → both), and clone-from-existing. - The unified profile JSON supersedes both `RIO.ini` and 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). --- ## 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:** `SendInput` targets 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.