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>
415 lines
26 KiB
Markdown
415 lines
26 KiB
Markdown
# 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 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 `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`
|
||
(241 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);
|
||
`RioCoordinator` owns 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 `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 — done ✅
|
||
- Driver test-signing + `pnputil` install scripted (`driver/sign.ps1`,
|
||
`driver/install.ps1`, `driver/uninstall.ps1`, [`driver/README.md`](../driver/README.md));
|
||
proven on the cabinet.
|
||
- **Deployment package** ([`deploy/`](../deploy/)): `build-package.ps1` produces
|
||
`dist/RIOJoy-<version>.zip` (framework-dependent net48 app + `postinstall.bat` /
|
||
`install-rio.ps1` / `pre-uninstall.bat` / `uninstall-rio.ps1`, all idempotent) with
|
||
the cabinet doc [`README-DEPLOY.txt`](../deploy/README-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/`](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 — 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`.
|
||
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 in
|
||
`RioJoy.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/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.
|
||
- **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 `.data` sheet 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
|
||
remembers `AppConfig.OverlayTemplatePath` on first use.
|
||
- **Mapping editor — done ✅ (cockpit control-panel layout).**
|
||
`RioJoy.Tray/Editor/ProfileEditorForm` shows 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; the `iRIO` word goes through
|
||
`ButtonBinding` ↔ `RioMapEntry.Create` — no hex. The value field is a
|
||
context-sensitive picker (keyboard key by name via `KeyCatalog`, 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.AxesUpdated` streams an `AxisReadout` (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 in `RioJoy.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.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).
|
||
|
||
### 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;net40` multi-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 in `ConfigStore`/`OverlayTemplateStore`
|
||
(both TFMs, so the config format can't drift); async on net40 via
|
||
**Microsoft.Bcl.Async** + a `Compat/TaskCompat` shim (6 call sites:
|
||
Task.Run/Delay/WhenAny/WhenAll → TaskEx; XP prereq: KB2468871, bundle it);
|
||
shim the one `HashCode.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 legacy `CommWatchProc` shape) 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 `vhidmini` sample (`HidRegisterMinidriver`, x86), exposing the **same
|
||
`Public.h` contract** as the modern driver — identical
|
||
`IOCTL_RIO_SUBMIT_REPORT`, identical 25-byte report, same descriptor
|
||
(6×16-bit axes, hat, 96 buttons) — so the existing `HidFeederJoystickSink`
|
||
drives it unchanged (only the device path differs). Precedent: the
|
||
original FASA `tasgame.sys` was 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) under `driver/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); gate `WallpaperMakerForm` + overlay
|
||
generation; `WallpaperApplier` converts PNG→BMP via GDI+ before
|
||
`SystemParametersInfo` (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.WallpaperPath` travels with
|
||
the config).
|
||
- **8D — Packaging — done ✅ (one universal archive, two install entry
|
||
points).** `build-package.ps1` produces a single `RIOJoy-<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 in
|
||
`RIOJoy\install-core.bat` (pure cmd on the XP path); shortcuts via
|
||
`make-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.bat` for a
|
||
standalone machine; `postinstall.bat` is 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
|
||
deletes `install.bat` and the root `README.txt`** — on cabinet deploys
|
||
the zip extracts into `C:\games`, and only launcher-managed files may
|
||
stay at that root (`del` tolerating 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.
|
||
- **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:**
|
||
1. The Xbox 360 pad (ViGEm) remains the preferred controller on
|
||
Windows 10/11 whenever ViGEmBus is present.
|
||
2. **No third-party virtual joystick drivers** (vJoy/PPJoy are
|
||
unmaintained) — XP gets our own RioGamepadXP.sys.
|
||
3. The **mapping editor ships in all instances**, XP included.
|
||
4. **Both install scenarios** in one archive: `postinstall.bat`
|
||
(TeslaConsole/launcher, unattended) and `install.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:** `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.
|